aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
authorWillian Gustavo Veiga <beberveiga@gmail.com>2018-10-02 12:55:36 -0300
committerWillian Gustavo Veiga <beberveiga@gmail.com>2018-10-02 12:57:37 -0300
commit2d4df1349efdf8dd2c8cc4503fd5a871b0066500 (patch)
tree9f71a297cf1bb30f1d59039997a60f97357d0782 /activerecord/lib
parent00c50c2b5966fa1d719c8a58564811c672a0e8c6 (diff)
parentcf608ee34dd833b0357ef4eefa692db33242d2aa (diff)
downloadrails-2d4df1349efdf8dd2c8cc4503fd5a871b0066500.tar.gz
rails-2d4df1349efdf8dd2c8cc4503fd5a871b0066500.tar.bz2
rails-2d4df1349efdf8dd2c8cc4503fd5a871b0066500.zip
Merge branch 'master' into feature/reselect-method
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/associations.rb4
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb52
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb62
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb8
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb40
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb19
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb4
-rw-r--r--activerecord/lib/active_record/autosave_association.rb12
-rw-r--r--activerecord/lib/active_record/base.rb2
-rw-r--r--activerecord/lib/active_record/callbacks.rb2
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb41
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb76
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb29
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb70
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb43
-rw-r--r--activerecord/lib/active_record/connection_handling.rb41
-rw-r--r--activerecord/lib/active_record/core.rb66
-rw-r--r--activerecord/lib/active_record/counter_cache.rb11
-rw-r--r--activerecord/lib/active_record/database_configurations.rb203
-rw-r--r--activerecord/lib/active_record/database_configurations/database_config.rb37
-rw-r--r--activerecord/lib/active_record/database_configurations/hash_config.rb50
-rw-r--r--activerecord/lib/active_record/database_configurations/url_config.rb74
-rw-r--r--activerecord/lib/active_record/explain.rb2
-rw-r--r--activerecord/lib/active_record/fixtures.rb1
-rw-r--r--activerecord/lib/active_record/inheritance.rb6
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb4
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb14
-rw-r--r--activerecord/lib/active_record/migration.rb26
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb38
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb15
-rw-r--r--activerecord/lib/active_record/model_schema.rb2
-rw-r--r--activerecord/lib/active_record/persistence.rb27
-rw-r--r--activerecord/lib/active_record/railtie.rb47
-rw-r--r--activerecord/lib/active_record/railties/databases.rake42
-rw-r--r--activerecord/lib/active_record/reflection.rb20
-rw-r--r--activerecord/lib/active_record/relation.rb85
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb2
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb2
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb8
-rw-r--r--activerecord/lib/active_record/relation/merger.rb13
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb6
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb15
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb8
-rw-r--r--activerecord/lib/active_record/result.rb15
-rw-r--r--activerecord/lib/active_record/sanitization.rb4
-rw-r--r--activerecord/lib/active_record/scoping.rb17
-rw-r--r--activerecord/lib/active_record/scoping/default.rb9
-rw-r--r--activerecord/lib/active_record/scoping/named.rb20
-rw-r--r--activerecord/lib/active_record/statement_cache.rb28
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb34
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb8
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/test_databases.rb27
-rw-r--r--activerecord/lib/arel.rb5
-rw-r--r--activerecord/lib/arel/collectors/composite.rb3
-rw-r--r--activerecord/lib/arel/collectors/plain_string.rb2
-rw-r--r--activerecord/lib/arel/collectors/sql_string.rb4
-rw-r--r--activerecord/lib/arel/collectors/substitute_binds.rb3
-rw-r--r--activerecord/lib/arel/delete_manager.rb11
-rw-r--r--activerecord/lib/arel/factory_methods.rb4
-rw-r--r--activerecord/lib/arel/nodes/bind_param.rb2
-rw-r--r--activerecord/lib/arel/nodes/delete_statement.rb15
-rw-r--r--activerecord/lib/arel/nodes/node.rb10
-rw-r--r--activerecord/lib/arel/nodes/select_core.rb6
-rw-r--r--activerecord/lib/arel/nodes/unary.rb1
-rw-r--r--activerecord/lib/arel/nodes/update_statement.rb7
-rw-r--r--activerecord/lib/arel/select_manager.rb8
-rw-r--r--activerecord/lib/arel/table.rb3
-rw-r--r--activerecord/lib/arel/tree_manager.rb34
-rw-r--r--activerecord/lib/arel/update_manager.rb29
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb3
-rw-r--r--activerecord/lib/arel/visitors/dot.rb2
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb7
-rw-r--r--activerecord/lib/arel/visitors/mysql.rb42
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb50
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb15
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb1
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb1
105 files changed, 1276 insertions, 714 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index d198466dbf..d43378c64f 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -40,7 +40,6 @@ module ActiveRecord
autoload :Core
autoload :ConnectionHandling
autoload :CounterCache
- autoload :DatabaseConfigurations
autoload :DynamicMatchers
autoload :Enum
autoload :InternalMetadata
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 1ee52945ea..ab1e7ad269 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1524,6 +1524,7 @@ module ActiveRecord
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
# Assigns the associate object, extracts the primary key, and sets it as the foreign key.
+ # No modification or deletion of existing records takes place.
# [build_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
@@ -1581,7 +1582,7 @@ module ActiveRecord
# association will use "taggable_type" as the default <tt>:foreign_type</tt>.
# [:primary_key]
# Specify the method that returns the primary key of associated object used for the association.
- # By default this is id.
+ # By default this is +id+.
# [:dependent]
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
@@ -1761,6 +1762,7 @@ module ActiveRecord
# has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
# has_and_belongs_to_many :categories, ->(post) {
# where("default_category = ?", post.default_category)
+ # }
#
# === Extensions
#
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 3d4ad1dd5b..3346725f2d 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -34,14 +34,28 @@ module ActiveRecord
@updated
end
- def decrement_counters # :nodoc:
+ def decrement_counters
update_counters(-1)
end
- def increment_counters # :nodoc:
+ def increment_counters
update_counters(1)
end
+ def decrement_counters_before_last_save
+ if reflection.polymorphic?
+ model_was = owner.attribute_before_last_save(reflection.foreign_type).try(:constantize)
+ else
+ model_was = klass
+ end
+
+ foreign_key_was = owner.attribute_before_last_save(reflection.foreign_key)
+
+ if foreign_key_was && model_was < ActiveRecord::Base
+ update_counters_via_scope(model_was, foreign_key_was, -1)
+ end
+ end
+
def target_changed?
owner.saved_change_to_attribute?(reflection.foreign_key)
end
@@ -50,11 +64,8 @@ module ActiveRecord
def replace(record)
if record
raise_on_type_mismatch!(record)
- update_counters_on_replace(record)
set_inverse_instance(record)
@updated = true
- else
- decrement_counters
end
replace_keys(record)
@@ -67,11 +78,16 @@ module ActiveRecord
if target && !stale_target?
target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
else
- counter_cache_target.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch])
+ update_counters_via_scope(klass, owner._read_attribute(reflection.foreign_key), by)
end
end
end
+ def update_counters_via_scope(klass, foreign_key, by)
+ scope = klass.unscoped.where!(primary_key(klass) => foreign_key)
+ scope.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch])
+ end
+
def find_target?
!loaded? && foreign_key_present? && klass
end
@@ -80,25 +96,12 @@ module ActiveRecord
reflection.counter_cache_column && owner.persisted?
end
- def update_counters_on_replace(record)
- if require_counter_update? && different_target?(record)
- owner.instance_variable_set :@_after_replace_counter_called, true
- record.increment!(reflection.counter_cache_column, touch: reflection.options[:touch])
- decrement_counters
- end
- end
-
- # Checks whether record is different to the current target, without loading it
- def different_target?(record)
- record._read_attribute(primary_key(record)) != owner._read_attribute(reflection.foreign_key)
- end
-
def replace_keys(record)
- owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record)) : nil
+ owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record.class)) : nil
end
- def primary_key(record)
- reflection.association_primary_key(record.class)
+ def primary_key(klass)
+ reflection.association_primary_key(klass)
end
def foreign_key_present?
@@ -112,11 +115,6 @@ module ActiveRecord
inverse && inverse.has_one?
end
- def counter_cache_target
- primary_key = reflection.association_primary_key(klass)
- klass.unscoped.where!(primary_key => owner._read_attribute(reflection.foreign_key))
- end
-
def stale_state
result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
result && result.to_s
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 3fd2fb5f67..9ae452e7a1 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -19,10 +19,6 @@ module ActiveRecord
owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil
end
- def different_target?(record)
- super || record.class != klass
- end
-
def inverse_reflection_for(record)
reflection.polymorphic_inverse_of(record.class)
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index b5fb0092d7..fc00f1e900 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -21,56 +21,16 @@ module ActiveRecord::Associations::Builder # :nodoc:
add_default_callbacks(model, reflection) if reflection.options[:default]
end
- def self.define_accessors(mixin, reflection)
- super
- add_counter_cache_methods mixin
- end
-
- def self.add_counter_cache_methods(mixin)
- return if mixin.method_defined? :belongs_to_counter_cache_after_update
-
- mixin.class_eval do
- def belongs_to_counter_cache_after_update(reflection)
- foreign_key = reflection.foreign_key
- cache_column = reflection.counter_cache_column
-
- if (@_after_replace_counter_called ||= false)
- @_after_replace_counter_called = false
- elsif association(reflection.name).target_changed?
- if reflection.polymorphic?
- model = attribute_in_database(reflection.foreign_type).try(:constantize)
- model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize)
- else
- model = reflection.klass
- model_was = reflection.klass
- end
-
- foreign_key_was = attribute_before_last_save foreign_key
- foreign_key = attribute_in_database foreign_key
-
- if foreign_key && model < ActiveRecord::Base
- counter_cache_target(reflection, model, foreign_key).update_counters(cache_column => 1)
- end
-
- if foreign_key_was && model_was < ActiveRecord::Base
- counter_cache_target(reflection, model_was, foreign_key_was).update_counters(cache_column => -1)
- end
- end
- end
-
- private
- def counter_cache_target(reflection, model, foreign_key)
- primary_key = reflection.association_primary_key(model)
- model.unscoped.where!(primary_key => foreign_key)
- end
- end
- end
-
def self.add_counter_cache_callbacks(model, reflection)
cache_column = reflection.counter_cache_column
model.after_update lambda { |record|
- record.belongs_to_counter_cache_after_update(reflection)
+ association = association(reflection.name)
+
+ if association.target_changed?
+ association.increment_counters
+ association.decrement_counters_before_last_save
+ end
}
klass = reflection.class_name.safe_constantize
@@ -121,12 +81,18 @@ module ActiveRecord::Associations::Builder # :nodoc:
BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method)
}}
- unless reflection.counter_cache_column
+ if reflection.counter_cache_column
+ touch_callback = callback.(:saved_changes)
+ update_callback = lambda { |record|
+ instance_exec(record, &touch_callback) unless association(reflection.name).target_changed?
+ }
+ model.after_update update_callback, if: :saved_changes?
+ else
model.after_create callback.(:saved_changes), if: :saved_changes?
+ model.after_update callback.(:saved_changes), if: :saved_changes?
model.after_destroy callback.(:changes_to_save)
end
- model.after_update callback.(:saved_changes), if: :saved_changes?
model.after_touch callback.(:changes_to_save)
end
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 e3070e0472..0140aa15c8 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
@@ -63,7 +63,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
def middle_reflection(join_model)
middle_name = [lhs_model.name.downcase.pluralize,
- association_name].join("_".freeze).gsub("::".freeze, "_".freeze).to_sym
+ association_name].join("_").gsub("::", "_").to_sym
middle_options = middle_options join_model
HasMany.create_reflection(lhs_model,
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 5c2ac5b374..a8f94b574d 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -88,7 +88,6 @@ module ActiveRecord
if records.empty?
[]
else
- records.uniq!
Array.wrap(associations).flat_map { |association|
preloaders_on association, records, preload_scope
}
@@ -99,12 +98,11 @@ module ActiveRecord
# Loads all the given data into +records+ for the +association+.
def preloaders_on(association, records, scope, polymorphic_parent = false)
- case association
- when Hash
+ if association.respond_to?(:to_hash)
preloaders_for_hash(association, records, scope, polymorphic_parent)
- when Symbol
+ elsif association.is_a?(Symbol)
preloaders_for_one(association, records, scope, polymorphic_parent)
- when String
+ elsif association.respond_to?(:to_str)
preloaders_for_one(association.to_sym, records, scope, polymorphic_parent)
else
raise ArgumentError, "#{association.inspect} was not recognized for preload"
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index e4b8b1a330..221ebea8ea 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -31,7 +31,7 @@ module ActiveRecord
end
}
- BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
+ RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
class GeneratedAttributeMethods < Module #:nodoc:
include Mutex_m
@@ -123,7 +123,7 @@ module ActiveRecord
# A class method is 'dangerous' if it is already (re)defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
def dangerous_class_method?(method_name)
- BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
+ RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
end
def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
@@ -167,12 +167,14 @@ module ActiveRecord
end
end
- # Regexp whitelist. Matches the following:
+ # Regexp for column names (with or without a table name prefix). Matches
+ # the following:
# "#{table_name}.#{column_name}"
# "#{column_name}"
- COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i
+ COLUMN_NAME = /\A(?:\w+\.)?\w+\z/i
- # Regexp whitelist. Matches the following:
+ # 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"
@@ -181,7 +183,7 @@ module ActiveRecord
# "#{column_name} #{direction}"
# "#{column_name} #{direction} NULLS FIRST"
# "#{column_name} NULLS LAST"
- COLUMN_NAME_ORDER_WHITELIST = /
+ COLUMN_NAME_WITH_ORDER = /
\A
(?:\w+\.)?
\w+
@@ -190,12 +192,10 @@ module ActiveRecord
\z
/ix
- def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc:
+ def disallow_raw_sql!(args, permit: COLUMN_NAME) # :nodoc:
unexpected = args.reject do |arg|
- arg.kind_of?(Arel::Node) ||
- arg.is_a?(Arel::Nodes::SqlLiteral) ||
- arg.is_a?(Arel::Attributes::Attribute) ||
- arg.to_s.split(/\s*,\s*/).all? { |part| whitelist.match?(part) }
+ Arel.arel_node?(arg) ||
+ arg.to_s.split(/\s*,\s*/).all? { |part| permit.match?(part) }
end
return if unexpected.none?
@@ -272,9 +272,9 @@ module ActiveRecord
case name
when :to_partial_path
- name = "to_partial_path".freeze
+ name = "to_partial_path"
when :to_model
- name = "to_model".freeze
+ name = "to_model"
else
name = name.to_s
end
@@ -449,14 +449,6 @@ module ActiveRecord
defined?(@attributes) && @attributes.key?(attr_name)
end
- def attributes_with_values_for_create(attribute_names)
- attributes_with_values(attributes_for_create(attribute_names))
- end
-
- def attributes_with_values_for_update(attribute_names)
- attributes_with_values(attributes_for_update(attribute_names))
- end
-
def attributes_with_values(attribute_names)
attribute_names.each_with_object({}) do |name, attrs|
attrs[name] = _read_attribute(name)
@@ -465,7 +457,8 @@ module ActiveRecord
# Filters the primary keys and readonly attributes from the attribute names.
def attributes_for_update(attribute_names)
- attribute_names.reject do |name|
+ attribute_names &= self.class.column_names
+ attribute_names.delete_if do |name|
readonly_attribute?(name)
end
end
@@ -473,7 +466,8 @@ module ActiveRecord
# Filters out the primary keys, from the attribute names, when the primary
# key is to be generated (e.g. the id attribute has no value).
def attributes_for_create(attribute_names)
- attribute_names.reject do |name|
+ attribute_names &= self.class.column_names
+ attribute_names.delete_if do |name|
pk_attribute?(name) && id.nil?
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 233ee29fac..ebc2252c50 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -16,9 +16,6 @@ module ActiveRecord
class_attribute :partial_writes, instance_writer: false, default: true
- after_create { changes_applied }
- after_update { changes_applied }
-
# Attribute methods for "changed in last call to save?"
attribute_method_affix(prefix: "saved_change_to_", suffix: "?")
attribute_method_prefix("saved_change_to_")
@@ -167,16 +164,20 @@ module ActiveRecord
result
end
- def _update_record(*)
- partial_writes? ? super(keys_for_partial_write) : super
+ def _update_record(attribute_names = attribute_names_for_partial_writes)
+ affected_rows = super
+ changes_applied
+ affected_rows
end
- def _create_record(*)
- partial_writes? ? super(keys_for_partial_write) : super
+ def _create_record(attribute_names = attribute_names_for_partial_writes)
+ id = super
+ changes_applied
+ id
end
- def keys_for_partial_write
- changed_attribute_names_to_save & self.class.column_names
+ def attribute_names_for_partial_writes
+ partial_writes? ? changed_attribute_names_to_save : attribute_names
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 0f7bcba564..903fe86e04 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -27,7 +27,7 @@ module ActiveRecord
# Making it frozen means that it doesn't get duped when used to
# key the @attributes in read_attribute.
def define_method_attribute(name)
- safe_name = name.unpack1("h*".freeze)
+ safe_name = name.unpack1("h*")
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
@@ -59,7 +59,7 @@ module ActiveRecord
end
primary_key = self.class.primary_key
- name = primary_key if name == "id".freeze && primary_key
+ name = primary_key if name == "id" && primary_key
sync_with_transaction_state if name == primary_key
_read_attribute(name, &block)
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index d2b7817b45..294a3dc32c 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -73,7 +73,7 @@ module ActiveRecord
# `skip_time_zone_conversion_for_attributes` would not be picked up.
subclass.class_eval do
matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
- decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
+ decorate_matching_attribute_types(matcher, "_time_zone_conversion") do |type|
TimeZoneConverter.new(type)
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index c7521422bb..62743bc9d8 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -13,7 +13,7 @@ module ActiveRecord
private
def define_method_attribute=(name)
- safe_name = name.unpack1("h*".freeze)
+ safe_name = name.unpack1("h*")
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
@@ -40,7 +40,7 @@ module ActiveRecord
end
primary_key = self.class.primary_key
- name = primary_key if name == "id".freeze && primary_key
+ name = primary_key if name == "id" && primary_key
sync_with_transaction_state if name == primary_key
_write_attribute(name, value)
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index a405f05e0b..d77d76cb1e 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -149,7 +149,7 @@ module ActiveRecord
private
def define_non_cyclic_method(name, &block)
- return if method_defined?(name)
+ return if instance_methods(false).include?(name)
define_method(name) do |*args|
result = true; @_already_called ||= {}
# Loop prevention for validation of associations
@@ -392,7 +392,7 @@ module ActiveRecord
records -= records_to_destroy
end
- records.each_with_index do |record, index|
+ records.each do |record|
next if record.destroyed?
saved = true
@@ -401,11 +401,11 @@ module ActiveRecord
if autosave
saved = association.insert_record(record, false)
elsif !reflection.nested?
+ association_saved = association.insert_record(record)
+
if reflection.validate?
- valid = association_valid?(reflection, record, index)
- saved = valid ? association.insert_record(record, false) : false
- else
- association.insert_record(record)
+ errors.add(reflection.name) unless association_saved
+ saved = association_saved
end
end
elsif autosave
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 5169f312f5..db097cb930 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -22,6 +22,7 @@ require "active_record/explain_subscriber"
require "active_record/relation/delegation"
require "active_record/attributes"
require "active_record/type_caster"
+require "active_record/database_configurations"
module ActiveRecord #:nodoc:
# = Active Record
@@ -291,7 +292,6 @@ module ActiveRecord #:nodoc:
extend Aggregations::ClassMethods
include Core
- include DatabaseConfigurations
include Persistence
include ReadonlyAttributes
include ModelSchema
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index b6852bfc71..1bffe89875 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -318,7 +318,7 @@ module ActiveRecord
_run_touch_callbacks { super }
end
- def increment!(*, touch: nil) # :nodoc:
+ def increment!(attribute, by = 1, touch: nil) # :nodoc:
touch ? _run_touch_callbacks { super } : super
end
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
index dfba78614e..61581b0451 100644
--- a/activerecord/lib/active_record/collection_cache_key.rb
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -16,7 +16,7 @@ module ActiveRecord
collection = collection.send(:apply_join_dependency)
end
column_type = type_for_attribute(timestamp_column)
- column = connection.column_name_from_arel_node(collection.arel_attribute(timestamp_column))
+ column = connection.visitor.compile(collection.arel_attribute(timestamp_column))
select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
if collection.has_limit_or_offset?
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 f721e91203..99934a0e31 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -188,7 +188,9 @@ module ActiveRecord
t0 = Time.now
elapsed = 0
loop do
- @cond.wait(timeout - elapsed)
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @cond.wait(timeout - elapsed)
+ end
return remove if any?
@@ -729,7 +731,7 @@ module ActiveRecord
# this block can't be easily moved into attempt_to_checkout_all_existing_connections's
# rescue block, because doing so would put it outside of synchronize section, without
# being in a critical section thread_report might become inaccurate
- msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds".dup
+ msg = +"could not obtain ownership of all database connections in #{checkout_timeout} seconds"
thread_report = []
@connections.each do |conn|
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 7a9e7add24..1305216be2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/deprecation"
+
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseLimits
@@ -12,11 +14,13 @@ module ActiveRecord
def column_name_length
64
end
+ deprecate :column_name_length
# Returns the maximum length of a table name.
def table_name_length
64
end
+ deprecate :table_name_length
# Returns the maximum allowed length for an index name. This
# limit is enforced by \Rails and is less than or equal to
@@ -36,16 +40,19 @@ module ActiveRecord
def columns_per_table
1024
end
+ deprecate :columns_per_table
# Returns the maximum number of indexes per table.
def indexes_per_table
16
end
+ deprecate :indexes_per_table
# Returns the maximum number of columns in a multicolumn index.
def columns_per_multicolumn_index
16
end
+ deprecate :columns_per_multicolumn_index
# Returns the maximum number of elements in an IN (x,y,z) clause.
# +nil+ means no limit.
@@ -57,11 +64,18 @@ module ActiveRecord
def sql_query_length
1048575
end
+ deprecate :sql_query_length
# Returns maximum number of joins in a single query.
def joins_per_query
256
end
+ deprecate :joins_per_query
+
+ private
+ def bind_params_length
+ 65535
+ end
end
end
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 41553cfa83..feacdf6931 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -20,7 +20,7 @@ module ActiveRecord
raise "Passing bind parameters with an arel AST is forbidden. " \
"The values must be stored on the AST directly"
end
- sql, binds = visitor.accept(arel_or_sql_string.ast, collector).value
+ sql, binds = visitor.compile(arel_or_sql_string.ast, collector)
[sql.freeze, binds || []]
else
[arel_or_sql_string.dup.freeze, binds]
@@ -32,11 +32,11 @@ module ActiveRecord
# can be used to query the database repeatedly.
def cacheable_query(klass, arel) # :nodoc:
if prepared_statements
- sql, binds = visitor.accept(arel.ast, collector).value
+ sql, binds = visitor.compile(arel.ast, collector)
query = klass.query(sql)
else
- collector = PartialQueryCollector.new
- parts, binds = visitor.accept(arel.ast, collector).value
+ collector = klass.partial_query_collector
+ parts, binds = visitor.compile(arel.ast, collector)
query = klass.partial_query(parts)
end
[query, binds]
@@ -46,11 +46,16 @@ module ActiveRecord
def select_all(arel, name = nil, binds = [], preparable: nil)
arel = arel_from_relation(arel)
sql, binds = to_sql_and_binds(arel, binds)
+
if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
preparable = false
+ elsif binds.length > bind_params_length
+ sql, binds = unprepared_statement { to_sql_and_binds(arel) }
+ preparable = false
else
preparable = visitor.preparable
end
+
if prepared_statements && preparable
select_prepared(sql, name, binds)
else
@@ -259,7 +264,9 @@ module ActiveRecord
attr_reader :transaction_manager #:nodoc:
- delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager
+ delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction,
+ :commit_transaction, :rollback_transaction, :materialize_transactions,
+ :disable_lazy_transactions!, :enable_lazy_transactions!, to: :transaction_manager
def transaction_open?
current_transaction.open?
@@ -372,7 +379,7 @@ module ActiveRecord
build_fixture_sql(fixtures, table_name)
end.compact
- table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup }
+ table_deletes = tables_to_delete.map { |table| +"DELETE FROM #{quote_table_name table}" }
total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts))
disable_referential_integrity do
@@ -500,28 +507,6 @@ module ActiveRecord
value
end
end
-
- class PartialQueryCollector
- def initialize
- @parts = []
- @binds = []
- end
-
- def <<(str)
- @parts << str
- self
- end
-
- def add_bind(obj)
- @binds << obj
- @parts << Arel::Nodes::BindParam.new(1)
- self
- end
-
- def value
- [@parts, @binds]
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 98b1348135..07e86afe9a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -60,7 +60,7 @@ module ActiveRecord
# Quotes a string, escaping any ' (single quote) and \ (backslash)
# characters.
def quote_string(s)
- s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode)
+ s.gsub('\\', '\&\&').gsub("'", "''") # ' (for ruby-mode)
end
# Quotes the column name. Defaults to no quoting.
@@ -95,7 +95,7 @@ module ActiveRecord
end
def quoted_true
- "TRUE".freeze
+ "TRUE"
end
def unquoted_true
@@ -103,7 +103,7 @@ module ActiveRecord
end
def quoted_false
- "FALSE".freeze
+ "FALSE"
end
def unquoted_false
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 529c9d8ca6..9d9e8a4110 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -21,7 +21,7 @@ module ActiveRecord
private
def visit_AlterTable(o)
- sql = "ALTER TABLE #{quote_table_name(o.name)} ".dup
+ sql = +"ALTER TABLE #{quote_table_name(o.name)} "
sql << o.adds.map { |col| accept col }.join(" ")
sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(" ")
sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(" ")
@@ -29,17 +29,17 @@ module ActiveRecord
def visit_ColumnDefinition(o)
o.sql_type = type_to_sql(o.type, o.options)
- column_sql = "#{quote_column_name(o.name)} #{o.sql_type}".dup
+ column_sql = +"#{quote_column_name(o.name)} #{o.sql_type}"
add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key
column_sql
end
def visit_AddColumnDefinition(o)
- "ADD #{accept(o.column)}".dup
+ +"ADD #{accept(o.column)}"
end
def visit_TableDefinition(o)
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} ".dup
+ create_sql = +"CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} "
statements = o.columns.map { |c| accept c }
statements << accept(o.primary_keys) if o.primary_keys
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 582ac516c7..015204c056 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -527,7 +527,9 @@ module ActiveRecord
#
# See TableDefinition#column for details of the options you can use.
def column(column_name, type, options = {})
+ index_options = options.delete(:index)
@base.add_column(name, column_name, type, options)
+ index(column_name, index_options.is_a?(Hash) ? index_options : {}) if index_options
end
# Checks to see if a column exists.
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 3be0906f2a..723d8c318d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -211,13 +211,13 @@ module ActiveRecord
#
# ====== Add a backend specific option to the generated SQL (MySQL)
#
- # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4')
#
# generates:
#
# CREATE TABLE suppliers (
# id bigint auto_increment PRIMARY KEY
- # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
#
# ====== Rename the primary key column
#
@@ -522,6 +522,9 @@ module ActiveRecord
# Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
# * <tt>:scale</tt> -
# Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
+ # * <tt>:collation</tt> -
+ # Specifies the collation for a <tt>:string</tt> or <tt>:text</tt> column. If not specified, the
+ # column will have the same collation as the table.
# * <tt>:comment</tt> -
# Specifies the comment for the column. This option is ignored by some backends.
#
@@ -599,6 +602,7 @@ module ActiveRecord
# The +type+ and +options+ parameters will be ignored if present. It can be helpful
# to provide these in a migration's +change+ method so it can be reverted.
# In that case, +type+ and +options+ will be used by #add_column.
+ # Indexes on the column are automatically removed.
def remove_column(table_name, column_name, type = nil, options = {})
execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}"
end
@@ -980,11 +984,18 @@ module ActiveRecord
#
# remove_foreign_key :accounts, column: :owner_id
#
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, to_table: :owners
+ #
# Removes the foreign key named +special_fk_name+ on the +accounts+ table.
#
# remove_foreign_key :accounts, name: :special_fk_name
#
- # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key
+ # with an addition of
+ # [<tt>:to_table</tt>]
+ # The name of the table that contains the referenced primary key.
def remove_foreign_key(from_table, options_or_to_table = {})
return unless supports_foreign_keys?
@@ -1369,7 +1380,7 @@ module ActiveRecord
sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
if versions.is_a?(Array)
- sql = "INSERT INTO #{sm_table} (version) VALUES\n".dup
+ sql = +"INSERT INTO #{sm_table} (version) VALUES\n"
sql << versions.map { |v| "(#{quote(v)})" }.join(",\n")
sql << ";\n\n"
sql
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index b59df2fff7..564b226b39 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -91,12 +91,14 @@ module ActiveRecord
end
class Transaction #:nodoc:
- attr_reader :connection, :state, :records, :savepoint_name
+ attr_reader :connection, :state, :records, :savepoint_name, :isolation_level
def initialize(connection, options, run_commit_callbacks: false)
@connection = connection
@state = TransactionState.new
@records = []
+ @isolation_level = options[:isolation]
+ @materialized = false
@joinable = options.fetch(:joinable, true)
@run_commit_callbacks = run_commit_callbacks
end
@@ -105,6 +107,14 @@ module ActiveRecord
records << record
end
+ def materialize!
+ @materialized = true
+ end
+
+ def materialized?
+ @materialized
+ end
+
def rollback_records
ite = records.uniq
while record = ite.shift
@@ -141,24 +151,30 @@ module ActiveRecord
end
class SavepointTransaction < Transaction
- def initialize(connection, savepoint_name, parent_transaction, options, *args)
- super(connection, options, *args)
+ def initialize(connection, savepoint_name, parent_transaction, *args)
+ super(connection, *args)
parent_transaction.state.add_child(@state)
- if options[:isolation]
+ if isolation_level
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
end
- connection.create_savepoint(@savepoint_name = savepoint_name)
+
+ @savepoint_name = savepoint_name
+ end
+
+ def materialize!
+ connection.create_savepoint(savepoint_name)
+ super
end
def rollback
- connection.rollback_to_savepoint(savepoint_name)
+ connection.rollback_to_savepoint(savepoint_name) if materialized?
@state.rollback!
end
def commit
- connection.release_savepoint(savepoint_name)
+ connection.release_savepoint(savepoint_name) if materialized?
@state.commit!
end
@@ -166,22 +182,23 @@ module ActiveRecord
end
class RealTransaction < Transaction
- def initialize(connection, options, *args)
- super
- if options[:isolation]
- connection.begin_isolated_db_transaction(options[:isolation])
+ def materialize!
+ if isolation_level
+ connection.begin_isolated_db_transaction(isolation_level)
else
connection.begin_db_transaction
end
+
+ super
end
def rollback
- connection.rollback_db_transaction
+ connection.rollback_db_transaction if materialized?
@state.full_rollback!
end
def commit
- connection.commit_db_transaction
+ connection.commit_db_transaction if materialized?
@state.full_commit!
end
end
@@ -190,6 +207,9 @@ module ActiveRecord
def initialize(connection)
@stack = []
@connection = connection
+ @has_unmaterialized_transactions = false
+ @materializing_transactions = false
+ @lazy_transactions_enabled = true
end
def begin_transaction(options = {})
@@ -203,11 +223,41 @@ module ActiveRecord
run_commit_callbacks: run_commit_callbacks)
end
+ transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled?
@stack.push(transaction)
+ @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions?
transaction
end
end
+ def disable_lazy_transactions!
+ materialize_transactions
+ @lazy_transactions_enabled = false
+ end
+
+ def enable_lazy_transactions!
+ @lazy_transactions_enabled = true
+ end
+
+ def lazy_transactions_enabled?
+ @lazy_transactions_enabled
+ end
+
+ def materialize_transactions
+ return if @materializing_transactions
+ return unless @has_unmaterialized_transactions
+
+ @connection.lock.synchronize do
+ begin
+ @materializing_transactions = true
+ @stack.each { |t| t.materialize! unless t.materialized? }
+ ensure
+ @materializing_transactions = false
+ end
+ @has_unmaterialized_transactions = false
+ end
+ end
+
def commit_transaction
@connection.lock.synchronize do
transaction = @stack.last
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index a4748dbeda..fa10f18cb7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -65,7 +65,7 @@ module ActiveRecord
# Most of the methods in the adapter are useful during migrations. Most
# notably, the instance methods provided by SchemaStatements are very useful.
class AbstractAdapter
- ADAPTER_NAME = "Abstract".freeze
+ ADAPTER_NAME = "Abstract"
include ActiveSupport::Callbacks
define_callbacks :checkout, :checkin
@@ -80,6 +80,8 @@ module ActiveRecord
attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock
alias :in_use? :owner
+ set_callback :checkin, :after, :enable_lazy_transactions!
+
def self.type_cast_config_to_integer(config)
if config.is_a?(Integer)
config
@@ -119,6 +121,14 @@ module ActiveRecord
else
@prepared_statements = false
end
+
+ @advisory_locks_enabled = self.class.type_cast_config_to_boolean(
+ config.fetch(:advisory_locks, true)
+ )
+ end
+
+ def replica?
+ @config[:replica] || false
end
def migrations_paths # :nodoc:
@@ -152,7 +162,7 @@ module ActiveRecord
# this method must only be called while holding connection pool's mutex
def lease
if in_use?
- msg = "Cannot lease connection, ".dup
+ msg = +"Cannot lease connection, "
if @owner == Thread.current
msg << "it is already leased by the current thread."
else
@@ -338,6 +348,10 @@ module ActiveRecord
false
end
+ def supports_lazy_transactions?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -346,6 +360,10 @@ module ActiveRecord
def enable_extension(name)
end
+ def advisory_locks_enabled? # :nodoc:
+ supports_advisory_locks? && @advisory_locks_enabled
+ end
+
# This is meant to be implemented by the adapters that support advisory
# locks
#
@@ -449,6 +467,7 @@ module ActiveRecord
# This is useful for when you need to call a proprietary method such as
# PostgreSQL's lo_* methods.
def raw_connection
+ disable_lazy_transactions!
@connection
end
@@ -475,11 +494,7 @@ module ActiveRecord
end
def column_name_for_operation(operation, node) # :nodoc:
- column_name_from_arel_node(node)
- end
-
- def column_name_from_arel_node(node) # :nodoc:
- visitor.accept(node, Arel::Collectors::SQLString.new).value
+ visitor.compile(node)
end
def default_index_type?(index) # :nodoc:
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 9de8242a58..09242a0f14 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -43,9 +43,11 @@ module ActiveRecord
}
class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
- private def dealloc(stmt)
- stmt.close
- end
+ private
+
+ def dealloc(stmt)
+ stmt.close
+ end
end
def initialize(connection, logger, connection_options, config)
@@ -53,8 +55,8 @@ module ActiveRecord
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
- if version < "5.1.10"
- raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.1.10."
+ if version < "5.5.8"
+ raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
end
end
@@ -114,6 +116,14 @@ module ActiveRecord
true
end
+ def supports_longer_index_key_prefix?
+ if mariadb?
+ version >= "10.2.2"
+ else
+ version >= "5.7.9"
+ end
+ end
+
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
end
@@ -127,7 +137,7 @@ module ActiveRecord
end
def index_algorithms
- { default: "ALGORITHM = DEFAULT".dup, copy: "ALGORITHM = COPY".dup, inplace: "ALGORITHM = INPLACE".dup }
+ { default: +"ALGORITHM = DEFAULT", copy: +"ALGORITHM = COPY", inplace: +"ALGORITHM = INPLACE" }
end
# HELPER METHODS ===========================================
@@ -180,6 +190,8 @@ module ActiveRecord
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
+ materialize_transactions
+
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@connection.query(sql)
@@ -239,7 +251,7 @@ module ActiveRecord
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
- # Charset defaults to utf8.
+ # Charset defaults to utf8mb4.
#
# Example:
# create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin'
@@ -248,8 +260,12 @@ module ActiveRecord
def create_database(name, options = {})
if options[:collation]
execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}"
+ elsif options[:charset]
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset])}"
+ elsif supports_longer_index_key_prefix?
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET `utf8mb4`"
else
- execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
+ raise "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns."
end
end
@@ -376,7 +392,7 @@ module ActiveRecord
def add_index(table_name, column_name, options = {}) #:nodoc:
index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
- sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}".dup
+ sql = +"CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
execute add_sql_comment!(sql, comment)
end
@@ -769,7 +785,7 @@ module ActiveRecord
# https://dev.mysql.com/doc/refman/5.7/en/set-names.html
# (trailing comma because variable_assignments will always have content)
if @config[:encoding]
- encoding = "NAMES #{@config[:encoding]}".dup
+ encoding = +"NAMES #{@config[:encoding]}"
encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
encoding << ", "
end
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 204691006c..2e7a78215a 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -114,8 +114,7 @@ module ActiveRecord
class Resolver # :nodoc:
attr_reader :configurations
- # Accepts a hash two layers deep, keys on the first layer represent
- # environments such as "production". Keys must be strings.
+ # Accepts a list of db config objects.
def initialize(configurations)
@configurations = configurations
end
@@ -136,33 +135,14 @@ module ActiveRecord
# Resolver.new(configurations).resolve(:production)
# # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
#
- def resolve(config)
- if config
- resolve_connection config
- elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call
- resolve_symbol_connection env.to_sym
+ def resolve(config_or_env, pool_name = nil)
+ if config_or_env
+ resolve_connection config_or_env, pool_name
else
raise AdapterNotSpecified
end
end
- # Expands each key in @configurations hash into fully resolved hash
- def resolve_all
- config = configurations.dup
-
- if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
- env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url"))
- end
-
- config.merge! env_config if env_config
-
- config.each do |key, value|
- config[key] = resolve(value) if value
- end
-
- config
- end
-
# Returns an instance of ConnectionSpecification for a given adapter.
# Accepts a hash one layer deep that contains all connection information.
#
@@ -176,7 +156,9 @@ module ActiveRecord
# # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
#
def spec(config)
- spec = resolve(config).symbolize_keys
+ pool_name = config if config.is_a?(Symbol)
+
+ spec = resolve(config, pool_name).symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
@@ -211,7 +193,6 @@ module ActiveRecord
end
private
-
# Returns fully resolved connection, accepts hash, string or symbol.
# Always returns a hash.
#
@@ -232,29 +213,42 @@ module ActiveRecord
# Resolver.new({}).resolve_connection("postgresql://localhost/foo")
# # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
#
- def resolve_connection(spec)
- case spec
+ def resolve_connection(config_or_env, pool_name = nil)
+ case config_or_env
when Symbol
- resolve_symbol_connection spec
+ resolve_symbol_connection config_or_env, pool_name
when String
- resolve_url_connection spec
+ resolve_url_connection config_or_env
when Hash
- resolve_hash_connection spec
+ resolve_hash_connection config_or_env
+ else
+ resolve_connection config_or_env
end
end
- # Takes the environment such as +:production+ or +:development+.
+ # Takes the environment such as +:production+ or +:development+ and a
+ # pool name the corresponds to the name given by the connection pool
+ # to the connection. That pool name is merged into the hash with the
+ # name key.
+ #
# This requires that the @configurations was initialized with a key that
# matches.
#
- # Resolver.new("production" => {}).resolve_symbol_connection(:production)
- # # => {}
+ # configurations = #<ActiveRecord::DatabaseConfigurations:0x00007fd9fdace3e0
+ # @configurations=[
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd9fdace250
+ # @env_name="production", @spec_name="primary", @config={"database"=>"my_db"}>
+ # ]>
#
- def resolve_symbol_connection(spec)
- if config = configurations[spec.to_s]
- resolve_connection(config).merge("name" => spec.to_s)
+ # Resolver.new(configurations).resolve_symbol_connection(:production, "primary")
+ # # => { "database" => "my_db" }
+ def resolve_symbol_connection(env_name, pool_name)
+ db_config = configurations.find_db_config(env_name)
+
+ if db_config
+ resolve_connection(db_config.config).merge("name" => pool_name.to_s)
else
- raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}")
+ raise(AdapterNotSpecified, "'#{env_name}' database is not configured. Available: #{configurations.configurations.map(&:env_name).join(", ")}")
end
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 d89eeb7f54..684c7042a7 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -29,6 +29,8 @@ module ActiveRecord
end
def exec_query(sql, name = "SQL", binds = [], prepare: false)
+ materialize_transactions
+
if without_prepared_statement?(binds)
execute_and_free(sql, name) do |result|
ActiveRecord::Result.new(result.fields, result.to_a) if result
@@ -41,6 +43,8 @@ module ActiveRecord
end
def exec_delete(sql, name = nil, binds = [])
+ materialize_transactions
+
if without_prepared_statement?(binds)
execute_and_free(sql, name) { @connection.affected_rows }
else
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
index be038403b8..75564a61d6 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module MySQL
module Quoting # :nodoc:
def quote_column_name(name)
- @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`".freeze
+ @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
end
def quote_table_name(name)
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 c9ea653b77..82ed320617 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -17,7 +17,7 @@ module ActiveRecord
end
def visit_ChangeColumnDefinition(o)
- change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}".dup
+ change_column_sql = +"CHANGE #{quote_column_name(o.name)} #{accept(o.column)}"
add_column_position!(change_column_sql, column_options(o.column))
end
@@ -64,7 +64,7 @@ module ActiveRecord
def index_in_create(table_name, column_name, options)
index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options)
- add_sql_comment!("#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})".dup, comment)
+ add_sql_comment!((+"#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})"), comment)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index 1cf210d85b..e167c01802 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -121,7 +121,7 @@ module ActiveRecord
def data_source_sql(name = nil, type: nil)
scope = quoted_scope(name, type: type)
- sql = "SELECT table_name FROM information_schema.tables".dup
+ sql = +"SELECT table_name FROM information_schema.tables"
sql << " WHERE table_schema = #{scope[:schema]}"
sql << " AND table_name = #{scope[:name]}" if scope[:name]
sql << " AND table_type = #{scope[:type]}" if scope[:type]
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 544d720428..9bdaa00336 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -3,7 +3,7 @@
require "active_record/connection_adapters/abstract_mysql_adapter"
require "active_record/connection_adapters/mysql/database_statements"
-gem "mysql2", ">= 0.4.4", "< 0.6.0"
+gem "mysql2", ">= 0.4.4"
require "mysql2"
module ActiveRecord
@@ -14,7 +14,7 @@ module ActiveRecord
config[:flags] ||= 0
if config[:flags].kind_of? Array
- config[:flags].push "FOUND_ROWS".freeze
+ config[:flags].push "FOUND_ROWS"
else
config[:flags] |= Mysql2::Client::FOUND_ROWS
end
@@ -32,7 +32,7 @@ module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter < AbstractMysqlAdapter
- ADAPTER_NAME = "Mysql2".freeze
+ ADAPTER_NAME = "Mysql2"
include MySQL::DatabaseStatements
@@ -58,6 +58,10 @@ module ActiveRecord
true
end
+ def supports_lazy_transactions?
+ true
+ end
+
# HELPER METHODS ===========================================
def each_hash(result) # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 8db2a645af..6bd6b67165 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -58,6 +58,8 @@ module ActiveRecord
# Queries the database and returns the results in an Array-like object
def query(sql, name = nil) #:nodoc:
+ materialize_transactions
+
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
result_as_array @connection.async_exec(sql)
@@ -70,6 +72,8 @@ module ActiveRecord
# Note: the PG::Result object is manually memory managed; if you don't
# need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
def execute(sql, name = nil)
+ materialize_transactions
+
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@connection.async_exec(sql)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index 26abeea7ed..6fbeaa2b9e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -33,7 +33,13 @@ module ActiveRecord
def cast(value)
if value.is_a?(::String)
- value = @pg_decoder.decode(value)
+ value = begin
+ @pg_decoder.decode(value)
+ rescue TypeError
+ # malformed array string is treated as [], will raise in PG 2.0 gem
+ # this keeps a consistent implementation
+ []
+ end
end
type_cast_array(value, :cast)
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index 231278c184..83c21ba6ea 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/array/extract"
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -16,12 +18,12 @@ module ActiveRecord
def run(records)
nodes = records.reject { |row| @store.key? row["oid"].to_i }
- mapped, nodes = nodes.partition { |row| @store.key? row["typname"] }
- ranges, nodes = nodes.partition { |row| row["typtype"] == "r".freeze }
- enums, nodes = nodes.partition { |row| row["typtype"] == "e".freeze }
- domains, nodes = nodes.partition { |row| row["typtype"] == "d".freeze }
- arrays, nodes = nodes.partition { |row| row["typinput"] == "array_in".freeze }
- composites, nodes = nodes.partition { |row| row["typelem"].to_i != 0 }
+ mapped = nodes.extract! { |row| @store.key? row["typname"] }
+ ranges = nodes.extract! { |row| row["typtype"] == "r" }
+ enums = nodes.extract! { |row| row["typtype"] == "e" }
+ domains = nodes.extract! { |row| row["typtype"] == "d" }
+ arrays = nodes.extract! { |row| row["typinput"] == "array_in" }
+ composites = nodes.extract! { |row| row["typelem"].to_i != 0 }
mapped.each { |row| register_mapped_type(row) }
enums.each { |row| register_enum_type(row) }
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 26b8113b0d..fae3ddbad4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -686,7 +686,7 @@ module ActiveRecord
def change_column_sql(table_name, column_name, type, options = {})
quoted_column_name = quote_column_name(column_name)
sql_type = type_to_sql(type, options)
- sql = "ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup
+ sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}"
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
end
@@ -700,6 +700,11 @@ module ActiveRecord
sql
end
+ def add_column_for_alter(table_name, column_name, type, options = {})
+ return super unless options.key?(:comment)
+ [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }]
+ end
+
def change_column_for_alter(table_name, column_name, type, options = {})
sqls = [change_column_sql(table_name, column_name, type, options)]
sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default)
@@ -708,7 +713,6 @@ module ActiveRecord
sqls
end
-
# Changes the default value of a table column.
def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc:
column = column_for(table_name, column_name)
@@ -753,7 +757,7 @@ module ActiveRecord
scope = quoted_scope(name, type: type)
scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table
- sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup
+ sql = +"SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace"
sql << " WHERE n.nspname = #{scope[:schema]}"
sql << " AND c.relname = #{scope[:name]}" if scope[:name]
sql << " AND c.relkind IN (#{scope[:type]})"
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 ffd3be26b0..cd69d28139 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -17,7 +17,7 @@ module ActiveRecord
end
def sql_type
- super.gsub(/\[\]$/, "".freeze)
+ super.gsub(/\[\]$/, "")
end
def ==(other)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index fdf6f75108..bc6eb11572 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -4,6 +4,14 @@
gem "pg", ">= 0.18", "< 2.0"
require "pg"
+# Use async_exec instead of exec_params on pg versions before 1.1
+class ::PG::Connection
+ unless self.public_method_defined?(:async_exec_params)
+ remove_method :exec_params
+ alias exec_params async_exec
+ end
+end
+
require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
require "active_record/connection_adapters/postgresql/column"
@@ -70,7 +78,7 @@ module ActiveRecord
# In addition, default connection parameters of libpq can be set per environment variables.
# See https://www.postgresql.org/docs/current/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
- ADAPTER_NAME = "PostgreSQL".freeze
+ ADAPTER_NAME = "PostgreSQL"
NATIVE_DATABASE_TYPES = {
primary_key: "bigserial primary key",
@@ -326,6 +334,10 @@ module ActiveRecord
postgresql_version >= 90400
end
+ def supports_lazy_transactions?
+ true
+ end
+
def get_advisory_lock(lock_id) # :nodoc:
unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
@@ -436,7 +448,7 @@ module ActiveRecord
end
end
- def get_oid_type(oid, fmod, column_name, sql_type = "".freeze)
+ def get_oid_type(oid, fmod, column_name, sql_type = "")
if !type_map.key?(oid)
load_additional_types([oid])
end
@@ -525,13 +537,13 @@ module ActiveRecord
# Quoted types
when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
# The default 'now'::date is CURRENT_DATE
- if $1 == "now".freeze && $2 == "date".freeze
+ if $1 == "now" && $2 == "date"
nil
else
- $1.gsub("''".freeze, "'".freeze)
+ $1.gsub("''", "'")
end
# Boolean types
- when "true".freeze, "false".freeze
+ when "true", "false"
default
# Numeric types
when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
@@ -597,15 +609,19 @@ module ActiveRecord
end
def exec_no_cache(sql, name, binds)
+ materialize_transactions
+
type_casted_binds = type_casted_binds(binds)
log(sql, name, binds, type_casted_binds) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
- @connection.async_exec(sql, type_casted_binds)
+ @connection.exec_params(sql, type_casted_binds)
end
end
end
def exec_cache(sql, name, binds)
+ materialize_transactions
+
stmt_key = prepare_statement(sql)
type_casted_binds = type_casted_binds(binds)
@@ -639,7 +655,7 @@ module ActiveRecord
#
# Check here for more details:
# https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
- CACHED_PLAN_HEURISTIC = "cached plan must not change result type".freeze
+ CACHED_PLAN_HEURISTIC = "cached plan must not change result type"
def is_cached_plan_failure?(e)
pgerror = e.cause
code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index abedf01f10..b2dcdb5373 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -13,7 +13,7 @@ module ActiveRecord
end
def quote_column_name(name)
- @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}").freeze
+ @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
end
def quoted_time(value)
@@ -26,19 +26,19 @@ module ActiveRecord
end
def quoted_true
- ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1".freeze : "'t'".freeze
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1" : "'t'"
end
def unquoted_true
- ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t".freeze
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t"
end
def quoted_false
- ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0".freeze : "'f'".freeze
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0" : "'f'"
end
def unquoted_false
- ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f".freeze
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f"
end
private
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index 24e7bc65fa..48277f0ae2 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -21,19 +21,24 @@ module ActiveRecord
WHERE name = #{quote(row['name'])} AND type = 'index'
SQL
- /\sWHERE\s+(?<where>.+)$/i =~ index_sql
+ /\bON\b\s*"?(\w+?)"?\s*\((?<expressions>.+?)\)(?:\s*WHERE\b\s*(?<where>.+))?\z/i =~ index_sql
columns = exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col|
col["name"]
end
- # Add info on sort order for columns (only desc order is explicitly specified, asc is
- # the default)
orders = {}
- if index_sql # index_sql can be null in case of primary key indexes
- index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column|
- orders[order_column] = :desc
- }
+
+ if columns.any?(&:nil?) # index created with an expression
+ columns = expressions
+ else
+ # Add info on sort order for columns (only desc order is explicitly specified,
+ # asc is the default)
+ if index_sql # index_sql can be null in case of primary key indexes
+ index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column|
+ orders[order_column] = :desc
+ }
+ end
end
IndexDefinition.new(
@@ -81,7 +86,7 @@ module ActiveRecord
scope = quoted_scope(name, type: type)
scope[:type] ||= "'table','view'"
- sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'".dup
+ sql = +"SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'"
sql << " AND name = #{scope[:name]}" if scope[:name]
sql << " AND type IN (#{scope[:type]})"
sql
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index bee74dc33d..81882f6cc1 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -56,7 +56,7 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
- ADAPTER_NAME = "SQLite".freeze
+ ADAPTER_NAME = "SQLite"
include SQLite3::Quoting
include SQLite3::SchemaStatements
@@ -125,6 +125,10 @@ module ActiveRecord
true
end
+ def supports_expression_index?
+ sqlite_version >= "3.9.0"
+ end
+
def requires_reloading?
true
end
@@ -186,6 +190,10 @@ module ActiveRecord
true
end
+ def supports_lazy_transactions?
+ true
+ end
+
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity # :nodoc:
@@ -212,6 +220,8 @@ module ActiveRecord
end
def exec_query(sql, name = nil, binds = [], prepare: false)
+ materialize_transactions
+
type_casted_binds = type_casted_binds(binds)
log(sql, name, binds, type_casted_binds) do
@@ -252,6 +262,8 @@ module ActiveRecord
end
def execute(sql, name = nil) #:nodoc:
+ materialize_transactions
+
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@connection.execute(sql)
@@ -409,12 +421,23 @@ module ActiveRecord
def alter_table(table_name, options = {})
altered_table_name = "a#{table_name}"
- caller = lambda { |definition| yield definition if block_given? }
+ foreign_keys = foreign_keys(table_name)
+
+ caller = lambda do |definition|
+ rename = options[:rename] || {}
+ foreign_keys.each do |fk|
+ if column = rename[fk.options[:column]]
+ fk.options[:column] = column
+ end
+ definition.foreign_key(fk.to_table, fk.options)
+ end
+
+ yield definition if block_given?
+ end
transaction do
disable_referential_integrity do
- move_table(table_name, altered_table_name,
- options.merge(temporary: true))
+ move_table(table_name, altered_table_name, options.merge(temporary: true))
move_table(altered_table_name, table_name, &caller)
end
end
@@ -446,6 +469,7 @@ module ActiveRecord
primary_key: column_name == from_primary_key
)
end
+
yield @definition if block_given?
end
copy_table_indexes(from, to, options[:rename] || {})
@@ -463,9 +487,12 @@ module ActiveRecord
name = name[1..-1]
end
- to_column_names = columns(to).map(&:name)
- columns = index.columns.map { |c| rename[c] || c }.select do |column|
- to_column_names.include?(column)
+ columns = index.columns
+ if columns.is_a?(Array)
+ to_column_names = columns(to).map(&:name)
+ columns = columns.map { |c| rename[c] || c }.select do |column|
+ to_column_names.include?(column)
+ end
end
unless columns.empty?
@@ -549,7 +576,7 @@ module ActiveRecord
column
end
else
- basic_structure.to_hash
+ basic_structure.to_a
end
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index ee0e651912..18114f9e1c 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -46,45 +46,18 @@ module ActiveRecord
#
# The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+
# may be returned on an error.
- def establish_connection(config = nil)
+ def establish_connection(config_or_env = nil)
raise "Anonymous class is not allowed." unless name
- config ||= DEFAULT_ENV.call.to_sym
- spec_name = self == Base ? "primary" : name
- self.connection_specification_name = spec_name
+ config_or_env ||= DEFAULT_ENV.call.to_sym
+ pool_name = self == Base ? "primary" : name
+ self.connection_specification_name = pool_name
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations)
- spec = resolver.resolve(config).symbolize_keys
- spec[:name] = spec_name
+ config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys
+ config_hash[:name] = pool_name
- # use the primary config if a config is not passed in and
- # it's a three tier config
- spec = spec[spec_name.to_sym] if spec[spec_name.to_sym]
-
- connection_handler.establish_connection(spec)
- end
-
- class MergeAndResolveDefaultUrlConfig # :nodoc:
- def initialize(raw_configurations)
- @raw_config = raw_configurations.dup
- @env = DEFAULT_ENV.call.to_s
- end
-
- # Returns fully resolved connection hashes.
- # Merges connection information from `ENV['DATABASE_URL']` if available.
- def resolve
- ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all
- end
-
- private
- def config
- @raw_config.dup.tap do |cfg|
- if url = ENV["DATABASE_URL"]
- cfg[@env] ||= {}
- cfg[@env]["url"] ||= url
- end
- end
- end
+ connection_handler.establish_connection(config_hash)
end
# Returns the connection currently associated with the class. This can
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index c983bc0d93..392602bc0f 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -3,11 +3,14 @@
require "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/string/filters"
require "concurrent/map"
+require "set"
module ActiveRecord
module Core
extend ActiveSupport::Concern
+ FILTERED = "[FILTERED]" # :nodoc:
+
included do
##
# :singleton-method:
@@ -26,7 +29,7 @@ module ActiveRecord
##
# Contains the database configuration - as is typically stored in config/database.yml -
- # as a Hash.
+ # as an ActiveRecord::DatabaseConfigurations object.
#
# For example, the following database.yml...
#
@@ -40,22 +43,18 @@ module ActiveRecord
#
# ...would result in ActiveRecord::Base.configurations to look like this:
#
- # {
- # 'development' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/development.sqlite3'
- # },
- # 'production' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/production.sqlite3'
- # }
- # }
+ # #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development",
+ # @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>,
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90 @env_name="production",
+ # @spec_name="primary", @config={"adapter"=>"mysql2", "database"=>"db/production.sqlite3"}>
+ # ]>
def self.configurations=(config)
- @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ @@configurations = ActiveRecord::DatabaseConfigurations.new(config)
end
self.configurations = {}
- # Returns fully resolved configurations hash
+ # Returns fully resolved ActiveRecord::DatabaseConfigurations object
def self.configurations
@@configurations
end
@@ -127,6 +126,8 @@ module ActiveRecord
class_attribute :default_connection_handler, instance_writer: false
+ self.filter_attributes = []
+
def self.connection_handler
ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
end
@@ -138,7 +139,7 @@ module ActiveRecord
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
end
- module ClassMethods # :nodoc:
+ module ClassMethods
def initialize_find_by_cache # :nodoc:
@find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new }
end
@@ -215,7 +216,7 @@ module ActiveRecord
generated_association_methods
end
- def generated_association_methods
+ def generated_association_methods # :nodoc:
@generated_association_methods ||= begin
mod = const_set(:GeneratedAssociationMethods, Module.new)
private_constant :GeneratedAssociationMethods
@@ -225,8 +226,22 @@ module ActiveRecord
end
end
+ # Returns columns which shouldn't be exposed while calling +#inspect+.
+ def filter_attributes
+ if defined?(@filter_attributes)
+ @filter_attributes
+ else
+ superclass.filter_attributes
+ end
+ end
+
+ # Specifies columns which shouldn't be exposed while calling +#inspect+.
+ def filter_attributes=(attributes_names)
+ @filter_attributes = attributes_names.map(&:to_s).to_set
+ end
+
# Returns a string like 'Post(id:integer, title:string, body:text)'
- def inspect
+ def inspect # :nodoc:
if self == Base
super
elsif abstract_class?
@@ -242,7 +257,7 @@ module ActiveRecord
end
# Overwrite the default class equality method to provide support for decorated models.
- def ===(object)
+ def ===(object) # :nodoc:
object.is_a?(self)
end
@@ -496,7 +511,11 @@ module ActiveRecord
inspection = if defined?(@attributes) && @attributes
self.class.attribute_names.collect do |name|
if has_attribute?(name)
- "#{name}: #{attribute_for_inspect(name)}"
+ if filter_attribute?(name)
+ "#{name}: #{ActiveRecord::Core::FILTERED}"
+ else
+ "#{name}: #{attribute_for_inspect(name)}"
+ end
end
end.compact.join(", ")
else
@@ -514,13 +533,16 @@ module ActiveRecord
if defined?(@attributes) && @attributes
column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? }
pp.seplist(column_names, proc { pp.text "," }) do |column_name|
- column_value = read_attribute(column_name)
pp.breakable " "
pp.group(1) do
pp.text column_name
pp.text ":"
pp.breakable
- pp.pp column_value
+ if filter_attribute?(column_name)
+ pp.text ActiveRecord::Core::FILTERED
+ else
+ pp.pp read_attribute(column_name)
+ end
end
end
else
@@ -571,5 +593,9 @@ module ActiveRecord
def custom_inspect_method_defined?
self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
end
+
+ def filter_attribute?(attribute_name)
+ self.class.filter_attributes.include?(attribute_name) && !read_attribute(attribute_name).nil?
+ end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index c7f0077a76..27c1b7a311 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -159,14 +159,11 @@ module ActiveRecord
end
private
-
- def _create_record(*)
+ def _create_record(attribute_names = self.attribute_names)
id = super
each_counter_cached_associations do |association|
- if send(association.reflection.name)
- association.increment_counters
- end
+ association.increment_counters
end
id
@@ -179,9 +176,7 @@ module ActiveRecord
each_counter_cached_associations do |association|
foreign_key = association.reflection.foreign_key.to_sym
unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
- if send(association.reflection.name)
- association.decrement_counters
- end
+ association.decrement_counters
end
end
end
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index ffeed45030..fa1589511e 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -1,63 +1,186 @@
# frozen_string_literal: true
+require "active_record/database_configurations/database_config"
+require "active_record/database_configurations/hash_config"
+require "active_record/database_configurations/url_config"
+
module ActiveRecord
- module DatabaseConfigurations # :nodoc:
- class DatabaseConfig
- attr_reader :env_name, :spec_name, :config
+ # ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig
+ # objects (either a HashConfig or UrlConfig) that are constructed from the
+ # application's database configuration hash or url string.
+ class DatabaseConfigurations
+ attr_reader :configurations
+ delegate :any?, to: :configurations
+
+ def initialize(configurations = {})
+ @configurations = build_configs(configurations)
+ end
- def initialize(env_name, spec_name, config)
- @env_name = env_name
- @spec_name = spec_name
- @config = config
+ # Collects the configs for the environment and optionally the specification
+ # name passed in. To include replica configurations pass `include_replicas: true`.
+ #
+ # If a spec name is provided a single DatabaseConfig object will be
+ # returned, otherwise an array of DatabaseConfig objects will be
+ # returned that corresponds with the environment and type requested.
+ #
+ # Options:
+ #
+ # <tt>env_name:</tt> The environment name. Defaults to nil which will collect
+ # configs for all environments.
+ # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults
+ # to +nil+.
+ # <tt>include_replicas:</tt> Determines whether to include replicas in the
+ # the returned list. Most of the time we're only iterating over the write
+ # connection (i.e. migrations don't need to run for the write and read connection).
+ # Defaults to +false+.
+ def configs_for(env_name: nil, spec_name: nil, include_replicas: false)
+ configs = env_with_configs(env_name)
+
+ unless include_replicas
+ configs = configs.select do |db_config|
+ !db_config.replica?
+ end
+ end
+
+ if spec_name
+ configs.find do |db_config|
+ db_config.spec_name == spec_name
+ end
+ else
+ configs
end
end
- # Selects the config for the specified environment and specification name
+ # Returns the config hash that corresponds with the environment
+ #
+ # If the application has multiple databases `default_hash` will
+ # return the first config hash for the environment.
+ #
+ # { database: "my_db", adapter: "mysql2" }
+ def default_hash(env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s)
+ default = find_db_config(env)
+ default.config if default
+ end
+ alias :[] :default_hash
+
+ # Returns a single DatabaseConfig object based on the requested environment.
#
- # For example if passed :development, and :animals it will select the database
- # under the :development and :animals configuration level
- def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc:
- configs_for(environment, configs).find do |db_config|
- db_config.spec_name == specification_name
+ # If the application has multiple databases `find_db_config` will return
+ # the first DatabaseConfig for the environment.
+ def find_db_config(env)
+ configurations.find do |db_config|
+ db_config.env_name == env.to_s ||
+ (db_config.for_current_env? && db_config.spec_name == env.to_s)
end
end
- # Collects the configs for the environment passed in.
+ # Returns the DatabaseConfigurations object as a Hash.
+ def to_h
+ configs = configurations.reverse.inject({}) do |memo, db_config|
+ memo.merge(db_config.to_legacy_hash)
+ end
+
+ Hash[configs.to_a.reverse]
+ end
+
+ # Checks if the application's configurations are empty.
#
- # If a block is given returns the specification name and configuration
- # otherwise returns an array of DatabaseConfig structs for the environment.
- def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc:
- env_with_configs = db_configs(configs).select do |db_config|
- db_config.env_name == env
+ # Aliased to blank?
+ def empty?
+ configurations.empty?
+ end
+ alias :blank? :empty?
+
+ private
+ def env_with_configs(env = nil)
+ if env
+ configurations.select { |db_config| db_config.env_name == env }
+ else
+ configurations
+ end
end
- if block_given?
- env_with_configs.each do |env_with_config|
- yield env_with_config.spec_name, env_with_config.config
+ def build_configs(configs)
+ return configs.configurations if configs.is_a?(DatabaseConfigurations)
+
+ build_db_config = configs.each_pair.flat_map do |env_name, config|
+ walk_configs(env_name.to_s, "primary", config)
+ end.compact
+
+ if url = ENV["DATABASE_URL"]
+ build_url_config(url, build_db_config)
+ else
+ build_db_config
end
- else
- env_with_configs
end
- end
- # Given an env, spec and config creates DatabaseConfig structs with
- # each attribute set.
- def self.walk_configs(env_name, spec_name, config) # :nodoc:
- if config["database"] || config["url"] || config["adapter"]
- DatabaseConfig.new(env_name, spec_name, config)
- else
- config.each_pair.map do |sub_spec_name, sub_config|
- walk_configs(env_name, sub_spec_name, sub_config)
+ def walk_configs(env_name, spec_name, config)
+ case config
+ when String
+ build_db_config_from_string(env_name, spec_name, config)
+ when Hash
+ build_db_config_from_hash(env_name, spec_name, config.stringify_keys)
end
end
- end
- # Walks all the configs passed in and returns an array
- # of DatabaseConfig structs for each configuration.
- def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc:
- configs.each_pair.flat_map do |env_name, config|
- walk_configs(env_name, "primary", config)
+ def build_db_config_from_string(env_name, spec_name, config)
+ begin
+ url = config
+ uri = URI.parse(url)
+ if uri.try(:scheme)
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url)
+ end
+ rescue URI::InvalidURIError
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
+ end
+ end
+
+ def build_db_config_from_hash(env_name, spec_name, config)
+ if url = config["url"]
+ config_without_url = config.dup
+ config_without_url.delete "url"
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
+ elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String })
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
+ else
+ config.each_pair.map do |sub_spec_name, sub_config|
+ walk_configs(env_name, sub_spec_name, sub_config)
+ end
+ end
+ end
+
+ def build_url_config(url, configs)
+ env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
+
+ if original_config = configs.find(&:for_current_env?)
+ if original_config.url_config?
+ configs
+ else
+ configs.map do |config|
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config)
+ end
+ end
+ else
+ configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)]
+ end
+ end
+
+ def method_missing(method, *args, &blk)
+ if Hash.method_defined?(method)
+ ActiveSupport::Deprecation.warn \
+ "Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations."
+ end
+
+ case method
+ when :each, :first
+ configurations.send(method, *args, &blk)
+ when :fetch
+ configs_for(env_name: args.first)
+ when :values
+ configurations.map(&:config)
+ else
+ super
+ end
end
- end
end
end
diff --git a/activerecord/lib/active_record/database_configurations/database_config.rb b/activerecord/lib/active_record/database_configurations/database_config.rb
new file mode 100644
index 0000000000..adc37cc439
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations/database_config.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class DatabaseConfigurations
+ # ActiveRecord::Base.configurations will return either a HashConfig or
+ # UrlConfig respectively. It will never return a DatabaseConfig object,
+ # as this is the parent class for the types of database configuration objects.
+ class DatabaseConfig # :nodoc:
+ attr_reader :env_name, :spec_name
+
+ def initialize(env_name, spec_name)
+ @env_name = env_name
+ @spec_name = spec_name
+ end
+
+ def replica?
+ raise NotImplementedError
+ end
+
+ def migrations_paths
+ raise NotImplementedError
+ end
+
+ def url_config?
+ false
+ end
+
+ def to_legacy_hash
+ { env_name => config }
+ end
+
+ def for_current_env?
+ env_name == ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/database_configurations/hash_config.rb b/activerecord/lib/active_record/database_configurations/hash_config.rb
new file mode 100644
index 0000000000..c176a62458
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations/hash_config.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class DatabaseConfigurations
+ # A HashConfig object is created for each database configuration entry that
+ # is created from a hash.
+ #
+ # A hash config:
+ #
+ # { "development" => { "database" => "db_name" } }
+ #
+ # Becomes:
+ #
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
+ # @env_name="development", @spec_name="primary", @config={"database"=>"db_name"}>
+ #
+ # Options are:
+ #
+ # <tt>:env_name</tt> - The Rails environment, ie "development"
+ # <tt>:spec_name</tt> - The specification name. In a standard two-tier
+ # database configuration this will default to "primary". In a multiple
+ # database three-tier database configuration this corresponds to the name
+ # used in the second tier, for example "primary_readonly".
+ # <tt>:config</tt> - The config hash. This is the hash that contains the
+ # database adapter, name, and other important information for database
+ # connections.
+ class HashConfig < DatabaseConfig
+ attr_reader :config
+
+ def initialize(env_name, spec_name, config)
+ super(env_name, spec_name)
+ @config = config
+ end
+
+ # Determines whether a database configuration is for a replica / readonly
+ # connection. If the `replica` key is present in the config, `replica?` will
+ # return +true+.
+ def replica?
+ config["replica"]
+ end
+
+ # The migrations paths for a database configuration. If the
+ # `migrations_paths` key is present in the config, `migrations_paths`
+ # will return its value.
+ def migrations_paths
+ config["migrations_paths"]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb
new file mode 100644
index 0000000000..81917fc4c1
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations/url_config.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class DatabaseConfigurations
+ # A UrlConfig object is created for each database configuration
+ # entry that is created from a URL. This can either be a URL string
+ # or a hash with a URL in place of the config hash.
+ #
+ # A URL config:
+ #
+ # postgres://localhost/foo
+ #
+ # Becomes:
+ #
+ # #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fdc3238f340
+ # @env_name="default_env", @spec_name="primary",
+ # @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"},
+ # @url="postgres://localhost/foo">
+ #
+ # Options are:
+ #
+ # <tt>:env_name</tt> - The Rails environment, ie "development"
+ # <tt>:spec_name</tt> - The specification name. In a standard two-tier
+ # database configuration this will default to "primary". In a multiple
+ # database three-tier database configuration this corresponds to the name
+ # used in the second tier, for example "primary_readonly".
+ # <tt>:url</tt> - The database URL.
+ # <tt>:config</tt> - The config hash. This is the hash that contains the
+ # database adapter, name, and other important information for database
+ # connections.
+ class UrlConfig < DatabaseConfig
+ attr_reader :url, :config
+
+ def initialize(env_name, spec_name, url, config = {})
+ super(env_name, spec_name)
+ @config = build_config(config, url)
+ @url = url
+ end
+
+ def url_config? # :nodoc:
+ true
+ end
+
+ # Determines whether a database configuration is for a replica / readonly
+ # connection. If the `replica` key is present in the config, `replica?` will
+ # return +true+.
+ def replica?
+ config["replica"]
+ end
+
+ # The migrations paths for a database configuration. If the
+ # `migrations_paths` key is present in the config, `migrations_paths`
+ # will return its value.
+ def migrations_paths
+ config["migrations_paths"]
+ end
+
+ private
+ def build_config(original_config, url)
+ if /^jdbc:/.match?(url)
+ hash = { "url" => url }
+ else
+ hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
+ end
+
+ if original_config[env_name]
+ original_config[env_name].merge(hash)
+ else
+ original_config.merge(hash)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 7ccb938888..919e96cd7a 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -18,7 +18,7 @@ module ActiveRecord
# Returns a formatted string ready to be logged.
def exec_explain(queries) # :nodoc:
str = queries.map do |sql, binds|
- msg = "EXPLAIN for: #{sql}".dup
+ msg = +"EXPLAIN for: #{sql}"
unless binds.empty?
msg << " "
msg << binds.map { |attr| render_bind(attr) }.inspect
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 6e0f1a0dfb..0d1fdcfb28 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -892,6 +892,7 @@ module ActiveRecord
def fixtures(*fixture_set_names)
if fixture_set_names.first == :all
+ raise StandardError, "No fixture path found. Please set `#{self}.fixture_path`." if fixture_path.blank?
fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"].uniq
fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
else
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 6891c575c7..138fd1cf53 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -55,6 +55,10 @@ module ActiveRecord
if has_attribute?(inheritance_column)
subclass = subclass_from_attributes(attributes)
+ if subclass.nil? && scope_attributes = current_scope&.scope_for_create
+ subclass = subclass_from_attributes(scope_attributes)
+ end
+
if subclass.nil? && base_class?
subclass = subclass_from_attributes(column_defaults)
end
@@ -176,7 +180,7 @@ module ActiveRecord
# 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)
- if type_name.start_with?("::".freeze)
+ if type_name.start_with?("::")
# If the type is prefixed with a scope operator then we assume that
# the type_name is an absolute reference.
ActiveSupport::Dependencies.constantize(type_name)
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 7f096bb532..4a3a31fc95 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -61,7 +61,7 @@ module ActiveRecord
end
private
- def _create_record(attribute_names = self.attribute_names, *)
+ def _create_record(attribute_names = self.attribute_names)
if locking_enabled?
# We always want to persist the locking version, even if we don't detect
# a change from the default, since the database might have no default
@@ -165,7 +165,7 @@ module ActiveRecord
def inherited(subclass)
subclass.class_eval do
is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
- decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
+ decorate_matching_attribute_types(is_lock_column, "_optimistic_locking") do |type|
LockingType.new(type)
end
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 1ae6840921..6b84431343 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -4,6 +4,8 @@ module ActiveRecord
class LogSubscriber < ActiveSupport::LogSubscriber
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
+ class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
+
def self.runtime=(value)
ActiveRecord::RuntimeRegistry.sql_runtime = value
end
@@ -100,21 +102,15 @@ module ActiveRecord
end
def log_query_source
- location = extract_query_source_location(caller_locations)
-
- if location
- source = "#{location.path}:#{location.lineno}"
- source = source.sub("#{::Rails.root}/", "") if defined?(::Rails.root)
+ source = extract_query_source_location(caller)
+ if source
logger.debug(" ↳ #{source}")
end
end
- RAILS_GEM_ROOT = File.expand_path("../../..", __dir__) + "/"
- PATHS_TO_IGNORE = /\A(#{RAILS_GEM_ROOT}|#{RbConfig::CONFIG["rubylibdir"]})/
-
def extract_query_source_location(locations)
- locations.find { |line| line.absolute_path && !line.absolute_path.match?(PATHS_TO_IGNORE) }
+ backtrace_cleaner.clean(locations).first
end
end
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index d68ed3cad9..6e5a610642 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -140,8 +140,8 @@ module ActiveRecord
end
class ConcurrentMigrationError < MigrationError #:nodoc:
- DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze
- RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze
+ DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running."
+ RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock"
def initialize(message = DEFAULT_MESSAGE)
super
@@ -161,7 +161,7 @@ module ActiveRecord
class ProtectedEnvironmentError < ActiveRecordError #:nodoc:
def initialize(env = "production")
- msg = "You are attempting to run a destructive action against your '#{env}' database.\n".dup
+ msg = +"You are attempting to run a destructive action against your '#{env}' database.\n"
msg << "If you are sure you want to continue, run the same command with the environment variable:\n"
msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
super(msg)
@@ -170,7 +170,7 @@ module ActiveRecord
class EnvironmentMismatchError < ActiveRecordError
def initialize(current: nil, stored: nil)
- msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n".dup
+ msg = +"You are attempting to modify a database that was last run in `#{ stored }` environment.\n"
msg << "You are running in `#{ current }` environment. "
msg << "If you are sure you want to continue, first set the environment using:\n\n"
msg << " rails db:environment:set"
@@ -678,15 +678,13 @@ module ActiveRecord
if connection.respond_to? :revert
connection.revert { yield }
else
- recorder = CommandRecorder.new(connection)
+ recorder = command_recorder
@connection = recorder
suppress_messages do
connection.revert { yield }
end
@connection = recorder.delegate
- recorder.commands.each do |cmd, args, block|
- send(cmd, *args, &block)
- end
+ recorder.replay(self)
end
end
end
@@ -891,7 +889,7 @@ module ActiveRecord
source_migrations.each do |migration|
source = File.binread(migration.filename)
inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n"
- magic_comments = "".dup
+ magic_comments = +""
loop do
# If we have a magic comment in the original migration,
# insert our comment after the first newline(end of the magic comment line)
@@ -962,6 +960,10 @@ module ActiveRecord
yield
end
end
+
+ def command_recorder
+ CommandRecorder.new(connection)
+ end
end
# MigrationProxy is used to defer loading of the actual migration classes
@@ -1169,7 +1171,7 @@ module ActiveRecord
def migrations_path=(path)
ActiveSupport::Deprecation.warn \
- "ActiveRecord::Migrator.migrations_paths= is now deprecated and will be removed in Rails 6.0." \
+ "`ActiveRecord::Migrator.migrations_path=` is now deprecated and will be removed in Rails 6.0. " \
"You can set the `migrations_paths` on the `connection` instead through the `database.yml`."
self.migrations_paths = [path]
end
@@ -1299,7 +1301,7 @@ module ActiveRecord
record_version_state_after_migrating(migration.version)
end
rescue => e
- msg = "An error has occurred, ".dup
+ msg = +"An error has occurred, "
msg << "this and " if use_transaction?(migration)
msg << "all later migrations canceled:\n\n#{e}"
raise StandardError, msg, e.backtrace
@@ -1357,7 +1359,7 @@ module ActiveRecord
end
def use_advisory_lock?
- Base.connection.supports_advisory_locks?
+ Base.connection.advisory_locks_enabled?
end
def with_advisory_lock
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 087632b10f..82f5121d94 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -108,11 +108,17 @@ module ActiveRecord
yield delegate.update_table_definition(table_name, self)
end
+ def replay(migration)
+ commands.each do |cmd, args, block|
+ migration.send(cmd, *args, &block)
+ end
+ end
+
private
module StraightReversions # :nodoc:
private
- { transaction: :transaction,
+ {
execute_block: :execute_block,
create_table: :drop_table,
create_join_table: :drop_join_table,
@@ -133,6 +139,17 @@ module ActiveRecord
include StraightReversions
+ def invert_transaction(args)
+ sub_recorder = CommandRecorder.new(delegate)
+ sub_recorder.revert { yield }
+
+ invertions_proc = proc {
+ sub_recorder.replay(self)
+ }
+
+ [:transaction, args, invertions_proc]
+ end
+
def invert_drop_table(args, &block)
if args.size == 1 && block == nil
raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)."
@@ -214,11 +231,24 @@ module ActiveRecord
end
def invert_remove_foreign_key(args)
- from_table, to_table, remove_options = args
- raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash)
+ from_table, options_or_to_table, options_or_nil = args
+
+ to_table = if options_or_to_table.is_a?(Hash)
+ options_or_to_table[:to_table]
+ else
+ options_or_to_table
+ end
+
+ remove_options = if options_or_to_table.is_a?(Hash)
+ options_or_to_table.except(:to_table)
+ else
+ options_or_nil
+ end
+
+ raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil?
reversed_args = [from_table, to_table]
- reversed_args << remove_options if remove_options
+ reversed_args << remove_options if remove_options.present?
[:add_foreign_key, reversed_args]
end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 0edaaa0cf9..8f6fcfcaea 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -16,6 +16,21 @@ module ActiveRecord
V6_0 = Current
class V5_2 < V6_0
+ module CommandRecorder
+ def invert_transaction(args, &block)
+ [:transaction, args, block]
+ end
+ end
+
+ private
+
+ def command_recorder
+ recorder = super
+ class << recorder
+ prepend CommandRecorder
+ end
+ recorder
+ end
end
class V5_1 < V5_2
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 694ff85fa1..9b985e049b 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -375,7 +375,7 @@ module ActiveRecord
# default values when instantiating the Active Record object for this table.
def column_defaults
load_schema
- @column_defaults ||= _default_attributes.to_hash
+ @column_defaults ||= _default_attributes.deep_dup.to_hash
end
def _default_attributes # :nodoc:
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 05963e5546..6eb2bfb452 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -70,17 +70,6 @@ module ActiveRecord
instantiate_instance_of(klass, attributes, column_types, &block)
end
- # Given a class, an attributes hash, +instantiate_instance_of+ returns a
- # new instance of the class. Accepts only keys as strings.
- #
- # This is private, don't call it. :)
- #
- # :nodoc:
- def instantiate_instance_of(klass, attributes, column_types = {}, &block)
- attributes = klass.attributes_builder.build_from_database(attributes, column_types)
- klass.allocate.init_from_db(attributes, &block)
- end
-
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
# The resulting object is returned whether the object was saved successfully to the database or not.
#
@@ -216,6 +205,13 @@ module ActiveRecord
end
private
+ # Given a class, an attributes hash, +instantiate_instance_of+ returns a
+ # new instance of the class. Accepts only keys as strings.
+ def instantiate_instance_of(klass, attributes, column_types = {}, &block)
+ attributes = klass.attributes_builder.build_from_database(attributes, column_types)
+ klass.allocate.init_from_db(attributes, &block)
+ end
+
# Called by +instantiate+ to decide which class to use for a new
# record instance.
#
@@ -718,7 +714,6 @@ module ActiveRecord
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
def _update_record(attribute_names = self.attribute_names)
- attribute_names &= self.class.column_names
attribute_names = attributes_for_update(attribute_names)
if attribute_names.empty?
@@ -737,10 +732,12 @@ module ActiveRecord
# Creates a record with values matching those of the instance attributes
# and returns its id.
def _create_record(attribute_names = self.attribute_names)
- attribute_names &= self.class.column_names
- attributes_values = attributes_with_values_for_create(attribute_names)
+ attribute_names = attributes_for_create(attribute_names)
+
+ new_id = self.class._insert_record(
+ attributes_with_values(attribute_names)
+ )
- new_id = self.class._insert_record(attributes_values)
self.id ||= new_id if self.class.primary_key
@new_record = false
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 009f412234..81ad9ef3a2 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -77,6 +77,10 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
end
+ initializer "active_record.backtrace_cleaner" do
+ ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner }
+ end
+
initializer "active_record.migration_error" do
if config.active_record.delete(:migration_error) == :page_load
config.app_middleware.insert_after ::ActionDispatch::Callbacks,
@@ -84,6 +88,31 @@ module ActiveRecord
end
end
+ initializer "Check for cache versioning support" do
+ config.after_initialize do |app|
+ ActiveSupport.on_load(:active_record) do
+ if app.config.active_record.cache_versioning && Rails.cache
+ unless Rails.cache.class.try(:supports_cache_versioning?)
+ raise <<-end_error
+
+You're using a cache store that doesn't support native cache versioning.
+Your best option is to upgrade to a newer version of #{Rails.cache.class}
+that supports cache versioning (#{Rails.cache.class}.supports_cache_versioning? #=> true).
+
+Next best, switch to a different cache store that does support cache versioning:
+https://guides.rubyonrails.org/caching_with_rails.html#cache-stores.
+
+To keep using the current cache store, you can turn off cache versioning entirely:
+
+ config.active_record.cache_versioning = false
+
+end_error
+ end
+ end
+ end
+ end
+ end
+
initializer "active_record.check_schema_cache_dump" do
if config.active_record.delete(:use_schema_cache_dump)
config.after_initialize do |app|
@@ -108,6 +137,14 @@ module ActiveRecord
end
end
+ initializer "active_record.define_attribute_methods" do |app|
+ config.after_initialize do
+ ActiveSupport.on_load(:active_record) do
+ descendants.each(&:define_attribute_methods) if app.config.eager_load
+ end
+ end
+ end
+
initializer "active_record.warn_on_records_fetched_greater_than" do
if config.active_record.warn_on_records_fetched_greater_than
ActiveSupport.on_load(:active_record) do
@@ -176,9 +213,7 @@ end_warning
end
initializer "active_record.set_executor_hooks" do
- ActiveSupport.on_load(:active_record) do
- ActiveRecord::QueryCache.install_executor_hooks
- end
+ ActiveRecord::QueryCache.install_executor_hooks
end
initializer "active_record.add_watchable_files" do |app|
@@ -231,5 +266,11 @@ MSG
end
end
end
+
+ initializer "active_record.set_filter_attributes" do
+ ActiveSupport.on_load(:active_record) do
+ self.filter_attributes += Rails.application.config.filter_parameters
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 8b7d18fb3d..be5ef350a9 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -26,7 +26,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
desc "Create #{spec_name} database for current environment"
task spec_name => :load_config do
- db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
end
end
@@ -45,7 +45,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
desc "Drop #{spec_name} database for current environment"
task spec_name => [:load_config, :check_protected_environments] do
- db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config)
end
end
@@ -73,8 +73,8 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task migrate: :load_config do
- ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
- ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate
end
db_namespace["_dump"].invoke
@@ -99,7 +99,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
desc "Migrate #{spec_name} database for current environment"
task spec_name => :load_config do
- db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate
end
@@ -274,11 +274,10 @@ db_namespace = namespace :db 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::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
- filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby)
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby)
File.open(filename, "w:utf-8") do |file|
- ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
end
@@ -314,11 +313,10 @@ db_namespace = namespace :db do
namespace :structure do
desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql"
task dump: :load_config do
- ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
- ActiveRecord::Base.establish_connection(config)
- filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql)
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename)
-
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.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
@@ -356,22 +354,30 @@ db_namespace = namespace :db do
begin
should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Schema.verbose = false
- ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"], "test"
+ ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :ruby, filename, "test")
+ end
ensure
if should_reconnect
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.default_hash(ActiveRecord::Tasks::DatabaseTasks.env))
end
end
end
# desc "Recreate the test database from an existent structure.sql file"
task load_structure: %w(db:test:purge) do
- ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"], "test"
+ ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :sql, filename, "test")
+ end
end
# desc "Empty the test database"
task purge: %w(load_config check_protected_environments) do
- ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"]
+ ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config|
+ ActiveRecord::Tasks::DatabaseTasks.purge(db_config.config)
+ end
end
# desc 'Load the test schema'
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 6d2f75a3ae..b2110f727c 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -612,9 +612,21 @@ module ActiveRecord
# returns either +nil+ or the inverse association name that it finds.
def automatic_inverse_of
- if can_find_inverse_of_automatically?(self)
- inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
+ return unless can_find_inverse_of_automatically?(self)
+ inverse_name_candidates =
+ if options[:as]
+ [options[:as]]
+ else
+ active_record_name = active_record.name.demodulize
+ [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)]
+ end
+
+ inverse_name_candidates.map! do |candidate|
+ ActiveSupport::Inflector.underscore(candidate).to_sym
+ end
+
+ inverse_name_candidates.detect do |inverse_name|
begin
reflection = klass._reflect_on_association(inverse_name)
rescue NameError
@@ -623,9 +635,7 @@ module ActiveRecord
reflection = false
end
- if valid_inverse_reflection?(reflection)
- return inverse_name
- end
+ valid_inverse_reflection?(reflection)
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 2d3e1eaa08..d8cff30b88 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -62,7 +62,7 @@ module ActiveRecord
# user = users.new { |user| user.name = 'Oscar' }
# user.name # => Oscar
def new(attributes = nil, &block)
- scoping { klass.new(scope_for_create(attributes), &block) }
+ scoping { klass.new(attributes, &block) }
end
alias build new
@@ -87,11 +87,7 @@ module ActiveRecord
# users.create(name: nil) # validation on name
# # => #<User id: nil, name: nil, ...>
def create(attributes = nil, &block)
- if attributes.is_a?(Array)
- attributes.collect { |attr| create(attr, &block) }
- else
- scoping { klass.create(scope_for_create(attributes), &block) }
- end
+ scoping { klass.create(attributes, &block) }
end
# Similar to #create, but calls
@@ -101,11 +97,7 @@ module ActiveRecord
# Expects arguments in the same format as
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!].
def create!(attributes = nil, &block)
- if attributes.is_a?(Array)
- attributes.collect { |attr| create!(attr, &block) }
- else
- scoping { klass.create!(scope_for_create(attributes), &block) }
- end
+ scoping { klass.create!(attributes, &block) }
end
def first_or_create(attributes = nil, &block) # :nodoc:
@@ -315,10 +307,7 @@ module ActiveRecord
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
- previous, klass.current_scope = klass.current_scope(true), self unless @delegate_to_klass
- yield
- ensure
- klass.current_scope = previous unless @delegate_to_klass
+ @delegate_to_klass ? yield : klass._scoping(self) { yield }
end
def _exec_scope(*args, &block) # :nodoc:
@@ -359,15 +348,20 @@ module ActiveRecord
end
stmt = Arel::UpdateManager.new
-
- stmt.set Arel.sql(@klass.sanitize_sql_for_assignment(updates))
stmt.table(table)
- if has_join_values? || offset_value
+ if updates.is_a?(Hash)
+ stmt.set _substitute_values(updates)
+ else
+ stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
+ end
+
+ if has_join_values?
@klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key))
else
stmt.key = arel_attribute(primary_key)
stmt.take(arel.limit)
+ stmt.offset(arel.offset)
stmt.order(*arel.orders)
stmt.wheres = arel.constraints
end
@@ -375,26 +369,33 @@ module ActiveRecord
@klass.connection.update stmt, "#{@klass} Update All"
end
- def update(attributes) # :nodoc:
- each { |record| record.update(attributes) }
+ def update(id = :all, attributes) # :nodoc:
+ if id == :all
+ each { |record| record.update(attributes) }
+ else
+ klass.update(id, attributes)
+ end
end
def update_counters(counters) # :nodoc:
touch = counters.delete(:touch)
- updates = counters.map do |counter_name, value|
- operator = value < 0 ? "-" : "+"
- quoted_column = connection.quote_column_name(counter_name)
- "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
+ updates = {}
+ counters.each do |counter_name, value|
+ attr = arel_attribute(counter_name)
+ bind = predicate_builder.build_bind_attribute(attr.name, value.abs)
+ expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attr), 0)
+ expr = value < 0 ? expr - bind : expr + bind
+ updates[counter_name] = expr.expr
end
if touch
names = touch if touch != true
touch_updates = klass.touch_attributes_with_time(*names)
- updates << klass.sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty?
+ updates.merge!(touch_updates) unless touch_updates.empty?
end
- update_all updates.join(", ")
+ update_all updates
end
# Touches all records in the current relation without instantiating records first with the updated_at/on attributes
@@ -421,14 +422,12 @@ module ActiveRecord
# Person.where(name: 'David').touch_all
# # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'"
def touch_all(*names, time: nil)
- updates = touch_attributes_with_time(*names, time: time)
-
if klass.locking_enabled?
- quoted_locking_column = connection.quote_column_name(klass.locking_column)
- updates = sanitize_sql_for_assignment(updates) + ", #{quoted_locking_column} = COALESCE(#{quoted_locking_column}, 0) + 1"
+ names << { time: time }
+ update_counters(klass.locking_column => 1, touch: names)
+ else
+ update_all klass.touch_attributes_with_time(*names, time: time)
end
-
- update_all(updates)
end
# Destroys the records by instantiating each
@@ -486,9 +485,13 @@ module ActiveRecord
stmt = Arel::DeleteManager.new
stmt.from(table)
- if has_join_values? || has_limit_or_offset?
+ if has_join_values?
@klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key))
else
+ stmt.key = arel_attribute(primary_key)
+ stmt.take(arel.limit)
+ stmt.offset(arel.offset)
+ stmt.order(*arel.orders)
stmt.wheres = arel.constraints
end
@@ -550,10 +553,8 @@ module ActiveRecord
where_clause.to_h(relation_table_name)
end
- def scope_for_create(attributes = nil)
- scope = where_values_hash.merge!(create_with_value.stringify_keys)
- scope.merge!(attributes) if attributes
- scope
+ def scope_for_create
+ where_values_hash.merge!(create_with_value.stringify_keys)
end
# Returns true if relation needs eager loading.
@@ -636,6 +637,16 @@ module ActiveRecord
end
private
+ def _substitute_values(values)
+ values.map do |name, value|
+ attr = arel_attribute(name)
+ unless Arel.arel_node?(value)
+ type = klass.type_for_attribute(attr.name)
+ value = predicate_builder.build_bind_attribute(attr.name, type.cast(value))
+ end
+ [attr, value]
+ end
+ end
def has_join_values?
joins_values.any? || left_outer_joins_values.any?
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 40fe39fa9d..0fa5ba2e50 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -190,7 +190,7 @@ module ActiveRecord
relation = apply_join_dependency
relation.pluck(*column_names)
else
- enforce_raw_sql_whitelist(column_names)
+ disallow_raw_sql!(column_names)
relation = spawn
relation.select_values = column_names.map { |cn|
@klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 488f71cdde..8f657840f5 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -17,7 +17,7 @@ module ActiveRecord
delegate = Class.new(klass) {
include ClassSpecificRelation
}
- mangled_name = klass.name.gsub("::".freeze, "_".freeze)
+ mangled_name = klass.name.gsub("::", "_")
const_set mangled_name, delegate
private_constant mangled_name
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index c5562c1ff0..6f420fe6bb 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -338,14 +338,14 @@ module ActiveRecord
name = @klass.name
if ids.nil?
- error = "Couldn't find #{name}".dup
+ error = +"Couldn't find #{name}"
error << " with#{conditions}" if conditions
raise RecordNotFound.new(error, name, key)
elsif Array(ids).size == 1
error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}"
raise RecordNotFound.new(error, name, key, ids)
else
- error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup
+ error = +"Couldn't find all #{name.pluralize} with '#{key}': "
error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})."
error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids
raise RecordNotFound.new(error, name, key, ids)
@@ -398,7 +398,7 @@ module ActiveRecord
def limited_ids_for(relation)
values = @klass.connection.columns_for_distinct(
- connection.column_name_from_arel_node(arel_attribute(primary_key)),
+ connection.visitor.compile(arel_attribute(primary_key)),
relation.order_values
)
@@ -416,7 +416,7 @@ module ActiveRecord
raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
expects_array = ids.first.kind_of?(Array)
- return ids.first if expects_array && ids.first.empty?
+ return [] if expects_array && ids.first.empty?
ids = ids.flatten.compact.uniq
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 10e4779ca4..4de7465128 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -152,10 +152,10 @@ module ActiveRecord
def merge_multi_values
if other.reordering_value
# override any order specified in the original relation
- relation.reorder! other.order_values
+ relation.reorder!(*other.order_values)
elsif other.order_values.any?
# merge in order_values from relation
- relation.order! other.order_values
+ relation.order!(*other.order_values)
end
extensions = other.extensions - relation.extensions
@@ -171,9 +171,7 @@ module ActiveRecord
end
def merge_clauses
- if relation.from_clause.empty? && !other.from_clause.empty?
- relation.from_clause = other.from_clause
- end
+ relation.from_clause = other.from_clause if replace_from_clause?
where_clause = relation.where_clause.merge(other.where_clause)
relation.where_clause = where_clause unless where_clause.empty?
@@ -181,6 +179,11 @@ module ActiveRecord
having_clause = relation.having_clause.merge(other.having_clause)
relation.having_clause = having_clause unless having_clause.empty?
end
+
+ def replace_from_clause?
+ relation.from_clause.empty? && !other.from_clause.empty? &&
+ relation.klass.base_class == other.klass.base_class
+ end
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index f734cd0ad8..b59ff912fe 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -27,7 +27,7 @@ module ActiveRecord
key
else
key = key.to_s
- key.split(".".freeze).first if key.include?(".".freeze)
+ key.split(".").first if key.include?(".")
end
end.compact
end
@@ -115,11 +115,11 @@ module ActiveRecord
def convert_dot_notation_to_hash(attributes)
dot_notation = attributes.select do |k, v|
- k.include?(".".freeze) && !v.is_a?(Hash)
+ k.include?(".") && !v.is_a?(Hash)
end
dot_notation.each_key do |key|
- table_name, column_name = key.split(".".freeze)
+ table_name, column_name = key.split(".")
value = attributes.delete(key)
attributes[table_name] ||= {}
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
index 64bf83e3c1..fadb3c420d 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/array/extract"
+
module ActiveRecord
class PredicateBuilder
class ArrayHandler # :nodoc:
@@ -11,18 +13,19 @@ module ActiveRecord
return attribute.in([]) if value.empty?
values = value.map { |x| x.is_a?(Base) ? x.id : x }
- nils, values = values.partition(&:nil?)
- ranges, values = values.partition { |v| v.is_a?(Range) }
+ nils = values.extract!(&:nil?)
+ ranges = values.extract! { |v| v.is_a?(Range) }
values_predicate =
case values.length
when 0 then NullPredicate
when 1 then predicate_builder.build(attribute, values.first)
else
- bind_values = values.map do |v|
- predicate_builder.build_bind_attribute(attribute.name, v)
- end
- attribute.in(bind_values)
+ values.map! do |v|
+ bind = predicate_builder.build_bind_attribute(attribute.name, v)
+ bind if bind.value.boundable?
+ end.compact!
+ values.empty? ? NullPredicate : attribute.in(values)
end
unless nils.empty?
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 74103d6cdd..b12497fd0a 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -953,7 +953,7 @@ module ActiveRecord
arel.having(having_clause.ast) unless having_clause.empty?
if limit_value
limit_attribute = ActiveModel::Attribute.with_cast_value(
- "LIMIT".freeze,
+ "LIMIT",
connection.sanitize_limit(limit_value),
Type.default_value,
)
@@ -961,7 +961,7 @@ module ActiveRecord
end
if offset_value
offset_attribute = ActiveModel::Attribute.with_cast_value(
- "OFFSET".freeze,
+ "OFFSET",
offset_value.to_i,
Type.default_value,
)
@@ -1147,9 +1147,9 @@ module ActiveRecord
end
order_args.flatten!
- @klass.enforce_raw_sql_whitelist(
+ @klass.disallow_raw_sql!(
order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
- whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
+ permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER
)
validate_order_args(order_args)
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 7f1c2fd7eb..da6d10b6ec 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -21,7 +21,7 @@ module ActiveRecord
# ]
#
# # Get an array of hashes representing the result (column => value):
- # result.to_hash
+ # result.to_a
# # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
# {"id" => 2, "title" => "title_2", "body" => "body_2"},
# ...
@@ -65,9 +65,12 @@ module ActiveRecord
end
end
- # Returns an array of hashes representing each row record.
def to_hash
- hash_rows
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `ActiveRecord::Result#to_hash` has been renamed to `to_a`.
+ `to_hash` is deprecated and will be removed in Rails 6.1.
+ MSG
+ to_a
end
alias :map! :map
@@ -83,6 +86,8 @@ module ActiveRecord
hash_rows
end
+ alias :to_a :to_ary
+
def [](idx)
hash_rows[idx]
end
@@ -140,6 +145,8 @@ module ActiveRecord
# We freeze the strings to prevent them getting duped when
# used as keys in ActiveRecord::Base's @attributes hash
columns = @columns.map(&:-@)
+ length = columns.length
+
@rows.map { |row|
# In the past we used Hash[columns.zip(row)]
# though elegant, the verbose way is much more efficient
@@ -148,8 +155,6 @@ module ActiveRecord
hash = {}
index = 0
- length = columns.length
-
while index < length
hash[columns[index]] = row[index]
index += 1
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index c6c268855e..3485d9e557 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -61,8 +61,8 @@ module ActiveRecord
# # => "id ASC"
def sanitize_sql_for_order(condition)
if condition.is_a?(Array) && condition.first.to_s.include?("?")
- enforce_raw_sql_whitelist([condition.first],
- whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
+ disallow_raw_sql!([condition.first],
+ permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER
)
# Ensure we aren't dealing with a subclass of String that might
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 01ac56570a..9eba1254a4 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -12,14 +12,6 @@ module ActiveRecord
end
module ClassMethods # :nodoc:
- def current_scope(skip_inherited_scope = false)
- ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
- end
-
- def current_scope=(scope)
- ScopeRegistry.set_value_for(:current_scope, self, scope)
- end
-
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
def scope_attributes
@@ -30,6 +22,15 @@ module ActiveRecord
def scope_attributes?
current_scope
end
+
+ private
+ def current_scope(skip_inherited_scope = false)
+ ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
+ end
+
+ def current_scope=(scope)
+ ScopeRegistry.set_value_for(:current_scope, self, scope)
+ end
end
def populate_with_current_scope_attributes # :nodoc:
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 8c612df27a..6caf9b3251 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -31,7 +31,14 @@ module ActiveRecord
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
def unscoped
- block_given? ? relation.scoping { yield } : relation
+ block_given? ? _scoping(relation) { yield } : relation
+ end
+
+ def _scoping(relation) # :nodoc:
+ previous, self.current_scope = current_scope(true), relation
+ yield
+ ensure
+ self.current_scope = previous
end
# Are there attributes associated with this scope?
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index a784001587..573d97b819 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -24,13 +24,13 @@ module ActiveRecord
# You can define a scope that applies to all finders using
# {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope].
def all
- current_scope = self.current_scope
+ scope = current_scope
- if current_scope
- if self == current_scope.klass
- current_scope.clone
+ if scope
+ if self == scope.klass
+ scope.clone
else
- relation.merge!(current_scope)
+ relation.merge!(scope)
end
else
default_scoped
@@ -38,9 +38,7 @@ module ActiveRecord
end
def scope_for_association(scope = relation) # :nodoc:
- current_scope = self.current_scope
-
- if current_scope && current_scope.empty_scope?
+ if current_scope&.empty_scope?
scope
else
default_scoped(scope)
@@ -182,15 +180,13 @@ module ActiveRecord
if body.respond_to?(:to_proc)
singleton_class.send(:define_method, name) do |*args|
- scope = all
- scope = scope._exec_scope(*args, &body)
+ scope = all._exec_scope(*args, &body)
scope = scope.extending(extension) if extension
scope
end
else
singleton_class.send(:define_method, name) do |*args|
- scope = all
- scope = scope.scoping { body.call(*args) || scope }
+ scope = body.call(*args) || all
scope = scope.extending(extension) if extension
scope
end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index b41d3504fd..1b1736dcab 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -44,7 +44,7 @@ module ActiveRecord
def initialize(values)
@values = values
@indexes = values.each_with_index.find_all { |thing, i|
- Arel::Nodes::BindParam === thing
+ Substitute === thing
}.map(&:last)
end
@@ -56,6 +56,28 @@ module ActiveRecord
end
end
+ class PartialQueryCollector
+ def initialize
+ @parts = []
+ @binds = []
+ end
+
+ def <<(str)
+ @parts << str
+ self
+ end
+
+ def add_bind(obj)
+ @binds << obj
+ @parts << Substitute.new
+ self
+ end
+
+ def value
+ [@parts, @binds]
+ end
+ end
+
def self.query(sql)
Query.new(sql)
end
@@ -64,6 +86,10 @@ module ActiveRecord
PartialQuery.new(values)
end
+ def self.partial_query_collector
+ PartialQueryCollector.new
+ end
+
class Params # :nodoc:
def bind; Substitute.new; end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index fd36c0abd2..5e29085aff 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_record/database_configurations"
+
module ActiveRecord
module Tasks # :nodoc:
class DatabaseAlreadyExists < StandardError; end # :nodoc:
@@ -101,16 +103,21 @@ module ActiveRecord
@env ||= Rails.env
end
+ def spec
+ @spec ||= "primary"
+ end
+
def seed_loader
@seed_loader ||= Rails.application
end
def current_config(options = {})
options.reverse_merge! env: env
+ options[:spec] ||= "primary"
if options.has_key?(:config)
@current_config = options[:config]
else
- @current_config ||= ActiveRecord::Base.configurations[options[:env]]
+ @current_config ||= ActiveRecord::Base.configurations.configs_for(env_name: options[:env], spec_name: options[:spec]).config
end
end
@@ -122,7 +129,7 @@ module ActiveRecord
$stderr.puts "Database '#{configuration['database']}' already exists" if verbose?
rescue Exception => error
$stderr.puts error
- $stderr.puts "Couldn't create database for #{configuration.inspect}"
+ $stderr.puts "Couldn't create '#{configuration['database']}' database. Please check your configuration."
raise
end
@@ -135,8 +142,8 @@ module ActiveRecord
end
def for_each
- databases = Rails.application.config.load_database_yaml
- database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases)
+ databases = Rails.application.config.database_configuration
+ database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env)
# if this is a single database application we don't want tasks for each primary database
return if database_configs.count == 1
@@ -180,9 +187,11 @@ module ActiveRecord
scope = ENV["SCOPE"]
verbose_was, Migration.verbose = Migration.verbose, verbose?
+
Base.connection.migration_context.migrate(target_version) do |migration|
scope.blank? || scope == migration.scope
end
+
ActiveRecord::Base.clear_cache!
ensure
Migration.verbose = verbose_was
@@ -198,8 +207,8 @@ module ActiveRecord
ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty?
end
- def charset_current(environment = env)
- charset ActiveRecord::Base.configurations[environment]
+ def charset_current(environment = env, specification_name = spec)
+ charset ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config
end
def charset(*arguments)
@@ -207,8 +216,8 @@ module ActiveRecord
class_for_adapter(configuration["adapter"]).new(*arguments).charset
end
- def collation_current(environment = env)
- collation ActiveRecord::Base.configurations[environment]
+ def collation_current(environment = env, specification_name = spec)
+ collation ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config
end
def collation(*arguments)
@@ -298,7 +307,7 @@ module ActiveRecord
def check_schema_file(filename)
unless File.exist?(filename)
- message = %{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.}.dup
+ message = +%{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.}
message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails.root)
Kernel.abort message
end
@@ -342,14 +351,15 @@ module ActiveRecord
environments << "test" if environment == "development"
environments.each do |env|
- ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration|
- yield configuration, spec_name, env
+ ActiveRecord::Base.configurations.configs_for(env_name: env).each do |db_config|
+ yield db_config.config, db_config.spec_name, env
end
end
end
def each_local_configuration
- ActiveRecord::Base.configurations.each_value do |configuration|
+ ActiveRecord::Base.configurations.configs_for.each do |db_config|
+ configuration = db_config.config
next unless configuration["database"]
if local_database?(configuration)
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index eddc6fa223..1c1b29b5e1 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -104,7 +104,7 @@ module ActiveRecord
end
def run_cmd_error(cmd, args, action)
- msg = "failed to execute: `#{cmd}`\n".dup
+ msg = +"failed to execute: `#{cmd}`\n"
msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
msg
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 533e6953a4..8acb11f75f 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -6,8 +6,8 @@ module ActiveRecord
module Tasks # :nodoc:
class PostgreSQLDatabaseTasks # :nodoc:
DEFAULT_ENCODING = ENV["CHARSET"] || "utf8"
- ON_ERROR_STOP_1 = "ON_ERROR_STOP=1".freeze
- SQL_COMMENT_BEGIN = "--".freeze
+ ON_ERROR_STOP_1 = "ON_ERROR_STOP=1"
+ SQL_COMMENT_BEGIN = "--"
delegate :connection, :establish_connection, :clear_active_connections!,
to: ActiveRecord::Base
@@ -82,7 +82,7 @@ module ActiveRecord
def structure_load(filename, extra_flags)
set_psql_env
- args = ["-v", ON_ERROR_STOP_1, "-q", "-f", filename]
+ args = ["-v", ON_ERROR_STOP_1, "-q", "-X", "-f", filename]
args.concat(Array(extra_flags)) if extra_flags
args << configuration["database"]
run_cmd("psql", args, "loading")
@@ -115,7 +115,7 @@ module ActiveRecord
end
def run_cmd_error(cmd, args, action)
- msg = "failed to execute:\n".dup
+ msg = +"failed to execute:\n"
msg << "#{cmd} #{args.join(' ')}\n\n"
msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
msg
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index d0bad05176..a82cea80ca 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -67,7 +67,7 @@ module ActiveRecord
end
def run_cmd_error(cmd, args)
- msg = "failed to execute:\n".dup
+ msg = +"failed to execute:\n"
msg << "#{cmd} #{args.join(' ')}\n\n"
msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
msg
diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb
index 5b4efe22c9..999830ba79 100644
--- a/activerecord/lib/active_record/test_databases.rb
+++ b/activerecord/lib/active_record/test_databases.rb
@@ -5,31 +5,32 @@ require "active_support/testing/parallelization"
module ActiveRecord
module TestDatabases # :nodoc:
ActiveSupport::Testing::Parallelization.after_fork_hook do |i|
- create_and_load_schema(i, spec_name: Rails.env)
+ create_and_load_schema(i, env_name: Rails.env)
end
- ActiveSupport::Testing::Parallelization.run_cleanup_hook do |_|
- drop(spec_name: Rails.env)
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do
+ drop(env_name: Rails.env)
end
- def self.create_and_load_schema(i, spec_name:)
+ def self.create_and_load_schema(i, env_name:)
old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
- connection_spec = ActiveRecord::Base.configurations[spec_name]
-
- connection_spec["database"] += "-#{i}"
- ActiveRecord::Tasks::DatabaseTasks.create(connection_spec)
- ActiveRecord::Tasks::DatabaseTasks.load_schema(connection_spec)
+ ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config|
+ db_config.config["database"] += "-#{i}"
+ ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, ActiveRecord::Base.schema_format, nil, env_name, db_config.spec_name)
+ end
ensure
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
+ ActiveRecord::Base.establish_connection(Rails.env.to_sym)
ENV["VERBOSE"] = old
end
- def self.drop(spec_name:)
+ def self.drop(env_name:)
old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
- connection_spec = ActiveRecord::Base.configurations[spec_name]
- ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec)
+ ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config|
+ ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config)
+ end
ensure
ENV["VERBOSE"] = old
end
diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb
index 7d04e1cac6..dab785738e 100644
--- a/activerecord/lib/arel.rb
+++ b/activerecord/lib/arel.rb
@@ -35,6 +35,11 @@ module Arel # :nodoc: all
def self.star
sql "*"
end
+
+ def self.arel_node?(value)
+ value.is_a?(Arel::Node) || value.is_a?(Arel::Attribute) || value.is_a?(Arel::Nodes::SqlLiteral)
+ end
+
## Convenience Alias
Node = Arel::Nodes::Node
end
diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb
index d040d8598d..0533544993 100644
--- a/activerecord/lib/arel/collectors/composite.rb
+++ b/activerecord/lib/arel/collectors/composite.rb
@@ -24,8 +24,7 @@ module Arel # :nodoc: all
[left.value, right.value]
end
- protected
-
+ private
attr_reader :left, :right
end
end
diff --git a/activerecord/lib/arel/collectors/plain_string.rb b/activerecord/lib/arel/collectors/plain_string.rb
index 687d7fbf2f..c0e9fff399 100644
--- a/activerecord/lib/arel/collectors/plain_string.rb
+++ b/activerecord/lib/arel/collectors/plain_string.rb
@@ -4,7 +4,7 @@ module Arel # :nodoc: all
module Collectors
class PlainString
def initialize
- @str = "".dup
+ @str = +""
end
def value
diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb
index c293a89a74..54e1e562c2 100644
--- a/activerecord/lib/arel/collectors/sql_string.rb
+++ b/activerecord/lib/arel/collectors/sql_string.rb
@@ -15,10 +15,6 @@ module Arel # :nodoc: all
@bind_index += 1
self
end
-
- def compile(bvs)
- value
- end
end
end
end
diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb
index 3f40eec8a8..4b894bc4b1 100644
--- a/activerecord/lib/arel/collectors/substitute_binds.rb
+++ b/activerecord/lib/arel/collectors/substitute_binds.rb
@@ -21,8 +21,7 @@ module Arel # :nodoc: all
delegate.value
end
- protected
-
+ private
attr_reader :quoter, :delegate
end
end
diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb
index 2def581009..fdba937d64 100644
--- a/activerecord/lib/arel/delete_manager.rb
+++ b/activerecord/lib/arel/delete_manager.rb
@@ -2,6 +2,8 @@
module Arel # :nodoc: all
class DeleteManager < Arel::TreeManager
+ include TreeManager::StatementMethods
+
def initialize
super
@ast = Nodes::DeleteStatement.new
@@ -12,14 +14,5 @@ module Arel # :nodoc: all
@ast.relation = relation
self
end
-
- def take(limit)
- @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
- self
- end
-
- def wheres=(list)
- @ast.wheres = list
- end
end
end
diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb
index b828bc274e..83ec23e403 100644
--- a/activerecord/lib/arel/factory_methods.rb
+++ b/activerecord/lib/arel/factory_methods.rb
@@ -41,5 +41,9 @@ module Arel # :nodoc: all
def lower(column)
Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)]
end
+
+ def coalesce(*exprs)
+ Nodes::NamedFunction.new "COALESCE", exprs
+ end
end
end
diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb
index 53c5563d93..91e9b2b70f 100644
--- a/activerecord/lib/arel/nodes/bind_param.rb
+++ b/activerecord/lib/arel/nodes/bind_param.rb
@@ -3,7 +3,7 @@
module Arel # :nodoc: all
module Nodes
class BindParam < Node
- attr_accessor :value
+ attr_reader :value
def initialize(value)
@value = value
diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb
index eaac05e2f6..a419975335 100644
--- a/activerecord/lib/arel/nodes/delete_statement.rb
+++ b/activerecord/lib/arel/nodes/delete_statement.rb
@@ -3,8 +3,7 @@
module Arel # :nodoc: all
module Nodes
class DeleteStatement < Arel::Nodes::Node
- attr_accessor :left, :right
- attr_accessor :limit
+ attr_accessor :left, :right, :orders, :limit, :offset, :key
alias :relation :left
alias :relation= :left=
@@ -15,6 +14,10 @@ module Arel # :nodoc: all
super()
@left = relation
@right = wheres
+ @orders = []
+ @limit = nil
+ @offset = nil
+ @key = nil
end
def initialize_copy(other)
@@ -24,13 +27,17 @@ module Arel # :nodoc: all
end
def hash
- [self.class, @left, @right].hash
+ [self.class, @left, @right, @orders, @limit, @offset, @key].hash
end
def eql?(other)
self.class == other.class &&
self.left == other.left &&
- self.right == other.right
+ self.right == other.right &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.offset == other.offset &&
+ self.key == other.key
end
alias :== :eql?
end
diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb
index 2b9b1e9828..8086102bde 100644
--- a/activerecord/lib/arel/nodes/node.rb
+++ b/activerecord/lib/arel/nodes/node.rb
@@ -8,16 +8,6 @@ module Arel # :nodoc: all
include Arel::FactoryMethods
include Enumerable
- if $DEBUG
- def _caller
- @caller
- end
-
- def initialize
- @caller = caller.dup
- end
- end
-
###
# Factory method to create a Nodes::Not node that has the recipient of
# the caller as a child.
diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb
index 2defe61974..73461ff683 100644
--- a/activerecord/lib/arel/nodes/select_core.rb
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -3,13 +3,12 @@
module Arel # :nodoc: all
module Nodes
class SelectCore < Arel::Nodes::Node
- attr_accessor :top, :projections, :wheres, :groups, :windows
+ attr_accessor :projections, :wheres, :groups, :windows
attr_accessor :havings, :source, :set_quantifier
def initialize
super()
@source = JoinSource.new nil
- @top = nil
# https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
@set_quantifier = nil
@@ -43,7 +42,7 @@ module Arel # :nodoc: all
def hash
[
- @source, @top, @set_quantifier, @projections,
+ @source, @set_quantifier, @projections,
@wheres, @groups, @havings, @windows
].hash
end
@@ -51,7 +50,6 @@ module Arel # :nodoc: all
def eql?(other)
self.class == other.class &&
self.source == other.source &&
- self.top == other.top &&
self.set_quantifier == other.set_quantifier &&
self.projections == other.projections &&
self.wheres == other.wheres &&
diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb
index a3c0045897..00639304e4 100644
--- a/activerecord/lib/arel/nodes/unary.rb
+++ b/activerecord/lib/arel/nodes/unary.rb
@@ -37,7 +37,6 @@ module Arel # :nodoc: all
On
Ordering
RollUp
- Top
}.each do |name|
const_set(name, Class.new(Unary))
end
diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb
index 5184b1180f..cfaa19e392 100644
--- a/activerecord/lib/arel/nodes/update_statement.rb
+++ b/activerecord/lib/arel/nodes/update_statement.rb
@@ -3,8 +3,7 @@
module Arel # :nodoc: all
module Nodes
class UpdateStatement < Arel::Nodes::Node
- attr_accessor :relation, :wheres, :values, :orders, :limit
- attr_accessor :key
+ attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key
def initialize
@relation = nil
@@ -12,6 +11,7 @@ module Arel # :nodoc: all
@values = []
@orders = []
@limit = nil
+ @offset = nil
@key = nil
end
@@ -22,7 +22,7 @@ module Arel # :nodoc: all
end
def hash
- [@relation, @wheres, @values, @orders, @limit, @key].hash
+ [@relation, @wheres, @values, @orders, @limit, @offset, @key].hash
end
def eql?(other)
@@ -32,6 +32,7 @@ module Arel # :nodoc: all
self.values == other.values &&
self.orders == other.orders &&
self.limit == other.limit &&
+ self.offset == other.offset &&
self.key == other.key
end
alias :== :eql?
diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb
index 22a04b00c6..a2b2838a3d 100644
--- a/activerecord/lib/arel/select_manager.rb
+++ b/activerecord/lib/arel/select_manager.rb
@@ -222,10 +222,8 @@ module Arel # :nodoc: all
def take(limit)
if limit
@ast.limit = Nodes::Limit.new(limit)
- @ctx.top = Nodes::Top.new(limit)
else
@ast.limit = nil
- @ctx.top = nil
end
self
end
@@ -252,9 +250,9 @@ module Arel # :nodoc: all
end
private
- def collapse(exprs, existing = nil)
- exprs = exprs.unshift(existing.expr) if existing
- exprs = exprs.compact.map { |expr|
+ def collapse(exprs)
+ exprs = exprs.compact
+ exprs.map! { |expr|
if String === expr
# FIXME: Don't do this automatically
Arel.sql(expr)
diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb
index 686fcdf962..c40c68715a 100644
--- a/activerecord/lib/arel/table.rb
+++ b/activerecord/lib/arel/table.rb
@@ -104,8 +104,7 @@ module Arel # :nodoc: all
!type_caster.nil?
end
- protected
-
+ private
attr_reader :type_caster
end
end
diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb
index ed47b09a37..0476399618 100644
--- a/activerecord/lib/arel/tree_manager.rb
+++ b/activerecord/lib/arel/tree_manager.rb
@@ -4,6 +4,40 @@ module Arel # :nodoc: all
class TreeManager
include Arel::FactoryMethods
+ module StatementMethods
+ def take(limit)
+ @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
+ self
+ end
+
+ def offset(offset)
+ @ast.offset = Nodes::Offset.new(Nodes.build_quoted(offset)) if offset
+ self
+ end
+
+ def order(*expr)
+ @ast.orders = expr
+ self
+ end
+
+ def key=(key)
+ @ast.key = Nodes.build_quoted(key)
+ end
+
+ def key
+ @ast.key
+ end
+
+ def wheres=(exprs)
+ @ast.wheres = exprs
+ end
+
+ def where(expr)
+ @ast.wheres << expr
+ self
+ end
+ end
+
attr_reader :ast
def initialize
diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb
index fe444343ba..a809dbb307 100644
--- a/activerecord/lib/arel/update_manager.rb
+++ b/activerecord/lib/arel/update_manager.rb
@@ -2,30 +2,14 @@
module Arel # :nodoc: all
class UpdateManager < Arel::TreeManager
+ include TreeManager::StatementMethods
+
def initialize
super
@ast = Nodes::UpdateStatement.new
@ctx = @ast
end
- def take(limit)
- @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
- self
- end
-
- def key=(key)
- @ast.key = Nodes.build_quoted(key)
- end
-
- def key
- @ast.key
- end
-
- def order(*expr)
- @ast.orders = expr
- self
- end
-
###
# UPDATE +table+
def table(table)
@@ -33,15 +17,6 @@ module Arel # :nodoc: all
self
end
- def wheres=(exprs)
- @ast.wheres = exprs
- end
-
- def where(expr)
- @ast.wheres << expr
- self
- end
-
def set(values)
if String === values
@ast.values = [values]
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
index bcf8f8f980..8f65d303ac 100644
--- a/activerecord/lib/arel/visitors/depth_first.rb
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -34,7 +34,6 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Ordering :unary
alias :visit_Arel_Nodes_Ascending :unary
alias :visit_Arel_Nodes_Descending :unary
- alias :visit_Arel_Nodes_Top :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
def function(o)
@@ -136,12 +135,10 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_True :terminal
alias :visit_Arel_Nodes_False :terminal
alias :visit_BigDecimal :terminal
- alias :visit_Bignum :terminal
alias :visit_Class :terminal
alias :visit_Date :terminal
alias :visit_DateTime :terminal
alias :visit_FalseClass :terminal
- alias :visit_Fixnum :terminal
alias :visit_Float :terminal
alias :visit_Integer :terminal
alias :visit_NilClass :terminal
diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb
index d352b81914..9054f0159b 100644
--- a/activerecord/lib/arel/visitors/dot.rb
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -81,7 +81,6 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Not :unary
alias :visit_Arel_Nodes_Offset :unary
alias :visit_Arel_Nodes_On :unary
- alias :visit_Arel_Nodes_Top :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
alias :visit_Arel_Nodes_Preceding :unary
alias :visit_Arel_Nodes_Following :unary
@@ -212,7 +211,6 @@ module Arel # :nodoc: all
alias :visit_TrueClass :visit_String
alias :visit_FalseClass :visit_String
alias :visit_Integer :visit_String
- alias :visit_Fixnum :visit_String
alias :visit_BigDecimal :visit_String
alias :visit_Float :visit_String
alias :visit_Symbol :visit_String
diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb
index 9aedc51d15..d564e19089 100644
--- a/activerecord/lib/arel/visitors/mssql.rb
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -12,13 +12,6 @@ module Arel # :nodoc: all
private
- # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate
- # "select top 10 distinct first_name from users", which is invalid query! it should be
- # "select distinct top 10 first_name from users"
- def visit_Arel_Nodes_Top(o)
- ""
- end
-
def visit_Arel_Visitors_MSSQL_RowNumber(o, collector)
collector << "ROW_NUMBER() OVER (ORDER BY "
inject_join(o.children, collector, ", ") << ") as _row_num"
diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb
index 37bfb661f0..eb8a449079 100644
--- a/activerecord/lib/arel/visitors/mysql.rb
+++ b/activerecord/lib/arel/visitors/mysql.rb
@@ -37,6 +37,10 @@ module Arel # :nodoc: all
visit o.expr, collector
end
+ def visit_Arel_Nodes_UnqualifiedColumn(o, collector)
+ visit o.expr, collector
+ end
+
###
# :'(
# http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
@@ -52,14 +56,31 @@ module Arel # :nodoc: all
super
end
- def visit_Arel_Nodes_UpdateStatement(o, collector)
- collector << "UPDATE "
- collector = visit o.relation, collector
+ def visit_Arel_Nodes_Concat(o, collector)
+ collector << " CONCAT("
+ visit o.left, collector
+ collector << ", "
+ visit o.right, collector
+ collector << ") "
+ collector
+ end
+
+ def build_subselect(key, o)
+ subselect = super
- unless o.values.empty?
- collector << " SET "
- collector = inject_join o.values, collector, ", "
+ # Materialize subquery by adding distinct
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
+ subselect.distinct unless subselect.limit || subselect.offset || subselect.orders.any?
+
+ Nodes::SelectStatement.new.tap do |stmt|
+ core = stmt.cores.last
+ core.froms = Nodes::Grouping.new(subselect).as("__active_record_temp")
+ core.projections = [Arel.sql(quote_column_name(key.name))]
end
+ end
+
+ def collect_where_for(o, collector)
+ return super if o.offset
unless o.wheres.empty?
collector << " WHERE "
@@ -73,15 +94,6 @@ module Arel # :nodoc: all
maybe_visit o.limit, collector
end
-
- def visit_Arel_Nodes_Concat(o, collector)
- collector << " CONCAT("
- visit o.left, collector
- collector << ", "
- visit o.right, collector
- collector << ") "
- collector
- end
end
end
end
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
index 5986fd5576..0172204fc8 100644
--- a/activerecord/lib/arel/visitors/to_sql.rb
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -67,8 +67,8 @@ module Arel # :nodoc: all
@connection = connection
end
- def compile(node, &block)
- accept(node, Arel::Collectors::SQLString.new, &block).value
+ def compile(node, collector = Arel::Collectors::SQLString.new)
+ accept(node, collector).value
end
private
@@ -76,12 +76,8 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_DeleteStatement(o, collector)
collector << "DELETE FROM "
collector = visit o.relation, collector
- if o.wheres.any?
- collector << WHERE
- collector = inject_join o.wheres, collector, AND
- end
- maybe_visit o.limit, collector
+ collect_where_for(o, collector)
end
# FIXME: we should probably have a 2-pass visitor for this
@@ -92,17 +88,12 @@ module Arel # :nodoc: all
core.wheres = o.wheres
core.projections = [key]
stmt.limit = o.limit
+ stmt.offset = o.offset
stmt.orders = o.orders
stmt
end
def visit_Arel_Nodes_UpdateStatement(o, collector)
- if o.orders.empty? && o.limit.nil?
- wheres = o.wheres
- else
- wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
- end
-
collector << "UPDATE "
collector = visit o.relation, collector
unless o.values.empty?
@@ -110,12 +101,7 @@ module Arel # :nodoc: all
collector = inject_join o.values, collector, ", "
end
- unless wheres.empty?
- collector << " WHERE "
- collector = inject_join wheres, collector, " AND "
- end
-
- collector
+ collect_where_for(o, collector)
end
def visit_Arel_Nodes_InsertStatement(o, collector)
@@ -237,8 +223,6 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_SelectCore(o, collector)
collector << "SELECT"
- collector = maybe_visit o.top, collector
-
collector = maybe_visit o.set_quantifier, collector
collect_nodes_for o.projections, collector, SPACE
@@ -405,11 +389,6 @@ module Arel # :nodoc: all
visit o.expr, collector
end
- # FIXME: this does nothing on most databases, but does on MSSQL
- def visit_Arel_Nodes_Top(o, collector)
- collector
- end
-
def visit_Arel_Nodes_Lock(o, collector)
visit o.expr, collector
end
@@ -644,7 +623,7 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_Assignment(o, collector)
case o.right
- when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam
+ when Arel::Nodes::Node, Arel::Attributes::Attribute
collector = visit o.left, collector
collector << " = "
visit o.right, collector
@@ -739,8 +718,6 @@ module Arel # :nodoc: all
end
alias :visit_Arel_Nodes_SqlLiteral :literal
- alias :visit_Bignum :literal
- alias :visit_Fixnum :literal
alias :visit_Integer :literal
def quoted(o, a)
@@ -823,6 +800,21 @@ module Arel # :nodoc: all
}
end
+ def collect_where_for(o, collector)
+ if o.orders.empty? && o.limit.nil? && o.offset.nil?
+ wheres = o.wheres
+ else
+ wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
+ end
+
+ unless wheres.empty?
+ collector << " WHERE "
+ collector = inject_join wheres, collector, " AND "
+ end
+
+ collector
+ end
+
def infix_value(o, collector, value)
collector = visit o.left, collector
collector << value
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
index 4ceb502c5d..cbb88d571d 100644
--- a/activerecord/lib/rails/generators/active_record/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration.rb
@@ -25,11 +25,24 @@ module ActiveRecord
def db_migrate_path
if defined?(Rails.application) && Rails.application
- Rails.application.config.paths["db/migrate"].to_ary.first
+ configured_migrate_path || default_migrate_path
else
"db/migrate"
end
end
+
+ def default_migrate_path
+ Rails.application.config.paths["db/migrate"].to_ary.first
+ end
+
+ def configured_migrate_path
+ return unless database = options[:database]
+ config = ActiveRecord::Base.configurations.configs_for(
+ env_name: Rails.env,
+ spec_name: database,
+ )
+ config&.migrations_paths
+ end
end
end
end
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index a07b00ef79..dd79bcf542 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -8,6 +8,7 @@ module ActiveRecord
argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
class_option :primary_key_type, type: :string, desc: "The type for primary key"
+ class_option :database, type: :string, aliases: %i(db), desc: "The database for your migration. By default, the current environment's primary database is used."
def create_migration_file
set_local_assigns!
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index 25e54f3ac8..eac504f9f1 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -14,6 +14,7 @@ module ActiveRecord
class_option :parent, type: :string, desc: "The parent class for the generated model"
class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns"
class_option :primary_key_type, type: :string, desc: "The type for primary key"
+ class_option :database, type: :string, aliases: %i(db), desc: "The database for your model's migration. By default, the current environment's primary database is used."
# creates the migration file for the model.
def create_migration_file