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