aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb10
-rw-r--r--activerecord/lib/active_record/aggregations.rb1
-rw-r--r--activerecord/lib/active_record/association_relation.rb1
-rw-r--r--activerecord/lib/active_record/associations.rb11
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb1
-rw-r--r--activerecord/lib/active_record/associations/association.rb33
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb32
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb7
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb20
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb36
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb34
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb64
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb22
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb39
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb19
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb28
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb18
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb71
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb87
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb16
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb17
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb62
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb55
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb35
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb25
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb1
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb19
-rw-r--r--activerecord/lib/active_record/attributes.rb14
-rw-r--r--activerecord/lib/active_record/autosave_association.rb45
-rw-r--r--activerecord/lib/active_record/base.rb2
-rw-r--r--activerecord/lib/active_record/callbacks.rb7
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb1
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb53
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb143
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb195
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb89
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb94
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb159
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb129
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb178
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb302
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb40
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/deduplicable.rb29
-rw-r--r--activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb94
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb1
-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.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb95
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb64
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb3
-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.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb187
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb133
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb56
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb181
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb76
-rw-r--r--activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb25
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb122
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb231
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb1
-rw-r--r--activerecord/lib/active_record/connection_handling.rb68
-rw-r--r--activerecord/lib/active_record/core.rb70
-rw-r--r--activerecord/lib/active_record/database_configurations.rb90
-rw-r--r--activerecord/lib/active_record/database_configurations/hash_config.rb22
-rw-r--r--activerecord/lib/active_record/database_configurations/url_config.rb36
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb7
-rw-r--r--activerecord/lib/active_record/enum.rb16
-rw-r--r--activerecord/lib/active_record/errors.rb59
-rw-r--r--activerecord/lib/active_record/explain.rb1
-rw-r--r--activerecord/lib/active_record/fixture_set/table_row.rb1
-rw-r--r--activerecord/lib/active_record/fixture_set/table_rows.rb1
-rw-r--r--activerecord/lib/active_record/fixtures.rb77
-rw-r--r--activerecord/lib/active_record/gem_version.rb2
-rw-r--r--activerecord/lib/active_record/inheritance.rb5
-rw-r--r--activerecord/lib/active_record/insert_all.rb180
-rw-r--r--activerecord/lib/active_record/integration.rb82
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb14
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb8
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/middleware/database_selector.rb74
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver.rb89
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver/session.rb45
-rw-r--r--activerecord/lib/active_record/migration.rb126
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb43
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb140
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb1
-rw-r--r--activerecord/lib/active_record/model_schema.rb21
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb3
-rw-r--r--activerecord/lib/active_record/null_relation.rb1
-rw-r--r--activerecord/lib/active_record/persistence.rb260
-rw-r--r--activerecord/lib/active_record/query_cache.rb15
-rw-r--r--activerecord/lib/active_record/querying.rb47
-rw-r--r--activerecord/lib/active_record/railtie.rb65
-rw-r--r--activerecord/lib/active_record/railties/collection_cache_association_loading.rb6
-rw-r--r--activerecord/lib/active_record/railties/databases.rake187
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb4
-rw-r--r--activerecord/lib/active_record/reflection.rb52
-rw-r--r--activerecord/lib/active_record/relation.rb242
-rw-r--r--activerecord/lib/active_record/relation/batches.rb1
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb104
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb65
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb39
-rw-r--r--activerecord/lib/active_record/relation/merger.rb28
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb23
-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.rb251
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb3
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb21
-rw-r--r--activerecord/lib/active_record/result.rb1
-rw-r--r--activerecord/lib/active_record/sanitization.rb72
-rw-r--r--activerecord/lib/active_record/schema.rb13
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb11
-rw-r--r--activerecord/lib/active_record/schema_migration.rb10
-rw-r--r--activerecord/lib/active_record/scoping.rb14
-rw-r--r--activerecord/lib/active_record/scoping/default.rb23
-rw-r--r--activerecord/lib/active_record/scoping/named.rb17
-rw-r--r--activerecord/lib/active_record/statement_cache.rb6
-rw-r--r--activerecord/lib/active_record/store.rb48
-rw-r--r--activerecord/lib/active_record/table_metadata.rb22
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb80
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb5
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb1
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb1
-rw-r--r--activerecord/lib/active_record/test_fixtures.rb26
-rw-r--r--activerecord/lib/active_record/timestamp.rb64
-rw-r--r--activerecord/lib/active_record/touch_later.rb14
-rw-r--r--activerecord/lib/active_record/transactions.rb105
-rw-r--r--activerecord/lib/active_record/type.rb8
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb3
-rw-r--r--activerecord/lib/active_record/type/hash_lookup_type_map.rb1
-rw-r--r--activerecord/lib/active_record/type/serialized.rb1
-rw-r--r--activerecord/lib/active_record/type/type_map.rb1
-rw-r--r--activerecord/lib/active_record/type/unsigned_integer.rb1
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb26
-rw-r--r--activerecord/lib/active_record/validations.rb1
-rw-r--r--activerecord/lib/active_record/validations/associated.rb1
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb42
-rw-r--r--activerecord/lib/arel.rb10
-rw-r--r--activerecord/lib/arel/attributes.rb22
-rw-r--r--activerecord/lib/arel/compatibility/wheres.rb35
-rw-r--r--activerecord/lib/arel/insert_manager.rb6
-rw-r--r--activerecord/lib/arel/nodes.rb3
-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/comment.rb29
-rw-r--r--activerecord/lib/arel/nodes/select_core.rb28
-rw-r--r--activerecord/lib/arel/nodes/unary.rb1
-rw-r--r--activerecord/lib/arel/nodes/values.rb16
-rw-r--r--activerecord/lib/arel/nodes/values_list.rb19
-rw-r--r--activerecord/lib/arel/predications.rb33
-rw-r--r--activerecord/lib/arel/select_manager.rb20
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb10
-rw-r--r--activerecord/lib/arel/visitors/dot.rb10
-rw-r--r--activerecord/lib/arel/visitors/ibm_db.rb13
-rw-r--r--activerecord/lib/arel/visitors/informix.rb9
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb17
-rw-r--r--activerecord/lib/arel/visitors/oracle.rb1
-rw-r--r--activerecord/lib/arel/visitors/oracle12.rb12
-rw-r--r--activerecord/lib/arel/visitors/postgresql.rb15
-rw-r--r--activerecord/lib/arel/visitors/sqlite.rb1
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb261
-rw-r--r--activerecord/lib/arel/visitors/visitor.rb15
-rw-r--r--activerecord/lib/arel/visitors/where_sql.rb1
-rw-r--r--activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb1
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb1
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb3
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt2
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt6
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb3
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt11
208 files changed, 5064 insertions, 2960 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index d43378c64f..fd8d2edf28 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
@@ -55,7 +55,6 @@ module ActiveRecord
autoload :Persistence
autoload :QueryCache
autoload :Querying
- autoload :CollectionCacheKey
autoload :ReadonlyAttributes
autoload :RecordInvalid, "active_record/validations"
autoload :Reflection
@@ -74,6 +73,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 +153,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/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 3250e29b82..aa08124158 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -14,7 +14,6 @@ module ActiveRecord
end
private
-
def clear_aggregation_cache
@aggregation_cache.clear if persisted?
end
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
index 4c538ef2bd..de9892e48d 100644
--- a/activerecord/lib/active_record/association_relation.rb
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -29,7 +29,6 @@ module ActiveRecord
end
private
-
def exec_queries
super do |record|
@association.set_inverse_instance_from_queries(record)
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index fb1df00dc8..64c20adc87 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -703,8 +703,9 @@ module ActiveRecord
# #belongs_to associations.
#
# Extra options on the associations, as defined in the
- # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
- # also prevent the association's inverse from being found automatically.
+ # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt>
+ # constant, or a custom scope, will also prevent the association's inverse
+ # from being found automatically.
#
# The automatic guessing of the inverse association uses a heuristic based
# on the name of the class, so it may not work for all associations,
@@ -1293,7 +1294,8 @@ 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>: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.
#
@@ -1436,7 +1438,8 @@ 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>: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
#
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 272eede824..ac90ba0137 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -72,7 +72,6 @@ module ActiveRecord
attr_reader :aliases
private
-
def truncate(name)
name.slice(0, @connection.table_alias_length - 2)
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index bf4942aac8..cf22b850b9 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
@@ -194,7 +225,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- AssociationRelation.create(klass, self).merge!(klass.all)
+ AssociationRelation.create(klass, self).merge!(klass.scope_for_association)
end
def scope_for_create
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/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 7c69cd65ee..0c61094d6c 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -27,40 +27,32 @@ module ActiveRecord::Associations::Builder # :nodoc:
"Please choose a different association name."
end
- extension = define_extensions model, name, &block
- reflection = create_reflection model, name, scope, options, extension
+ reflection = create_reflection(model, name, scope, options, &block)
define_accessors model, reflection
define_callbacks model, reflection
define_validations model, reflection
reflection
end
- def self.create_reflection(model, name, scope, options, extension = nil)
+ def self.create_reflection(model, name, scope, options, &block)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
validate_options(options)
- scope = build_scope(scope, extension)
+ extension = define_extensions(model, name, &block)
+ options[:extend] = [*options[:extend], extension] if extension
+
+ scope = build_scope(scope)
ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
- def self.build_scope(scope, extension)
- new_scope = scope
-
+ def self.build_scope(scope)
if scope && scope.arity == 0
- new_scope = proc { instance_exec(&scope) }
- end
-
- if extension
- new_scope = wrap_scope new_scope, extension
+ proc { instance_exec(&scope) }
+ else
+ scope
end
-
- new_scope
- end
-
- def self.wrap_scope(scope, extension)
- scope
end
def self.macro
@@ -136,5 +128,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
name = reflection.name
model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
+
+ private_class_method :build_scope, :macro, :valid_options, :validate_options, :define_extensions,
+ :define_callbacks, :define_accessors, :define_readers, :define_writers, :define_validations,
+ :valid_dependent_options, :check_dependent_options, :add_destroy_callbacks
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index fc00f1e900..321ccba918 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -74,11 +74,11 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.add_touch_callbacks(model, reflection)
foreign_key = reflection.foreign_key
- n = reflection.name
+ name = reflection.name
touch = reflection.options[:touch]
callback = lambda { |changes_method| lambda { |record|
- BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method)
+ BelongsTo.touch_record(record, record.send(changes_method), foreign_key, name, touch, belongs_to_touch_method)
}}
if reflection.counter_cache_column
@@ -123,5 +123,8 @@ module ActiveRecord::Associations::Builder # :nodoc:
model.validates_presence_of reflection.name, message: :required
end
end
+
+ private_class_method :macro, :valid_options, :valid_dependent_options, :define_callbacks, :define_validations,
+ :add_counter_cache_callbacks, :add_touch_callbacks, :add_default_callbacks, :add_destroy_callbacks
end
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index ff57c40121..e78d25441b 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -20,11 +20,11 @@ module ActiveRecord::Associations::Builder # :nodoc:
}
end
- def self.define_extensions(model, name)
+ def self.define_extensions(model, name, &block)
if block_given?
- extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
- extension = Module.new(&Proc.new)
- model.module_parent.const_set(extension_module_name, extension)
+ extension_module_name = "#{name.to_s.camelize}AssociationExtension"
+ extension = Module.new(&block)
+ model.const_set(extension_module_name, extension)
end
end
@@ -67,16 +67,6 @@ module ActiveRecord::Associations::Builder # :nodoc:
CODE
end
- def self.wrap_scope(scope, mod)
- if scope
- if scope.arity > 0
- proc { |owner| instance_exec(owner, &scope).extending(mod) }
- else
- proc { instance_exec(&scope).extending(mod) }
- end
- else
- proc { extending(mod) }
- end
- end
+ private_class_method :valid_options, :define_callback, :define_extensions, :define_readers, :define_writers
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 0140aa15c8..6ad4c75fb5 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -46,7 +46,6 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
private
-
def self.suppress_composite_primary_key(pk)
pk unless pk.is_a?(Array)
end
@@ -73,7 +72,6 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
private
-
def middle_options(join_model)
middle_options = {}
middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}"
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 5b9617bc6d..556e2988f5 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -13,5 +13,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.valid_dependent_options
[:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
end
+
+ private_class_method :macro, :valid_options, :valid_dependent_options
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index bfb37d6eee..27ebe8cb71 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -7,7 +7,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
def self.valid_options(options)
- valid = super + [:as]
+ valid = super + [:as, :touch]
valid += [:through, :source, :source_type] if options[:through]
valid
end
@@ -16,6 +16,11 @@ module ActiveRecord::Associations::Builder # :nodoc:
[:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
end
+ def self.define_callbacks(model, reflection)
+ super
+ add_touch_callbacks(model, reflection) if reflection.options[:touch]
+ end
+
def self.add_destroy_callbacks(model, reflection)
super unless reflection.options[:through]
end
@@ -26,5 +31,34 @@ module ActiveRecord::Associations::Builder # :nodoc:
model.validates_presence_of reflection.name, message: :required
end
end
+
+ def self.touch_record(o, name, touch)
+ record = o.send name
+
+ return unless record && record.persisted?
+
+ if touch != true
+ record.touch(touch)
+ else
+ record.touch
+ end
+ end
+
+ def self.add_touch_callbacks(model, reflection)
+ name = reflection.name
+ touch = reflection.options[:touch]
+
+ callback = lambda { |record|
+ HasOne.touch_record(record, name, touch)
+ }
+
+ model.after_create callback, if: :saved_changes?
+ model.after_update callback, if: :saved_changes?
+ model.after_destroy callback
+ model.after_touch callback
+ end
+
+ private_class_method :macro, :valid_options, :valid_dependent_options, :add_destroy_callbacks,
+ :define_callbacks, :define_validations, :add_touch_callbacks
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 0a02ef4cc1..0e22563b41 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -38,5 +38,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
CODE
end
+
+ private_class_method :valid_options, :define_accessors, :define_constructors
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 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..0db0ad8595 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 <tt><<</tt> 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,8 +1027,9 @@ module ActiveRecord
end
alias_method :push, :<<
alias_method :append, :<<
+ alias_method :concat, :<<
- def prepend(*args)
+ def prepend(*args) # :nodoc:
raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
end
@@ -1130,7 +1101,6 @@ module ActiveRecord
delegate(*delegate_methods, to: :scope)
private
-
def find_nth_with_limit(index, limit)
load_target if find_from_target?
super
diff --git a/activerecord/lib/active_record/associations/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..dd2ed55279 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -36,16 +36,7 @@ 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.
#
# If the association has a counter cache it gets that value. Otherwise
@@ -69,7 +60,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 +83,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 +122,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..0d384950fe 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -57,21 +57,14 @@ module ActiveRecord
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build(*options_for_through_record)
- through_record.send("#{source_reflection.name}=", record)
+ attributes = through_scope_attributes
+ attributes[source_reflection.name] = record
+ attributes[source_reflection.foreign_type] = options[:source_type] if options[:source_type]
- if options[:source_type]
- through_record.send("#{source_reflection.foreign_type}=", options[:source_type])
- end
-
- through_record
+ through_association.build(attributes)
end
end
- def options_for_through_record
- [through_scope_attributes]
- end
-
def through_scope_attributes
scope.where_values_hash(through_association.reflection.name.to_s).
except!(through_association.reflection.foreign_key,
@@ -161,6 +154,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.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index b76005b587..f35a40fb2f 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -64,16 +64,17 @@ module ActiveRecord
end
end
- def initialize(base, table, associations)
+ def initialize(base, table, associations, join_type)
tree = self.class.make_tree associations
@join_root = JoinBase.new(base, table, build(tree, base))
+ @join_type = join_type
end
def reflections
join_root.drop(1).map!(&:reflection)
end
- def join_constraints(joins_to_add, join_type, alias_tracker)
+ def join_constraints(joins_to_add, alias_tracker)
@alias_tracker = alias_tracker
construct_tables!(join_root)
@@ -82,9 +83,9 @@ module ActiveRecord
joins.concat joins_to_add.flat_map { |oj|
construct_tables!(oj.join_root)
if join_root.match? oj.join_root
- walk join_root, oj.join_root
+ walk(join_root, oj.join_root, oj.join_type)
else
- make_join_constraints(oj.join_root, join_type)
+ make_join_constraints(oj.join_root, oj.join_type)
end
}
end
@@ -125,7 +126,7 @@ module ActiveRecord
end
protected
- attr_reader :join_root
+ attr_reader :join_root, :join_type
private
attr_reader :alias_tracker
@@ -151,7 +152,7 @@ module ActiveRecord
end
end
- def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin)
+ def make_constraints(parent, child, join_type)
foreign_table = parent.table
foreign_klass = parent.base_klass
joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
@@ -173,13 +174,13 @@ module ActiveRecord
join ? "#{name}_join" : name
end
- def walk(left, right)
+ def walk(left, right, join_type)
intersection, missing = right.children.map { |node1|
[left.children.find { |node2| node1.match? node2 }, node1]
}.partition(&:first)
- joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) }
- joins.concat missing.flat_map { |_, n| make_constraints(left, n) }
+ joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r, join_type) }
+ joins.concat missing.flat_map { |_, n| make_constraints(left, n, join_type) }
end
def find_reflection(klass, name)
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 4583d89cba..6a7e92dc28 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,20 @@ 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)
+ append_constraints(joins.last, others)
end
# The current table in this iteration becomes the foreign table in the next
@@ -60,6 +64,16 @@ module ActiveRecord
@readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value
end
+
+ private
+ def append_constraints(join, constraints)
+ if join.is_a?(Arel::Nodes::StringJoin)
+ join_string = table.create_and(constraints.unshift(join.left))
+ join.left = Arel.sql(base_klass.connection.visitor.compile(join_string))
+ else
+ join.right.expr.children.concat(constraints)
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 8997579527..d4e8b364e1 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -95,7 +95,6 @@ module ActiveRecord
end
private
-
# Loads all the given data into +records+ for the +association+.
def preloaders_on(association, records, scope, polymorphic_parent = false)
case association
@@ -143,16 +142,13 @@ module ActiveRecord
def preloaders_for_reflection(reflection, records, scope)
records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
- loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
- loader.run self
- loader
+ preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope).run
end
end
def grouped_records(association, records, polymorphic_parent)
h = {}
records.each do |record|
- next unless record
reflection = record.class._reflect_on_association(association)
next if polymorphic_parent && !reflection || !record.association(association).klass
(h[reflection] ||= []) << record
@@ -166,10 +162,18 @@ module ActiveRecord
@reflection = reflection
end
- def run(preloader); end
+ def run
+ self
+ end
def preloaded_records
- owners.flat_map { |owner| owner.association(reflection.name).target }
+ @preloaded_records ||= records_by_owner.flat_map(&:last)
+ end
+
+ def records_by_owner
+ @records_by_owner ||= owners.each_with_object({}) do |owner, result|
+ result[owner] = Array(owner.association(reflection.name).target)
+ end
end
private
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index d6f7359055..4c7b0e6f07 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -4,29 +4,43 @@ module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
- attr_reader :preloaded_records
-
def initialize(klass, owners, reflection, preload_scope)
@klass = klass
@owners = owners
@reflection = reflection
@preload_scope = preload_scope
@model = owners.first && owners.first.class
- @preloaded_records = []
end
- def run(preloader)
- records = load_records do |record|
- owner = owners_by_key[convert_key(record[association_key_name])]
- association = owner.association(reflection.name)
- association.set_inverse_instance(record)
+ def run
+ if !preload_scope || preload_scope.empty_scope?
+ owners.each do |owner|
+ associate_records_to_owner(owner, records_by_owner[owner] || [])
+ end
+ else
+ # Custom preload scope is used and
+ # the association can not be marked as loaded
+ # Loading into a Hash instead
+ records_by_owner
end
+ self
+ end
- owners.each do |owner|
- associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || [])
+ def records_by_owner
+ # owners can be duplicated when a relation has a collection association join
+ # #compare_by_identity makes such owners different hash keys
+ @records_by_owner ||= preloaded_records.each_with_object({}.compare_by_identity) do |record, result|
+ owners_by_key[convert_key(record[association_key_name])].each do |owner|
+ (result[owner] ||= []) << record
+ end
end
end
+ def preloaded_records
+ return @preloaded_records if defined?(@preloaded_records)
+ @preloaded_records = owner_keys.empty? ? [] : records_for(owner_keys)
+ end
+
private
attr_reader :owners, :reflection, :preload_scope, :model, :klass
@@ -42,11 +56,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
@@ -55,13 +68,10 @@ module ActiveRecord
end
def owners_by_key
- unless defined?(@owners_by_key)
- @owners_by_key = owners.each_with_object({}) do |owner, h|
- key = convert_key(owner[owner_key_name])
- h[key] = owner if key
- end
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
+ key = convert_key(owner[owner_key_name])
+ (result[key] ||= []) << owner if key
end
- @owners_by_key
end
def key_conversion_required?
@@ -88,23 +98,16 @@ module ActiveRecord
@model.type_for_attribute(owner_key_name).type
end
- def load_records(&block)
- return {} if owner_keys.empty?
- # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
- # Make several smaller queries if necessary or make one query if the adapter supports it
- slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
- @preloaded_records = slices.flat_map do |slice|
- records_for(slice, &block)
- end
- @preloaded_records.group_by do |record|
- convert_key(record[association_key_name])
+ def records_for(ids)
+ scope.where(association_key_name => ids).load do |record|
+ # Processing only the first owner
+ # because the record is modified but not an owner
+ owner = owners_by_key[convert_key(record[association_key_name])].first
+ association = owner.association(reflection.name)
+ association.set_inverse_instance(record)
end
end
- def records_for(ids, &block)
- scope.where(association_key_name => ids).load(&block)
- end
-
def scope
@scope ||= build_scope
end
@@ -116,7 +119,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..bec1c4c94a 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -4,42 +4,57 @@ module ActiveRecord
module Associations
class Preloader
class ThroughAssociation < Association # :nodoc:
- 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)
- @preloaded_records = preloaders.flat_map(&:preloaded_records)
-
- owners.each do |owner|
- through_records = Array(owner.association(through_reflection.name).target)
- if already_loaded
+ PRELOADER = ActiveRecord::Associations::Preloader.new
+
+ def initialize(*)
+ super
+ @already_loaded = owners.first.association(through_reflection.name).loaded?
+ end
+
+ def preloaded_records
+ @preloaded_records ||= source_preloaders.flat_map(&:preloaded_records)
+ end
+
+ def records_by_owner
+ return @records_by_owner if defined?(@records_by_owner)
+ source_records_by_owner = source_preloaders.map(&:records_by_owner).reduce(:merge)
+ through_records_by_owner = through_preloaders.map(&:records_by_owner).reduce(:merge)
+
+ @records_by_owner = owners.each_with_object({}) do |owner, result|
+ through_records = through_records_by_owner[owner] || []
+
+ if @already_loaded
if source_type = reflection.options[:source_type]
through_records = through_records.select do |record|
record[reflection.foreign_type] == source_type
end
end
- 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
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
+
+ records = through_records.flat_map do |record|
+ source_records_by_owner[record]
end
- associate_records_to_owner(owner, result)
+
+ records.compact!
+ records.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any?
+ records.uniq! if scope.distinct_value
+ result[owner] = records
end
end
private
+ def source_preloaders
+ @source_preloaders ||= PRELOADER.preload(middle_records, source_reflection.name, scope)
+ end
+
+ def middle_records
+ through_preloaders.flat_map(&:preloaded_records)
+ end
+
+ def through_preloaders
+ @through_preloaders ||= PRELOADER.preload(owners, through_reflection.name, through_scope)
+ end
+
def through_reflection
reflection.through_reflection
end
@@ -49,8 +64,8 @@ module ActiveRecord
end
def preload_index
- @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index|
- result[id] = index
+ @preload_index ||= preloaded_records.each_with_object({}).with_index do |(record, result), index|
+ result[record] = index
end
end
@@ -58,11 +73,15 @@ module ActiveRecord
scope = through_reflection.klass.unscoped
options = reflection.options
+ values = reflection_scope.values
+ if annotations = values[:annotate]
+ scope.annotate!(*annotations)
+ end
+
if options[:source_type]
scope.where! reflection.foreign_type => options[:source_type]
elsif !reflection_scope.where_clause.empty?
scope.where_clause = reflection_scope.where_clause
- values = reflection_scope.values
if includes = values[:includes]
scope.includes!(source_reflection.name => includes)
@@ -89,17 +108,7 @@ module ActiveRecord
end
end
- 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
+ scope
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..acb8ba7e5a 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -7,7 +7,6 @@ module ActiveRecord
include ActiveModel::AttributeAssignment
private
-
def _assign_attributes(attributes)
multi_parameter_attributes = {}
nested_parameter_attributes = {}
@@ -45,16 +44,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_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
index 98b7805c0a..0b66043d2a 100644
--- a/activerecord/lib/active_record/attribute_decorators.rb
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -46,7 +46,6 @@ module ActiveRecord
end
private
-
def load_schema!
super
attribute_types.each do |name, type|
@@ -75,7 +74,6 @@ module ActiveRecord
end
private
-
def decorators_for(name, type)
matching(name, type).map(&:last)
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index fd8c1da842..21f72bb6c7 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -35,7 +35,8 @@ module ActiveRecord
end
def initialize_generated_modules # :nodoc:
- @generated_attribute_methods = GeneratedAttributeMethods.new
+ @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new)
+ private_constant :GeneratedAttributeMethods
@attribute_methods_generated = false
include @generated_attribute_methods
@@ -158,57 +159,6 @@ module ActiveRecord
end
end
- # Regexp for column names (with or without a table name prefix). Matches
- # the following:
- # "#{table_name}.#{column_name}"
- # "#{column_name}"
- COLUMN_NAME = /\A(?:\w+\.)?\w+\z/i
-
- # Regexp for column names with order (with or without a table name
- # prefix, with or without various order modifiers). Matches the following:
- # "#{table_name}.#{column_name}"
- # "#{table_name}.#{column_name} #{direction}"
- # "#{table_name}.#{column_name} #{direction} NULLS FIRST"
- # "#{table_name}.#{column_name} NULLS LAST"
- # "#{column_name}"
- # "#{column_name} #{direction}"
- # "#{column_name} #{direction} NULLS FIRST"
- # "#{column_name} NULLS LAST"
- COLUMN_NAME_WITH_ORDER = /
- \A
- (?:\w+\.)?
- \w+
- (?:\s+asc|\s+desc)?
- (?:\s+nulls\s+(?:first|last))?
- \z
- /ix
-
- def disallow_raw_sql!(args, permit: COLUMN_NAME) # :nodoc:
- unexpected = args.reject do |arg|
- Arel.arel_node?(arg) ||
- arg.to_s.split(/\s*,\s*/).all? { |part| permit.match?(part) }
- end
-
- return if unexpected.none?
-
- if allow_unsafe_raw_sql == :deprecated
- ActiveSupport::Deprecation.warn(
- "Dangerous query method (method whose arguments are used as raw " \
- "SQL) called with non-attribute argument(s): " \
- "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \
- "arguments will be disallowed in Rails 6.0. This method should " \
- "not be called with user-provided values, such as request " \
- "parameters or model attributes. Known-safe values can be passed " \
- "by wrapping them in Arel.sql()."
- )
- else
- raise(ActiveRecord::UnknownAttributeReference,
- "Query method called with non-attribute argument(s): " +
- unexpected.map(&:inspect).join(", ")
- )
- end
- end
-
# Returns true if the given attribute exists, otherwise false.
#
# class Person < ActiveRecord::Base
@@ -436,7 +386,7 @@ module ActiveRecord
def attributes_for_update(attribute_names)
attribute_names &= self.class.column_names
attribute_names.delete_if do |name|
- readonly_attribute?(name)
+ self.class.readonly_attribute?(name)
end
end
@@ -459,12 +409,8 @@ module ActiveRecord
end
end
- def readonly_attribute?(name)
- self.class.readonly_attributes.include?(name)
- end
-
def pk_attribute?(name)
- name == self.class.primary_key
+ name == @primary_key
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index 5941f51a1a..4a7b6c60e5 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -46,6 +46,7 @@ module ActiveRecord
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
# task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes[attr_name.to_s].value_before_type_cast
end
@@ -60,17 +61,18 @@ module ActiveRecord
# task.attributes_before_type_cast
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes.values_before_type_cast
end
private
-
- # Handle *_before_type_cast for method_missing.
+ # Dispatch target for <tt>*_before_type_cast</tt> attribute methods.
def attribute_before_type_cast(attribute_name)
read_attribute_before_type_cast(attribute_name)
end
def attribute_came_from_user?(attribute_name)
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes[attribute_name].came_from_user?
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index ebc2252c50..45341765c1 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -29,9 +29,7 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
@mutations_from_database = nil
end
end
@@ -51,7 +49,7 @@ module ActiveRecord
# +to+ When passed, this method will return false unless the value was
# changed to the given value
def saved_change_to_attribute?(attr_name, **options)
- mutations_before_last_save.changed?(attr_name, **options)
+ mutations_before_last_save.changed?(attr_name.to_s, options)
end
# Returns the change to an attribute during the last save. If the
@@ -63,7 +61,7 @@ module ActiveRecord
# invoked as +saved_change_to_name+ instead of
# <tt>saved_change_to_attribute("name")</tt>.
def saved_change_to_attribute(attr_name)
- mutations_before_last_save.change_to_attribute(attr_name)
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
end
# Returns the original value of an attribute before the last save.
@@ -73,7 +71,7 @@ module ActiveRecord
# invoked as +name_before_last_save+ instead of
# <tt>attribute_before_last_save("name")</tt>.
def attribute_before_last_save(attr_name)
- mutations_before_last_save.original_value(attr_name)
+ mutations_before_last_save.original_value(attr_name.to_s)
end
# Did the last call to +save+ have any changes to change?
@@ -101,7 +99,7 @@ module ActiveRecord
# +to+ When passed, this method will return false unless the value will be
# changed to the given value
def will_save_change_to_attribute?(attr_name, **options)
- mutations_from_database.changed?(attr_name, **options)
+ mutations_from_database.changed?(attr_name.to_s, options)
end
# Returns the change to an attribute that will be persisted during the
@@ -115,7 +113,7 @@ module ActiveRecord
# If the attribute will change, the result will be an array containing the
# original value and the new value about to be saved.
def attribute_change_to_be_saved(attr_name)
- mutations_from_database.change_to_attribute(attr_name)
+ mutations_from_database.change_to_attribute(attr_name.to_s)
end
# Returns the value of an attribute in the database, as opposed to the
@@ -127,7 +125,7 @@ module ActiveRecord
# saved. It can be invoked as +name_in_database+ instead of
# <tt>attribute_in_database("name")</tt>.
def attribute_in_database(attr_name)
- mutations_from_database.original_value(attr_name)
+ mutations_from_database.original_value(attr_name.to_s)
end
# Will the next call to +save+ have any changes to persist?
@@ -158,12 +156,51 @@ module ActiveRecord
end
private
- def write_attribute_without_type_cast(attr_name, _)
+ def mutations_from_database
+ sync_with_transaction_state if @transaction_state&.finalized?
+ super
+ end
+
+ def mutations_before_last_save
+ sync_with_transaction_state if @transaction_state&.finalized?
+ super
+ end
+
+ def write_attribute_without_type_cast(attr_name, value)
result = super
clear_attribute_change(attr_name)
result
end
+ def _touch_row(attribute_names, time)
+ @_touch_attr_names = Set.new(attribute_names)
+
+ affected_rows = super
+
+ if @_skip_dirty_tracking ||= false
+ clear_attribute_changes(@_touch_attr_names)
+ return affected_rows
+ end
+
+ changes = {}
+ @attributes.keys.each do |attr_name|
+ next if @_touch_attr_names.include?(attr_name)
+
+ if attribute_changed?(attr_name)
+ changes[attr_name] = _read_attribute(attr_name)
+ _write_attribute(attr_name, attribute_was(attr_name))
+ clear_attribute_change(attr_name)
+ end
+ end
+
+ changes_applied
+ changes.each { |attr_name, value| _write_attribute(attr_name, value) }
+
+ affected_rows
+ ensure
+ @_touch_attr_names, @_skip_dirty_tracking = nil, nil
+ end
+
def _update_record(attribute_names = attribute_names_for_partial_writes)
affected_rows = super
changes_applied
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 9b267bb7c0..768c5f8c05 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -14,45 +14,37 @@ 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
+ _read_attribute(@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
+ _write_attribute(@primary_key, value)
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)
+ query_attribute(@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)
+ read_attribute_before_type_cast(@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)
+ attribute_was(@primary_key)
end
+ # Returns the primary key column's value from the database.
def id_in_database
- sync_with_transaction_state
- attribute_in_database(self.class.primary_key)
+ attribute_in_database(@primary_key)
end
private
-
def attribute_method?(attr_name)
attr_name == "id" || super
end
@@ -121,13 +113,12 @@ module ActiveRecord
#
# Project.primary_key # => "foo_id"
def primary_key=(value)
- @primary_key = value && value.to_s
+ @primary_key = value && -value.to_s
@quoted_primary_key = nil
@attributes_builder = nil
end
private
-
def suppress_composite_primary_key(pk)
return pk unless pk.is_a?(Array)
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 6757e9b66a..0cf67644af 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -16,8 +16,7 @@ module ActiveRecord
when true then true
when false, nil then false
else
- column = self.class.columns_hash[attr_name]
- if column.nil?
+ if !type_for_attribute(attr_name) { false }
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
@@ -33,7 +32,7 @@ module ActiveRecord
end
private
- # Handle *? for method_missing.
+ # Dispatch target for <tt>*?</tt> attribute methods.
def attribute?(attribute_name)
query_attribute(attribute_name)
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 6e1275e990..0f0e721b24 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -7,16 +7,12 @@ module ActiveRecord
module ClassMethods # :nodoc:
private
-
def define_method_attribute(name)
- sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
-
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}
- #{sync_with_transaction_state}
name = #{attr_name_expr}
_read_attribute(name) { |n| missing_attribute(n, caller) }
end
@@ -30,28 +26,17 @@ module ActiveRecord
# to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name, &block)
name = attr_name.to_s
- if self.class.attribute_alias?(name)
- name = self.class.attribute_alias(name)
- end
+ name = self.class.attribute_aliases[name] || name
- primary_key = self.class.primary_key
- name = primary_key if name == "id" && primary_key
- sync_with_transaction_state if name == primary_key
+ name = @primary_key if name == "id" && @primary_key
_read_attribute(name, &block)
end
# This method exists to avoid the expensive primary_key check internally, without
# breaking compatibility with the read_attribute API
- 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
+ sync_with_transaction_state if @transaction_state&.finalized?
+ @attributes.fetch_value(attr_name.to_s, &block)
end
alias :attribute :_read_attribute
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 6e0e90f39c..7bc03b9eed 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -79,7 +79,6 @@ module ActiveRecord
end
private
-
def type_incompatible_with_serialize?(type, class_name)
type.is_a?(ActiveRecord::Type::Json) && class_name == ::JSON ||
type.respond_to?(:type_cast_array, true) && class_name == ::Array
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 294a3dc32c..fb44232dff 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -25,7 +25,6 @@ module ActiveRecord
end
private
-
def convert_time_to_time_zone(value)
return if value.nil?
@@ -64,7 +63,6 @@ module ActiveRecord
module ClassMethods # :nodoc:
private
-
def inherited(subclass)
super
# We need to apply this decorator here, rather than on module inclusion. The closure
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 455e67e19b..66536a8ddf 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -11,17 +11,13 @@ module ActiveRecord
module ClassMethods # :nodoc:
private
-
def define_method_attribute=(name)
- sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
-
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name, writer: true,
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}(value)
name = #{attr_name_expr}
- #{sync_with_transaction_state}
_write_attribute(name, value)
end
RUBY
@@ -34,31 +30,28 @@ module ActiveRecord
# turned into +nil+.
def write_attribute(attr_name, value)
name = attr_name.to_s
- if self.class.attribute_alias?(name)
- name = self.class.attribute_alias(name)
- end
+ name = self.class.attribute_aliases[name] || name
- primary_key = self.class.primary_key
- name = primary_key if name == "id" && primary_key
- sync_with_transaction_state if name == primary_key
+ name = @primary_key if name == "id" && @primary_key
_write_attribute(name, value)
end
# This method exists to avoid the expensive primary_key check internally, without
# breaking compatibility with the write_attribute API
def _write_attribute(attr_name, value) # :nodoc:
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes.write_from_user(attr_name.to_s, value)
value
end
private
def write_attribute_without_type_cast(attr_name, value)
- name = attr_name.to_s
- @attributes.write_cast_value(name, value)
+ sync_with_transaction_state if @transaction_state&.finalized?
+ @attributes.write_cast_value(attr_name.to_s, value)
value
end
- # Handle *= for method_missing.
+ # Dispatch target for <tt>*=</tt> attribute methods.
def attribute=(attribute_name, value)
_write_attribute(attribute_name, value)
end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 35150889d9..c7846dbe7a 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -41,6 +41,9 @@ module ActiveRecord
# +range+ (PostgreSQL only) specifies that the type should be a range (see the
# examples below).
#
+ # When using a symbol for +cast_type+, extra options are forwarded to the
+ # constructor of the type object.
+ #
# ==== Examples
#
# The type detected by Active Record can be overridden.
@@ -112,6 +115,16 @@ module ActiveRecord
# my_float_range: 1.0..3.5
# }
#
+ # Passing options to the type constructor
+ #
+ # # app/models/my_model.rb
+ # class MyModel < ActiveRecord::Base
+ # attribute :small_int, :integer, limit: 2
+ # end
+ #
+ # MyModel.create(small_int: 65537)
+ # # => Error: 65537 is out of range for the limit of two bytes
+ #
# ==== Creating Custom Types
#
# Users may also define their own custom types, as long as they respond
@@ -242,7 +255,6 @@ module ActiveRecord
end
private
-
NO_DEFAULT_PROVIDED = Object.new # :nodoc:
private_constant :NO_DEFAULT_PROVIDED
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index d77d76cb1e..94d8134b55 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -147,7 +147,6 @@ module ActiveRecord
module ClassMethods # :nodoc:
private
-
def define_non_cyclic_method(name, &block)
return if instance_methods(false).include?(name)
define_method(name) do |*args|
@@ -267,7 +266,6 @@ module ActiveRecord
end
private
-
# Returns the record for an association collection that should be validated
# or saved. If +autosave+ is +false+ only new records will be returned,
# unless the parent is/was a new record itself.
@@ -330,21 +328,16 @@ module ActiveRecord
if reflection.options[:autosave]
indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors)
- record.errors.each do |attribute, message|
+ record.errors.group_by_attribute.each { |attribute, errors|
attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
- errors[attribute] << message
- errors[attribute].uniq!
- end
- record.errors.details.each_key do |attribute|
- reflection_attribute =
- normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym
-
- record.errors.details[attribute].each do |error|
- errors.details[reflection_attribute] << error
- errors.details[reflection_attribute].uniq!
- end
- end
+ errors.each { |error|
+ self.errors.import(
+ error,
+ attribute: attribute
+ )
+ }
+ }
else
errors.add(reflection.name)
end
@@ -382,10 +375,14 @@ module ActiveRecord
if association = association_instance_get(reflection.name)
autosave = reflection.options[:autosave]
+ # By saving the instance variable in a local variable,
+ # we make the whole callback re-entrant.
+ new_record_before_save = @new_record_before_save
+
# reconstruct the scope now that we know the owner's id
association.reset_scope
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+ if records = associated_records_to_validate_or_save(association, new_record_before_save, autosave)
if autosave
records_to_destroy = records.select(&:marked_for_destruction?)
records_to_destroy.each { |record| association.destroy(record) }
@@ -397,7 +394,7 @@ module ActiveRecord
saved = true
- if autosave != false && (@new_record_before_save || record.new_record?)
+ if autosave != false && (new_record_before_save || record.new_record?)
if autosave
saved = association.insert_record(record, false)
elsif !reflection.nested?
@@ -412,7 +409,7 @@ module ActiveRecord
saved = record.save(validate: false)
end
- raise ActiveRecord::Rollback unless saved
+ raise(RecordInvalid.new(association.owner)) unless saved
end
end
end
@@ -457,10 +454,16 @@ module ActiveRecord
# If the record is new or it has changed, returns true.
def record_changed?(reflection, record, key)
record.new_record? ||
- (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) ||
+ association_foreign_key_changed?(reflection, record, key) ||
record.will_save_change_to_attribute?(reflection.foreign_key)
end
+ def association_foreign_key_changed?(reflection, record, key)
+ return false if reflection.through_reflection?
+
+ record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key
+ end
+
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
#
# In addition, it will destroy the association if it was marked for destruction.
@@ -490,9 +493,7 @@ module ActiveRecord
end
def _ensure_no_duplicate_errors
- errors.messages.each_key do |attribute|
- errors[attribute].uniq!
- end
+ errors.uniq!
end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index db097cb930..282c9fcf30 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -12,7 +12,6 @@ require "active_support/core_ext/hash/slice"
require "active_support/core_ext/string/behavior"
require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/module/introspection"
-require "active_support/core_ext/object/duplicable"
require "active_support/core_ext/class/subclasses"
require "active_record/attribute_decorators"
require "active_record/define_callbacks"
@@ -288,7 +287,6 @@ module ActiveRecord #:nodoc:
extend Explain
extend Enum
extend Delegation::DelegateCache
- extend CollectionCacheKey
extend Aggregations::ClassMethods
include Core
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 5407af85ea..a9ab9ab7a9 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.where(parent_id: id).delete_all
+ # self.class.delete_by(parent_id: id)
# end
# end
#
@@ -323,8 +323,7 @@ module ActiveRecord
end
private
-
- def create_or_update(*)
+ def create_or_update(**)
_run_save_callbacks { super }
end
@@ -332,7 +331,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/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 11559141c7..881f0bcdb0 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -39,7 +39,6 @@ module ActiveRecord
end
private
-
def check_arity_of_constructor
load(nil)
rescue ArgumentError
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
deleted file mode 100644
index 4b6db8a96c..0000000000
--- a/activerecord/lib/active_record/collection_cache_key.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module CollectionCacheKey
- def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
- query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql)
- key = "#{collection.model_name.cache_key}/query-#{query_signature}"
-
- if collection.loaded? || collection.distinct_value
- size = collection.records.size
- if size > 0
- timestamp = collection.max_by(&timestamp_column)._read_attribute(timestamp_column)
- end
- else
- if collection.eager_loading?
- collection = collection.send(:apply_join_dependency)
- end
- column_type = type_for_attribute(timestamp_column)
- column = connection.visitor.compile(collection.arel_attribute(timestamp_column))
- select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
-
- if collection.has_limit_or_offset?
- query = collection.select("#{column} AS collection_cache_key_timestamp")
- subquery_alias = "subquery_for_cache_key"
- subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
- subquery = query.arel.as(subquery_alias)
- arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column)
- else
- query = collection.unscope(:order)
- query.select_values = [select_values % column]
- arel = query.arel
- end
-
- result = connection.select_one(arel, nil)
-
- if result.blank?
- size = 0
- timestamp = nil
- else
- size = result["size"]
- timestamp = column_type.deserialize(result["timestamp"])
- end
-
- end
-
- if timestamp
- "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
- else
- "#{key}-#{size}"
- end
- end
- 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..36001efdd5 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -3,6 +3,7 @@
require "thread"
require "concurrent/map"
require "monitor"
+require "weakref"
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -19,6 +20,26 @@ module ActiveRecord
end
module ConnectionAdapters
+ module AbstractPool # :nodoc:
+ def get_schema_cache(connection)
+ @schema_cache ||= SchemaCache.new(connection)
+ @schema_cache.connection = connection
+ @schema_cache
+ end
+
+ def set_schema_cache(cache)
+ @schema_cache = cache
+ end
+ end
+
+ class NullPool # :nodoc:
+ include ConnectionAdapters::AbstractPool
+
+ def initialize
+ @schema_cache = nil
+ end
+ end
+
# Connection pool base class for managing Active Record database
# connections.
#
@@ -146,7 +167,6 @@ module ActiveRecord
end
private
-
def internal_poll(timeout)
no_wait_poll || (timeout && wait_poll(timeout))
end
@@ -185,7 +205,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 +214,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]
@@ -294,23 +314,50 @@ module ActiveRecord
@frequency = frequency
end
+ @mutex = Mutex.new
+ @pools = {}
+
+ class << self
+ def register_pool(pool, frequency) # :nodoc:
+ @mutex.synchronize do
+ unless @pools.key?(frequency)
+ @pools[frequency] = []
+ spawn_thread(frequency)
+ end
+ @pools[frequency] << WeakRef.new(pool)
+ end
+ end
+
+ private
+ def spawn_thread(frequency)
+ Thread.new(frequency) do |t|
+ loop do
+ sleep t
+ @mutex.synchronize do
+ @pools[frequency].select!(&:weakref_alive?)
+ @pools[frequency].each do |p|
+ p.reap
+ p.flush
+ rescue WeakRef::RefError
+ end
+ end
+ end
+ end
+ end
+ end
+
def run
return unless frequency && frequency > 0
- Thread.new(frequency, pool) { |t, p|
- loop do
- sleep t
- p.reap
- p.flush
- end
- }
+ self.class.register_pool(pool, frequency)
end
end
include MonitorMixin
include QueryCache::ConnectionPoolConfiguration
+ include ConnectionAdapters::AbstractPool
attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache
- attr_reader :spec, :connections, :size, :reaper
+ attr_reader :spec, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
# object which describes database connection information (e.g. adapter,
@@ -379,7 +426,7 @@ module ActiveRecord
# #connection can be called any number of times; the connection is
# held in a cache keyed by a thread.
def connection
- @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout
+ @thread_cached_conns[connection_cache_key(current_thread)] ||= checkout
end
# Returns true if there is an open connection being used for the current thread.
@@ -388,7 +435,7 @@ module ActiveRecord
# #connection or #with_connection methods. Connections obtained through
# #checkout will not be detected by #active_connection?
def active_connection?
- @thread_cached_conns[connection_cache_key(Thread.current)]
+ @thread_cached_conns[connection_cache_key(current_thread)]
end
# Signal that the thread is finished with the current connection.
@@ -423,6 +470,21 @@ module ActiveRecord
synchronize { @connections.any? }
end
+ # Returns an array containing the connections currently in the pool.
+ # Access to the array does not require synchronization on the pool because
+ # the array is newly created and not retained by the pool.
+ #
+ # However; this method bypasses the ConnectionPool's thread-safe connection
+ # access pattern. A returned connection may be owned by another thread,
+ # unowned, or by happen-stance owned by the calling thread.
+ #
+ # Calling methods on a connection without ownership is subject to the
+ # thread-safety guarantees of the underlying method. Many of the methods
+ # on connection adapter classes are inherently multi-thread unsafe.
+ def connections
+ synchronize { @connections.dup }
+ end
+
# Disconnects all connections in the pool, and clears the pool.
#
# Raises:
@@ -668,6 +730,10 @@ module ActiveRecord
thread
end
+ def current_thread
+ @lock_thread || Thread.current
+ end
+
# Take control of all existing connections so a "group" action such as
# reload/disconnect can be performed safely. It is no longer enough to
# wrap it in +synchronize+ because some pool's actions are allowed
@@ -686,13 +752,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
@@ -809,7 +875,7 @@ module ActiveRecord
def new_connection
Base.send(spec.adapter_method, spec.config).tap do |conn|
- conn.schema_cache = schema_cache.dup if schema_cache
+ conn.check_version
end
end
@@ -915,6 +981,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)
@@ -927,21 +1003,30 @@ module ActiveRecord
end
end
+ attr_reader :prevent_writes
+
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
+ @prevent_writes = false
# Backup finalizer: if the forked child never needed a pool, the above
# early discard has not occurred
ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool)
end
+ # Prevent writing to the database regardless of role.
+ #
+ # In some cases you may want to prevent writes to the database
+ # even if you are on a database that can write. `while_preventing_writes`
+ # will prevent writes to the database for the duration of the block.
+ def while_preventing_writes
+ original, @prevent_writes = @prevent_writes, true
+ yield
+ ensure
+ @prevent_writes = original
+ end
+
def connection_pool_list
owner_to_pool.values.compact
end
@@ -1006,7 +1091,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
@@ -1050,7 +1144,6 @@ module ActiveRecord
end
private
-
def owner_to_pool
@owner_to_pool[Process.pid]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 1305216be2..d932f068f2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -1,24 +1,26 @@
# frozen_string_literal: true
-require "active_support/deprecation"
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseLimits
+ def max_identifier_length # :nodoc:
+ 64
+ end
+
# Returns the maximum length of a table alias.
def table_alias_length
- 255
+ max_identifier_length
end
# Returns the maximum length of a column name.
def column_name_length
- 64
+ max_identifier_length
end
deprecate :column_name_length
# Returns the maximum length of a table name.
def table_name_length
- 64
+ max_identifier_length
end
deprecate :table_name_length
@@ -33,7 +35,7 @@ module ActiveRecord
# Returns the maximum length of an index name.
def index_name_length
- 64
+ max_identifier_length
end
# Returns the maximum number of columns per table.
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..044272ea51 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, binds)
exec_query(sql, name, binds)
end
@@ -129,11 +142,6 @@ module ActiveRecord
exec_query(sql, name, binds)
end
- # Executes the truncate statement.
- def truncate(table_name, name = nil)
- raise NotImplementedError
- end
-
# Executes update +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
@@ -168,12 +176,22 @@ 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
+ # Executes the truncate statement.
+ def truncate(table_name, name = nil)
+ execute(build_truncate_statements(table_name), name)
+ end
+
+ def truncate_tables(*table_names) # :nodoc:
+ return if table_names.empty?
+
+ with_multi_statements do
+ disable_referential_integrity do
+ Array(build_truncate_statements(*table_names)).each do |sql|
+ execute_batch(sql, "Truncate Tables")
+ end
+ end
+ end
end
- deprecate :supports_statement_cache?
# Runs the given block in a database transaction, and returns the result
# of the block.
@@ -187,8 +205,6 @@ module ActiveRecord
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
# https://dev.mysql.com/doc/refman/5.7/en/savepoint.html
- # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8'
- # supports savepoints.
#
# It is safe to call this method if a database transaction is already open,
# i.e. if #transaction is called within another #transaction block. In case
@@ -331,62 +347,24 @@ 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)
- fixture = fixture.stringify_keys
-
- columns = schema_cache.columns_hash(table_name)
- binds = fixture.map do |name, value|
- if column = columns[name]
- type = lookup_cast_type_from_column(column)
- Relation::QueryAttribute.new(name, value, type)
- else
- raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.)
- end
- end
-
- table = Arel::Table.new(table_name)
-
- values = binds.map do |bind|
- value = with_yaml_fallback(bind.value_for_database)
- [table[bind.name], value]
- end
-
- manager = Arel::InsertManager.new
- manager.into(table)
- manager.insert(values)
- 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")
+ execute(build_fixture_sql(Array.wrap(fixture), table_name), "Fixture Insert")
end
def insert_fixtures_set(fixture_set, tables_to_delete = [])
- fixture_inserts = fixture_set.map do |table_name, fixtures|
- next if fixtures.empty?
-
- build_fixture_sql(fixtures, table_name)
- end.compact
-
- table_deletes = tables_to_delete.map { |table| +"DELETE FROM #{quote_table_name table}" }
- total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts))
-
- disable_referential_integrity do
- transaction(requires_new: true) do
- total_sql.each do |sql|
- execute sql, "Fixtures Load"
- yield if block_given?
+ fixture_inserts = build_fixture_statements(fixture_set)
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name(table)}" }
+ total_sql = Array(combine_multi_statements(table_deletes + fixture_inserts))
+
+ with_multi_statements do
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ total_sql.each do |sql|
+ execute_batch(sql, "Fixtures Load")
+ end
end
end
end
@@ -410,15 +388,33 @@ module ActiveRecord
end
end
+ # Fixture value is quoted by Arel, however scalar values
+ # are not quotable. In this case we want to convert
+ # the column value to YAML.
+ def with_yaml_fallback(value) # :nodoc:
+ if value.is_a?(Hash) || value.is_a?(Array)
+ YAML.dump(value)
+ else
+ value
+ end
+ end
+
private
+ def execute_batch(sql, name = nil)
+ execute(sql, name)
+ end
+
+ DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
+ private_constant :DEFAULT_INSERT_VALUE
+
def default_insert_value(column)
- Arel.sql("DEFAULT")
+ DEFAULT_INSERT_VALUE
end
def build_fixture_sql(fixtures, table_name)
columns = schema_cache.columns_hash(table_name)
- values = fixtures.map do |fixture|
+ values_list = fixtures.map do |fixture|
fixture = fixture.stringify_keys
unknown_columns = fixture.keys - columns.keys
@@ -429,8 +425,7 @@ module ActiveRecord
columns.map do |name, column|
if fixture.key?(name)
type = lookup_cast_type_from_column(column)
- bind = Relation::QueryAttribute.new(name, fixture[name], type)
- with_yaml_fallback(bind.value_for_database)
+ with_yaml_fallback(type.serialize(fixture[name]))
else
default_insert_value(column)
end
@@ -440,12 +435,43 @@ module ActiveRecord
table = Arel::Table.new(table_name)
manager = Arel::InsertManager.new
manager.into(table)
- columns.each_key { |column| manager.columns << table[column] }
- manager.values = manager.create_values_list(values)
+ if values_list.size == 1
+ values = values_list.shift
+ new_values = []
+ columns.each_key.with_index { |column, i|
+ unless values[i].equal?(DEFAULT_INSERT_VALUE)
+ new_values << values[i]
+ manager.columns << table[column]
+ end
+ }
+ values_list << new_values
+ else
+ columns.each_key { |column| manager.columns << table[column] }
+ end
+
+ manager.values = manager.create_values_list(values_list)
manager.to_sql
end
+ def build_fixture_statements(fixture_set)
+ fixture_set.map do |table_name, fixtures|
+ next if fixtures.empty?
+ build_fixture_sql(fixtures, table_name)
+ end.compact
+ end
+
+ def build_truncate_statements(*table_names)
+ truncate_tables = table_names.map do |table_name|
+ "TRUNCATE TABLE #{quote_table_name(table_name)}"
+ end
+ combine_multi_statements(truncate_tables)
+ end
+
+ def with_multi_statements
+ yield
+ end
+
def combine_multi_statements(total_sql)
total_sql.join(";\n")
end
@@ -459,7 +485,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, binds)
[sql, binds]
end
@@ -479,17 +505,6 @@ module ActiveRecord
relation
end
end
-
- # Fixture value is quoted by Arel, however scalar values
- # are not quotable. In this case we want to convert
- # the column value to YAML.
- def with_yaml_fallback(value)
- if value.is_a?(Hash) || value.is_a?(Array)
- YAML.dump(value)
- else
- value
- end
- end
end
end
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..768122b4d2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -7,7 +7,8 @@ module ActiveRecord
module QueryCache
class << self
def included(base) #:nodoc:
- dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction
+ dirties_query_cache base, :insert, :update, :delete, :truncate, :truncate_tables,
+ :rollback_to_savepoint, :rollback_db_transaction
base.set_callback :checkout, :after, :configure_query_cache!
base.set_callback :checkin, :after, :disable_query_cache!
@@ -17,7 +18,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
@@ -32,17 +33,17 @@ module ActiveRecord
end
def enable_query_cache!
- @query_cache_enabled[connection_cache_key(Thread.current)] = true
+ @query_cache_enabled[connection_cache_key(current_thread)] = true
connection.enable_query_cache! if active_connection?
end
def disable_query_cache!
- @query_cache_enabled.delete connection_cache_key(Thread.current)
+ @query_cache_enabled.delete connection_cache_key(current_thread)
connection.disable_query_cache! if active_connection?
end
def query_cache_enabled
- @query_cache_enabled[connection_cache_key(Thread.current)]
+ @query_cache_enabled[connection_cache_key(current_thread)]
end
end
@@ -96,6 +97,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
@@ -103,7 +109,6 @@ module ActiveRecord
end
private
-
def cache_sql(sql, name, binds)
@lock.synchronize do
result =
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 07e86afe9a..93273f6cf6 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -114,16 +114,16 @@ module ActiveRecord
# if the value is a Time responding to usec.
def quoted_date(value)
if value.acts_like?(:time)
- zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
-
- if value.respond_to?(zone_conversion_method)
- value = value.send(zone_conversion_method)
+ if ActiveRecord::Base.default_timezone == :utc
+ value = value.getutc if value.respond_to?(:getutc) && !value.utc?
+ else
+ value = value.getlocal if value.respond_to?(:getlocal)
end
end
result = value.to_s(:db)
if value.respond_to?(:usec) && value.usec > 0
- "#{result}.#{sprintf("%06d", value.usec)}"
+ result << "." << sprintf("%06d", value.usec)
else
result
end
@@ -138,15 +138,72 @@ module ActiveRecord
"'#{quote_string(value.to_s)}'"
end
- def type_casted_binds(binds) # :nodoc:
- if binds.first.is_a?(Array)
- binds.map { |column, value| type_cast(value, column) }
- else
- binds.map { |attr| type_cast(attr.value_for_database) }
- end
+ def sanitize_as_sql_comment(value) # :nodoc:
+ value.to_s.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "")
+ end
+
+ def column_name_matcher # :nodoc:
+ COLUMN_NAME
+ end
+
+ def column_name_with_order_matcher # :nodoc:
+ COLUMN_NAME_WITH_ORDER
end
+ # Regexp for column names (with or without a table name prefix).
+ # Matches the following:
+ #
+ # "#{table_name}.#{column_name}"
+ # "#{column_name}"
+ COLUMN_NAME = /
+ \A
+ (
+ (?:
+ # table_name.column_name | function(one or no argument)
+ ((?:\w+\.)?\w+) | \w+\((?:|\g<2>)\)
+ )
+ (?:(?:\s+AS)?\s+\w+)?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ # Regexp for column names with order (with or without a table name prefix,
+ # with or without various order modifiers). Matches the following:
+ #
+ # "#{table_name}.#{column_name}"
+ # "#{table_name}.#{column_name} #{direction}"
+ # "#{table_name}.#{column_name} #{direction} NULLS FIRST"
+ # "#{table_name}.#{column_name} NULLS LAST"
+ # "#{column_name}"
+ # "#{column_name} #{direction}"
+ # "#{column_name} #{direction} NULLS FIRST"
+ # "#{column_name} NULLS LAST"
+ COLUMN_NAME_WITH_ORDER = /
+ \A
+ (
+ (?:
+ # table_name.column_name | function(one or no argument)
+ ((?:\w+\.)?\w+) | \w+\((?:|\g<2>)\)
+ )
+ (?:\s+ASC|\s+DESC)?
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
+
private
+ def type_casted_binds(binds)
+ if binds.first.is_a?(Array)
+ binds.map { |column, value| type_cast(value, column) }
+ else
+ binds.map { |attr| type_cast(attr.value_for_database) }
+ end
+ end
+
def lookup_cast_type(sql_type)
type_map.lookup(sql_type)
end
@@ -157,13 +214,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 +227,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 +240,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/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
index 52a796b926..d6dbef3fc8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -8,15 +8,15 @@ module ActiveRecord
end
def create_savepoint(name = current_savepoint_name)
- execute("SAVEPOINT #{name}")
+ execute("SAVEPOINT #{name}", "TRANSACTION")
end
def exec_rollback_to_savepoint(name = current_savepoint_name)
- execute("ROLLBACK TO SAVEPOINT #{name}")
+ execute("ROLLBACK TO SAVEPOINT #{name}", "TRANSACTION")
end
def release_savepoint(name = current_savepoint_name)
- execute("RELEASE SAVEPOINT #{name}")
+ execute("RELEASE SAVEPOINT #{name}", "TRANSACTION")
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 2cb0a2a4df..23c993cfc3 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -15,11 +15,10 @@ 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
-
def visit_AlterTable(o)
sql = +"ALTER TABLE #{quote_table_name(o.name)} "
sql << o.adds.map { |col| accept col }.join(" ")
@@ -50,7 +49,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
@@ -127,6 +126,9 @@ module ActiveRecord
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 db489143af..dbd533b4b3 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/deprecation"
-
module ActiveRecord
module ConnectionAdapters #:nodoc:
# Abstract representation of an index definition on a table. Instances of
@@ -104,16 +102,12 @@ 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)
- if to_table_ord
- self.to_table == to_table_ord.to_s
- else
- (to_table.nil? || to_table.to_s == self.to_table) &&
- options.all? { |k, v| self.options[k].to_s == v.to_s }
- end
+ def defined_for?(to_table: nil, **options)
+ (to_table.nil? || to_table.to_s == self.to_table) &&
+ options.all? { |k, v| self.options[k].to_s == v.to_s }
end
private
@@ -200,41 +194,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
@@ -259,18 +256,17 @@ module ActiveRecord
include ColumnMethods
attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys
- attr_writer :indexes
- deprecate :indexes=
def initialize(
+ conn,
name,
temporary: false,
if_not_exists: false,
options: nil,
as: nil,
- comment: nil,
- **
+ comment: nil
)
+ @conn = conn
@columns_hash = {}
@indexes = []
@foreign_keys = []
@@ -363,7 +359,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
@@ -397,10 +393,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
@@ -410,6 +403,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
@@ -418,6 +415,7 @@ module ActiveRecord
#
# t.references(:user)
# t.belongs_to(:supplier, foreign_key: true)
+ # t.belongs_to(:supplier, foreign_key: true, type: :integer)
#
# See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
def references(*args, **options)
@@ -517,6 +515,7 @@ module ActiveRecord
# t.json
# t.virtual
# t.remove
+ # t.remove_foreign_key
# t.remove_references
# t.remove_belongs_to
# t.remove_index
@@ -538,7 +537,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
@@ -680,15 +679,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_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index 622e00fffb..fb56e712be 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -15,7 +15,7 @@ module ActiveRecord
def column_spec_for_primary_key(column)
return {} if default_primary_key?(column)
spec = { id: schema_type(column).inspect }
- spec.merge!(prepare_column_options(column).except!(:null))
+ spec.merge!(prepare_column_options(column).except!(:null, :comment))
spec[:default] ||= "nil" if explicit_primary_key_default?(column)
spec
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 38cfc3a241..13f94a4722 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -129,11 +129,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
@@ -290,25 +290,27 @@ module ActiveRecord
# SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
#
# See also TableDefinition#column for details on how to create columns.
- def create_table(table_name, **options)
- td = create_table_definition(table_name, options)
+ def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
+ td = create_table_definition(
+ table_name, options.extract!(:temporary, :if_not_exists, :options, :as, :comment)
+ )
- if options[:id] != false && !options[:as]
- pk = options.fetch(:primary_key) do
- Base.get_primary_key table_name.to_s.singularize
- end
+ if id && !td.as
+ pk = primary_key || Base.get_primary_key(table_name.to_s.singularize)
if pk.is_a?(Array)
td.primary_keys pk
else
- td.primary_key pk, options.fetch(:id, :primary_key), options
+ td.primary_key pk, id, options
end
end
yield td if block_given?
- if options[:force]
- drop_table(table_name, options.merge(if_exists: true))
+ if force
+ drop_table(table_name, force: force, if_exists: true)
+ else
+ schema_cache.clear_data_source_cache!(table_name.to_s)
end
result = execute schema_creation.accept td
@@ -320,7 +322,7 @@ module ActiveRecord
end
if supports_comments? && !supports_comments_in_create?
- if table_comment = options[:comment].presence
+ if table_comment = td.comment.presence
change_table_comment(table_name, table_comment)
end
@@ -498,6 +500,7 @@ module ActiveRecord
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by #create_table.
def drop_table(table_name, options = {})
+ schema_cache.clear_data_source_cache!(table_name.to_s)
execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
end
@@ -517,14 +520,15 @@ module ActiveRecord
# Available options are (none of these exists by default):
# * <tt>:limit</tt> -
# Requests a maximum column length. This is the number of characters for a <tt>:string</tt> column
- # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns.
+ # and number of bytes for <tt>:text</tt>, <tt>:binary</tt>, and <tt>:integer</tt> columns.
# This option is ignored by some backends.
# * <tt>:default</tt> -
# The column's default value. Use +nil+ for +NULL+.
# * <tt>:null</tt> -
# Allows or disallows +NULL+ values in the column.
# * <tt>:precision</tt> -
- # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
+ # Specifies the precision for the <tt>:decimal</tt>, <tt>:numeric</tt>,
+ # <tt>:datetime</tt>, and <tt>:time</tt> columns.
# * <tt>:scale</tt> -
# Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
# * <tt>:collation</tt> -
@@ -583,7 +587,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
@@ -734,7 +738,7 @@ module ActiveRecord
#
# CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
#
- # Note: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+.
+ # Note: Partial indexes are only supported for PostgreSQL and SQLite.
#
# ====== Creating an index with a specific method
#
@@ -769,6 +773,17 @@ module ActiveRecord
# CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL
#
# Note: only supported by MySQL.
+ #
+ # ====== Creating an index with a specific algorithm
+ #
+ # add_index(:developers, :name, algorithm: :concurrently)
+ # # CREATE INDEX CONCURRENTLY developers_on_name on developers (name)
+ #
+ # Note: only supported by PostgreSQL.
+ #
+ # Concurrently adding an index is not supported in a transaction.
+ #
+ # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration].
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}"
@@ -792,6 +807,15 @@ module ActiveRecord
#
# remove_index :accounts, name: :by_branch_party
#
+ # Removes the index named +by_branch_party+ in the +accounts+ table +concurrently+.
+ #
+ # remove_index :accounts, name: :by_branch_party, algorithm: :concurrently
+ #
+ # Note: only supported by PostgreSQL.
+ #
+ # Concurrently removing an index is not supported in a transaction.
+ #
+ # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration].
def remove_index(table_name, options = {})
index_name = index_name_for_remove(table_name, options)
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
@@ -851,7 +875,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)
#
@@ -965,7 +989,7 @@ module ActiveRecord
# [<tt>:on_update</tt>]
# Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
# [<tt>:validate</tt>]
- # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+.
+ # (PostgreSQL only) Specify whether or not the constraint should be validated. Defaults to +true+.
def add_foreign_key(from_table, to_table, options = {})
return unless supports_foreign_keys?
@@ -1001,10 +1025,10 @@ module ActiveRecord
# with an addition of
# [<tt>:to_table</tt>]
# The name of the table that contains the referenced primary key.
- def remove_foreign_key(from_table, options_or_to_table = {})
+ def remove_foreign_key(from_table, to_table = nil, **options)
return unless supports_foreign_keys?
- fk_name_to_delete = foreign_key_for!(from_table, options_or_to_table).name
+ fk_name_to_delete = foreign_key_for!(from_table, to_table: to_table, **options).name
at = create_alter_table from_table
at.drop_foreign_key fk_name_to_delete
@@ -1023,14 +1047,12 @@ module ActiveRecord
# # Checks to see if a foreign key with a custom name exists.
# foreign_key_exists?(:accounts, name: "special_fk_name")
#
- def foreign_key_exists?(from_table, options_or_to_table = {})
- foreign_key_for(from_table, options_or_to_table).present?
+ def foreign_key_exists?(from_table, to_table = nil, **options)
+ foreign_key_for(from_table, to_table: to_table, **options).present?
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
@@ -1041,8 +1063,8 @@ module ActiveRecord
options
end
- def dump_schema_information #:nodoc:
- versions = ActiveRecord::SchemaMigration.all_versions
+ def dump_schema_information # :nodoc:
+ versions = schema_migration.all_versions
insert_versions_sql(versions) if versions.any?
end
@@ -1050,15 +1072,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.squish)
+ 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)
+ sm_table = quote_table_name(schema_migration.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)})"
@@ -1095,7 +1120,7 @@ module ActiveRecord
if (0..6) === precision
column_type_sql << "(#{precision})"
else
- raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6")
+ raise ArgumentError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6"
end
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
column_type_sql << "(#{limit})"
@@ -1125,6 +1150,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
@@ -1179,12 +1208,22 @@ module ActiveRecord
end
# Changes the comment for a table or removes it if +nil+.
- def change_table_comment(table_name, comment)
+ #
+ # Passing a hash containing +:from+ and +:to+ will make this change
+ # reversible in migration:
+ #
+ # change_table_comment(:posts, from: "old_comment", to: "new_comment")
+ def change_table_comment(table_name, comment_or_changes)
raise NotImplementedError, "#{self.class} does not support changing table comments"
end
# Changes the comment for a column or removes it if +nil+.
- def change_column_comment(table_name, column_name, comment)
+ #
+ # Passing a hash containing +:from+ and +:to+ will make this change
+ # reversible in migration:
+ #
+ # change_column_comment(:posts, :state, from: "old_comment", to: "new_comment")
+ def change_column_comment(table_name, column_name, comment_or_changes)
raise NotImplementedError, "#{self.class} does not support changing column comments"
end
@@ -1286,7 +1325,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- TableDefinition.new(*args)
+ TableDefinition.new(self, *args)
end
def create_alter_table(name)
@@ -1320,6 +1359,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"
@@ -1329,14 +1374,14 @@ module ActiveRecord
end
end
- def foreign_key_for(from_table, options_or_to_table = {})
+ def foreign_key_for(from_table, **options)
return unless supports_foreign_keys?
- foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table }
+ foreign_keys(from_table).detect { |fk| fk.defined_for?(options) }
end
- def foreign_key_for!(from_table, options_or_to_table = {})
- foreign_key_for(from_table, options_or_to_table) || \
- raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}")
+ def foreign_key_for!(from_table, to_table: nil, **options)
+ foreign_key_for(from_table, to_table: to_table, **options) ||
+ raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table || options}")
end
def extract_foreign_key_action(specifier)
@@ -1362,11 +1407,37 @@ module ActiveRecord
default_or_changes
end
end
+ alias :extract_new_comment_value :extract_new_default_value
def can_remove_index_by_name?(options)
options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty?
end
+ def bulk_change_table(table_name, operations)
+ sql_fragments = []
+ non_combinable_operations = []
+
+ operations.each do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_for_alter"
+
+ if respond_to?(method, true)
+ sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
+ sql_fragments << sqls
+ non_combinable_operations.concat(procs)
+ else
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ sql_fragments = []
+ non_combinable_operations = []
+ send(command, table, *arguments)
+ end
+ end
+
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ end
+
def add_column_for_alter(table_name, column_name, type, options = {})
td = create_table_definition(table_name)
cd = td.new_column_definition(column_name, type, options)
@@ -1382,7 +1453,7 @@ module ActiveRecord
end
def insert_versions_sql(versions)
- sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
+ sm_table = quote_table_name(schema_migration.table_name)
if versions.is_a?(Array)
sql = +"INSERT INTO #{sm_table} (version) VALUES\n"
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 0f2b1e85ff..53ce8df491 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -5,10 +5,11 @@ module ActiveRecord
class TransactionState
def initialize(state = nil)
@state = state
- @children = []
+ @children = nil
end
def add_child(state)
+ @children ||= []
@children << state
end
@@ -40,31 +41,13 @@ 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! }
+ @children&.each { |c| c.rollback! }
@state = :rolledback
end
def full_rollback!
- @children.each { |c| c.rollback! }
+ @children&.each { |c| c.rollback! }
@state = :fully_rolledback
end
@@ -93,18 +76,19 @@ module ActiveRecord
class Transaction #:nodoc:
attr_reader :connection, :state, :records, :savepoint_name, :isolation_level
- def initialize(connection, options, run_commit_callbacks: false)
+ def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)
@connection = connection
@state = TransactionState.new
- @records = []
- @isolation_level = options[:isolation]
+ @records = nil
+ @isolation_level = isolation
@materialized = false
- @joinable = options.fetch(:joinable, true)
+ @joinable = joinable
@run_commit_callbacks = run_commit_callbacks
end
def add_record(record)
- records << record
+ @records ||= []
+ @records << record
end
def materialize!
@@ -116,32 +100,42 @@ module ActiveRecord
end
def rollback_records
- ite = records.uniq
+ return unless records
+ ite = records.uniq(&:object_id)
+ already_run_callbacks = {}
while record = ite.shift
- record.rolledback!(force_restore_state: full_rollback?)
+ trigger_callbacks = record.trigger_transactional_callbacks?
+ should_run_callbacks = !already_run_callbacks[record] && trigger_callbacks
+ already_run_callbacks[record] ||= trigger_callbacks
+ record.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: should_run_callbacks)
end
ensure
- ite.each do |i|
+ ite&.each do |i|
i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false)
end
end
def before_commit_records
- records.uniq.each(&:before_committed!) if @run_commit_callbacks
+ records.uniq.each(&:before_committed!) if records && @run_commit_callbacks
end
def commit_records
- ite = records.uniq
+ return unless records
+ ite = records.uniq(&:object_id)
+ already_run_callbacks = {}
while record = ite.shift
if @run_commit_callbacks
- record.committed!
+ trigger_callbacks = record.trigger_transactional_callbacks?
+ should_run_callbacks = !already_run_callbacks[record] && trigger_callbacks
+ already_run_callbacks[record] ||= trigger_callbacks
+ record.committed!(should_run_callbacks: should_run_callbacks)
else
# if not running callbacks, only adds the record to the parent transaction
connection.add_transaction_record(record)
end
end
ensure
- ite.each { |i| i.committed!(should_run_callbacks: false) }
+ ite&.each { |i| i.committed!(should_run_callbacks: false) }
end
def full_rollback?; true; end
@@ -151,8 +145,8 @@ module ActiveRecord
end
class SavepointTransaction < Transaction
- def initialize(connection, savepoint_name, parent_transaction, *args)
- super(connection, *args)
+ def initialize(connection, savepoint_name, parent_transaction, **options)
+ super(connection, options)
parent_transaction.state.add_child(@state)
@@ -212,20 +206,34 @@ module ActiveRecord
@lazy_transactions_enabled = true
end
- def begin_transaction(options = {})
+ def begin_transaction(isolation: nil, joinable: true, _lazy: true)
@connection.lock.synchronize do
run_commit_callbacks = !current_transaction.joinable?
transaction =
if @stack.empty?
- RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks)
+ RealTransaction.new(
+ @connection,
+ isolation: isolation,
+ joinable: joinable,
+ run_commit_callbacks: run_commit_callbacks
+ )
else
- SavepointTransaction.new(@connection, "active_record_#{@stack.size}", @stack.last, options,
- run_commit_callbacks: run_commit_callbacks)
+ SavepointTransaction.new(
+ @connection,
+ "active_record_#{@stack.size}",
+ @stack.last,
+ isolation: isolation,
+ joinable: joinable,
+ run_commit_callbacks: run_commit_callbacks
+ )
end
- transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled?
+ if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && _lazy
+ @has_unmaterialized_transactions = true
+ else
+ transaction.materialize!
+ end
@stack.push(transaction)
- @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions?
transaction
end
end
@@ -281,28 +289,26 @@ module ActiveRecord
end
end
- def within_new_transaction(options = {})
+ def within_new_transaction(isolation: nil, joinable: true)
@connection.lock.synchronize do
- begin
- transaction = begin_transaction options
- yield
- rescue Exception => error
- if transaction
+ transaction = begin_transaction(isolation: isolation, joinable: joinable)
+ 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
@@ -318,7 +324,6 @@ module ActiveRecord
end
private
-
NULL_TRANSACTION = NullTransaction.new
# Deallocate invalidated prepared statements outside of the transaction
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 0fe868478c..dc970c384b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -76,8 +76,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 :visitor, :owner, :logger, :lock, :prepared_statements
alias :in_use? :owner
set_callback :checkin, :after, :enable_lazy_transactions!
@@ -100,6 +100,19 @@ 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 self.quoted_column_names # :nodoc:
+ @quoted_column_names ||= {}
+ end
+
+ def self.quoted_table_names # :nodoc:
+ @quoted_table_names ||= {}
+ end
+
def initialize(connection, logger = nil, config = {}) # :nodoc:
super()
@@ -108,11 +121,10 @@ module ActiveRecord
@instrumenter = ActiveSupport::Notifications.instrumenter
@logger = logger
@config = config
- @pool = nil
+ @pool = ActiveRecord::ConnectionAdapters::NullPool.new
@idle_since = Concurrent.monotonic_time
- @schema_cache = SchemaCache.new self
- @quoted_column_names, @quoted_table_names = {}, {}
@visitor = arel_visitor
+ @statements = build_statement_pool
@lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@@ -125,27 +137,51 @@ module ActiveRecord
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
config.fetch(:advisory_locks, true)
)
-
- check_version
end
def replica?
@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? || ActiveRecord::Base.connection_handler.prevent_writes
+ end
+
def migrations_paths # :nodoc:
@config[:migrations_paths] || Migrator.migrations_paths
end
def migration_context # :nodoc:
- MigrationContext.new(migrations_paths)
+ MigrationContext.new(migrations_paths, schema_migration)
+ end
+
+ def schema_migration # :nodoc:
+ @schema_migration ||= begin
+ conn = self
+ spec_name = conn.pool.spec.name
+ name = "#{spec_name}::SchemaMigration"
+
+ Class.new(ActiveRecord::SchemaMigration) do
+ define_singleton_method(:name) { name }
+ define_singleton_method(:to_s) { name }
+
+ self.connection_specification_name = spec_name
+ end
+ end
end
class Version
include Comparable
- def initialize(version_string)
+ attr_reader :full_version_string
+
+ def initialize(version_string, full_version_string = nil)
@version = version_string.split(".").map(&:to_i)
+ @full_version_string = full_version_string
end
def <=>(version_string)
@@ -177,9 +213,13 @@ module ActiveRecord
@owner = Thread.current
end
+ def schema_cache
+ @pool.get_schema_cache(self)
+ end
+
def schema_cache=(cache)
cache.connection = self
- @schema_cache = cache
+ @pool.set_schema_cache(cache)
end
# this method must only be called while holding connection pool's mutex
@@ -230,6 +270,11 @@ module ActiveRecord
self.class::ADAPTER_NAME
end
+ # Does the database for this adapter exist?
+ def self.database_exists?(config)
+ raise NotImplementedError
+ end
+
# Does this adapter support DDL rollbacks in transactions? That is, would
# CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
@@ -308,12 +353,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
@@ -350,10 +401,31 @@ module ActiveRecord
false
end
+ # Does this adapter support optimizer hints?
+ def supports_optimizer_hints?
+ false
+ end
+
def supports_lazy_transactions?
false
end
+ def supports_insert_returning?
+ false
+ end
+
+ def supports_insert_on_duplicate_skip?
+ false
+ end
+
+ def supports_insert_on_duplicate_update?
+ false
+ end
+
+ def supports_insert_conflict_target?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -431,6 +503,9 @@ module ActiveRecord
#
# Prevent @connection's finalizer from touching the socket, or
# otherwise communicating with its server, when it is collected.
+ if schema_cache.connection == self
+ schema_cache.connection = nil
+ end
end
# Reset the state of this connection, directing the DBMS to clear
@@ -443,11 +518,9 @@ module ActiveRecord
# this should be overridden by concrete adapters
end
- ###
- # Clear any caching the database adapter may be doing, for example
- # clearing the prepared statement cache. This is database specific.
+ # Clear any caching the database adapter may be doing.
def clear_cache!
- # this should be overridden by concrete adapters
+ @lock.synchronize { @statements.clear } if @statements
end
# Returns true if its required to reload the connection between requests for development mode.
@@ -473,15 +546,21 @@ module ActiveRecord
@connection
end
- def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
- table[attribute].eq(value)
+ def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
+ attribute.eq(value)
end
- def case_insensitive_comparison(table, attribute, column, value) # :nodoc:
+ def case_sensitive_comparison(attribute, value) # :nodoc:
+ attribute.eq(value)
+ end
+
+ 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
@@ -503,10 +582,30 @@ module ActiveRecord
index.using.nil?
end
- private
- def check_version
+ # Called by ActiveRecord::InsertAll,
+ # Passed an instance of ActiveRecord::InsertAll::Builder,
+ # This method implements standard bulk inserts for all databases, but
+ # should be overridden by adapters to implement common features with
+ # non-standard syntax like handling duplicates or returning values.
+ def build_insert_sql(insert) # :nodoc:
+ if insert.skip_duplicates? || insert.update_duplicates?
+ raise NotImplementedError, "#{self.class} should define `build_insert_sql` to implement adapter-specific logic for handling duplicates during INSERT"
end
+ "INSERT #{insert.into} #{insert.values_list}"
+ end
+
+ def get_database_version # :nodoc:
+ end
+
+ def database_version # :nodoc:
+ schema_cache.database_version
+ end
+
+ def check_version # :nodoc:
+ end
+
+ private
def type_map
@type_map ||= Type::TypeMap.new.tap do |mapping|
initialize_type_map(mapping)
@@ -580,14 +679,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 +697,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 +727,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(
@@ -648,6 +749,9 @@ module ActiveRecord
def arel_visitor
Arel::Visitors::ToSql.new(self)
end
+
+ def build_statement_pool
+ end
end
end
end
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 13c799b64a..405fecb603 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,14 +37,14 @@ 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" },
}
class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private
-
def dealloc(stmt)
stmt.close
end
@@ -52,28 +52,28 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
-
- @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
end
- def version #:nodoc:
- @version ||= Version.new(version_string)
+ def get_database_version #:nodoc:
+ full_version_string = get_full_version
+ version_string = version_string(full_version_string)
+ Version.new(version_string, full_version_string)
end
def mariadb? # :nodoc:
/mariadb/i.match?(full_version)
end
- def supports_bulk_alter? #:nodoc:
+ def supports_bulk_alter?
true
end
def supports_index_sort_order?
- !mariadb? && version >= "8.0.1"
+ !mariadb? && database_version >= "8.0.1"
end
def supports_expression_index?
- !mariadb? && version >= "8.0.13"
+ !mariadb? && database_version >= "8.0.13"
end
def supports_transaction_isolation?
@@ -97,31 +97,28 @@ module ActiveRecord
end
def supports_datetime_with_precision?
- if mariadb?
- version >= "5.3.0"
- else
- version >= "5.6.4"
- end
+ mariadb? || database_version >= "5.6.4"
end
def supports_virtual_columns?
- if mariadb?
- version >= "5.2.0"
- else
- version >= "5.7.5"
- end
+ mariadb? || database_version >= "5.7.5"
+ end
+
+ # See https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html for more details.
+ def supports_optimizer_hints?
+ !mariadb? && database_version >= "5.7.7"
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
+ def supports_insert_on_duplicate_skip?
+ true
+ end
+
+ def supports_insert_on_duplicate_update?
+ true
end
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
@@ -169,25 +166,15 @@ module ActiveRecord
# CONNECTION MANAGEMENT ====================================
- # Clears the prepared statements cache.
- def clear_cache!
+ def clear_cache! # :nodoc:
reload_type_map
- @statements.clear
+ super
end
#--
# DATABASE STATEMENTS ======================================
#++
- def explain(arel, binds = [])
- sql = "EXPLAIN #{to_sql(arel, binds)}"
- start = Time.now
- result = exec_query(sql, "EXPLAIN", binds)
- elapsed = Time.now - start
-
- MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
- end
-
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
materialize_transactions
@@ -207,7 +194,7 @@ module ActiveRecord
end
def begin_db_transaction
- execute "BEGIN"
+ execute("BEGIN", "TRANSACTION")
end
def begin_isolated_db_transaction(isolation)
@@ -216,11 +203,11 @@ module ActiveRecord
end
def commit_db_transaction #:nodoc:
- execute "COMMIT"
+ execute("COMMIT", "TRANSACTION")
end
def exec_rollback_db_transaction #:nodoc:
- execute "ROLLBACK"
+ execute("ROLLBACK", "TRANSACTION")
end
def empty_insert_statement_value(primary_key = nil)
@@ -250,7 +237,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."
@@ -279,10 +266,6 @@ module ActiveRecord
show_variable "collation_database"
end
- def truncate(table_name, name = nil)
- execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
- end
-
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name)
@@ -294,22 +277,8 @@ module ActiveRecord
SQL
end
- def bulk_change_table(table_name, operations) #:nodoc:
- sqls = operations.flat_map do |command, args|
- table, arguments = args.shift, args
- method = :"#{command}_for_alter"
-
- if respond_to?(method, true)
- send(method, table, *arguments)
- else
- raise "Unknown method called : #{method}(#{arguments.inspect})"
- end
- end.join(", ")
-
- execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
- end
-
- def change_table_comment(table_name, comment) #:nodoc:
+ def change_table_comment(table_name, comment_or_changes) # :nodoc:
+ comment = extract_new_comment_value(comment_or_changes)
comment = "" if comment.nil?
execute("ALTER TABLE #{quote_table_name(table_name)} COMMENT #{quote(comment)}")
end
@@ -319,6 +288,8 @@ module ActiveRecord
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
+ schema_cache.clear_data_source_cache!(table_name.to_s)
+ schema_cache.clear_data_source_cache!(new_name.to_s)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
rename_table_indexes(table_name, new_name)
end
@@ -339,6 +310,7 @@ module ActiveRecord
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
+ schema_cache.clear_data_source_cache!(table_name.to_s)
execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
@@ -365,7 +337,8 @@ module ActiveRecord
change_column table_name, column_name, nil, null: null
end
- def change_column_comment(table_name, column_name, comment) #:nodoc:
+ def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
+ comment = extract_new_comment_value(comment_or_changes)
change_column table_name, column_name, nil, comment: comment
end
@@ -446,30 +419,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")
@@ -492,9 +441,26 @@ module ActiveRecord
SQL
end
- def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
+ def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
+ column = column_for_attribute(attribute)
+
+ if column.collation && !column.case_sensitive? && !value.nil?
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
+ Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
+ To continue case sensitive comparison on the :#{attribute.name} attribute in #{klass} model,
+ pass `case_sensitive: true` option explicitly to the uniqueness validator.
+ MSG
+ attribute.eq(Arel::Nodes::Bin.new(value))
+ else
+ super
+ end
+ end
+
+ 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
@@ -528,46 +494,27 @@ module ActiveRecord
index.using == :btree || super
end
- def insert_fixtures_set(fixture_set, tables_to_delete = [])
- with_multi_statements do
- super { discard_remaining_results }
- end
- end
+ def build_insert_sql(insert) # :nodoc:
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
- private
- def check_version
- if version < "5.5.8"
- raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
- end
+ if insert.skip_duplicates?
+ no_op_column = quote_column_name(insert.keys.first)
+ sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
+ elsif insert.update_duplicates?
+ sql << " ON DUPLICATE KEY UPDATE "
+ sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")
end
- def combine_multi_statements(total_sql)
- total_sql.each_with_object([]) do |sql, total_sql_chunks|
- previous_packet = total_sql_chunks.last
- sql << ";\n"
- if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty?
- total_sql_chunks << sql
- else
- previous_packet << sql
- end
- end
- end
-
- def max_allowed_packet_reached?(current_packet, previous_packet)
- if current_packet.bytesize > max_allowed_packet
- raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
- elsif previous_packet.nil?
- false
- else
- (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
- end
- end
+ sql
+ end
- def max_allowed_packet
- bytes_margin = 2
- @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin)
+ def check_version # :nodoc:
+ if database_version < "5.5.8"
+ raise "Your version of MySQL (#{database_version}) is too old. Active Record supports MySQL >= 5.5.8."
end
+ end
+ private
def initialize_type_map(m = type_map)
super
@@ -595,13 +542,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
@@ -641,37 +588,42 @@ 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)
+ 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)
- when ER_CANNOT_ADD_FOREIGN
- mismatched_foreign_key(message)
+ 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
+ if exception.is_a?(Mysql2::Error::TimeoutError)
+ ActiveRecord::AdapterTimeout.new(message, sql: sql, binds: binds)
+ else
+ super
+ end
end
end
@@ -722,6 +674,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
@@ -730,7 +688,7 @@ module ActiveRecord
end
def supports_rename_index?
- mariadb? ? false : version >= "5.7.6"
+ mariadb? ? false : database_version >= "5.7.6"
end
def configure_connection
@@ -783,7 +741,7 @@ module ActiveRecord
end.compact.join(", ")
# ...and send them all in one query
- execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
+ execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA")
end
def column_definitions(table_name) # :nodoc:
@@ -800,51 +758,36 @@ 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],
- )
+ def build_statement_pool
+ StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
end
- 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
+ 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 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}")
- end
- end
+ options = {
+ message: message,
+ sql: sql,
+ binds: binds,
+ }
- 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}")
+ 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
+
+ MismatchedForeignKey.new(options)
end
- def version_string
- full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
+ def version_string(full_version_string)
+ full_version_string.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
end
class MysqlString < Type::String # :nodoc:
@@ -857,7 +800,6 @@ module ActiveRecord
end
private
-
def cast_value(value)
case value
when true then "1"
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 5d81de9fe1..2708d2756b 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,7 +5,9 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment
+ include Deduplicable
+
+ attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
@@ -15,9 +17,8 @@ module ActiveRecord
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil, **)
+ def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **)
@name = name.freeze
- @table_name = table_name
@sql_type_metadata = sql_type_metadata
@null = null
@default = default
@@ -44,7 +45,6 @@ module ActiveRecord
def init_with(coder)
@name = coder["name"]
- @table_name = coder["table_name"]
@sql_type_metadata = coder["sql_type_metadata"]
@null = coder["null"]
@default = coder["default"]
@@ -55,7 +55,6 @@ module ActiveRecord
def encode_with(coder)
coder["name"] = @name
- coder["table_name"] = @table_name
coder["sql_type_metadata"] = @sql_type_metadata
coder["null"] = @null
coder["default"] = @default
@@ -66,18 +65,37 @@ module ActiveRecord
def ==(other)
other.is_a?(Column) &&
- attributes_for_hash == other.attributes_for_hash
+ name == other.name &&
+ default == other.default &&
+ sql_type_metadata == other.sql_type_metadata &&
+ null == other.null &&
+ default_function == other.default_function &&
+ collation == other.collation &&
+ comment == other.comment
end
alias :eql? :==
def hash
- attributes_for_hash.hash
+ Column.hash ^
+ name.hash ^
+ name.encoding.hash ^
+ default.hash ^
+ sql_type_metadata.hash ^
+ null.hash ^
+ default_function.hash ^
+ collation.hash ^
+ comment.hash
end
- protected
-
- def attributes_for_hash
- [self.class, name, default, sql_type_metadata, null, table_name, default_function, collation]
+ private
+ def deduplicated
+ @name = -name
+ @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
+ @default = -default if default
+ @default_function = -default_function if default_function
+ @collation = -collation if collation
+ @comment = -comment if comment
+ super
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 2e7a78215a..df26f67c6e 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -56,7 +56,6 @@ module ActiveRecord
end
private
-
attr_reader :uri
def uri_parser
@@ -174,12 +173,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 +247,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/deduplicable.rb b/activerecord/lib/active_record/connection_adapters/deduplicable.rb
new file mode 100644
index 0000000000..fb2fd60bbc
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/deduplicable.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters # :nodoc:
+ module Deduplicable
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def registry
+ @registry ||= {}
+ end
+
+ def new(*)
+ super.deduplicate
+ end
+ end
+
+ def deduplicate
+ self.class.registry[self] ||= deduplicated
+ end
+ alias :-@ :deduplicate
+
+ private
+ def deduplicated
+ freeze
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
index 883747b84b..97d74df529 100644
--- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
+++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
@@ -3,9 +3,9 @@
module ActiveRecord
module ConnectionAdapters
module DetermineIfPreparableVisitor
- attr_reader :preparable
+ attr_accessor :preparable
- def accept(*)
+ def accept(object, collector)
@preparable = true
super
end
@@ -20,7 +20,7 @@ module ActiveRecord
super
end
- def visit_Arel_Nodes_SqlLiteral(*)
+ def visit_Arel_Nodes_SqlLiteral(o, collector)
@preparable = false
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index 684c7042a7..bbcdc96cdc 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -11,7 +11,7 @@ module ActiveRecord
else
super
end
- discard_remaining_results
+ @connection.abandon_results!
result
end
@@ -19,8 +19,28 @@ 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
+
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
+ start = Concurrent.monotonic_time
+ result = exec_query(sql, "EXPLAIN", binds)
+ elapsed = Concurrent.monotonic_time - start
+
+ MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
+ if preventing_writes? && write_query?(sql)
+ 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,24 +49,30 @@ 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 }
+ @lock.synchronize do
+ execute_and_free(sql, name) { @connection.affected_rows }
+ end
else
exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows }
end
@@ -54,22 +80,31 @@ module ActiveRecord
alias :exec_update :exec_delete
private
+ def execute_batch(sql, name = nil)
+ super
+ @connection.abandon_results!
+ end
+
def default_insert_value(column)
- Arel.sql("DEFAULT") unless column.auto_increment?
+ super unless column.auto_increment?
end
def last_inserted_id(result)
@connection.last_id
end
- def discard_remaining_results
- @connection.abandon_results!
- end
-
def supports_set_server_option?
@connection.respond_to?(:set_server_option)
end
+ def build_truncate_statements(*table_names)
+ if table_names.size == 1
+ super.first
+ else
+ super
+ end
+ end
+
def multi_statements_enabled?(flags)
if flags.is_a?(Array)
flags.include?("MULTI_STATEMENTS")
@@ -102,7 +137,40 @@ module ActiveRecord
end
end
+ def combine_multi_statements(total_sql)
+ total_sql.each_with_object([]) do |sql, total_sql_chunks|
+ previous_packet = total_sql_chunks.last
+ if max_allowed_packet_reached?(sql, previous_packet)
+ total_sql_chunks << +sql
+ else
+ previous_packet << ";\n"
+ previous_packet << sql
+ end
+ end
+ end
+
+ def max_allowed_packet_reached?(current_packet, previous_packet)
+ if current_packet.bytesize > max_allowed_packet
+ raise ActiveRecordError,
+ "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
+ elsif previous_packet.nil?
+ true
+ else
+ (current_packet.bytesize + previous_packet.bytesize + 2) > max_allowed_packet
+ end
+ end
+
+ def max_allowed_packet
+ @max_allowed_packet ||= show_variable("max_allowed_packet")
+ 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/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
index 20c3c83664..edd5ea0542 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
@@ -37,7 +37,6 @@ module ActiveRecord
end
private
-
def compute_column_widths(result)
[].tap do |widths|
result.columns.each_with_index do |column, i|
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
index 75564a61d6..0069f5871c 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -5,11 +5,11 @@ module ActiveRecord
module MySQL
module Quoting # :nodoc:
def quote_column_name(name)
- @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
+ self.class.quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
end
def quote_table_name(name)
- @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
+ self.class.quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
end
def unquoted_true
@@ -32,12 +32,49 @@ module ActiveRecord
"x'#{value.hex}'"
end
- def _type_cast(value)
- case value
- when Date, Time then value
- else super
- end
+ def column_name_matcher
+ COLUMN_NAME
+ end
+
+ def column_name_with_order_matcher
+ COLUMN_NAME_WITH_ORDER
end
+
+ COLUMN_NAME = /
+ \A
+ (
+ (?:
+ # `table_name`.`column_name` | function(one or no argument)
+ ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`)) | \w+\((?:|\g<2>)\)
+ )
+ (?:(?:\s+AS)?\s+(?:\w+|`\w+`))?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ COLUMN_NAME_WITH_ORDER = /
+ \A
+ (
+ (?:
+ # `table_name`.`column_name` | function(one or no argument)
+ ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`)) | \w+\((?:|\g<2>)\)
+ )
+ (?:\s+ASC|\s+DESC)?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
+
+ private
+ def _type_cast(value)
+ case value
+ when Date, Time then value
+ else super
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
index 82ed320617..0f5ab7562a 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -7,7 +7,6 @@ module ActiveRecord
delegate :add_sql_comment!, :mariadb?, to: :@conn, private: true
private
-
def visit_DropForeignKey(name)
"DROP FOREIGN KEY #{name}"
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_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..bcd300f3db 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,19 +41,23 @@ module ActiveRecord
case column.sql_type
when /\Atimestamp\b/
:timestamp
- when "tinyblob"
- :blob
+ when /\A(?:enum|set)\b/
+ column.sql_type
else
super
end
end
+ def schema_limit(column)
+ super unless /\A(?:enum|set|(?:tiny|medium|long)?(?:text|blob))\b/.match?(column.sql_type)
+ end
+
def schema_precision(column)
super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0
end
def schema_collation(column)
- if column.collation && table_name = column.table_name
+ if column.collation
@table_collation_cache ||= {}
@table_collation_cache[table_name] ||=
@connection.exec_query("SHOW TABLE STATUS LIKE #{@connection.quote(table_name)}", "SCHEMA").first["Collation"]
@@ -58,14 +66,14 @@ module ActiveRecord
end
def extract_expression_for_virtual_column(column)
- if @connection.mariadb? && @connection.version < "10.2.5"
- create_table_info = @connection.send(:create_table_info, column.table_name)
+ if @connection.mariadb? && @connection.database_version < "10.2.5"
+ create_table_info = @connection.send(:create_table_info, table_name)
column_name = @connection.quote_column_name(column.name)
if %r/#{column_name} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info
$~[:expression].inspect
end
else
- scope = @connection.send(:quoted_scope, column.table_name)
+ scope = @connection.send(:quoted_scope, table_name)
column_name = @connection.quote(column.name)
sql = "SELECT generation_expression FROM information_schema.columns" \
" WHERE table_schema = #{scope[:schema]}" \
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 4894fd1c08..25a1fb234a 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -77,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
@@ -93,15 +97,65 @@ 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
+
+ def table_alias_length
+ 256 # https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
+ end
+
private
CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"]
+ def row_format_dynamic_by_default?
+ if mariadb?
+ database_version >= "10.2.2"
+ else
+ database_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)
@@ -120,9 +174,8 @@ module ActiveRecord
default,
type_metadata,
field[:Null] == "YES",
- table_name,
default_function,
- field[:Collation],
+ collation: field[:Collation],
comment: field[:Comment].presence
)
end
@@ -171,6 +224,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 ArgumentError, "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 ArgumentError, "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/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
index 7ad0944d51..a7232fa249 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
@@ -6,28 +6,33 @@ module ActiveRecord
class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
undef to_yaml if method_defined?(:to_yaml)
+ include Deduplicable
+
attr_reader :extra
- def initialize(type_metadata, extra: "")
+ def initialize(type_metadata, extra: nil)
super(type_metadata)
- @type_metadata = type_metadata
@extra = extra
end
def ==(other)
- other.is_a?(MySQL::TypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
+ other.is_a?(TypeMetadata) &&
+ __getobj__ == other.__getobj__ &&
+ extra == other.extra
end
alias eql? ==
def hash
- attributes_for_hash.hash
+ TypeMetadata.hash ^
+ __getobj__.hash ^
+ extra.hash
end
- protected
-
- def attributes_for_hash
- [self.class, @type_metadata, extra]
+ private
+ def deduplicated
+ __setobj__(__getobj__.deduplicate)
+ @extra = -extra if extra
+ super
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 9bdaa00336..1df9ac32c9 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -8,6 +8,8 @@ require "mysql2"
module ActiveRecord
module ConnectionHandling # :nodoc:
+ ER_BAD_DB_ERROR = 1049
+
# Establishes a connection to the database that's used by all Active Record objects.
def mysql2_connection(config)
config = config.symbolize_keys
@@ -22,7 +24,7 @@ module ActiveRecord
client = Mysql2::Client.new(config)
ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config)
rescue Mysql2::Error => error
- if error.message.include?("Unknown database")
+ if error.error_number == ER_BAD_DB_ERROR
raise ActiveRecord::NoDatabaseError
else
raise
@@ -42,8 +44,14 @@ module ActiveRecord
configure_connection
end
+ def self.database_exists?(config)
+ !!ActiveRecord::Base.mysql2_connection(config)
+ rescue ActiveRecord::NoDatabaseError
+ false
+ end
+
def supports_json?
- !mariadb? && version >= "5.7.8"
+ !mariadb? && database_version >= "5.7.8"
end
def supports_comments?
@@ -109,12 +117,12 @@ module ActiveRecord
end
def discard! # :nodoc:
+ super
@connection.automatic_close = false
@connection = nil
end
private
-
def connect
@connection = Mysql2::Client.new(@config)
configure_connection
@@ -126,7 +134,11 @@ module ActiveRecord
end
def full_version
- @full_version ||= @connection.server_info[:version]
+ schema_cache.database_version.full_version_string
+ end
+
+ def get_full_version
+ @connection.server_info[:version]
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index 3ccc7271ab..f1ecf6df30 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -2,42 +2,52 @@
module ActiveRecord
module ConnectionAdapters
- # PostgreSQL-specific extensions to column definitions in a table.
- class PostgreSQLColumn < Column #:nodoc:
- delegate :array, :oid, :fmod, to: :sql_type_metadata
- alias :array? :array
-
- def initialize(*, max_identifier_length: 63, **)
- super
- @max_identifier_length = max_identifier_length
- end
+ module PostgreSQL
+ class Column < ConnectionAdapters::Column # :nodoc:
+ delegate :oid, :fmod, to: :sql_type_metadata
- def serial?
- return unless default_function
+ def initialize(*, serial: nil, **)
+ super
+ @serial = serial
+ end
- if %r{\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z} =~ default_function
- sequence_name_from_parts(table_name, name, suffix) == sequence_name
+ def serial?
+ @serial
end
- end
- private
- attr_reader :max_identifier_length
+ def array
+ sql_type_metadata.sql_type.end_with?("[]")
+ end
+ alias :array? :array
+
+ def sql_type
+ super.sub(/\[\]\z/, "")
+ end
- def sequence_name_from_parts(table_name, column_name, suffix)
- over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length
+ def init_with(coder)
+ @serial = coder["serial"]
+ super
+ end
- if over_length > 0
- column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
- over_length -= column_name.length - column_name_length
- column_name = column_name[0, column_name_length - [over_length, 0].min]
- end
+ def encode_with(coder)
+ coder["serial"] = @serial
+ super
+ end
- if over_length > 0
- table_name = table_name[0, table_name.length - over_length]
- end
+ def ==(other)
+ other.is_a?(Column) &&
+ super &&
+ serial? == other.serial?
+ end
+ alias :eql? :==
- "#{table_name}_#{column_name}_#{suffix}"
+ def hash
+ Column.hash ^
+ super.hash ^
+ serial?.hash
end
+ end
end
+ PostgreSQLColumn = PostgreSQL::Column # :nodoc:
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..45ec79ca78 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, binds) # :nodoc:
if pk.nil?
# Extract the table from the insert sql. Yuck.
table_ref = extract_table_ref_from_insert_sql(sql)
@@ -134,7 +145,7 @@ module ActiveRecord
# Begins a transaction.
def begin_db_transaction
- execute "BEGIN"
+ execute("BEGIN", "TRANSACTION")
end
def begin_isolated_db_transaction(isolation)
@@ -144,15 +155,19 @@ module ActiveRecord
# Commits a transaction.
def commit_db_transaction
- execute "COMMIT"
+ execute("COMMIT", "TRANSACTION")
end
# Aborts a transaction.
def exec_rollback_db_transaction
- execute "ROLLBACK"
+ execute("ROLLBACK", "TRANSACTION")
end
private
+ def build_truncate_statements(*table_names)
+ "TRUNCATE TABLE #{table_names.map(&method(:quote_table_name)).join(", ")}"
+ end
+
# Returns the current ID of a table's sequence.
def last_insert_id_result(sequence_name)
exec_query("SELECT currval(#{quote(sequence_name)})", "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..0bbe98145a 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:
@@ -77,7 +77,6 @@ module ActiveRecord
end
private
-
def type_cast_array(value, method)
if value.is_a?(::Array)
value.map { |item| type_cast_array(item, method) }
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
index f70f09ad95..bae34472e1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -10,7 +10,6 @@ module ActiveRecord
end
private
-
def cast_value(value)
value.to_s
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
index aabe83b85d..8d4dacbd64 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
@@ -46,7 +46,6 @@ module ActiveRecord
end
private
-
HstorePair = begin
quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
index 7b057a8452..e52d4385ef 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
@@ -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
@@ -34,7 +34,6 @@ module ActiveRecord
end
private
-
def number_for_point(number)
number.to_s.gsub(/\.0$/, "")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
index 6434377b57..357493dfc0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -26,9 +26,9 @@ module ActiveRecord
value = value.sub(/^\((.+)\)$/, '-\1') # (4)
case value
- when /^-?\D+[\d,]+\.\d{2}$/ # (1)
+ when /^-?\D*[\d,]+\.\d{2}$/ # (1)
value.gsub!(/[^-\d.]/, "")
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
+ when /^-?\D*[\d.]+,\d{2}$/ # (2)
value.gsub!(/[^-\d,]/, "").sub!(/,/, ".")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index 02a9c506f6..e81e18ff70 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
@@ -50,7 +50,6 @@ module ActiveRecord
end
private
-
def number_for_point(number)
number.to_s.gsub(/\.0$/, "")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index d85f9ab3ef..d19f1f9cf8 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -58,13 +58,12 @@ module ActiveRecord
end
private
-
def type_cast_single(value)
infinity?(value) ? value : @subtype.deserialize(value)
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..74a28eef58 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,11 @@ 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 0895d06356..07b66de366 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -30,7 +30,7 @@ module ActiveRecord
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name) # :nodoc:
- @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
+ self.class.quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
end
# Quotes schema names for use in SQL queries.
@@ -44,7 +44,7 @@ module ActiveRecord
# Quotes column names for use in SQL queries.
def quote_column_name(name) # :nodoc:
- @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
+ self.class.quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
end
# Quote date/time values for use in SQL input.
@@ -78,6 +78,43 @@ module ActiveRecord
type_map.lookup(column.oid, column.fmod, column.sql_type)
end
+ def column_name_matcher
+ COLUMN_NAME
+ end
+
+ def column_name_with_order_matcher
+ COLUMN_NAME_WITH_ORDER
+ end
+
+ COLUMN_NAME = /
+ \A
+ (
+ (?:
+ # "table_name"."column_name"::type_name | function(one or no argument)::type_name
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
+ )
+ (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ COLUMN_NAME_WITH_ORDER = /
+ \A
+ (
+ (?:
+ # "table_name"."column_name"::type_name | function(one or no argument)::type_name
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
+ )
+ (?:\s+ASC|\s+DESC)?
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
+
private
def lookup_cast_type(sql_type)
super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
@@ -138,7 +175,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 ceb8b40bd9..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,6 +17,42 @@ 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]}\""
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 dc4a0bb26e..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,124 +53,131 @@ 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
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
index 84643d20da..d201e40190 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -5,7 +5,6 @@ module ActiveRecord
module PostgreSQL
class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
private
-
def extensions(stream)
extensions = @connection.extensions
if extensions.any?
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index fae3ddbad4..0062952667 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
@@ -55,6 +55,7 @@ module ActiveRecord
end
def drop_table(table_name, options = {}) # :nodoc:
+ schema_cache.clear_data_source_cache!(table_name.to_s)
execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
@@ -68,7 +69,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 +86,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 +125,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 +197,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_.*'
@@ -287,7 +288,7 @@ module ActiveRecord
quoted_sequence = quote_table_name(sequence)
max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
if max_pk.nil?
- if postgresql_version >= 100000
+ if database_version >= 100000
minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
else
minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
@@ -302,7 +303,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 +320,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 +340,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
@@ -368,31 +369,6 @@ module ActiveRecord
SQL
end
- def bulk_change_table(table_name, operations)
- sql_fragments = []
- non_combinable_operations = []
-
- operations.each do |command, args|
- table, arguments = args.shift, args
- method = :"#{command}_for_alter"
-
- if respond_to?(method, true)
- sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
- sql_fragments << sqls
- non_combinable_operations.concat(procs)
- else
- execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
- non_combinable_operations.each(&:call)
- sql_fragments = []
- non_combinable_operations = []
- send(command, table, *arguments)
- end
- end
-
- execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
- non_combinable_operations.each(&:call)
- end
-
# Renames a table.
# Also renames a table's primary key sequence if the sequence name exists and
# matches the Active Record default.
@@ -401,6 +377,8 @@ module ActiveRecord
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
clear_cache!
+ schema_cache.clear_data_source_cache!(table_name.to_s)
+ schema_cache.clear_data_source_cache!(new_name.to_s)
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
pk, seq = pk_and_sequence_for(new_name)
if pk
@@ -443,14 +421,16 @@ module ActiveRecord
end
# Adds comment for given table column or drops it if +comment+ is a +nil+
- def change_column_comment(table_name, column_name, comment) # :nodoc:
+ def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
clear_cache!
+ comment = extract_new_comment_value(comment_or_changes)
execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
end
# Adds comment for given table or drops it if +comment+ is a +nil+
- def change_table_comment(table_name, comment) # :nodoc:
+ def change_table_comment(table_name, comment_or_changes) # :nodoc:
clear_cache!
+ comment = extract_new_comment_value(comment_or_changes)
execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
end
@@ -548,21 +528,21 @@ 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 ArgumentError, "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 ArgumentError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte."
end
when "integer"
case limit
when 1, 2; "smallint"
when nil, 3, 4; "integer"
when 5..8; "bigint"
- else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.")
+ else raise ArgumentError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead."
end
else
super
@@ -623,10 +603,10 @@ module ActiveRecord
# validate_foreign_key :accounts, name: :special_fk_name
#
# The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
- def validate_foreign_key(from_table, options_or_to_table = {})
+ def validate_foreign_key(from_table, to_table = nil, **options)
return unless supports_validate_constraints?
- fk_name_to_validate = foreign_key_for!(from_table, options_or_to_table).name
+ fk_name_to_validate = foreign_key_for!(from_table, to_table: to_table, **options).name
validate_constraint from_table, fk_name_to_validate
end
@@ -637,7 +617,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- PostgreSQL::TableDefinition.new(*args)
+ PostgreSQL::TableDefinition.new(self, *args)
end
def create_alter_table(name)
@@ -650,16 +630,19 @@ module ActiveRecord
default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
- PostgreSQLColumn.new(
+ if match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/)
+ serial = sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
+ end
+
+ PostgreSQL::Column.new(
column_name,
default_value,
type_metadata,
!notnull,
- table_name,
default_function,
- collation,
+ collation: collation,
comment: comment.presence,
- max_identifier_length: max_identifier_length
+ serial: serial
)
end
@@ -672,7 +655,23 @@ module ActiveRecord
precision: cast_type.precision,
scale: cast_type.scale,
)
- PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod)
+ PostgreSQL::TypeMetadata.new(simple_type, oid: oid, fmod: fmod)
+ end
+
+ def sequence_name_from_parts(table_name, column_name, suffix)
+ over_length = [table_name, column_name, suffix].sum(&:length) + 2 - max_identifier_length
+
+ if over_length > 0
+ column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
+ over_length -= column_name.length - column_name_length
+ column_name = column_name[0, column_name_length - [over_length, 0].min]
+ end
+
+ if over_length > 0
+ table_name = table_name[0, table_name.length - over_length]
+ end
+
+ "#{table_name}_#{column_name}_#{suffix}"
end
def extract_foreign_key_action(specifier)
@@ -683,38 +682,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 +710,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/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
index cd69d28139..b7f6479357 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -3,38 +3,42 @@
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
- class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata)
- undef to_yaml if method_defined?(:to_yaml)
+ module PostgreSQL
+ class TypeMetadata < DelegateClass(SqlTypeMetadata)
+ undef to_yaml if method_defined?(:to_yaml)
- attr_reader :oid, :fmod, :array
+ include Deduplicable
- def initialize(type_metadata, oid: nil, fmod: nil)
- super(type_metadata)
- @type_metadata = type_metadata
- @oid = oid
- @fmod = fmod
- @array = /\[\]$/.match?(type_metadata.sql_type)
- end
-
- def sql_type
- super.gsub(/\[\]$/, "")
- end
-
- def ==(other)
- other.is_a?(PostgreSQLTypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
- end
- alias eql? ==
+ attr_reader :oid, :fmod
- def hash
- attributes_for_hash.hash
- end
+ def initialize(type_metadata, oid: nil, fmod: nil)
+ super(type_metadata)
+ @oid = oid
+ @fmod = fmod
+ end
- protected
+ def ==(other)
+ other.is_a?(TypeMetadata) &&
+ __getobj__ == other.__getobj__ &&
+ oid == other.oid &&
+ fmod == other.fmod
+ end
+ alias eql? ==
- def attributes_for_hash
- [self.class, @type_metadata, oid, fmod]
+ def hash
+ TypeMetadata.hash ^
+ __getobj__.hash ^
+ oid.hash ^
+ fmod.hash
end
+
+ private
+ def deduplicated
+ __setobj__(__getobj__.deduplicate)
+ super
+ end
+ end
end
+ PostgreSQLTypeMetadata = PostgreSQL::TypeMetadata
end
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..e8caeb8132 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -37,7 +37,6 @@ module ActiveRecord
end
protected
-
def parts
@parts ||= [@schema, @identifier].compact
end
@@ -68,7 +67,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 d2ed699ee2..0a7c6d8ac4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -46,7 +46,7 @@ module ActiveRecord
conn = PG.connect(conn_params)
ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config)
rescue ::PG::Error => error
- if error.message.include?("does not exist")
+ if error.message.include?(conn_params[:dbname])
raise ActiveRecord::NoDatabaseError
else
raise
@@ -185,7 +185,7 @@ module ActiveRecord
end
def supports_json?
- postgresql_version >= 90200
+ true
end
def supports_comments?
@@ -196,6 +196,17 @@ module ActiveRecord
true
end
+ def supports_insert_returning?
+ true
+ end
+
+ def supports_insert_on_conflict?
+ database_version >= 90500
+ end
+ alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
+ alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
+ alias supports_insert_conflict_target? supports_insert_on_conflict?
+
def index_algorithms
{ concurrently: "CONCURRENTLY" }
end
@@ -240,9 +251,6 @@ module ActiveRecord
configure_connection
add_pg_encoders
- @statements = StatementPool.new @connection,
- self.class.type_cast_config_to_integer(config[:statement_limit])
-
add_pg_decoders
@type_map = Type::HashLookupTypeMap.new
@@ -251,15 +259,10 @@ module ActiveRecord
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
- # Clears the prepared statements cache.
- def clear_cache!
- @lock.synchronize do
- @statements.clear
- end
- end
-
- def truncate(table_name, name = nil)
- exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
+ def self.database_exists?(config)
+ !!ActiveRecord::Base.postgresql_connection(config)
+ rescue ActiveRecord::NoDatabaseError
+ false
end
# Is this connection alive and ready for queries?
@@ -278,6 +281,8 @@ module ActiveRecord
super
@connection.reset
configure_connection
+ rescue PG::ConnectionBad
+ connect
end
end
@@ -303,6 +308,7 @@ module ActiveRecord
end
def discard! # :nodoc:
+ super
@connection.socket_io.reopen(IO::NULL) rescue nil
@connection = nil
end
@@ -332,20 +338,27 @@ 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?
- postgresql_version >= 90400
+ database_version >= 90400
+ end
+
+ def supports_optimizer_hints?
+ unless defined?(@has_pg_hint_plan)
+ @has_pg_hint_plan = extension_available?("pg_hint_plan")
+ end
+ @has_pg_hint_plan
end
def supports_lazy_transactions?
@@ -378,9 +391,12 @@ module ActiveRecord
}
end
+ def extension_available?(name)
+ query_value("SELECT true FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
+ end
+
def extension_enabled?(name)
- res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA")
- res.cast_values.first
+ query_value("SELECT installed_version IS NOT NULL FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
end
def extensions
@@ -391,8 +407,6 @@ module ActiveRecord
def max_identifier_length
@max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i
end
- alias table_alias_length max_identifier_length
- alias index_name_length max_identifier_length
# Set the authorized user for this session
def session_auth=(user)
@@ -415,21 +429,36 @@ module ActiveRecord
}
# Returns the version of the connected PostgreSQL server.
- def postgresql_version
+ def get_database_version # :nodoc:
@connection.server_version
end
+ alias :postgresql_version :database_version
def default_index_type?(index) # :nodoc:
index.using == :btree || super
end
- private
- def check_version
- if postgresql_version < 90100
- raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1."
- end
+ def build_insert_sql(insert) # :nodoc:
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
+
+ if insert.skip_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
+ elsif insert.update_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
+ sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
end
+ sql << " RETURNING #{insert.returning}" if insert.returning
+ sql
+ end
+
+ def check_version # :nodoc:
+ if database_version < 90300
+ raise "Your version of PostgreSQL (#{database_version}) is too old. Active Record supports PostgreSQL >= 9.3."
+ end
+ end
+
+ private
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
NUMERIC_VALUE_OUT_OF_RANGE = "22003"
@@ -441,28 +470,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
@@ -589,18 +618,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(", ")
@@ -616,6 +638,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
@@ -631,6 +657,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
@@ -641,8 +671,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
@@ -696,7 +727,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
@@ -704,7 +735,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
@@ -719,6 +750,8 @@ module ActiveRecord
def connect
@connection = PG.connect(@connection_parameters)
configure_connection
+ add_pg_encoders
+ add_pg_decoders
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -776,7 +809,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
@@ -787,7 +820,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)
@@ -799,10 +832,14 @@ module ActiveRecord
Arel::Visitors::PostgreSQL.new(self)
end
+ def build_statement_pool
+ StatementPool.new(@connection, self.class.type_cast_config_to_integer(@config[:statement_limit]))
+ end
+
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'
@@ -814,7 +851,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
@@ -829,7 +866,22 @@ 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
+ @default_timezone = nil
+ @timestamp_decoder = nil
+
coders_by_name = {
"int2" => PG::TextDecoder::Integer,
"int4" => PG::TextDecoder::Integer,
@@ -839,8 +891,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)
@@ -854,6 +913,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..7d54fcf9a0 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -13,6 +13,7 @@ module ActiveRecord
@columns_hash = {}
@primary_keys = {}
@data_sources = {}
+ @indexes = {}
end
def initialize_dup(other)
@@ -21,22 +22,27 @@ module ActiveRecord
@columns_hash = @columns_hash.dup
@primary_keys = @primary_keys.dup
@data_sources = @data_sources.dup
+ @indexes = @indexes.dup
end
def encode_with(coder)
- coder["columns"] = @columns
- coder["columns_hash"] = @columns_hash
- coder["primary_keys"] = @primary_keys
- coder["data_sources"] = @data_sources
- coder["version"] = connection.migration_context.current_version
+ coder["columns"] = @columns
+ coder["primary_keys"] = @primary_keys
+ coder["data_sources"] = @data_sources
+ coder["indexes"] = @indexes
+ coder["version"] = connection.migration_context.current_version
+ coder["database_version"] = database_version
end
def init_with(coder)
- @columns = coder["columns"]
- @columns_hash = coder["columns_hash"]
- @primary_keys = coder["primary_keys"]
- @data_sources = coder["data_sources"]
- @version = coder["version"]
+ @columns = coder["columns"]
+ @primary_keys = coder["primary_keys"]
+ @data_sources = coder["data_sources"]
+ @indexes = coder["indexes"] || {}
+ @version = coder["version"]
+ @database_version = coder["database_version"]
+
+ derive_columns_hash_and_deduplicate_values
end
def primary_keys(table_name)
@@ -57,6 +63,7 @@ module ActiveRecord
primary_keys(table_name)
columns(table_name)
columns_hash(table_name)
+ indexes(table_name)
end
end
@@ -72,9 +79,20 @@ module ActiveRecord
# Get the columns for a table as a hash, key is the column name
# value is the column object.
def columns_hash(table_name)
- @columns_hash[table_name] ||= Hash[columns(table_name).map { |col|
- [col.name, col]
- }]
+ @columns_hash[table_name] ||= columns(table_name).index_by(&:name)
+ end
+
+ # Checks whether the columns hash is already cached for a table.
+ def columns_hash?(table_name)
+ @columns_hash.key?(table_name)
+ end
+
+ def indexes(table_name)
+ @indexes[table_name] ||= connection.indexes(table_name)
+ end
+
+ def database_version # :nodoc:
+ @database_version ||= connection.get_database_version
end
# Clears out internal caches
@@ -83,11 +101,13 @@ module ActiveRecord
@columns_hash.clear
@primary_keys.clear
@data_sources.clear
+ @indexes.clear
@version = nil
+ @database_version = nil
end
def size
- [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+
+ [@columns, @columns_hash, @primary_keys, @data_sources].sum(&:size)
end
# Clear out internal caches for the data source +name+.
@@ -96,19 +116,43 @@ module ActiveRecord
@columns_hash.delete name
@primary_keys.delete name
@data_sources.delete name
+ @indexes.delete name
end
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = connection.migration_context.current_version
- [@version, @columns, @columns_hash, @primary_keys, @data_sources]
+ [@version, @columns, {}, @primary_keys, @data_sources, @indexes, database_version]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @data_sources = array
+ @version, @columns, _columns_hash, @primary_keys, @data_sources, @indexes, @database_version = array
+ @indexes ||= {}
+
+ derive_columns_hash_and_deduplicate_values
end
private
+ def derive_columns_hash_and_deduplicate_values
+ @columns = deep_deduplicate(@columns)
+ @columns_hash = @columns.transform_values { |columns| columns.index_by(&:name) }
+ @primary_keys = deep_deduplicate(@primary_keys)
+ @data_sources = deep_deduplicate(@data_sources)
+ @indexes = deep_deduplicate(@indexes)
+ end
+
+ def deep_deduplicate(value)
+ case value
+ when Hash
+ value.transform_keys { |k| deep_deduplicate(k) }.transform_values { |v| deep_deduplicate(v) }
+ when Array
+ value.map { |i| deep_deduplicate(i) }
+ when String, Deduplicable
+ -value
+ else
+ value
+ end
+ end
def prepare_data_sources
connection.data_sources.each { |source| @data_sources[source] = true }
diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
index 8489bcbf1d..969867e70f 100644
--- a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
@@ -1,9 +1,13 @@
# frozen_string_literal: true
+require "active_record/connection_adapters/deduplicable"
+
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
class SqlTypeMetadata
+ include Deduplicable
+
attr_reader :sql_type, :type, :limit, :precision, :scale
def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil)
@@ -16,18 +20,27 @@ module ActiveRecord
def ==(other)
other.is_a?(SqlTypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
+ sql_type == other.sql_type &&
+ type == other.type &&
+ limit == other.limit &&
+ precision == other.precision &&
+ scale == other.scale
end
alias eql? ==
def hash
- attributes_for_hash.hash
+ SqlTypeMetadata.hash ^
+ sql_type.hash ^
+ type.hash ^
+ limit.hash ^
+ precision.hash >> 1 ^
+ scale.hash >> 2
end
- protected
-
- def attributes_for_hash
- [self.class, sql_type, type, limit, precision, scale]
+ private
+ def deduplicated
+ @sql_type = -sql_type
+ super
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
new file mode 100644
index 0000000000..85053acf91
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module DatabaseStatements
+ 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 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
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.execute(sql)
+ end
+ end
+ 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)
+
+ log(sql, name, binds, type_casted_binds) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ # Don't cache statements if they are not prepared
+ unless prepare
+ stmt = @connection.prepare(sql)
+ begin
+ cols = stmt.columns
+ unless without_prepared_statement?(binds)
+ stmt.bind_params(type_casted_binds)
+ end
+ records = stmt.to_a
+ ensure
+ stmt.close
+ end
+ else
+ stmt = @statements[sql] ||= @connection.prepare(sql)
+ cols = stmt.columns
+ stmt.reset!
+ stmt.bind_params(type_casted_binds)
+ records = stmt.to_a
+ end
+
+ ActiveRecord::Result.new(cols, records)
+ end
+ end
+ end
+
+ def exec_delete(sql, name = "SQL", binds = [])
+ exec_query(sql, name, binds)
+ @connection.changes
+ end
+ alias :exec_update :exec_delete
+
+ def begin_db_transaction #:nodoc:
+ log("begin transaction", "TRANSACTION") { @connection.transaction }
+ end
+
+ def commit_db_transaction #:nodoc:
+ log("commit transaction", "TRANSACTION") { @connection.commit }
+ end
+
+ def exec_rollback_db_transaction #:nodoc:
+ log("rollback transaction", "TRANSACTION") { @connection.rollback }
+ end
+
+ private
+ def execute_batch(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
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.execute_batch2(sql)
+ end
+ end
+ end
+
+ def last_inserted_id(result)
+ @connection.last_insert_row_id
+ end
+
+ def build_fixture_statements(fixture_set)
+ fixture_set.flat_map do |table_name, fixtures|
+ next if fixtures.empty?
+ fixtures.map { |fixture| build_fixture_sql([fixture], table_name) }
+ end.compact
+ end
+
+ def build_truncate_statements(*table_names)
+ truncate_tables = table_names.map do |table_name|
+ "DELETE FROM #{quote_table_name(table_name)}"
+ end
+ combine_multi_statements(truncate_tables)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index b2dcdb5373..9b74a774e5 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -12,8 +12,12 @@ module ActiveRecord
quote_column_name(attr)
end
+ def quote_table_name(name)
+ self.class.quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze
+ end
+
def quote_column_name(name)
- @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
+ self.class.quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
end
def quoted_time(value)
@@ -26,23 +30,58 @@ 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
+ def column_name_matcher
+ COLUMN_NAME
+ end
+
+ def column_name_with_order_matcher
+ COLUMN_NAME_WITH_ORDER
+ end
+
+ COLUMN_NAME = /
+ \A
+ (
+ (?:
+ # "table_name"."column_name" | function(one or no argument)
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")) | \w+\((?:|\g<2>)\)
+ )
+ (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ COLUMN_NAME_WITH_ORDER = /
+ \A
+ (
+ (?:
+ # "table_name"."column_name" | function(one or no argument)
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")) | \w+\((?:|\g<2>)\)
+ )
+ (?:\s+ASC|\s+DESC)?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
+
+ private
def _type_cast(value)
case value
when BigDecimal
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index 48277f0ae2..e48f59b4f0 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 || options}")
+
+ 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)
@@ -79,7 +105,7 @@ module ActiveRecord
end
type_metadata = fetch_type_metadata(field["type"])
- Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, table_name, nil, field["collation"])
+ Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, collation: field["collation"])
end
def data_source_sql(name = nil, type: nil)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 3312c3de01..f4847eb6c0 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -4,12 +4,13 @@ require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
require "active_record/connection_adapters/sqlite3/explain_pretty_printer"
require "active_record/connection_adapters/sqlite3/quoting"
+require "active_record/connection_adapters/sqlite3/database_statements"
require "active_record/connection_adapters/sqlite3/schema_creation"
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.4"
require "sqlite3"
module ActiveRecord
@@ -36,8 +37,6 @@ module ActiveRecord
config.merge(results_as_hash: true)
)
- db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
-
ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
rescue Errno::ENOENT => error
if error.message.include?("No such file or directory")
@@ -49,8 +48,8 @@ module ActiveRecord
end
module ConnectionAdapters #:nodoc:
- # The SQLite3 adapter works SQLite 3.6.16 or newer
- # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3).
+ # The SQLite3 adapter works with the sqlite3-ruby drivers
+ # (available as gem from https://rubygems.org/gems/sqlite3).
#
# Options:
#
@@ -60,6 +59,7 @@ module ActiveRecord
include SQLite3::Quoting
include SQLite3::SchemaStatements
+ include SQLite3::DatabaseStatements
NATIVE_DATABASE_TYPES = {
primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
@@ -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
@@ -102,12 +95,19 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
-
- @active = true
- @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
configure_connection
end
+ def self.database_exists?(config)
+ config = config.symbolize_keys
+ if config[:database] == ":memory:"
+ return true
+ else
+ database_file = defined?(Rails.root) ? File.expand_path(config[:database], Rails.root) : config[:database]
+ File.exist?(database_file)
+ end
+ end
+
def supports_ddl_transactions?
true
end
@@ -121,14 +121,14 @@ module ActiveRecord
end
def supports_expression_index?
- sqlite_version >= "3.9.0"
+ database_version >= "3.9.0"
end
def requires_reloading?
true
end
- def supports_foreign_keys_in_create?
+ def supports_foreign_keys?
true
end
@@ -144,23 +144,29 @@ module ActiveRecord
true
end
+ def supports_insert_on_conflict?
+ database_version >= "3.24.0"
+ end
+ alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
+ alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
+ alias supports_insert_conflict_target? supports_insert_on_conflict?
+
def active?
- @active
+ !@connection.closed?
+ end
+
+ def reconnect!
+ super
+ connect if @connection.closed?
end
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
super
- @active = false
@connection.close rescue nil
end
- # Clears the prepared statements cache.
- def clear_cache!
- @statements.clear
- end
-
def supports_index_sort_order?
true
end
@@ -205,79 +211,6 @@ module ActiveRecord
end
end
- #--
- # DATABASE STATEMENTS ======================================
- #++
-
- def explain(arel, binds = [])
- sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
- SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
- end
-
- def exec_query(sql, name = nil, binds = [], prepare: false)
- materialize_transactions
-
- type_casted_binds = type_casted_binds(binds)
-
- log(sql, name, binds, type_casted_binds) do
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
- # Don't cache statements if they are not prepared
- unless prepare
- stmt = @connection.prepare(sql)
- begin
- cols = stmt.columns
- unless without_prepared_statement?(binds)
- stmt.bind_params(type_casted_binds)
- end
- records = stmt.to_a
- ensure
- stmt.close
- end
- else
- stmt = @statements[sql] ||= @connection.prepare(sql)
- cols = stmt.columns
- stmt.reset!
- stmt.bind_params(type_casted_binds)
- records = stmt.to_a
- end
-
- ActiveRecord::Result.new(cols, records)
- end
- end
- end
-
- def exec_delete(sql, name = "SQL", binds = [])
- exec_query(sql, name, binds)
- @connection.changes
- end
- alias :exec_update :exec_delete
-
- def last_inserted_id(result)
- @connection.last_insert_row_id
- end
-
- def execute(sql, name = nil) #:nodoc:
- materialize_transactions
-
- log(sql, name) do
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
- @connection.execute(sql)
- end
- end
- end
-
- def begin_db_transaction #:nodoc:
- log("begin transaction", nil) { @connection.transaction }
- end
-
- def commit_db_transaction #:nodoc:
- log("commit transaction", nil) { @connection.commit }
- end
-
- def exec_rollback_db_transaction #:nodoc:
- log("rollback transaction", nil) { @connection.rollback }
- end
-
# SCHEMA STATEMENTS ========================================
def primary_keys(table_name) # :nodoc:
@@ -295,15 +228,12 @@ module ActiveRecord
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
+ schema_cache.clear_data_source_cache!(table_name.to_s)
+ schema_cache.clear_data_source_cache!(new_name.to_s)
exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
rename_table_indexes(table_name, new_name)
end
- 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 +247,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,23 +308,26 @@ 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)
+ def build_insert_sql(insert) # :nodoc:
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
+
+ if insert.skip_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
+ elsif insert.update_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
+ sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
+ end
+
+ sql
end
- def insert_fixtures_set(fixture_set, tables_to_delete = [])
- disable_referential_integrity do
- transaction(requires_new: true) do
- tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" }
+ def get_database_version # :nodoc:
+ SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)"))
+ end
- fixture_set.each do |table_name, rows|
- rows.each { |row| insert_fixture(row, table_name) }
- end
- end
+ def check_version # :nodoc:
+ if database_version < "3.8.0"
+ raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.8."
end
end
@@ -402,12 +338,6 @@ module ActiveRecord
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."
- end
- end
-
def initialize_type_map(m = type_map)
super
register_class_with_limit m, %r(int)i, SQLite3Integer
@@ -426,9 +356,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] || {}
@@ -436,7 +365,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?
@@ -463,6 +393,7 @@ module ActiveRecord
if from_primary_key.is_a?(Array)
@definition.primary_keys from_primary_key
end
+
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||
@@ -525,22 +456,18 @@ module ActiveRecord
SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}")
end
- def sqlite_version
- @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
@@ -550,7 +477,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)
@@ -563,9 +490,9 @@ module ActiveRecord
result = exec_query(sql, "SCHEMA").first
if result
- # Splitting with left parentheses and picking up last will return all
+ # Splitting with left parentheses and discarding the first part will return all
# columns separated with comma(,).
- columns_string = result["sql"].split("(").last
+ columns_string = result["sql"].split("(", 2).last
columns_string.split(",").each do |column_string|
# This regex will match the column name and collation type and will save
@@ -591,7 +518,21 @@ module ActiveRecord
Arel::Visitors::SQLite.new(self)
end
+ def build_statement_pool
+ StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
+ end
+
+ def connect
+ @connection = ::SQLite3::Database.new(
+ @config[:database].to_s,
+ @config.merge(results_as_hash: true)
+ )
+ configure_connection
+ end
+
def configure_connection
+ @connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout])) if @config[:timeout]
+
execute("PRAGMA foreign_keys = ON", "SCHEMA")
end
diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
index 46bd831da7..0960feed84 100644
--- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -48,7 +48,6 @@ module ActiveRecord
end
private
-
def cache
@cache[Process.pid]
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 3ce9aad5fc..c8cefa9906 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -85,14 +85,14 @@ module ActiveRecord
# based on the requested role:
#
# ActiveRecord::Base.connected_to(role: :writing) do
- # Dog.create! # creates dog using dog connection
+ # Dog.create! # creates dog using dog writing connection
# end
#
# ActiveRecord::Base.connected_to(role: :reading) do
# Dog.create! # throws exception because we're on a replica
# end
#
- # ActiveRecord::Base.connected_to(role: :unknown_ode) do
+ # ActiveRecord::Base.connected_to(role: :unknown_role) do
# # raises exception due to non-existent role
# end
#
@@ -100,11 +100,20 @@ module ActiveRecord
# you can use +connected_to+ with a +database+ argument. The +database+ argument
# expects a symbol that corresponds to the database key in your config.
#
- # This will connect to a new database for the queries inside the block.
- #
# ActiveRecord::Base.connected_to(database: :animals_slow_replica) do
# Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+
# end
+ #
+ # This will connect to a new database for the queries inside the block. By
+ # default the `:writing` role will be used since all connections must be assigned
+ # a role. If you would like to use a different role you can pass a hash to database:
+ #
+ # ActiveRecord::Base.connected_to(database: { readonly_slow: :animals_slow_replica }) do
+ # # runs a long query while connected to the +animals_slow_replica+ using the readonly_slow role.
+ # Dog.run_a_long_query
+ # end
+ #
+ # When using the database key a new connection will be established every time.
def connected_to(database: nil, role: nil, &blk)
if database && role
raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
@@ -112,17 +121,14 @@ module ActiveRecord
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(role)
- with_handler(role) do
- handler.establish_connection(config_hash)
- yield
- end
+ handler.establish_connection(config_hash)
+
+ with_handler(role, &blk)
elsif role
with_handler(role.to_sym, &blk)
else
@@ -130,7 +136,31 @@ 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:
+ handler_key ||= ActiveRecord::Base.writing_role
connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
end
@@ -143,7 +173,7 @@ module ActiveRecord
raise "Anonymous class is not allowed." unless name
config_or_env ||= DEFAULT_ENV.call.to_sym
- pool_name = self == Base ? "primary" : name
+ pool_name = primary_class? ? "primary" : name
self.connection_specification_name = pool_name
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations)
@@ -153,6 +183,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.
@@ -165,11 +204,15 @@ module ActiveRecord
# Return the specification name from the current class or its parent.
def connection_specification_name
if !defined?(@connection_specification_name) || @connection_specification_name.nil?
- return self == Base ? "primary" : superclass.connection_specification_name
+ return primary_class? ? "primary" : superclass.connection_specification_name
end
@connection_specification_name
end
+ def primary_class? # :nodoc:
+ self == Base || defined?(ApplicationRecord) && self == ApplicationRecord
+ end
+
# Returns the configuration of the associated connection as a hash:
#
# ActiveRecord::Base.connection_config
@@ -213,7 +256,6 @@ module ActiveRecord
:clear_all_connections!, :flush_idle_connections!, to: :connection_handler
private
-
def swap_connection_handler(handler, &blk) # :nodoc:
old_handler, ActiveRecord::Base.connection_handler = ActiveRecord::Base.connection_handler, handler
yield
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 50f3087c51..595ef4ee25 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -124,20 +124,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
@@ -157,7 +160,7 @@ module ActiveRecord
return super if block_given? ||
primary_key.nil? ||
scope_attributes? ||
- columns_hash.include?(inheritance_column)
+ columns_hash.key?(inheritance_column) && !base_class?
id = ids.first
@@ -169,19 +172,16 @@ 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)
+ raise RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, 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
@@ -201,11 +201,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
@@ -270,7 +268,8 @@ module ActiveRecord
end
def arel_attribute(name, table = arel_table) # :nodoc:
- name = attribute_alias(name) if attribute_alias?(name)
+ name = name.to_s
+ name = attribute_aliases[name] || name
table[name]
end
@@ -282,8 +281,11 @@ module ActiveRecord
TypeCaster::Map.new(self)
end
- private
+ def _internal? # :nodoc:
+ false
+ end
+ private
def cached_find_by_statement(key, &block)
cache = @find_by_statement_cache[connection.prepared_statements]
cache.compute_if_absent(key) { StatementCache.create(connection, &block) }
@@ -314,7 +316,7 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil)
- self.class.define_attribute_methods
+ @new_record = true
@attributes = self.class._default_attributes.deep_dup
init_internals
@@ -350,15 +352,11 @@ 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)
- init_internals
-
+ def init_with_attributes(attributes, new_record = false) # :nodoc:
@new_record = new_record
@attributes = attributes
- self.class.define_attribute_methods
+ init_internals
yield self if block_given?
@@ -397,13 +395,13 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
@attributes = @attributes.deep_dup
- @attributes.reset(self.class.primary_key)
+ @attributes.reset(@primary_key)
_run_initialize_callbacks
@new_record = true
@destroyed = false
- @_start_transaction_state = {}
+ @_start_transaction_state = nil
@transaction_state = nil
super
@@ -464,6 +462,7 @@ module ActiveRecord
# Returns +true+ if the attributes hash has been frozen.
def frozen?
+ sync_with_transaction_state if @transaction_state&.finalized?
@attributes.frozen?
end
@@ -476,6 +475,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?
@@ -546,7 +553,6 @@ module ActiveRecord
end
private
-
# +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
# the array, and then rescues from the possible +NoMethodError+. If those elements are
# +ActiveRecord::Base+'s, then this triggers the various +method_missing+'s that we have,
@@ -560,22 +566,18 @@ module ActiveRecord
end
def init_internals
+ @primary_key = self.class.primary_key
@readonly = false
@destroyed = false
@marked_for_destruction = false
@destroyed_by_association = nil
- @new_record = true
- @_start_transaction_state = {}
+ @_start_transaction_state = nil
@transaction_state = nil
- end
- def initialize_internals_callback
+ self.class.define_attribute_methods
end
- def thaw
- if frozen?
- @attributes = @attributes.dup
- end
+ def initialize_internals_callback
end
def custom_inspect_method_defined?
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index 30cb0a27e7..bf31bb7c22 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -7,7 +7,7 @@ require "active_record/database_configurations/url_config"
module ActiveRecord
# ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig
# objects (either a HashConfig or UrlConfig) that are constructed from the
- # application's database configuration hash or url string.
+ # application's database configuration hash or URL string.
class DatabaseConfigurations
attr_reader :configurations
delegate :any?, to: :configurations
@@ -17,22 +17,22 @@ module ActiveRecord
end
# Collects the configs for the environment and optionally the specification
- # name passed in. To include replica configurations pass `include_replicas: true`.
+ # name passed in. To include replica configurations pass <tt>include_replicas: true</tt>.
#
# If a spec name is provided a single DatabaseConfig object will be
# returned, otherwise an array of DatabaseConfig objects will be
# returned that corresponds with the environment and type requested.
#
- # Options:
+ # ==== Options
#
- # <tt>env_name:</tt> The environment name. Defaults to nil which will collect
- # configs for all environments.
- # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults
- # to +nil+.
- # <tt>include_replicas:</tt> Determines whether to include replicas in
- # the 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+.
+ # * <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect
+ # configs for all environments.
+ # * <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 returned list. Most of the time we're only iterating over the write
+ # connection (i.e. migrations don't need to run for the write and read connection).
+ # Defaults to +false+.
def configs_for(env_name: nil, spec_name: nil, include_replicas: false)
configs = env_with_configs(env_name)
@@ -53,7 +53,7 @@ module ActiveRecord
# Returns the config hash that corresponds with the environment
#
- # If the application has multiple databases `default_hash` will
+ # If the application has multiple databases +default_hash+ will
# return the first config hash for the environment.
#
# { database: "my_db", adapter: "mysql2" }
@@ -65,7 +65,7 @@ module ActiveRecord
# Returns a single DatabaseConfig object based on the requested environment.
#
- # If the application has multiple databases `find_db_config` will return
+ # If the application has multiple databases +find_db_config+ will return
# the first DatabaseConfig for the environment.
def find_db_config(env)
configurations.find do |db_config|
@@ -102,10 +102,11 @@ 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)
- end.compact
+ end.flatten.compact
if url = ENV["DATABASE_URL"]
build_url_config(url, build_db_config)
@@ -124,23 +125,23 @@ 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 })
+ elsif config["database"] || config["adapter"] || ENV["DATABASE_URL"]
ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
else
config.each_pair.map do |sub_spec_name, sub_config|
@@ -152,12 +153,12 @@ module ActiveRecord
def build_url_config(url, configs)
env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
- if original_config = configs.find(&:for_current_env?)
- if original_config.url_config?
- configs
- else
- configs.map do |config|
- ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config)
+ if configs.find(&:for_current_env?)
+ configs.map do |config|
+ if config.url_config?
+ config
+ else
+ 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..e31ff09391 100644
--- a/activerecord/lib/active_record/database_configurations/hash_config.rb
+++ b/activerecord/lib/active_record/database_configurations/hash_config.rb
@@ -14,16 +14,16 @@ module ActiveRecord
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
# @env_name="development", @spec_name="primary", @config={"database"=>"db_name"}>
#
- # Options are:
+ # ==== Options
#
- # <tt>:env_name</tt> - The Rails environment, ie "development"
- # <tt>:spec_name</tt> - The specification name. In a standard two-tier
- # database configuration this will default to "primary". In a multiple
- # database three-tier database configuration this corresponds to the name
- # used in the second tier, for example "primary_readonly".
- # <tt>:config</tt> - The config hash. This is the hash that contains the
- # database adapter, name, and other important information for database
- # connections.
+ # * <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
+ # used in the second tier, for example "primary_readonly".
+ # * <tt>:config</tt> - The config hash. This is the hash that contains the
+ # database adapter, name, and other important information for database
+ # connections.
class HashConfig < DatabaseConfig
attr_reader :config
@@ -33,14 +33,14 @@ module ActiveRecord
end
# Determines whether a database configuration is for a replica / readonly
- # connection. If the `replica` key is present in the config, `replica?` will
+ # connection. If the +replica+ key is present in the config, +replica?+ will
# return +true+.
def replica?
config["replica"]
end
# The migrations paths for a database configuration. If the
- # `migrations_paths` key is present in the config, `migrations_paths`
+ # +migrations_paths+ key is present in the config, +migrations_paths+
# will return its value.
def migrations_paths
config["migrations_paths"]
diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb
index 81917fc4c1..e6b4acc647 100644
--- a/activerecord/lib/active_record/database_configurations/url_config.rb
+++ b/activerecord/lib/active_record/database_configurations/url_config.rb
@@ -17,17 +17,17 @@ module ActiveRecord
# @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"},
# @url="postgres://localhost/foo">
#
- # Options are:
+ # ==== Options
#
- # <tt>:env_name</tt> - The Rails environment, ie "development"
- # <tt>:spec_name</tt> - The specification name. In a standard two-tier
- # database configuration this will default to "primary". In a multiple
- # database three-tier database configuration this corresponds to the name
- # used in the second tier, for example "primary_readonly".
- # <tt>:url</tt> - The database URL.
- # <tt>:config</tt> - The config hash. This is the hash that contains the
- # database adapter, name, and other important information for database
- # connections.
+ # * <tt>:env_name</tt> - The Rails environment, ie "development".
+ # * <tt>:spec_name</tt> - The specification name. In a standard two-tier
+ # database configuration this will default to "primary". In a multiple
+ # database three-tier database configuration this corresponds to the name
+ # used in the second tier, for example "primary_readonly".
+ # * <tt>:url</tt> - The database URL.
+ # * <tt>:config</tt> - The config hash. This is the hash that contains the
+ # database adapter, name, and other important information for database
+ # connections.
class UrlConfig < DatabaseConfig
attr_reader :url, :config
@@ -42,26 +42,30 @@ module ActiveRecord
end
# Determines whether a database configuration is for a replica / readonly
- # connection. If the `replica` key is present in the config, `replica?` will
+ # connection. If the +replica+ key is present in the config, +replica?+ will
# return +true+.
def replica?
config["replica"]
end
# The migrations paths for a database configuration. If the
- # `migrations_paths` key is present in the config, `migrations_paths`
+ # +migrations_paths+ key is present in the config, +migrations_paths+
# will return its value.
def migrations_paths
config["migrations_paths"]
end
private
- def build_config(original_config, url)
- if /^jdbc:/.match?(url)
- hash = { "url" => url }
+ 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/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 3bb8c6f4e3..7d9e221faa 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -49,11 +49,11 @@ module ActiveRecord
attr_reader :model, :name, :attribute_names
- def initialize(model, name)
+ def initialize(model, method_name)
@model = model
- @name = name.to_s
+ @name = method_name.to_s
@attribute_names = @name.match(self.class.pattern)[1].split("_and_")
- @attribute_names.map! { |n| @model.attribute_aliases[n] || n }
+ @attribute_names.map! { |name| @model.attribute_aliases[name] || name }
end
def valid?
@@ -69,7 +69,6 @@ module ActiveRecord
end
private
-
def body
"#{finder}(#{attributes_hash})"
end
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index 3a600835e1..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
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index f61bc7b9e8..20cc987d6e 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -38,6 +38,10 @@ module ActiveRecord
class AdapterNotSpecified < ActiveRecordError
end
+ # Raised when a model makes a query but it has not specified an associated table.
+ class TableNotSpecified < ActiveRecordError
+ end
+
# Raised when Active Record cannot find database adapter specified in
# +config/database.yml+ or programmatically.
class AdapterNotFound < ActiveRecordError
@@ -49,6 +53,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
@@ -64,7 +72,7 @@ module ActiveRecord
# Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]
- # methods when a record is invalid and can not be saved.
+ # methods when a record is invalid and cannot be saved.
class RecordNotSaved < ActiveRecordError
attr_reader :record
@@ -97,9 +105,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 +130,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 +157,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.
@@ -336,16 +353,24 @@ module ActiveRecord
class IrreversibleOrderError < ActiveRecordError
end
+ # Superclass for errors that have been aborted (either by client or server).
+ class QueryAborted < StatementInvalid
+ end
+
# LockWaitTimeout will be raised when lock wait timeout exceeded.
class LockWaitTimeout < StatementInvalid
end
# StatementTimeout will be raised when statement timeout exceeded.
- class StatementTimeout < StatementInvalid
+ class StatementTimeout < QueryAborted
end
# QueryCanceled will be raised when canceling statement due to user request.
- class QueryCanceled < StatementInvalid
+ class QueryCanceled < QueryAborted
+ end
+
+ # AdapterTimeout will be raised when database clients times out while waiting from the server.
+ class AdapterTimeout < QueryAborted
end
# UnknownAttributeReference is raised when an unknown and potentially unsafe
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 919e96cd7a..5dca75c539 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -36,7 +36,6 @@ module ActiveRecord
end
private
-
def render_bind(attr)
value = if attr.type.binary? && attr.value
"<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
diff --git a/activerecord/lib/active_record/fixture_set/table_row.rb b/activerecord/lib/active_record/fixture_set/table_row.rb
index cb4726f1ee..f65329f91d 100644
--- a/activerecord/lib/active_record/fixture_set/table_row.rb
+++ b/activerecord/lib/active_record/fixture_set/table_row.rb
@@ -48,7 +48,6 @@ module ActiveRecord
end
private
-
def model_metadata
@table_rows.model_metadata
end
diff --git a/activerecord/lib/active_record/fixture_set/table_rows.rb b/activerecord/lib/active_record/fixture_set/table_rows.rb
index 23814b6cb5..df1cd63963 100644
--- a/activerecord/lib/active_record/fixture_set/table_rows.rb
+++ b/activerecord/lib/active_record/fixture_set/table_rows.rb
@@ -29,7 +29,6 @@ module ActiveRecord
end
private
-
def build_table_rows_from(table_name, fixtures, config)
now = config.default_timezone == :utc ? Time.now.utc : Time.now
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 1248ed00c5..046ed0e95c 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -464,7 +464,6 @@ module ActiveRecord
end
private
-
def insert_class(class_names, name, klass)
# We only want to deal with AR objects.
if klass && klass < ActiveRecord::Base
@@ -519,11 +518,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
@@ -572,50 +569,49 @@ module ActiveRecord
end
private
+ def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc:
+ fixtures_map = {}
+ fixture_sets = fixture_files.map do |fixture_set_name|
+ 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)
- 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)
-
- insert(fixture_sets, connection)
-
- 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
@@ -663,7 +659,6 @@ module ActiveRecord
end
private
-
def model_class=(class_name)
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
@model_class = class_name
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
index 72035a986b..7f92174f87 100644
--- a/activerecord/lib/active_record/gem_version.rb
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -8,7 +8,7 @@ module ActiveRecord
module VERSION
MAJOR = 6
- MINOR = 0
+ MINOR = 1
TINY = 0
PRE = "alpha"
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 138fd1cf53..5ca48fa18c 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -176,7 +176,6 @@ module ActiveRecord
end
protected
-
# Returns the class type of the record using the current module as a prefix. So descendants of
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
def compute_type(type_name)
@@ -208,7 +207,6 @@ module ActiveRecord
end
private
-
# Called by +instantiate+ to decide which class to use for a new
# record instance. For single-table inheritance, we check the record
# for a +type+ column and return the corresponding class.
@@ -249,7 +247,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
@@ -272,7 +270,6 @@ module ActiveRecord
end
private
-
def initialize_internals_callback
super
ensure_proper_type
diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
new file mode 100644
index 0000000000..f6577dcbc4
--- /dev/null
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class InsertAll # :nodoc:
+ attr_reader :model, :connection, :inserts, :keys
+ attr_reader :on_duplicate, :returning, :unique_by
+
+ def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
+ raise ArgumentError, "Empty list of attributes passed" if inserts.blank?
+
+ @model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s).to_set
+ @on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
+
+ @returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil?
+ @returning = false if @returning == []
+
+ @unique_by = find_unique_index_for(unique_by) if unique_by
+ @on_duplicate = :skip if @on_duplicate == :update && updatable_columns.empty?
+
+ ensure_valid_options_for_connection!
+ end
+
+ def execute
+ message = +"#{model} "
+ message << "Bulk " if inserts.many?
+ message << (on_duplicate == :update ? "Upsert" : "Insert")
+ connection.exec_query to_sql, message
+ end
+
+ def updatable_columns
+ keys - readonly_columns - unique_by_columns
+ end
+
+ def primary_keys
+ Array(model.primary_key)
+ end
+
+
+ def skip_duplicates?
+ on_duplicate == :skip
+ end
+
+ def update_duplicates?
+ on_duplicate == :update
+ end
+
+ def map_key_with_value
+ inserts.map do |attributes|
+ attributes = attributes.stringify_keys
+ verify_attributes(attributes)
+
+ keys.map do |key|
+ yield key, attributes[key]
+ end
+ end
+ end
+
+ private
+ def find_unique_index_for(unique_by)
+ match = Array(unique_by).map(&:to_s)
+
+ if index = unique_indexes.find { |i| match.include?(i.name) || i.columns == match }
+ index
+ else
+ raise ArgumentError, "No unique index found for #{unique_by}"
+ end
+ end
+
+ def unique_indexes
+ connection.schema_cache.indexes(model.table_name).select(&:unique)
+ end
+
+
+ def ensure_valid_options_for_connection!
+ if returning && !connection.supports_insert_returning?
+ raise ArgumentError, "#{connection.class} does not support :returning"
+ end
+
+ if skip_duplicates? && !connection.supports_insert_on_duplicate_skip?
+ raise ArgumentError, "#{connection.class} does not support skipping duplicates"
+ end
+
+ if update_duplicates? && !connection.supports_insert_on_duplicate_update?
+ raise ArgumentError, "#{connection.class} does not support upsert"
+ end
+
+ if unique_by && !connection.supports_insert_conflict_target?
+ raise ArgumentError, "#{connection.class} does not support :unique_by"
+ end
+ end
+
+
+ def to_sql
+ connection.build_insert_sql(ActiveRecord::InsertAll::Builder.new(self))
+ end
+
+
+ def readonly_columns
+ primary_keys + model.readonly_attributes.to_a
+ end
+
+ def unique_by_columns
+ Array(unique_by&.columns)
+ end
+
+
+ def verify_attributes(attributes)
+ if keys != attributes.keys.to_set
+ raise ArgumentError, "All objects being inserted must have the same keys"
+ end
+ end
+
+
+ class Builder
+ attr_reader :model
+
+ delegate :skip_duplicates?, :update_duplicates?, :keys, to: :insert_all
+
+ def initialize(insert_all)
+ @insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
+ end
+
+ def into
+ "INTO #{model.quoted_table_name}(#{columns_list})"
+ end
+
+ def values_list
+ types = extract_types_from_columns_on(model.table_name, keys: keys)
+
+ values_list = insert_all.map_key_with_value do |key, value|
+ connection.with_yaml_fallback(types[key].serialize(value))
+ end
+
+ Arel::InsertManager.new.create_values_list(values_list).to_sql
+ end
+
+ def returning
+ format_columns(insert_all.returning) if insert_all.returning
+ end
+
+ def conflict_target
+ if index = insert_all.unique_by
+ sql = +"(#{format_columns(index.columns)})"
+ sql << " WHERE #{index.where}" if index.where
+ sql
+ elsif update_duplicates?
+ "(#{format_columns(insert_all.primary_keys)})"
+ end
+ end
+
+ def updatable_columns
+ quote_columns(insert_all.updatable_columns)
+ end
+
+ private
+ attr_reader :connection, :insert_all
+
+ def columns_list
+ format_columns(insert_all.keys)
+ end
+
+ def extract_types_from_columns_on(table_name, keys:)
+ columns = connection.schema_cache.columns_hash(table_name)
+
+ unknown_column = (keys - columns.keys).first
+ raise UnknownAttributeError.new(model.new, unknown_column) if unknown_column
+
+ keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h
+ end
+
+ def format_columns(columns)
+ quote_columns(columns).join(",")
+ end
+
+ def quote_columns(columns)
+ columns.map(&connection.method(:quote_column_name))
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 33c4066b89..4a97061731 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -22,6 +22,14 @@ module ActiveRecord
#
# This is +true+, by default on Rails 5.2 and above.
class_attribute :cache_versioning, instance_writer: false, default: false
+
+ ##
+ # :singleton-method:
+ # Indicates whether to use a stable #cache_key method that is accompanied
+ # by a changing version in the #cache_version method on collections.
+ #
+ # This is +false+, by default until Rails 6.1.
+ class_attribute :collection_cache_versioning, instance_writer: false, default: false
end
# Returns a +String+, which Action Pack uses for constructing a URL to this
@@ -61,23 +69,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)
@@ -94,10 +93,21 @@ module ActiveRecord
# cache_version, but this method can be overwritten to return something else.
#
# Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
- # +false+ (which it is by default until Rails 6.0).
+ # +false+.
def cache_version
- 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
@@ -150,6 +160,48 @@ module ActiveRecord
end
end
end
+
+ def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
+ collection.send(:compute_cache_key, timestamp_column)
+ 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"
+ #
+ # PostgreSQL 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..8f3c6d0ee3 100644
--- a/activerecord/lib/active_record/internal_metadata.rb
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -8,12 +8,16 @@ 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
def table_name
- "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}"
+ "#{table_name_prefix}#{internal_metadata_table_name}#{table_name_suffix}"
end
def []=(key, value)
@@ -24,10 +28,6 @@ module ActiveRecord
where(key: key).pluck(:value).first
end
- def table_exists?
- connection.table_exists?(table_name)
- end
-
# Creates an internal metadata table with columns +key+ and +value+
def create_table
unless table_exists?
@@ -40,6 +40,10 @@ module ActiveRecord
end
end
end
+
+ def drop_table
+ connection.drop_table table_name, if_exists: true
+ end
end
end
end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 4a3a31fc95..c2a083bf3b 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -71,9 +71,8 @@ module ActiveRecord
end
def _touch_row(attribute_names, time)
+ @_touch_attr_names << self.class.locking_column if locking_enabled?
super
- ensure
- clear_attribute_change(self.class.locking_column) if locking_enabled?
end
def _update_row(attribute_names, attempted_action = "update")
@@ -88,7 +87,7 @@ module ActiveRecord
affected_rows = self.class._update_record(
attributes_with_values(attribute_names),
- self.class.primary_key => id_in_database,
+ @primary_key => id_in_database,
locking_column => previous_lock_value
)
@@ -111,7 +110,7 @@ module ActiveRecord
locking_column = self.class.locking_column
affected_rows = self.class._delete_record(
- self.class.primary_key => id_in_database,
+ @primary_key => id_in_database,
locking_column => read_attribute_before_type_cast(locking_column)
)
@@ -157,7 +156,6 @@ module ActiveRecord
end
private
-
# We need to apply this decorator here, rather than on module inclusion. The closure
# created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
# sub class being decorated. As such, changes to `lock_optimistically`, or
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 6b84431343..6248c2f578 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -110,7 +110,7 @@ module ActiveRecord
end
def extract_query_source_location(locations)
- backtrace_cleaner.clean(locations).first
+ backtrace_cleaner.clean(locations.lazy).first
end
end
end
diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb
new file mode 100644
index 0000000000..7374107048
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector.rb
@@ -0,0 +1,74 @@
+# 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 = nil, context_klass = nil, options = {})
+ @app = app
+ @resolver_klass = resolver_klass || Resolver
+ @context_klass = context_klass || Resolver::Session
+ @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..3eb1039c50
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
@@ -0,0 +1,89 @@
+# 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.connected_to(role: ActiveRecord::Base.writing_role) do
+ ActiveRecord::Base.connection_handler.while_preventing_writes do
+ instrumenter.instrument("database_selector.active_record.read_from_primary") do
+ yield
+ end
+ 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..7edfec9903 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -4,9 +4,10 @@ require "benchmark"
require "set"
require "zlib"
require "active_support/core_ext/module/attribute_accessors"
+require "active_support/actionable_error"
module ActiveRecord
- class MigrationError < ActiveRecordError#:nodoc:
+ class MigrationError < ActiveRecordError #:nodoc:
def initialize(message = nil)
message = "\n\n#{message}\n\n" if message
super
@@ -23,7 +24,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 +42,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 +50,7 @@ module ActiveRecord
# end
#
# def down
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# DROP CONSTRAINT zipchk
# SQL
@@ -68,7 +69,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 +77,7 @@ module ActiveRecord
# end
#
# dir.down do
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# DROP CONSTRAINT zipchk
# SQL
@@ -87,7 +88,7 @@ module ActiveRecord
class IrreversibleMigration < MigrationError
end
- class DuplicateMigrationVersionError < MigrationError#:nodoc:
+ class DuplicateMigrationVersionError < MigrationError #:nodoc:
def initialize(version = nil)
if version
super("Multiple migrations have the version number #{version}.")
@@ -97,7 +98,7 @@ module ActiveRecord
end
end
- class DuplicateMigrationNameError < MigrationError#:nodoc:
+ class DuplicateMigrationNameError < MigrationError #:nodoc:
def initialize(name = nil)
if name
super("Multiple migrations have the name #{name}.")
@@ -117,7 +118,7 @@ module ActiveRecord
end
end
- class IllegalMigrationNameError < MigrationError#:nodoc:
+ class IllegalMigrationNameError < MigrationError #:nodoc:
def initialize(name = nil)
if name
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
@@ -127,7 +128,13 @@ module ActiveRecord
end
end
- class PendingMigrationError < MigrationError#:nodoc:
+ class PendingMigrationError < MigrationError #:nodoc:
+ include ActiveSupport::ActionableError
+
+ action "Run pending migrations" do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+
def initialize(message = nil)
if !message && defined?(Rails.env)
super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}")
@@ -308,7 +315,7 @@ module ActiveRecord
# named +column_name+ from the table called +table_name+.
# * <tt>remove_columns(table_name, *column_names)</tt>: Removes the given
# columns from the table definition.
- # * <tt>remove_foreign_key(from_table, options_or_to_table)</tt>: Removes the
+ # * <tt>remove_foreign_key(from_table, to_table = nil, **options)</tt>: Removes the
# given foreign key from the table called +table_name+.
# * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index
# specified by +column_names+.
@@ -487,9 +494,9 @@ module ActiveRecord
# This migration will create the horses table for you on the way up, and
# automatically figure out how to drop the table on the way down.
#
- # Some commands like +remove_column+ cannot be reversed. If you care to
- # define how to move up and down in these cases, you should define the +up+
- # and +down+ methods as before.
+ # Some commands cannot be reversed. If you care to define how to move up
+ # and down in these cases, you should define the +up+ and +down+ methods
+ # as before.
#
# If a command cannot be reversed, an
# <tt>ActiveRecord::IrreversibleMigration</tt> exception will be raised when
@@ -520,10 +527,10 @@ module ActiveRecord
autoload :Compatibility, "active_record/migration/compatibility"
# This must be defined before the inherited hook, below
- class Current < Migration # :nodoc:
+ class Current < Migration #:nodoc:
end
- def self.inherited(subclass) # :nodoc:
+ def self.inherited(subclass) #:nodoc:
super
if subclass.superclass == Migration
raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \
@@ -541,7 +548,7 @@ module ActiveRecord
ActiveRecord::VERSION::STRING.to_f
end
- MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc:
+ MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ #:nodoc:
# This class is used to verify that all migrations have been run before
# loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load
@@ -561,17 +568,16 @@ module ActiveRecord
end
private
-
def connection
ActiveRecord::Base.connection
end
end
class << self
- attr_accessor :delegate # :nodoc:
- attr_accessor :disable_ddl_transaction # :nodoc:
+ attr_accessor :delegate #:nodoc:
+ attr_accessor :disable_ddl_transaction #:nodoc:
- def nearest_delegate # :nodoc:
+ def nearest_delegate #:nodoc:
delegate || superclass.nearest_delegate
end
@@ -595,13 +601,13 @@ module ActiveRecord
end
end
- def maintain_test_schema! # :nodoc:
+ def maintain_test_schema! #:nodoc:
if ActiveRecord::Base.maintain_test_schema
suppress_messages { load_schema_if_pending! }
end
end
- def method_missing(name, *args, &block) # :nodoc:
+ def method_missing(name, *args, &block) #:nodoc:
nearest_delegate.send(name, *args, &block)
end
@@ -618,7 +624,7 @@ module ActiveRecord
end
end
- def disable_ddl_transaction # :nodoc:
+ def disable_ddl_transaction #:nodoc:
self.class.disable_ddl_transaction
end
@@ -693,7 +699,7 @@ module ActiveRecord
connection.respond_to?(:reverting) && connection.reverting
end
- ReversibleBlockHelper = Struct.new(:reverting) do # :nodoc:
+ ReversibleBlockHelper = Struct.new(:reverting) do #:nodoc:
def up
yield unless reverting
end
@@ -878,13 +884,14 @@ module ActiveRecord
def copy(destination, sources, options = {})
copied = []
+ schema_migration = options[:schema_migration] || ActiveRecord::SchemaMigration
FileUtils.mkdir_p(destination) unless File.exist?(destination)
- destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations
+ destination_migrations = ActiveRecord::MigrationContext.new(destination, schema_migration).migrations
last = destination_migrations.last
sources.each do |scope, path|
- source_migrations = ActiveRecord::MigrationContext.new(path).migrations
+ source_migrations = ActiveRecord::MigrationContext.new(path, schema_migration).migrations
source_migrations.each do |migration|
source = File.binread(migration.filename)
@@ -985,7 +992,6 @@ module ActiveRecord
delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
private
-
def migration
@migration ||= load_migration
end
@@ -1006,11 +1012,12 @@ module ActiveRecord
end
end
- class MigrationContext # :nodoc:
- attr_reader :migrations_paths
+ class MigrationContext #:nodoc:
+ attr_reader :migrations_paths, :schema_migration
- def initialize(migrations_paths)
+ def initialize(migrations_paths, schema_migration)
@migrations_paths = migrations_paths
+ @schema_migration = schema_migration
end
def migrate(target_version = nil, &block)
@@ -1041,7 +1048,7 @@ module ActiveRecord
migrations
end
- Migrator.new(:up, selected_migrations, target_version).migrate
+ Migrator.new(:up, selected_migrations, schema_migration, target_version).migrate
end
def down(target_version = nil)
@@ -1051,20 +1058,20 @@ module ActiveRecord
migrations
end
- Migrator.new(:down, selected_migrations, target_version).migrate
+ Migrator.new(:down, selected_migrations, schema_migration, target_version).migrate
end
def run(direction, target_version)
- Migrator.new(direction, migrations, target_version).run
+ Migrator.new(direction, migrations, schema_migration, target_version).run
end
def open
- Migrator.new(:up, migrations, nil)
+ Migrator.new(:up, migrations, schema_migration)
end
def get_all_versions
- if SchemaMigration.table_exists?
- SchemaMigration.all_versions.map(&:to_i)
+ if schema_migration.table_exists?
+ schema_migration.all_versions.map(&:to_i)
else
[]
end
@@ -1087,10 +1094,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)
@@ -1105,12 +1108,12 @@ module ActiveRecord
end
def migrations_status
- db_list = ActiveRecord::SchemaMigration.normalized_versions
+ db_list = schema_migration.normalized_versions
file_list = migration_files.map do |file|
version, name, scope = parse_migration_filename(file)
raise IllegalMigrationNameError.new(file) unless version
- version = ActiveRecord::SchemaMigration.normalize_migration_number(version)
+ version = schema_migration.normalize_migration_number(version)
status = db_list.delete(version) ? "up" : "down"
[status, version, (name + scope).humanize]
end.compact
@@ -1122,11 +1125,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,8 +1143,17 @@ 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)
+ migrator = Migrator.new(direction, migrations, schema_migration)
if current_version != 0 && !migrator.current_migration
raise UnknownMigrationVersionError.new(current_version)
@@ -1169,30 +1176,24 @@ 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
+ MigrationContext.new(migrations_paths, SchemaMigration).current_version
end
end
self.migrations_paths = ["db/migrate"]
- def initialize(direction, migrations, target_version = nil)
+ def initialize(direction, migrations, schema_migration, target_version = nil)
@direction = direction
@target_version = target_version
@migrated_versions = nil
@migrations = migrations
+ @schema_migration = schema_migration
validate(@migrations)
- ActiveRecord::SchemaMigration.create_table
+ @schema_migration.create_table
ActiveRecord::InternalMetadata.create_table
end
@@ -1246,11 +1247,10 @@ module ActiveRecord
end
def load_migrated
- @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions)
+ @migrated_versions = Set.new(@schema_migration.all_versions.map(&:to_i))
end
private
-
# Used for running a specific migration.
def run_without_lock
migration = migrations.detect { |m| m.version == @target_version }
@@ -1330,10 +1330,10 @@ module ActiveRecord
def record_version_state_after_migrating(version)
if down?
migrated.delete(version)
- ActiveRecord::SchemaMigration.where(version: version.to_s).delete_all
+ @schema_migration.delete_by(version: version.to_s)
else
migrated << version
- ActiveRecord::SchemaMigration.create!(version: version.to_s)
+ @schema_migration.create!(version: version.to_s)
end
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 82f5121d94..67172ef395 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -14,6 +14,8 @@ module ActiveRecord
# * change_column
# * change_column_default (must supply a :from and :to option)
# * change_column_null
+ # * change_column_comment (must supply a :from and :to option)
+ # * change_table_comment (must supply a :from and :to option)
# * create_join_table
# * create_table
# * disable_extension
@@ -35,7 +37,8 @@ module ActiveRecord
:change_column_default, :add_reference, :remove_reference, :transaction,
:drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension,
:change_column, :execute, :remove_columns, :change_column_null,
- :add_foreign_key, :remove_foreign_key
+ :add_foreign_key, :remove_foreign_key,
+ :change_column_comment, :change_table_comment
]
include JoinTable
@@ -115,7 +118,6 @@ module ActiveRecord
end
private
-
module StraightReversions # :nodoc:
private
{
@@ -231,28 +233,39 @@ module ActiveRecord
end
def invert_remove_foreign_key(args)
- from_table, options_or_to_table, options_or_nil = args
+ options = args.extract_options!
+ from_table, to_table = args
- to_table = if options_or_to_table.is_a?(Hash)
- options_or_to_table[:to_table]
- else
- options_or_to_table
- end
-
- remove_options = if options_or_to_table.is_a?(Hash)
- options_or_to_table.except(:to_table)
- else
- options_or_nil
- end
+ to_table ||= options.delete(:to_table)
raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil?
reversed_args = [from_table, to_table]
- reversed_args << remove_options if remove_options.present?
+ reversed_args << options unless options.empty?
[:add_foreign_key, reversed_args]
end
+ def invert_change_column_comment(args)
+ table, column, options = *args
+
+ unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
+ raise ActiveRecord::IrreversibleMigration, "change_column_comment is only reversible if given a :from and :to option."
+ end
+
+ [:change_column_comment, [table, column, from: options[:to], to: options[:from]]]
+ end
+
+ def invert_change_table_comment(args)
+ table, options = *args
+
+ unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
+ raise ActiveRecord::IrreversibleMigration, "change_table_comment is only reversible if given a :from and :to option."
+ end
+
+ [:change_table_comment, [table, from: options[:to], to: options[:from]]]
+ end
+
def respond_to_missing?(method, _)
super || delegate.respond_to?(method)
end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 8f6fcfcaea..ef78a9161e 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -13,16 +13,71 @@ module ActiveRecord
const_get(name)
end
- V6_0 = Current
+ V6_1 = Current
+
+ class V6_0 < V6_1
+ end
class V5_2 < V6_0
+ module TableDefinition
+ def timestamps(**options)
+ options[:precision] ||= nil
+ super
+ end
+ end
+
module CommandRecorder
def invert_transaction(args, &block)
[:transaction, args, block]
end
+
+ def invert_change_column_comment(args)
+ table_name, column_name, comment = args
+ [:change_column_comment, [table_name, column_name, from: comment, to: comment]]
+ end
+
+ def invert_change_table_comment(args)
+ table_name, comment = args
+ [:change_table_comment, [table_name, from: comment, to: comment]]
+ 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 +90,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 +123,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 +142,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 +168,7 @@ module ActiveRecord
class << t
prepend TableDefinition
end
- t
+ super
end
end
@@ -156,33 +186,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 +203,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 +223,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/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb
index 9abb289bb0..45169617c1 100644
--- a/activerecord/lib/active_record/migration/join_table.rb
+++ b/activerecord/lib/active_record/migration/join_table.rb
@@ -4,7 +4,6 @@ module ActiveRecord
class Migration
module JoinTable #:nodoc:
private
-
def find_join_table_name(table_1, table_2, options = {})
options.delete(:table_name) || join_table_name(table_1, table_2)
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index c50a420432..18f19af6be 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"
@@ -440,13 +456,11 @@ module ActiveRecord
end
protected
-
def initialize_load_schema_monitor
@load_schema_monitor = Monitor.new
end
private
-
def inherited(child_class)
super
child_class.initialize_load_schema_monitor
@@ -468,6 +482,9 @@ module ActiveRecord
end
def load_schema!
+ unless table_name
+ raise ActiveRecord::TableNotSpecified, "#{self} has no table configured. Set one with #{self}.table_name="
+ end
@columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns)
@columns_hash.each do |name, column|
define_attribute(
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 8b9098df6c..ab107742ed 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -2,7 +2,6 @@
require "active_support/core_ext/hash/except"
require "active_support/core_ext/module/redefine_method"
-require "active_support/core_ext/object/try"
require "active_support/core_ext/hash/indifferent_access"
module ActiveRecord
@@ -354,7 +353,6 @@ module ActiveRecord
end
private
-
# Generates a writer method for this association. Serves as a point for
# accessing the objects in the association. For example, this method
# could generate the following:
@@ -386,7 +384,6 @@ module ActiveRecord
end
private
-
# Attribute hash keys that should not be assigned as normal attributes.
# These hash keys are nested attributes implementation details.
UNASSIGNABLE_KEYS = %w( id _destroy )
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index cf0de0fdeb..bee5b5f24a 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -60,7 +60,6 @@ module ActiveRecord
end
private
-
def exec_queries
@records = [].freeze
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 7bf8d568df..323b01ab2d 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_record/insert_all"
+
module ActiveRecord
# = Active Record \Persistence
module Persistence
@@ -55,6 +57,192 @@ module ActiveRecord
end
end
+ # Inserts a single record into the database in a single SQL INSERT
+ # statement. It does not instantiate any models nor does it trigger
+ # Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # See <tt>ActiveRecord::Persistence#insert_all</tt> for documentation.
+ def insert(attributes, returning: nil, unique_by: nil)
+ insert_all([ attributes ], returning: returning, unique_by: unique_by)
+ end
+
+ # Inserts multiple records into the database in a single SQL INSERT
+ # statement. It does not instantiate any models nor does it trigger
+ # Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # The +attributes+ parameter is an Array of Hashes. Every Hash determines
+ # the attributes for a single row and must have the same keys.
+ #
+ # Rows are considered to be unique by every unique index on the table. Any
+ # duplicate rows are skipped.
+ # Override with <tt>:unique_by</tt> (see below).
+ #
+ # Returns an <tt>ActiveRecord::Result</tt> with its contents based on
+ # <tt>:returning</tt> (see below).
+ #
+ # ==== Options
+ #
+ # [:returning]
+ # (PostgreSQL only) An array of attributes to return for all successfully
+ # inserted records, which by default is the primary key.
+ # Pass <tt>returning: %w[ id name ]</tt> for both id and name
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
+ # clause entirely.
+ #
+ # [:unique_by]
+ # (PostgreSQL and SQLite only) By default rows are considered to be unique
+ # by every unique index on the table. Any duplicate rows are skipped.
+ #
+ # To skip rows according to just one unique index pass <tt>:unique_by</tt>.
+ #
+ # Consider a Book model where no duplicate ISBNs make sense, but if any
+ # row has an existing id, or is not unique by another unique index,
+ # <tt>ActiveRecord::RecordNotUnique</tt> is raised.
+ #
+ # Unique indexes can be identified by columns or name:
+ #
+ # unique_by: :isbn
+ # unique_by: %i[ author_id name ]
+ # unique_by: :index_books_on_isbn
+ #
+ # Because it relies on the index information from the database
+ # <tt>:unique_by</tt> is recommended to be paired with
+ # Active Record's schema_cache.
+ #
+ # ==== Example
+ #
+ # # Insert records and skip inserting any duplicates.
+ # # Here "Eloquent Ruby" is skipped because its id is not unique.
+ #
+ # Book.insert_all([
+ # { id: 1, title: "Rework", author: "David" },
+ # { id: 1, title: "Eloquent Ruby", author: "Russ" }
+ # ])
+ def insert_all(attributes, returning: nil, unique_by: nil)
+ InsertAll.new(self, attributes, on_duplicate: :skip, returning: returning, unique_by: unique_by).execute
+ end
+
+ # Inserts a single record into the database in a single SQL INSERT
+ # statement. It does not instantiate any models nor does it trigger
+ # Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # See <tt>ActiveRecord::Persistence#insert_all!</tt> for more.
+ def insert!(attributes, returning: nil)
+ insert_all!([ attributes ], returning: returning)
+ end
+
+ # Inserts multiple records into the database in a single SQL INSERT
+ # statement. It does not instantiate any models nor does it trigger
+ # Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # The +attributes+ parameter is an Array of Hashes. Every Hash determines
+ # the attributes for a single row and must have the same keys.
+ #
+ # Raises <tt>ActiveRecord::RecordNotUnique</tt> if any rows violate a
+ # unique index on the table. In that case, no rows are inserted.
+ #
+ # To skip duplicate rows, see <tt>ActiveRecord::Persistence#insert_all</tt>.
+ # To replace them, see <tt>ActiveRecord::Persistence#upsert_all</tt>.
+ #
+ # Returns an <tt>ActiveRecord::Result</tt> with its contents based on
+ # <tt>:returning</tt> (see below).
+ #
+ # ==== Options
+ #
+ # [:returning]
+ # (PostgreSQL only) An array of attributes to return for all successfully
+ # inserted records, which by default is the primary key.
+ # Pass <tt>returning: %w[ id name ]</tt> for both id and name
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
+ # clause entirely.
+ #
+ # ==== Examples
+ #
+ # # Insert multiple records
+ # Book.insert_all!([
+ # { title: "Rework", author: "David" },
+ # { title: "Eloquent Ruby", author: "Russ" }
+ # ])
+ #
+ # # Raises ActiveRecord::RecordNotUnique because "Eloquent Ruby"
+ # # does not have a unique id.
+ # Book.insert_all!([
+ # { id: 1, title: "Rework", author: "David" },
+ # { id: 1, title: "Eloquent Ruby", author: "Russ" }
+ # ])
+ def insert_all!(attributes, returning: nil)
+ InsertAll.new(self, attributes, on_duplicate: :raise, returning: returning).execute
+ end
+
+ # Updates or inserts (upserts) a single record into the database in a
+ # single SQL INSERT statement. It does not instantiate any models nor does
+ # it trigger Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # See <tt>ActiveRecord::Persistence#upsert_all</tt> for documentation.
+ def upsert(attributes, returning: nil, unique_by: nil)
+ upsert_all([ attributes ], returning: returning, unique_by: unique_by)
+ end
+
+ # Updates or inserts (upserts) multiple records into the database in a
+ # single SQL INSERT statement. It does not instantiate any models nor does
+ # it trigger Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # The +attributes+ parameter is an Array of Hashes. Every Hash determines
+ # the attributes for a single row and must have the same keys.
+ #
+ # Returns an <tt>ActiveRecord::Result</tt> with its contents based on
+ # <tt>:returning</tt> (see below).
+ #
+ # ==== Options
+ #
+ # [:returning]
+ # (PostgreSQL only) An array of attributes to return for all successfully
+ # inserted records, which by default is the primary key.
+ # Pass <tt>returning: %w[ id name ]</tt> for both id and name
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
+ # clause entirely.
+ #
+ # [:unique_by]
+ # (PostgreSQL and SQLite only) By default rows are considered to be unique
+ # by every unique index on the table. Any duplicate rows are skipped.
+ #
+ # To skip rows according to just one unique index pass <tt>:unique_by</tt>.
+ #
+ # Consider a Book model where no duplicate ISBNs make sense, but if any
+ # row has an existing id, or is not unique by another unique index,
+ # <tt>ActiveRecord::RecordNotUnique</tt> is raised.
+ #
+ # Unique indexes can be identified by columns or name:
+ #
+ # unique_by: :isbn
+ # unique_by: %i[ author_id name ]
+ # unique_by: :index_books_on_isbn
+ #
+ # Because it relies on the index information from the database
+ # <tt>:unique_by</tt> is recommended to be paired with
+ # Active Record's schema_cache.
+ #
+ # ==== Examples
+ #
+ # # Inserts multiple records, performing an upsert when records have duplicate ISBNs.
+ # # Here "Eloquent Ruby" overwrites "Rework" because its ISBN is duplicate.
+ #
+ # Book.upsert_all([
+ # { title: "Rework", author: "David", isbn: "1" },
+ # { title: "Eloquent Ruby", author: "Russ", isbn: "1" }
+ # ], unique_by: :isbn)
+ #
+ # Book.find_by(isbn: "1").title # => "Eloquent Ruby"
+ def upsert_all(attributes, returning: nil, unique_by: nil)
+ InsertAll.new(self, attributes, on_duplicate: :update, returning: returning, unique_by: unique_by).execute
+ end
+
# Given an attributes hash, +instantiate+ returns a new instance of
# the appropriate class. Accepts only keys as strings.
#
@@ -96,11 +284,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 +330,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,10 +349,11 @@ 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:
+ primary_key = self.primary_key
primary_key_value = nil
if primary_key && Hash === values
@@ -233,20 +424,20 @@ module ActiveRecord
# Returns true if this object hasn't been saved yet -- that is, a record
# for the object doesn't exist in the database yet; otherwise, returns false.
def new_record?
- sync_with_transaction_state
+ sync_with_transaction_state if @transaction_state&.finalized?
@new_record
end
# Returns true if this object has been destroyed, otherwise returns false.
def destroyed?
- sync_with_transaction_state
+ sync_with_transaction_state if @transaction_state&.finalized?
@destroyed
end
# Returns true if the record is persisted, i.e. it's not a new record and it was
# not destroyed, otherwise returns false.
def persisted?
- sync_with_transaction_state
+ sync_with_transaction_state if @transaction_state&.finalized?
!(@new_record || @destroyed)
end
@@ -340,7 +531,6 @@ module ActiveRecord
def destroy
_raise_readonly_record_error if readonly?
destroy_associations
- self.class.connection.add_transaction_record(self)
@_trigger_destroy_callback = if persisted?
destroy_row > 0
else
@@ -378,7 +568,6 @@ module ActiveRecord
became.send(:initialize)
became.instance_variable_set("@attributes", @attributes)
became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= nil)
- became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.errors.copy!(errors)
@@ -434,7 +623,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 +637,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)
@@ -475,8 +664,13 @@ module ActiveRecord
raise ActiveRecordError, "cannot update a new record" if new_record?
raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
+ attributes = attributes.transform_keys do |key|
+ name = key.to_s
+ self.class.attribute_aliases[name] || name
+ end
+
attributes.each_key do |key|
- verify_readonly_attribute(key.to_s)
+ verify_readonly_attribute(key)
end
id_in_database = self.id_in_database
@@ -486,7 +680,7 @@ module ActiveRecord
affected_rows = self.class._update_record(
attributes,
- self.class.primary_key => id_in_database
+ @primary_key => id_in_database
)
affected_rows == 1
@@ -655,15 +849,12 @@ module ActiveRecord
# ball.touch(:updated_at) # => raises ActiveRecordError
#
def touch(*names, time: nil)
- unless persisted?
- raise ActiveRecordError, <<-MSG.squish
- cannot touch on a new or destroyed record object. Consider using
- persisted?, new_record?, or destroyed? before touching
- MSG
- end
+ _raise_record_not_touched_error unless persisted?
attribute_names = timestamp_attributes_for_update_in_model
- attribute_names |= names.map(&:to_s)
+ attribute_names |= names.map!(&:to_s).map! { |name|
+ self.class.attribute_aliases[name] || name
+ }
unless attribute_names.empty?
affected_rows = _touch_row(attribute_names, time)
@@ -674,7 +865,6 @@ module ActiveRecord
end
private
-
# A hook to be overridden by association modules.
def destroy_associations
end
@@ -684,15 +874,14 @@ module ActiveRecord
end
def _delete_row
- self.class._delete_record(self.class.primary_key => id_in_database)
+ self.class._delete_record(@primary_key => id_in_database)
end
def _touch_row(attribute_names, time)
time ||= current_time_from_proper_timezone
attribute_names.each do |attr_name|
- write_attribute(attr_name, time)
- clear_attribute_change(attr_name)
+ _write_attribute(attr_name, time)
end
_update_row(attribute_names, "touch")
@@ -701,14 +890,14 @@ module ActiveRecord
def _update_row(attribute_names, attempted_action = "update")
self.class._update_record(
attributes_with_values(attribute_names),
- self.class.primary_key => id_in_database
+ @primary_key => id_in_database
)
end
- 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
@@ -739,7 +928,7 @@ module ActiveRecord
attributes_with_values(attribute_names)
)
- self.id ||= new_id if self.class.primary_key
+ self.id ||= new_id if @primary_key
@new_record = false
@@ -749,7 +938,7 @@ module ActiveRecord
end
def verify_readonly_attribute(name)
- raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
+ raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attribute?(name)
end
def _raise_record_not_destroyed
@@ -759,14 +948,21 @@ module ActiveRecord
@_association_destroy_exception = nil
end
+ def _raise_readonly_record_error
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly"
+ end
+
+ def _raise_record_not_touched_error
+ raise ActiveRecordError, <<~MSG.squish
+ Cannot touch on a new or destroyed record object. Consider using
+ persisted?, new_record?, or destroyed? before touching.
+ MSG
+ end
+
# The name of the method used to touch a +belongs_to+ association when the
# +:touch+ option is used.
def belongs_to_touch_method
:touch
end
-
- def _raise_readonly_record_error
- raise ReadOnlyRecord, "#{self.class} is marked as readonly"
- end
end
end
diff --git a/activerecord/lib/active_record/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..08cfc3fe5f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -2,31 +2,36 @@
module ActiveRecord
module Querying
- delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all
- delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all
- 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 :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,
- :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
- delegate :pluck, :pick, :ids, to: :all
+ QUERYING_METHODS = [
+ :find, :find_by, :find_by!, :take, :take!, :first, :first!, :last, :last!,
+ :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!,
+ :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!,
+ :exists?, :any?, :many?, :none?, :one?,
+ :first_or_create, :first_or_create!, :first_or_initialize,
+ :find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
+ :create_or_find_by, :create_or_find_by!,
+ :destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by,
+ :find_each, :find_in_batches, :in_batches,
+ :select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
+ :where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
+ :having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
+ :count, :average, :minimum, :maximum, :sum, :calculate, :annotate,
+ :pluck, :pick, :ids
+ ].freeze # :nodoc:
+ delegate(*QUERYING_METHODS, 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 +45,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 +65,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..d5375390c7 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
@@ -126,7 +134,6 @@ end_error
cache = YAML.load(File.read(filename))
if cache.version == current_version
- connection.schema_cache = cache
connection_pool.schema_cache = cache.dup
else
warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}."
@@ -140,7 +147,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 +174,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 +196,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 +254,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 475baa7559..4d9acc911b 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -2,6 +2,8 @@
require "active_record"
+databases = ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml
+
db_namespace = namespace :db do
desc "Set the environment value for the database"
task "environment:set" => :load_config do
@@ -23,7 +25,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
desc "Create #{spec_name} database for current environment"
task spec_name => :load_config do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
@@ -42,7 +44,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
desc "Drop #{spec_name} database for current environment"
task spec_name => [:load_config, :check_protected_environments] do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
@@ -66,6 +68,11 @@ db_namespace = namespace :db do
end
end
+ # desc "Truncates tables of each database for current environment"
+ task truncate_all: [:load_config, :check_protected_environments] do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all
+ end
+
# desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
task purge: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.purge_current
@@ -73,7 +80,7 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task migrate: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate
end
@@ -96,7 +103,7 @@ db_namespace = namespace :db do
end
namespace :migrate do
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
desc "Migrate #{spec_name} database for current environment"
task spec_name => :load_config do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
@@ -105,7 +112,7 @@ db_namespace = namespace :db do
end
end
- # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
+ desc "Rolls back the database one migration and re-migrates up (options: STEP=x, VERSION=x)."
task redo: :load_config do
raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
@@ -121,8 +128,10 @@ db_namespace = namespace :db do
# desc 'Resets your database using your migrations for the current environment'
task reset: ["db:drop", "db:create", "db:migrate"]
- # desc 'Runs the "up" for a given migration VERSION.'
+ desc 'Runs the "up" for a given migration VERSION.'
task up: :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:up")
+
raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
ActiveRecord::Tasks::DatabaseTasks.check_target_version
@@ -134,8 +143,29 @@ db_namespace = namespace :db do
db_namespace["_dump"].invoke
end
- # desc 'Runs the "down" for a given migration VERSION.'
+ namespace :up do
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
+ task spec_name => :load_config do
+ raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
+
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+ ActiveRecord::Base.connection.migration_context.run(
+ :up,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
+
+ db_namespace["_dump"].invoke
+ end
+ end
+ end
+
+ desc 'Runs the "down" for a given migration VERSION.'
task down: :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:down")
+
raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty?
ActiveRecord::Tasks::DatabaseTasks.check_target_version
@@ -147,16 +177,35 @@ db_namespace = namespace :db do
db_namespace["_dump"].invoke
end
+ namespace :down do
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
+ task spec_name => :load_config do
+ raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
+
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+ ActiveRecord::Base.connection.migration_context.run(
+ :down,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
+
+ db_namespace["_dump"].invoke
+ end
+ end
+ end
+
desc "Display status of migrations"
task status: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate_status
end
end
namespace :status do
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
desc "Display status of migrations for #{spec_name} database"
task spec_name => :load_config do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
@@ -181,7 +230,7 @@ db_namespace = namespace :db do
db_namespace["_dump"].invoke
end
- # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
+ desc "Drops and recreates the database from db/schema.rb for the current environment and loads the seeds."
task reset: [ "db:drop", "db:setup" ]
# desc "Retrieves the charset for the current environment's database"
@@ -191,11 +240,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"
@@ -205,7 +252,11 @@ db_namespace = namespace :db do
# desc "Raises an error if there are pending migrations"
task abort_if_pending_migrations: :load_config do
- pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations
+ pending_migrations = ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).flat_map do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+
+ ActiveRecord::Base.connection.migration_context.open.pending_migrations
+ end
if pending_migrations.any?
puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
@@ -216,15 +267,70 @@ db_namespace = namespace :db do
end
end
+ namespace :abort_if_pending_migrations do
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
+ # desc "Raises an error if there are pending migrations for #{spec_name} database"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+ ActiveRecord::Base.establish_connection(db_config.config)
+
+ pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations
+
+ if pending_migrations.any?
+ puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
+ pending_migrations.each do |pending_migration|
+ puts " %4d %s" % [pending_migration.version, pending_migration.name]
+ end
+ abort %{Run `rails db:migrate:#{spec_name}` to update your database then try again.}
+ end
+ end
+ end
+ end
+
desc "Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)"
task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed]
+ desc "Runs setup if database does not exist, or runs migrations if it does"
+ task prepare: :load_config do
+ seed = false
+
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+
+ # Skipped when no database
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ if ActiveRecord::Base.dump_schema_after_migration
+ ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, ActiveRecord::Base.schema_format, db_config.spec_name)
+ end
+
+ rescue ActiveRecord::NoDatabaseError
+ ActiveRecord::Tasks::DatabaseTasks.create_current(db_config.env_name, db_config.spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(
+ db_config.config,
+ ActiveRecord::Base.schema_format,
+ nil,
+ db_config.env_name,
+ db_config.spec_name
+ )
+
+ seed = true
+ end
+
+ ActiveRecord::Base.establish_connection
+ ActiveRecord::Tasks::DatabaseTasks.load_seed if seed
+ end
+
desc "Loads the seed data from db/seeds.rb"
task seed: :load_config do
db_namespace["abort_if_pending_migrations"].invoke
ActiveRecord::Tasks::DatabaseTasks.load_seed
end
+ namespace :seed do
+ desc "Truncates tables of each database for current environment and loads the seeds"
+ task replant: [:load_config, :truncate_all, :seed]
+ end
+
namespace :fixtures do
desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
task load: :load_config do
@@ -276,13 +382,9 @@ db_namespace = namespace :db do
namespace :schema do
desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record"
task dump: :load_config do
- require "active_record/schema_dumper"
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
- filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby)
- File.open(filename, "w:utf-8") do |file|
- ActiveRecord::Base.establish_connection(db_config.config)
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
- end
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, :ruby, db_config.spec_name)
end
db_namespace["schema:dump"].reenable
@@ -300,7 +402,7 @@ db_namespace = namespace :db do
namespace :cache do
desc "Creates a db/schema_cache.yml file."
task dump: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name)
ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(
@@ -312,7 +414,7 @@ db_namespace = namespace :db do
desc "Clears a db/schema_cache.yml file."
task clear: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name)
rm_f filename, verbose: false
end
@@ -323,16 +425,9 @@ db_namespace = namespace :db do
namespace :structure do
desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql"
task dump: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
- filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql)
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename)
- if ActiveRecord::SchemaMigration.table_exists?
- File.open(filename, "a") do |f|
- f.puts ActiveRecord::Base.connection.dump_schema_information
- f.print "\n"
- end
- end
+ ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, :sql, db_config.spec_name)
end
db_namespace["structure:dump"].reenable
@@ -361,17 +456,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
@@ -411,6 +504,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/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index 7bc26993d5..c851ed52c3 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -19,6 +19,10 @@ module ActiveRecord
def readonly_attributes
_attr_readonly
end
+
+ def readonly_attribute?(name) # :nodoc:
+ _attr_readonly.include?(name)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index b2110f727c..cbfa60d4d9 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -21,12 +21,12 @@ module ActiveRecord
def add_reflection(ar, name, reflection)
ar.clear_reflections_cache
- name = name.to_s
+ name = -name.to_s
ar._reflections = ar._reflections.except(name).merge!(name => reflection)
end
def add_aggregate_reflection(ar, name, reflection)
- ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(-name.to_s => reflection)
end
private
@@ -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
@@ -481,7 +477,7 @@ module ActiveRecord
def check_preloadable!
return unless scope
- if scope.arity > 0
+ unless scope.arity == 0
raise ArgumentError, <<-MSG.squish
The association scope '#{name}' is instance dependent (the scope
block takes an argument). Preloading instance dependent scopes is
@@ -594,7 +590,6 @@ module ActiveRecord
end
private
-
def calculate_constructable(macro, options)
true
end
@@ -612,21 +607,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)
-
- 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
+ if can_find_inverse_of_automatically?(self)
+ inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
- inverse_name_candidates.detect do |inverse_name|
begin
reflection = klass._reflect_on_association(inverse_name)
rescue NameError
@@ -635,7 +618,9 @@ module ActiveRecord
reflection = false
end
- valid_inverse_reflection?(reflection)
+ if valid_inverse_reflection?(reflection)
+ return inverse_name
+ end
end
end
@@ -718,7 +703,6 @@ module ActiveRecord
end
private
-
def calculate_constructable(macro, options)
!options[:through]
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index d5b6082d13..ea8f44752b 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class Relation
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :left_outer_joins, :references,
- :extending, :unscope]
+ :extending, :unscope, :optimizer_hints, :annotate]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache]
@@ -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,
@@ -181,6 +197,10 @@ module ActiveRecord
# if a DELETE between those two statements is run by another client. But for most applications,
# that's a significantly less likely condition to hit.
# * It relies on exception handling to handle control flow, which may be marginally slower.
+ # * The primary key may auto-increment on each create, even if it fails. This can accelerate
+ # the problem of running out of integers, if the underlying table is still stuck on a primary
+ # key of type int (note: All Rails apps since 5.1+ have defaulted to bigint, which is not liable
+ # to this problem).
#
# This method will return a record if all given attributes are covered by unique constraints
# (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted
@@ -271,32 +291,100 @@ module ActiveRecord
limit_value ? records.many? : size > 1
end
- # Returns a cache key that can be used to identify the records fetched by
- # this query. The cache key is built with a fingerprint of the sql query,
- # the number of records matched by the query and a timestamp of the last
- # updated record. When a new record comes to match the query, or any of
- # the existing records is updated or deleted, the cache key changes.
+ # Returns a stable cache key that can be used to identify this query.
+ # The cache key is built with a fingerprint of the SQL query.
#
- # Product.where("name like ?", "%Cosmic Encounter%").cache_key
- # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # # => "products/query-1850ab3d302391b85b8693e941286659"
#
- # If the collection is loaded, the method will iterate through the records
- # to generate the timestamp, otherwise it will trigger one SQL query like:
+ # If ActiveRecord::Base.collection_cache_versioning is turned off, as it was
+ # in Rails 6.0 and earlier, the cache key will also include a version.
#
- # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ # ActiveRecord::Base.collection_cache_versioning = false
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
#
# You can also pass a custom timestamp column to fetch the timestamp of the
# last updated record.
#
# Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
- #
- # You can customize the strategy to generate the key on a per model basis
- # overriding ActiveRecord::Base#collection_cache_key.
def cache_key(timestamp_column = :updated_at)
@cache_keys ||= {}
- @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column)
+ @cache_keys[timestamp_column] ||= klass.collection_cache_key(self, timestamp_column)
end
+ def compute_cache_key(timestamp_column = :updated_at) # :nodoc:
+ query_signature = ActiveSupport::Digest.hexdigest(to_sql)
+ key = "#{klass.model_name.cache_key}/query-#{query_signature}"
+
+ if cache_version(timestamp_column)
+ key
+ else
+ "#{key}-#{compute_cache_version(timestamp_column)}"
+ end
+ end
+ private :compute_cache_key
+
+ # Returns a cache version that can be used together with the cache key to form
+ # a recyclable caching scheme. The cache version is built with the number of records
+ # matching the query, and the timestamp of the last updated record. When a new record
+ # comes to match the query, or any of the existing records is updated or deleted,
+ # the cache version changes.
+ #
+ # If the collection is loaded, the method will iterate through the records
+ # to generate the timestamp, otherwise it will trigger one SQL query like:
+ #
+ # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ def cache_version(timestamp_column = :updated_at)
+ if collection_cache_versioning
+ @cache_versions ||= {}
+ @cache_versions[timestamp_column] ||= compute_cache_version(timestamp_column)
+ end
+ end
+
+ def compute_cache_version(timestamp_column) # :nodoc:
+ if loaded? || distinct_value
+ size = records.size
+ if size > 0
+ timestamp = max_by(&timestamp_column)._read_attribute(timestamp_column)
+ end
+ else
+ collection = eager_loading? ? apply_join_dependency : self
+
+ column = connection.visitor.compile(arel_attribute(timestamp_column))
+ select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
+
+ if collection.has_limit_or_offset?
+ query = collection.select("#{column} AS collection_cache_key_timestamp")
+ subquery_alias = "subquery_for_cache_key"
+ subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
+ arel = query.build_subquery(subquery_alias, select_values % subquery_column)
+ else
+ query = collection.unscope(:order)
+ query.select_values = [select_values % column]
+ arel = query.arel
+ end
+
+ result = connection.select_one(arel, nil)
+
+ if result
+ column_type = klass.type_for_attribute(timestamp_column)
+ timestamp = column_type.deserialize(result["timestamp"])
+ size = result["size"]
+ else
+ timestamp = nil
+ size = 0
+ end
+ end
+
+ if timestamp
+ "#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
+ else
+ "#{size}"
+ end
+ end
+ private :compute_cache_version
+
# Scope all queries to the current scope.
#
# Comment.where(post_id: 1).scoping do
@@ -307,12 +395,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
@@ -322,6 +410,8 @@ module ActiveRecord
# trigger Active Record callbacks or validations. However, values passed to #update_all will still go through
# Active Record's normal type casting and serialization.
#
+ # Note: As Active Record callbacks are not triggered, this method will not automatically update +updated_at+/+updated_on+ columns.
+ #
# ==== Parameters
#
# * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
@@ -356,6 +446,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))
@@ -372,16 +468,25 @@ module ActiveRecord
end
end
- def update_counters(counters) # :nodoc:
+ # Updates the counters of the records in the current relation.
+ #
+ # === Parameters
+ #
+ # * +counter+ - A Hash containing the names of the fields to update as keys and the amount to update as values.
+ # * <tt>:touch</tt> option - Touch the timestamp columns when updating.
+ # * If attributes names are passed, they are updated along with update_at/on attributes.
+ #
+ # === Examples
+ #
+ # # For Posts by a given author increment the comment_count by 1.
+ # Post.where(author_id: author.id).update_counters(comment_count: 1)
+ def update_counters(counters)
touch = counters.delete(:touch)
updates = {}
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
@@ -393,10 +498,10 @@ module ActiveRecord
update_all updates
end
- # Touches all records in the current relation without instantiating records first with the updated_at/on attributes
+ # Touches all records in the current relation without instantiating records first with the +updated_at+/+updated_on+ attributes
# set to the current time or the time specified.
# This method can be passed attribute names and an optional time argument.
- # If attribute names are passed, they are updated along with updated_at/on attributes.
+ # If attribute names are passed, they are updated along with +updated_at+/+updated_on+ attributes.
# If no time argument is passed, the current time is used as default.
#
# === Examples
@@ -417,12 +522,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
@@ -465,8 +565,8 @@ module ActiveRecord
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct
def delete_all
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
- value = get_value(method)
- SINGLE_VALUE_METHODS.include?(method) ? value : value.any?
+ value = @values[method]
+ method == :distinct ? value : value&.any?
end
if invalid_methods.any?
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
@@ -491,6 +591,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 +637,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,14 +746,46 @@ module ActiveRecord
end
end
+ attr_reader :_deprecated_scope_source # :nodoc:
+
protected
+ attr_writer :_deprecated_scope_source # :nodoc:
def load_records(records)
@records = records.freeze
@loaded = true
end
+ def null_relation? # :nodoc:
+ is_a?(NullRelation)
+ 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,12 +797,19 @@ 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 =
if eager_loading?
apply_join_dependency do |relation, join_dependency|
- if ActiveRecord::NullRelation === relation
+ if relation.null_relation?
[]
else
relation = join_dependency.apply_column_aliases(relation)
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 9c579843b1..30b8edd0bd 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -258,7 +258,6 @@ module ActiveRecord
end
private
-
def apply_limits(relation, start, finish)
relation = apply_start_limit(relation, start) if start
relation = apply_finish_limit(relation, finish) if finish
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 0fa5ba2e50..0a14a33c1d 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,
@@ -133,11 +129,12 @@ module ActiveRecord
relation = apply_join_dependency
if operation.to_s.downcase == "count"
- relation.distinct!
- # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
- if (column_name == :all || column_name.nil?) && select_values.empty?
- relation.order_values = []
+ unless distinct_value || distinct_select?(column_name || select_for_count)
+ relation.distinct!
+ relation.select_values = [ klass.primary_key || table[Arel.star] ]
end
+ # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
+ relation.order_values = []
end
relation.calculate(operation, column_name)
@@ -190,11 +187,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 +222,6 @@ module ActiveRecord
end
private
-
def has_include?(column_name)
eager_loading? || (includes_values.present? && column_name && column_name != :all)
end
@@ -242,10 +236,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,13 +253,15 @@ 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
- if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name)
- @klass.arel_attribute(column_name)
- else
- Arel.sql(column_name == :all ? "*" : column_name.to_s)
+ arel_column(column_name.to_s) do |name|
+ Arel.sql(column_name == :all ? "*" : name)
end
end
@@ -308,25 +306,22 @@ module ActiveRecord
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
- group_attrs = group_values
+ group_fields = group_values
- if group_attrs.first.respond_to?(:to_sym)
- association = @klass._reflect_on_association(group_attrs.first)
- associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
- group_fields = Array(associated ? association.foreign_key : group_attrs)
- else
- group_fields = group_attrs
+ if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
+ association = klass._reflect_on_association(group_fields.first)
+ associated = association && association.belongs_to? # only count belongs_to associations
+ group_fields = Array(association.foreign_key) if associated
end
group_fields = arel_columns(group_fields)
- group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_aliases = group_fields.map { |field|
+ field = connection.visitor.compile(field) if Arel.arel_node?(field)
+ column_alias_for(field.to_s.downcase)
+ }
group_columns = group_aliases.zip(group_fields)
- if operation == "count" && column_name == :all
- aggregate_alias = "count_all"
- else
- aggregate_alias = column_alias_for([operation, column_name].join(" "))
- end
+ aggregate_alias = column_alias_for("#{operation}_#{column_name.to_s.downcase}")
select_values = [
operation_over_aggregate_column(
@@ -371,25 +366,23 @@ module ActiveRecord
end]
end
- # Converts the given keys to the value that the database adapter returns as
+ # Converts the given field to the value that the database adapter returns as
# a usable column name:
#
# column_alias_for("users.id") # => "users_id"
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
- def column_alias_for(keys)
- if keys.respond_to? :name
- keys = "#{keys.relation.name}.#{keys.name}"
- end
+ def column_alias_for(field)
+ return field if field.match?(/\A\w{,#{connection.table_alias_length}}\z/)
- table_name = keys.to_s.downcase
- table_name.gsub!(/\*/, "all")
- table_name.gsub!(/\W+/, " ")
- table_name.strip!
- table_name.gsub!(/ +/, "_")
+ column_alias = +field
+ column_alias.gsub!(/\*/, "all")
+ column_alias.gsub!(/\W+/, " ")
+ column_alias.strip!
+ column_alias.gsub!(/ +/, "_")
- @klass.connection.table_alias_for(table_name)
+ connection.table_alias_for(column_alias)
end
def type_for(field, &block)
@@ -401,7 +394,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
@@ -417,16 +410,17 @@ module ActiveRecord
def build_count_subquery(relation, column_name, distinct)
if column_name == :all
+ column_alias = Arel.star
relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
else
column_alias = Arel.sql("count_column")
relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
end
- subquery = relation.arel.as(Arel.sql("subquery_for_count"))
- select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false)
+ subquery_alias = Arel.sql("subquery_for_count")
+ select_value = operation_over_aggregate_column(column_alias, "count", false)
- Arel::SelectManager.new(subquery).project(select_value)
+ relation.build_subquery(subquery_alias, select_value)
end
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 383dc1bf4b..2f61c05eca 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,35 @@ 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
+ @generated_relation_methods ||= GeneratedRelationMethods.new.tap do |mod|
+ const_set(:GeneratedRelationMethods, mod)
+ private_constant :GeneratedRelationMethods
end
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 +92,17 @@ 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
@@ -133,7 +115,6 @@ module ActiveRecord
end
private
-
def relation_class_for(klass)
klass.relation_delegate_class(self)
end
@@ -141,7 +122,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 afaa900442..1dbf4808fd 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 cannot 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
@@ -353,13 +346,18 @@ module ActiveRecord
end
private
-
def offset_index
offset_value || 0
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 = except(:order).limit!(1)
+ else
+ relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
+ end
case conditions
when Array, Hash
@@ -371,14 +369,10 @@ module ActiveRecord
relation
end
- def construct_join_dependency(associations)
- ActiveRecord::Associations::JoinDependency.new(
- klass, table, associations
- )
- end
-
def apply_join_dependency(eager_loading: group_values.empty?)
- join_dependency = construct_join_dependency(eager_load_values + includes_values)
+ join_dependency = construct_join_dependency(
+ eager_load_values + includes_values, Arel::Nodes::OuterJoin
+ )
relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
if eager_loading && !using_limitable_reflections?(join_dependency.reflections)
@@ -432,9 +426,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 +541,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/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 4de7465128..e1735c0522 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -89,7 +89,6 @@ module ActiveRecord
end
private
-
def merge_preloads
return if other.preload_values.empty? && other.includes_values.empty?
@@ -117,16 +116,16 @@ module ActiveRecord
if other.klass == relation.klass
relation.joins!(*other.joins_values)
else
- joins_dependency = other.joins_values.map do |join|
+ associations, others = other.joins_values.partition do |join|
case join
- when Hash, Symbol, Array
- other.send(:construct_join_dependency, join)
- else
- join
+ when Hash, Symbol, Array; true
end
end
- relation.joins!(*joins_dependency)
+ join_dependency = other.construct_join_dependency(
+ associations, Arel::Nodes::InnerJoin
+ )
+ relation.joins!(join_dependency, *others)
end
end
@@ -136,16 +135,11 @@ module ActiveRecord
if other.klass == relation.klass
relation.left_outer_joins!(*other.left_outer_joins_values)
else
- joins_dependency = other.left_outer_joins_values.map do |join|
- case join
- when Hash, Symbol, Array
- other.send(:construct_join_dependency, join)
- else
- join
- end
- end
-
- relation.left_outer_joins!(*joins_dependency)
+ associations = other.left_outer_joins_values
+ join_dependency = other.construct_join_dependency(
+ associations, Arel::Nodes::OuterJoin
+ )
+ relation.joins!(join_dependency)
end
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/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 eb80aab701..6a181882ae 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -41,18 +41,31 @@ module ActiveRecord
#
# User.where.not(name: %w(Ko1 Nobu))
# # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
- #
- # User.where.not(name: "Jon", role: "admin")
- # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
def not(opts, *rest)
opts = sanitize_forbidden_attributes(opts)
where_clause = @scope.send(:where_clause_factory).build(opts, rest)
@scope.references!(PredicateBuilder.references(opts)) if Hash === opts
- @scope.where_clause += where_clause.invert
+
+ if not_behaves_as_nor?(opts)
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
+ NOT conditions will no longer behave as NOR in Rails 6.1.
+ To continue using NOR conditions, NOT each conditions manually
+ (`#{ opts.keys.map { |key| ".where.not(#{key.inspect} => ...)" }.join }`).
+ MSG
+ @scope.where_clause += where_clause.invert(:nor)
+ else
+ @scope.where_clause += where_clause.invert
+ end
+
@scope
end
+
+ private
+ def not_behaves_as_nor?(opts)
+ opts.is_a?(Hash) && opts.size > 1
+ end
end
FROZEN_EMPTY_ARRAY = [].freeze
@@ -67,11 +80,13 @@ module ActiveRecord
end
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{method_name} # def includes_values
- get_value(#{name.inspect}) # get_value(:includes)
+ default = DEFAULT_VALUES[:#{name}] # default = DEFAULT_VALUES[:includes]
+ @values.fetch(:#{name}, default) # @values.fetch(:includes, default)
end # end
def #{method_name}=(value) # def includes_values=(value)
- set_value(#{name.inspect}, value) # set_value(:includes, value)
+ assert_mutability! # assert_mutability!
+ @values[:#{name}] = value # @values[:includes] = value
end # end
CODE
end
@@ -100,7 +115,7 @@ module ActiveRecord
#
# === conditions
#
- # If you want to add conditions to your included models you'll have
+ # If you want to add string conditions to your included models, you'll have
# to explicitly reference them. For example:
#
# User.includes(:posts).where('posts.name = ?', 'example')
@@ -111,6 +126,12 @@ module ActiveRecord
#
# Note that #includes works with association names while #references needs
# the actual table name.
+ #
+ # If you pass the conditions via hash, you don't need to call #references
+ # explicitly, as #where references the tables for you. For example, this
+ # will work correctly:
+ #
+ # User.includes(:posts).where(posts: { name: 'example' })
def includes(*args)
check_if_method_has_arguments!(:includes, args)
spawn.includes!(*args)
@@ -154,6 +175,19 @@ module ActiveRecord
self
end
+ # Extracts a named +association+ from the relation. The named association is first preloaded,
+ # then the individual association records are collected from the relation. Like so:
+ #
+ # account.memberships.extract_associated(:user)
+ # # => Returns collection of User records
+ #
+ # This is short-hand for:
+ #
+ # account.memberships.preload(:user).collect(&:user)
+ def extract_associated(association)
+ preload(association).collect(&association)
+ end
+
# Use to indicate that the given +table_names+ are referenced by an SQL string,
# and should therefore be JOINed in any query rather than loaded separately.
# This method only works in conjunction with #includes.
@@ -233,13 +267,31 @@ module ActiveRecord
def _select!(*fields) # :nodoc:
fields.reject!(&:blank?)
fields.flatten!
- fields.map! do |field|
- klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
- end
self.select_values += fields
self
end
+ # Allows you to change a previously set select statement.
+ #
+ # Post.select(:title, :body)
+ # # SELECT `posts`.`title`, `posts`.`body` FROM `posts`
+ #
+ # Post.select(:title, :body).reselect(:created_at)
+ # # SELECT `posts`.`created_at` FROM `posts`
+ #
+ # This is short-hand for <tt>unscope(:select).select(fields)</tt>.
+ # Note that we're unscoping the entire select statement.
+ def reselect(*args)
+ check_if_method_has_arguments!(:reselect, args)
+ spawn.reselect!(*args)
+ end
+
+ # Same as #reselect but operates on relation in-place instead of copying.
+ def reselect!(*args) # :nodoc:
+ self.select_values = args
+ self
+ end
+
# Allows to specify a group attribute:
#
# User.group(:name)
@@ -328,8 +380,8 @@ module ActiveRecord
end
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
- :limit, :offset, :joins, :left_outer_joins,
- :includes, :from, :readonly, :having])
+ :limit, :offset, :joins, :left_outer_joins, :annotate,
+ :includes, :from, :readonly, :having, :optimizer_hints])
# Removes an unwanted relation that is already defined on a chain of relations.
# This is useful when passing around chains of relations and would like to
@@ -380,7 +432,8 @@ module ActiveRecord
if !VALID_UNSCOPING_VALUES.include?(scope)
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
end
- set_value(scope, DEFAULT_VALUES[scope])
+ assert_mutability!
+ @values[scope] = DEFAULT_VALUES[scope]
when Hash
scope.each do |key, target_value|
if key != :where
@@ -880,6 +933,29 @@ module ActiveRecord
self
end
+ # Specify optimizer hints to be used in the SELECT statement.
+ #
+ # Example (for MySQL):
+ #
+ # Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
+ # # SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`
+ #
+ # Example (for PostgreSQL with pg_hint_plan):
+ #
+ # Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
+ # # SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
+ def optimizer_hints(*args)
+ check_if_method_has_arguments!(:optimizer_hints, args)
+ spawn.optimizer_hints!(*args)
+ end
+
+ def optimizer_hints!(*args) # :nodoc:
+ args.flatten!
+
+ self.optimizer_hints_values |= args
+ self
+ end
+
# Reverse the existing order clause on the relation.
#
# User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC'
@@ -904,23 +980,47 @@ module ActiveRecord
self
end
+ # Adds an SQL comment to queries generated from this relation. For example:
+ #
+ # User.annotate("selecting user names").select(:name)
+ # # SELECT "users"."name" FROM "users" /* selecting user names */
+ #
+ # User.annotate("selecting", "user", "names").select(:name)
+ # # SELECT "users"."name" FROM "users" /* selecting */ /* user */ /* names */
+ #
+ # The SQL block comment delimiters, "/*" and "*/", will be added automatically.
+ def annotate(*args)
+ check_if_method_has_arguments!(:annotate, args)
+ spawn.annotate!(*args)
+ end
+
+ # Like #annotate, but modifies relation in place.
+ def annotate!(*args) # :nodoc:
+ self.annotate_values += args
+ self
+ end
+
# Returns the Arel object associated with the relation.
def arel(aliases = nil) # :nodoc:
@arel ||= build_arel(aliases)
end
- private
- # Returns a relation value with a given name
- def get_value(name)
- @values.fetch(name, DEFAULT_VALUES[name])
- end
+ def construct_join_dependency(associations, join_type) # :nodoc:
+ ActiveRecord::Associations::JoinDependency.new(
+ klass, table, associations, join_type
+ )
+ end
+
+ protected
+ def build_subquery(subquery_alias, select_value) # :nodoc:
+ subquery = except(:optimizer_hints).arel.as(subquery_alias)
- # Sets the relation value with the given name
- def set_value(name, value)
- assert_mutability!
- @values[name] = value
+ Arel::SelectManager.new(subquery).project(select_value).tap do |arel|
+ arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
+ end
end
+ private
def assert_mutability!
raise ImmutableRelation if @loaded
raise ImmutableRelation if defined?(@arel) && @arel
@@ -929,8 +1029,11 @@ module ActiveRecord
def build_arel(aliases)
arel = Arel::SelectManager.new(table)
- aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty?
- build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty?
+ if !joins_values.empty?
+ build_joins(arel, joins_values.flatten, aliases)
+ elsif !left_outer_joins_values.empty?
+ build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases)
+ end
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
@@ -956,9 +1059,11 @@ module ActiveRecord
build_select(arel)
+ arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
arel.distinct(distinct_value)
arel.from(build_from) unless from_clause.empty?
arel.lock(lock_value) if lock_value
+ arel.comment(*annotate_values) unless annotate_values.empty?
arel
end
@@ -978,22 +1083,28 @@ module ActiveRecord
end
end
- def build_left_outer_joins(manager, outer_joins, aliases)
- buckets = outer_joins.group_by do |join|
- case join
+ def valid_association_list(associations)
+ associations.each do |association|
+ case association
when Hash, Symbol, Array
- :association_join
- when ActiveRecord::Associations::JoinDependency
- :stashed_join
+ # valid
else
raise ArgumentError, "only Hash, Symbol and Array are allowed"
end
end
+ end
+ def build_left_outer_joins(manager, outer_joins, aliases)
+ buckets = { association_join: valid_association_list(outer_joins) }
build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
end
def build_joins(manager, joins, aliases)
+ unless left_outer_joins_values.empty?
+ left_joins = valid_association_list(left_outer_joins_values.flatten)
+ joins.unshift construct_join_dependency(left_joins, Arel::Nodes::OuterJoin)
+ end
+
buckets = joins.group_by do |join|
case join
when String
@@ -1017,27 +1128,21 @@ module ActiveRecord
association_joins = buckets[:association_join]
stashed_joins = buckets[:stashed_join]
- join_nodes = buckets[:join_node].uniq
- string_joins = buckets[:string_join].map(&:strip).uniq
+ join_nodes = buckets[:join_node].tap(&:uniq!)
+ string_joins = buckets[:string_join].delete_if(&:blank?).map!(&:strip).tap(&:uniq!)
- join_list = join_nodes + convert_join_strings_to_ast(string_joins)
- alias_tracker = alias_tracker(join_list, aliases)
+ string_joins.map! { |join| table.create_string_join(Arel.sql(join)) }
- join_dependency = construct_join_dependency(association_joins)
+ join_sources = manager.join_sources
+ join_sources.concat(join_nodes) unless join_nodes.empty?
- joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker)
- joins.each { |join| manager.from(join) }
-
- manager.join_sources.concat(join_list)
-
- alias_tracker.aliases
- end
+ unless association_joins.empty? && stashed_joins.empty?
+ alias_tracker = alias_tracker(join_nodes + string_joins, aliases)
+ join_dependency = construct_join_dependency(association_joins, join_type)
+ join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker))
+ end
- def convert_join_strings_to_ast(joins)
- joins
- .flatten
- .reject(&:blank?)
- .map { |join| table.create_string_join(Arel.sql(join)) }
+ join_sources.concat(string_joins) unless string_joins.empty?
end
def build_select(arel)
@@ -1052,11 +1157,14 @@ 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
+ arel_column(field.to_s) do |attr_name|
+ connection.quote_table_name(attr_name)
+ end
+ when String
+ arel_column(field, &:itself)
+ when Proc
field.call
else
field
@@ -1064,6 +1172,21 @@ module ActiveRecord
end
end
+ def arel_column(field)
+ field = klass.attribute_aliases[field] || field
+ from = from_clause.name || from_clause.value
+
+ if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
+ arel_attribute(field)
+ else
+ yield field
+ end
+ end
+
+ def table_name_matches?(from)
+ /(?:\A|(?<!FROM)\s)(?:\b#{table.name}\b|#{connection.quote_table_name(table.name)})(?!\.)/i.match?(from.to_s)
+ end
+
def reverse_sql_order(order_query)
if order_query.empty?
return [arel_attribute(primary_key).desc] if primary_key
@@ -1079,7 +1202,7 @@ module ActiveRecord
o.reverse
when String
if does_not_support_reverse?(o)
- raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
+ raise IrreversibleOrderError, "Order #{o.inspect} cannot be reversed automatically"
end
o.split(",").map! do |s|
s.strip!
@@ -1099,7 +1222,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)
@@ -1125,6 +1248,7 @@ module ActiveRecord
end
def preprocess_order_args(order_args)
+ order_args.reject!(&:blank?)
order_args.map! do |arg|
klass.sanitize_sql_for_order(arg)
end
@@ -1132,7 +1256,7 @@ module ActiveRecord
@klass.disallow_raw_sql!(
order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
- permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER
+ permit: connection.column_name_with_order_matcher
)
validate_order_args(order_args)
@@ -1145,14 +1269,14 @@ module ActiveRecord
order_args.map! do |arg|
case arg
when Symbol
- arel_attribute(arg).asc
+ order_column(arg.to_s).asc
when Hash
arg.map { |field, dir|
case field
when Arel::Nodes::SqlLiteral
field.send(dir.downcase)
else
- arel_attribute(field).send(dir.downcase)
+ order_column(field.to_s).send(dir.downcase)
end
}
else
@@ -1161,6 +1285,16 @@ module ActiveRecord
end.flatten!
end
+ def order_column(field)
+ arel_column(field) do |attr_name|
+ if attr_name == "count" && !group_values.empty?
+ arel_attribute(attr_name)
+ else
+ Arel.sql(connection.quote_table_name(attr_name))
+ end
+ end
+ end
+
# Checks to make sure that the arguments are not blank. Note that if some
# blank-like object were initially passed into the query method, then this
# method will not raise an error.
@@ -1187,7 +1321,8 @@ module ActiveRecord
def structurally_incompatible_values_for_or(other)
values = other.values
STRUCTURAL_OR_METHODS.reject do |method|
- get_value(method) == values.fetch(method, DEFAULT_VALUES[method])
+ default = DEFAULT_VALUES[method]
+ @values.fetch(method, default) == values.fetch(method, default)
end
end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 562e04194c..3f6dd50139 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.
@@ -67,7 +67,6 @@ module ActiveRecord
end
private
-
def relation_with(values)
result = Relation.create(klass, values: values)
result.extend(*extending_values) if extending_values.any?
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index e225628bae..8fae380b0a 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -70,7 +70,15 @@ module ActiveRecord
predicates == other.predicates
end
- def invert
+ def invert(as = :nand)
+ if predicates.size == 1
+ inverted_predicates = [ invert_predicate(predicates.first) ]
+ elsif as == :nor
+ inverted_predicates = predicates.map { |node| invert_predicate(node) }
+ else
+ inverted_predicates = [ Arel::Nodes::Not.new(ast) ]
+ end
+
WhereClause.new(inverted_predicates)
end
@@ -79,7 +87,6 @@ module ActiveRecord
end
protected
-
attr_reader :predicates
def referenced_columns
@@ -115,10 +122,6 @@ module ActiveRecord
node.respond_to?(:operator) && node.operator == :==
end
- def inverted_predicates
- predicates.map { |node| invert_predicate(node) }
- end
-
def invert_predicate(node)
case node
when NilClass
@@ -140,11 +143,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/result.rb b/activerecord/lib/active_record/result.rb
index da6d10b6ec..3b615f29a3 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -132,7 +132,6 @@ module ActiveRecord
end
private
-
def column_type(name, type_overrides = {})
type_overrides.fetch(name) do
column_types.fetch(name, Type.default_value)
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 3485d9e557..b16cbb0f84 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -61,8 +61,9 @@ module ActiveRecord
# # => "id ASC"
def sanitize_sql_for_order(condition)
if condition.is_a?(Array) && condition.first.to_s.include?("?")
- disallow_raw_sql!([condition.first],
- permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER
+ disallow_raw_sql!(
+ [condition.first],
+ permit: connection.column_name_with_order_matcher
)
# Ensure we aren't dealing with a subclass of String that might
@@ -133,44 +134,34 @@ module ActiveRecord
end
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
+ def disallow_raw_sql!(args, permit: connection.column_name_matcher) # :nodoc:
+ unexpected = nil
+ args.each do |arg|
+ next if arg.is_a?(Symbol) || Arel.arel_node?(arg) || permit.match?(arg.to_s)
+ (unexpected ||= []) << arg
end
- deprecate :expand_hash_conditions_for_aggregates
+ return unless unexpected
+
+ if allow_unsafe_raw_sql == :deprecated
+ ActiveSupport::Deprecation.warn(
+ "Dangerous query method (method whose arguments are used as raw " \
+ "SQL) called with non-attribute argument(s): " \
+ "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \
+ "arguments will be disallowed in Rails 6.1. This method should " \
+ "not be called with user-provided values, such as request " \
+ "parameters or model attributes. Known-safe values can be passed " \
+ "by wrapping them in Arel.sql()."
+ )
+ else
+ raise(ActiveRecord::UnknownAttributeReference,
+ "Query method called with non-attribute argument(s): " +
+ unexpected.map(&:inspect).join(", ")
+ )
+ end
+ end
+
+ private
def replace_bind_variables(statement, values)
raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size)
bound = values.dup
@@ -202,10 +193,11 @@ module ActiveRecord
def quote_bound_value(value, c = connection)
if value.respond_to?(:map) && !value.acts_like?(:string)
- if value.respond_to?(:empty?) && value.empty?
+ quoted = value.map { |v| c.quote(v) }
+ if quoted.empty?
c.quote(nil)
else
- value.map { |v| c.quote(v) }.join(",")
+ quoted.join(",")
end
else
c.quote(value)
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 216359867c..aba25fb375 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -50,21 +50,12 @@ module ActiveRecord
instance_eval(&block)
if info[:version].present?
- ActiveRecord::SchemaMigration.create_table
- connection.assume_migrated_upto_version(info[:version], migrations_paths)
+ connection.schema_migration.create_table
+ 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_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index d475e77444..f4b1f536b3 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -47,6 +47,7 @@ module ActiveRecord
end
private
+ attr_accessor :table_name
def initialize(connection, options = {})
@connection = connection
@@ -110,6 +111,8 @@ HEADER
def table(table, stream)
columns = @connection.columns(table)
begin
+ self.table_name = table
+
tbl = StringIO.new
# first dump primary key column
@@ -143,7 +146,11 @@ HEADER
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
next if column.name == pk
type, colspec = column_spec(column)
- tbl.print " t.#{type} #{column.name.inspect}"
+ if type.is_a?(Symbol)
+ tbl.print " t.#{type} #{column.name.inspect}"
+ else
+ tbl.print " t.column #{column.name.inspect}, #{type.inspect}"
+ end
tbl.print ", #{format_colspec(colspec)}" if colspec.present?
tbl.puts
end
@@ -159,6 +166,8 @@ HEADER
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
stream.puts "# #{e.message}"
stream.puts
+ ensure
+ self.table_name = nil
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index f2d8b038fa..dec7fee986 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -10,16 +10,16 @@ module ActiveRecord
# to be executed the next time.
class SchemaMigration < ActiveRecord::Base # :nodoc:
class << self
+ def _internal?
+ true
+ end
+
def primary_key
"version"
end
def table_name
- "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
- end
-
- def table_exists?
- connection.table_exists?(table_name)
+ "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
end
def create_table
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 9eba1254a4..62c7988bd8 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:
@@ -96,7 +95,6 @@ module ActiveRecord
end
private
-
def raise_invalid_scope_type!(scope_type)
if !VALID_SCOPE_TYPES.include?(scope_type)
raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES"
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 6caf9b3251..151eef362b 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?
@@ -51,7 +44,6 @@ module ActiveRecord
end
private
-
# Use this macro in your model to set a default scope for all operations on
# the model.
#
@@ -93,8 +85,8 @@ module ActiveRecord
# # Should return a scope, you can call 'super' here etc.
# end
# end
- def default_scope(scope = nil) # :doc:
- scope = Proc.new if block_given?
+ def default_scope(scope = nil, &block) # :doc:
+ scope = block if block_given?
if scope.is_a?(Relation) || !scope.respond_to?(:call)
raise ArgumentError,
@@ -107,7 +99,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 +110,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..7baef99e83 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
@@ -50,7 +58,7 @@ module ActiveRecord
end
def default_extensions # :nodoc:
- if scope = current_scope || build_default_scope
+ if scope = scope_for_association || build_default_scope
scope.extensions
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
@@ -196,7 +204,6 @@ module ActiveRecord
end
private
-
def valid_scope_name?(name)
if respond_to?(name, true) && logger
logger.warn "Creating scope :#{name}. " \
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index 1b1736dcab..93bce15230 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -113,8 +113,8 @@ module ActiveRecord
end
end
- def self.create(connection, block = Proc.new)
- relation = block.call Params.new
+ def self.create(connection, callable = nil, &block)
+ relation = (callable || block).call Params.new
query_builder, binds = connection.cacheable_query(self, relation.arel)
bind_map = BindMap.new(binds)
new(query_builder, bind_map, relation.klass)
@@ -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/store.rb b/activerecord/lib/active_record/store.rb
index 3537e2d008..6fecb06897 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -11,6 +11,12 @@ module ActiveRecord
# of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
# already built around just accessing attributes on the model.
#
+ # Every accessor comes with dirty tracking methods (+key_changed?+, +key_was+ and +key_change+) and
+ # methods to access the changes made during the last save (+saved_change_to_key?+, +saved_change_to_key+ and
+ # +key_before_last_save+).
+ #
+ # NOTE: There is no +key_will_change!+ method for accessors, use +store_will_change!+ instead.
+ #
# Make sure that you declare the database column used for the serialized store as a text, so there's
# plenty of room.
#
@@ -49,6 +55,12 @@ module ActiveRecord
# u.settings[:country] # => 'Denmark'
# u.settings['country'] # => 'Denmark'
#
+ # # Dirty tracking
+ # u.color = 'green'
+ # u.color_changed? # => true
+ # u.color_was # => 'black'
+ # u.color_change # => ['black', 'red']
+ #
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
@@ -127,6 +139,42 @@ module ActiveRecord
define_method(accessor_key) do
read_store_attribute(store_attribute, key)
end
+
+ define_method("#{accessor_key}_changed?") do
+ return false unless attribute_changed?(store_attribute)
+ prev_store, new_store = changes[store_attribute]
+ prev_store&.dig(key) != new_store&.dig(key)
+ end
+
+ define_method("#{accessor_key}_change") do
+ return unless attribute_changed?(store_attribute)
+ prev_store, new_store = changes[store_attribute]
+ [prev_store&.dig(key), new_store&.dig(key)]
+ end
+
+ define_method("#{accessor_key}_was") do
+ return unless attribute_changed?(store_attribute)
+ prev_store, _new_store = changes[store_attribute]
+ prev_store&.dig(key)
+ end
+
+ define_method("saved_change_to_#{accessor_key}?") do
+ return false unless saved_change_to_attribute?(store_attribute)
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
+ prev_store&.dig(key) != new_store&.dig(key)
+ end
+
+ define_method("saved_change_to_#{accessor_key}") do
+ return unless saved_change_to_attribute?(store_attribute)
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
+ [prev_store&.dig(key), new_store&.dig(key)]
+ end
+
+ define_method("#{accessor_key}_before_last_save") do
+ return unless saved_change_to_attribute?(store_attribute)
+ prev_store, _new_store = saved_change_to_attribute(store_attribute)
+ prev_store&.dig(key)
+ end
end
end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index b67479fb6a..9a1176db6a 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -4,17 +4,18 @@ module ActiveRecord
class TableMetadata # :nodoc:
delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true
- def initialize(klass, arel_table, association = nil)
+ def initialize(klass, arel_table, association = nil, types = klass)
@klass = klass
+ @types = types
@arel_table = arel_table
@association = association
end
def resolve_column_aliases(hash)
new_hash = hash.dup
- hash.each do |key, _|
- if (key.is_a?(Symbol)) && klass.attribute_alias?(key)
- new_hash[klass.attribute_alias(key)] = new_hash.delete(key)
+ hash.each_key do |key|
+ if key.is_a?(Symbol) && new_key = klass.attribute_aliases[key.to_s]
+ new_hash[new_key] = new_hash.delete(key)
end
end
new_hash
@@ -29,11 +30,7 @@ module ActiveRecord
end
def type(column_name)
- if klass
- klass.type_for_attribute(column_name)
- else
- Type.default_value
- end
+ types.type_for_attribute(column_name)
end
def has_column?(column_name)
@@ -52,13 +49,12 @@ module ActiveRecord
elsif association && !association.polymorphic?
association_klass = association.klass
arel_table = association_klass.arel_table.alias(table_name)
+ TableMetadata.new(association_klass, arel_table, association)
else
type_caster = TypeCaster::Connection.new(klass, table_name)
- association_klass = nil
arel_table = Arel::Table.new(table_name, type_caster: type_caster)
+ TableMetadata.new(nil, arel_table, association, type_caster)
end
-
- TableMetadata.new(association_klass, arel_table, association)
end
def polymorphic_association?
@@ -74,6 +70,6 @@ module ActiveRecord
end
private
- attr_reader :klass, :arel_table, :association
+ attr_reader :klass, :types, :arel_table, :association
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 27e401a756..a78bebf764 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -141,8 +141,19 @@ module ActiveRecord
end
end
- def for_each
- databases = Rails.application.config.database_configuration
+ def setup_initial_database_yaml
+ return {} unless defined?(Rails)
+
+ begin
+ Rails.application.config.load_database_yaml
+ rescue
+ $stderr.puts "Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB."
+
+ {}
+ end
+ end
+
+ def for_each(databases)
database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env)
# if this is a single database application we don't want tasks for each primary database
@@ -153,8 +164,22 @@ module ActiveRecord
end
end
- def create_current(environment = env)
- each_current_configuration(environment) { |configuration|
+ def raise_for_multi_db(environment = env, command:)
+ db_configs = ActiveRecord::Base.configurations.configs_for(env_name: environment)
+
+ if db_configs.count > 1
+ dbs_list = []
+
+ db_configs.each do |db|
+ dbs_list << "#{command}:#{db.spec_name}"
+ end
+
+ raise "You're using a multiple database application. To use `#{command}` you must run the namespaced task with a VERSION. Available tasks are #{dbs_list.to_sentence}."
+ end
+ end
+
+ def create_current(environment = env, spec_name = nil)
+ each_current_configuration(environment, spec_name) { |configuration|
create configuration
}
ActiveRecord::Base.establish_connection(environment.to_sym)
@@ -182,6 +207,26 @@ module ActiveRecord
}
end
+ def truncate_tables(configuration)
+ ActiveRecord::Base.connected_to(database: { truncation: configuration }) do
+ conn = ActiveRecord::Base.connection
+ table_names = conn.tables
+ table_names -= [
+ conn.schema_migration.table_name,
+ InternalMetadata.table_name
+ ]
+
+ ActiveRecord::Base.connection.truncate_tables(*table_names)
+ end
+ end
+ private :truncate_tables
+
+ def truncate_all(environment = env)
+ ActiveRecord::Base.configurations.configs_for(env_name: environment).each do |db_config|
+ truncate_tables db_config.config
+ end
+ end
+
def migrate
check_target_version
@@ -198,7 +243,7 @@ module ActiveRecord
end
def migrate_status
- unless ActiveRecord::SchemaMigration.table_exists?
+ unless ActiveRecord::Base.connection.schema_migration.table_exists?
Kernel.abort "Schema migrations table does not exist yet."
end
@@ -290,6 +335,27 @@ module ActiveRecord
Migration.verbose = verbose_was
end
+ def dump_schema(configuration, format = ActiveRecord::Base.schema_format, spec_name = "primary") # :nodoc:
+ require "active_record/schema_dumper"
+ filename = dump_filename(spec_name, format)
+ connection = ActiveRecord::Base.connection
+
+ case format
+ when :ruby
+ File.open(filename, "w:utf-8") do |file|
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ end
+ when :sql
+ structure_dump(configuration, filename)
+ if connection.schema_migration.table_exists?
+ File.open(filename, "a") do |f|
+ f.puts connection.dump_schema_information
+ f.print "\n"
+ end
+ end
+ end
+ end
+
def schema_file(format = ActiveRecord::Base.schema_format)
File.join(db_dir, schema_file_type(format))
end
@@ -371,12 +437,14 @@ module ActiveRecord
task.is_a?(String) ? task.constantize : task
end
- def each_current_configuration(environment)
+ def each_current_configuration(environment, spec_name = nil)
environments = [environment]
environments << "test" if environment == "development"
environments.each do |env|
ActiveRecord::Base.configurations.configs_for(env_name: env).each do |db_config|
+ next if spec_name && spec_name != db_config.spec_name
+
yield db_config.config, db_config.spec_name, env
end
end
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 1c1b29b5e1..a7e04007a9 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -3,6 +3,8 @@
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
+ ER_DB_CREATE_EXISTS = 1007
+
delegate :connection, :establish_connection, to: ActiveRecord::Base
def initialize(configuration)
@@ -14,7 +16,7 @@ module ActiveRecord
connection.create_database configuration["database"], creation_options
establish_connection configuration
rescue ActiveRecord::StatementInvalid => error
- if error.message.include?("database exists")
+ if error.cause.error_number == ER_DB_CREATE_EXISTS
raise DatabaseAlreadyExists
else
raise
@@ -67,7 +69,6 @@ module ActiveRecord
end
private
-
attr_reader :configuration
def configuration_without_database
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 8acb11f75f..626ffdfdf9 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -89,7 +89,6 @@ module ActiveRecord
end
private
-
attr_reader :configuration
def encoding
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index a82cea80ca..f67a3498b6 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -59,7 +59,6 @@ module ActiveRecord
end
private
-
attr_reader :configuration, :root
def run_cmd(cmd, args, out)
diff --git a/activerecord/lib/active_record/test_fixtures.rb b/activerecord/lib/active_record/test_fixtures.rb
index 7b7b3f7112..1d6fef1eb9 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,32 @@ 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..c883d368b5 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -56,22 +56,29 @@ 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
- def timestamp_attributes_for_create_in_model
- timestamp_attributes_for_create.select { |c| column_names.include?(c) }
- end
+ def timestamp_attributes_for_create_in_model
+ @timestamp_attributes_for_create_in_model ||=
+ (timestamp_attributes_for_create & column_names).freeze
+ end
- def timestamp_attributes_for_update_in_model
- timestamp_attributes_for_update.select { |c| column_names.include?(c) }
- end
+ def timestamp_attributes_for_update_in_model
+ @timestamp_attributes_for_update_in_model ||=
+ (timestamp_attributes_for_update & column_names).freeze
+ end
- def all_timestamp_attributes_in_model
- timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
- end
+ def all_timestamp_attributes_in_model
+ @all_timestamp_attributes_in_model ||=
+ (timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model).freeze
+ end
+
+ def current_time_from_proper_timezone
+ default_timezone == :utc ? Time.now.utc : Time.now
+ end
+ private
def timestamp_attributes_for_create
["created_at", "created_on"]
end
@@ -80,13 +87,15 @@ module ActiveRecord
["updated_at", "updated_on"]
end
- def current_time_from_proper_timezone
- default_timezone == :utc ? Time.now.utc : Time.now
+ def reload_schema_from_cache
+ @timestamp_attributes_for_create_in_model = nil
+ @timestamp_attributes_for_update_in_model = nil
+ @all_timestamp_attributes_in_model = nil
+ super
end
end
private
-
def _create_record
if record_timestamps
current_time = current_time_from_proper_timezone
@@ -101,8 +110,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 +119,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?
@@ -118,26 +133,25 @@ module ActiveRecord
end
def timestamp_attributes_for_create_in_model
- self.class.send(:timestamp_attributes_for_create_in_model)
+ self.class.timestamp_attributes_for_create_in_model
end
def timestamp_attributes_for_update_in_model
- self.class.send(:timestamp_attributes_for_update_in_model)
+ self.class.timestamp_attributes_for_update_in_model
end
def all_timestamp_attributes_in_model
- self.class.send(:all_timestamp_attributes_in_model)
+ self.class.all_timestamp_attributes_in_model
end
def current_time_from_proper_timezone
- self.class.send(:current_time_from_proper_timezone)
+ self.class.current_time_from_proper_timezone
end
- def max_updated_column_timestamp(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/touch_later.rb b/activerecord/lib/active_record/touch_later.rb
index f70b7c50a2..3981bd46ad 100644
--- a/activerecord/lib/active_record/touch_later.rb
+++ b/activerecord/lib/active_record/touch_later.rb
@@ -2,7 +2,7 @@
module ActiveRecord
# = Active Record Touch Later
- module TouchLater
+ module TouchLater # :nodoc:
extend ActiveSupport::Concern
included do
@@ -10,19 +10,15 @@ module ActiveRecord
end
def touch_later(*names) # :nodoc:
- unless persisted?
- raise ActiveRecordError, <<-MSG.squish
- cannot touch on a new or destroyed record object. Consider using
- persisted?, new_record?, or destroyed? before touching
- MSG
- end
+ _raise_record_not_touched_error unless persisted?
@_defer_touch_attrs ||= timestamp_attributes_for_update_in_model
@_defer_touch_attrs |= names
@_touch_time = current_time_from_proper_timezone
surreptitiously_touch @_defer_touch_attrs
- self.class.connection.add_transaction_record self
+ add_to_transaction
+ @_new_record_before_last_commit ||= false
# touch the parents as we are not calling the after_save callbacks
self.class.reflect_on_all_associations(:belongs_to).each do |r|
@@ -40,7 +36,6 @@ module ActiveRecord
end
private
-
def surreptitiously_touch(attrs)
attrs.each { |attr| write_attribute attr, @_touch_time }
clear_attribute_changes attrs
@@ -48,6 +43,7 @@ module ActiveRecord
def touch_deferred_attributes
if has_defer_touch_attrs? && persisted?
+ @_skip_dirty_tracking = true
touch(*@_defer_touch_attrs, time: @_touch_time)
@_defer_touch_attrs, @_touch_time = nil, nil
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index fe3842b905..5113e08e8e 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -164,12 +164,12 @@ module ActiveRecord
# end
# end
#
- # only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it.
+ # only "Kotori" is created.
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
- # transactions by using savepoints on MySQL and PostgreSQL. See
+ # transactions by using savepoints. See
# https://dev.mysql.com/doc/refman/5.7/en/savepoint.html
# for more information about savepoints.
#
@@ -234,6 +234,12 @@ module ActiveRecord
set_callback(:commit, :after, *args, &block)
end
+ # Shortcut for <tt>after_commit :hook, on: [ :create, :update ]</tt>.
+ def after_save_commit(*args, &block)
+ set_options_for_callbacks!(args, on: [ :create, :update ])
+ set_callback(:commit, :after, *args, &block)
+ end
+
# Shortcut for <tt>after_commit :hook, on: :create</tt>.
def after_create_commit(*args, &block)
set_options_for_callbacks!(args, on: :create)
@@ -276,7 +282,6 @@ module ActiveRecord
end
private
-
def set_options_for_callbacks!(args, enforced_options = {})
options = args.extract_options!.merge!(enforced_options)
args << options
@@ -327,7 +332,7 @@ module ActiveRecord
# Ensure that it is not called if the object was never persisted (failed create),
# but call it after the commit of a destroyed object.
def committed!(should_run_callbacks: true) #:nodoc:
- if should_run_callbacks && (destroyed? || persisted?)
+ if should_run_callbacks
@_committed_already_called = true
_run_commit_without_transaction_enrollment_callbacks
_run_commit_callbacks
@@ -349,18 +354,6 @@ module ActiveRecord
clear_transaction_record_state
end
- # Add the record to the current transaction so that the #after_rollback and #after_commit callbacks
- # can be called.
- def add_to_transaction
- if has_transactional_callbacks?
- self.class.connection.add_transaction_record(self)
- else
- sync_with_transaction_state
- set_transaction_state(self.class.connection.transaction_state)
- end
- remember_transaction_record_state
- end
-
# Executes +method+ within a transaction and captures its return value as a
# status flag. If the status is true the transaction is committed, otherwise
# a ROLLBACK is issued. In any case the status flag is returned.
@@ -370,29 +363,40 @@ module ActiveRecord
def with_transaction_returning_status
status = nil
self.class.transaction do
- add_to_transaction
+ if has_transactional_callbacks?
+ add_to_transaction
+ else
+ sync_with_transaction_state if @transaction_state&.finalized?
+ @transaction_state = self.class.connection.transaction_state
+ end
+ remember_transaction_record_state
+
status = yield
raise ActiveRecord::Rollback unless status
end
status
end
+ def trigger_transactional_callbacks? # :nodoc:
+ (@_new_record_before_last_commit || _trigger_update_callback) && persisted? ||
+ _trigger_destroy_callback && destroyed?
+ end
+
private
attr_reader :_committed_already_called, :_trigger_update_callback, :_trigger_destroy_callback
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state
- @_start_transaction_state.reverse_merge!(
+ @_start_transaction_state ||= {
id: id,
new_record: @new_record,
destroyed: @destroyed,
+ attributes: @attributes,
frozen?: frozen?,
- )
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
- remember_new_record_before_last_commit
- end
+ level: 0
+ }
+ @_start_transaction_state[:level] += 1
- def remember_new_record_before_last_commit
if _committed_already_called
@_new_record_before_last_commit = false
else
@@ -402,27 +406,32 @@ module ActiveRecord
# Clear the new record state and id of a record.
def clear_transaction_record_state
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
+ return unless @_start_transaction_state
+ @_start_transaction_state[:level] -= 1
force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
end
# Force to clear the transaction record state.
def force_clear_transaction_record_state
- @_start_transaction_state.clear
+ @_start_transaction_state = nil
+ @transaction_state = nil
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
- def restore_transaction_record_state(force = false)
- unless @_start_transaction_state.empty?
- transaction_level = (@_start_transaction_state[:level] || 0) - 1
- if transaction_level < 1 || force
- restore_state = @_start_transaction_state
- thaw
+ def restore_transaction_record_state(force_restore_state = false)
+ if restore_state = @_start_transaction_state
+ if force_restore_state || restore_state[:level] <= 1
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
- pk = self.class.primary_key
- if pk && _read_attribute(pk) != restore_state[:id]
- _write_attribute(pk, restore_state[:id])
+ @attributes = restore_state[:attributes].map do |attr|
+ value = @attributes.fetch_value(attr.name)
+ attr = attr.with_value_from_user(value) if attr.value != value
+ attr
+ end
+ @mutations_from_database = nil
+ @mutations_before_last_save = nil
+ if @attributes.fetch_value(@primary_key) != restore_state[:id]
+ @attributes.write_from_user(@primary_key, restore_state[:id])
end
freeze if restore_state[:frozen?]
end
@@ -443,8 +452,10 @@ module ActiveRecord
end
end
- def set_transaction_state(state)
- @transaction_state = state
+ # Add the record to the current transaction so that the #after_rollback and #after_commit
+ # callbacks can be called.
+ def add_to_transaction
+ self.class.connection.add_transaction_record(self)
end
def has_transactional_callbacks?
@@ -464,19 +475,17 @@ module ActiveRecord
# This method checks to see if the ActiveRecord object's state reflects
# the TransactionState, and rolls back or commits the Active Record object
# as appropriate.
- #
- # Since Active Record objects can be inside multiple transactions, this
- # method recursively goes through the parent of the TransactionState and
- # checks if the Active Record object reflects the state of the object.
def sync_with_transaction_state
- update_attributes_from_transaction_state(@transaction_state)
- end
-
- def update_attributes_from_transaction_state(transaction_state)
- if transaction_state && transaction_state.finalized?
- restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback?
- force_clear_transaction_record_state if transaction_state.fully_committed?
- clear_transaction_record_state if transaction_state.fully_completed?
+ if transaction_state = @transaction_state
+ if transaction_state.fully_committed?
+ force_clear_transaction_record_state
+ elsif transaction_state.committed?
+ clear_transaction_record_state
+ elsif transaction_state.rolledback?
+ force_restore_state = transaction_state.fully_rolledback?
+ restore_transaction_record_state(force_restore_state)
+ clear_transaction_record_state
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index c303186ef2..4c1ef1a7e4 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -47,13 +47,11 @@ module ActiveRecord
end
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/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
index b300fdfa05..c8c16635b1 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -11,7 +11,6 @@ module ActiveRecord
end
private
-
def registration_klass
Registration
end
@@ -53,7 +52,6 @@ module ActiveRecord
end
protected
-
attr_reader :name, :block, :adapter, :override
def priority
@@ -72,7 +70,6 @@ module ActiveRecord
end
private
-
def matches_adapter?(adapter: nil, **)
(self.adapter.nil? || adapter == self.adapter)
end
diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
index db9853fbcc..b260464df5 100644
--- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb
+++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
@@ -16,7 +16,6 @@ module ActiveRecord
end
private
-
def perform_fetch(type, *args, &block)
@mapping.fetch(type, block).call(type, *args)
end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index 0a2f6cb9fb..a34b2fe702 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -56,7 +56,6 @@ module ActiveRecord
end
private
-
def default_value?(value)
value == coder.load(nil)
end
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
index fc40b460f0..58f25ba075 100644
--- a/activerecord/lib/active_record/type/type_map.rb
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -45,7 +45,6 @@ module ActiveRecord
end
private
-
def perform_fetch(lookup_key, *args)
matching_pair = @mapping.reverse_each.detect do |key, _|
key === lookup_key
diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb
index 4619528f81..535369e630 100644
--- a/activerecord/lib/active_record/type/unsigned_integer.rb
+++ b/activerecord/lib/active_record/type/unsigned_integer.rb
@@ -4,7 +4,6 @@ module ActiveRecord
module Type
class UnsignedInteger < ActiveModel::Type::Integer # :nodoc:
private
-
def max_value
super * 2
end
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 7cf8181d8e..f43559f4cb 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -8,21 +8,27 @@ module ActiveRecord
@table_name = table_name
end
- def type_cast_for_database(attribute_name, value)
+ def type_cast_for_database(attr_name, value)
return value if value.is_a?(Arel::Nodes::BindParam)
- column = column_for(attribute_name)
- connection.type_cast_from_column(column, value)
+ type = type_for_attribute(attr_name)
+ type.serialize(value)
end
- private
- attr_reader :table_name
- delegate :connection, to: :@klass
+ def type_for_attribute(attr_name)
+ schema_cache = connection.schema_cache
- def column_for(attribute_name)
- if connection.schema_cache.data_source_exists?(table_name)
- connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
- end
+ if schema_cache.data_source_exists?(table_name)
+ column = schema_cache.columns_hash(table_name)[attr_name.to_s]
+ type = connection.lookup_cast_type_from_column(column) if column
end
+
+ type || Type.default_value
+ end
+
+ delegate :connection, to: :@klass, private: true
+
+ private
+ attr_reader :table_name
end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index ca27a3f0ab..23e8d53168 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -71,7 +71,6 @@ module ActiveRecord
alias_method :validate, :valid?
private
-
def default_validation_context
new_record? ? :create : :update
end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 3538aeec22..dc89df4be7 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -10,7 +10,6 @@ module ActiveRecord
end
private
-
def valid_object?(record)
(record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid?
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 5a1dbc8e53..2c3a2fb797 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
@@ -25,7 +25,7 @@ module ActiveRecord
if finder_class.primary_key
relation = relation.where.not(finder_class.primary_key => record.id_in_database)
else
- raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
+ raise UnknownPrimaryKey.new(finder_class, "Cannot validate uniqueness for persisted record without primary key.")
end
end
relation = scope_relation(record, relation)
@@ -56,33 +56,21 @@ 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 !options.key?(:case_sensitive) || bind.nil?
+ klass.connection.default_uniqueness_comparison(attr, bind, klass)
+ 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..0fc07e1ede 100644
--- a/activerecord/lib/arel.rb
+++ b/activerecord/lib/arel.rb
@@ -12,8 +12,7 @@ require "arel/math"
require "arel/alias_predication"
require "arel/order_predications"
require "arel/table"
-require "arel/attributes"
-require "arel/compatibility/wheres"
+require "arel/attributes/attribute"
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/attributes.rb b/activerecord/lib/arel/attributes.rb
deleted file mode 100644
index 35d586c948..0000000000
--- a/activerecord/lib/arel/attributes.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require "arel/attributes/attribute"
-
-module Arel # :nodoc: all
- module Attributes
- ###
- # Factory method to wrap a raw database +column+ to an Arel Attribute.
- def self.for(column)
- case column.type
- when :string, :text, :binary then String
- when :integer then Integer
- when :float then Float
- when :decimal then Decimal
- when :date, :datetime, :timestamp, :time then Time
- when :boolean then Boolean
- else
- Undefined
- end
- end
- end
-end
diff --git a/activerecord/lib/arel/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/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb
index c90fc33a48..cb31e3060b 100644
--- a/activerecord/lib/arel/insert_manager.rb
+++ b/activerecord/lib/arel/insert_manager.rb
@@ -33,13 +33,13 @@ module Arel # :nodoc: all
@ast.columns << column
values << value
end
- @ast.values = create_values values, @ast.columns
+ @ast.values = create_values(values)
end
self
end
- def create_values(values, columns)
- Nodes::Values.new values, columns
+ def create_values(values)
+ Nodes::ValuesList.new([values])
end
def create_values_list(rows)
diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb
index 5af0e532e2..f994754620 100644
--- a/activerecord/lib/arel/nodes.rb
+++ b/activerecord/lib/arel/nodes.rb
@@ -45,7 +45,6 @@ require "arel/nodes/and"
require "arel/nodes/function"
require "arel/nodes/count"
require "arel/nodes/extract"
-require "arel/nodes/values"
require "arel/nodes/values_list"
require "arel/nodes/named_function"
@@ -62,6 +61,8 @@ require "arel/nodes/outer_join"
require "arel/nodes/right_outer_join"
require "arel/nodes/string_join"
+require "arel/nodes/comment"
+
require "arel/nodes/sql_literal"
require "arel/nodes/casted"
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 ba8340558a..344e46479f 100644
--- a/activerecord/lib/arel/nodes/bind_param.rb
+++ b/activerecord/lib/arel/nodes/bind_param.rb
@@ -24,8 +24,12 @@ module Arel # :nodoc: all
value.nil?
end
- def boundable?
- !value.respond_to?(:boundable?) || value.boundable?
+ def infinite?
+ value.respond_to?(:infinite?) && value.infinite?
+ end
+
+ def unboundable?
+ value.respond_to?(:unboundable?) && value.unboundable?
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/comment.rb b/activerecord/lib/arel/nodes/comment.rb
new file mode 100644
index 0000000000..237ff27e7e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/comment.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Comment < Arel::Nodes::Node
+ attr_reader :values
+
+ def initialize(values)
+ super()
+ @values = values
+ end
+
+ def initialize_copy(other)
+ super
+ @values = @values.clone
+ end
+
+ def hash
+ [@values].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.values == other.values
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb
index 73461ff683..11b4f39ece 100644
--- a/activerecord/lib/arel/nodes/select_core.rb
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -3,20 +3,22 @@
module Arel # :nodoc: all
module Nodes
class SelectCore < Arel::Nodes::Node
- attr_accessor :projections, :wheres, :groups, :windows
- attr_accessor :havings, :source, :set_quantifier
+ attr_accessor :projections, :wheres, :groups, :windows, :comment
+ attr_accessor :havings, :source, :set_quantifier, :optimizer_hints
def initialize
super()
- @source = JoinSource.new nil
+ @source = JoinSource.new nil
# https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
- @set_quantifier = nil
- @projections = []
- @wheres = []
- @groups = []
- @havings = []
- @windows = []
+ @set_quantifier = nil
+ @optimizer_hints = nil
+ @projections = []
+ @wheres = []
+ @groups = []
+ @havings = []
+ @windows = []
+ @comment = nil
end
def from
@@ -42,8 +44,8 @@ module Arel # :nodoc: all
def hash
[
- @source, @set_quantifier, @projections,
- @wheres, @groups, @havings, @windows
+ @source, @set_quantifier, @projections, @optimizer_hints,
+ @wheres, @groups, @havings, @windows, @comment
].hash
end
@@ -51,11 +53,13 @@ module Arel # :nodoc: all
self.class == other.class &&
self.source == other.source &&
self.set_quantifier == other.set_quantifier &&
+ self.optimizer_hints == other.optimizer_hints &&
self.projections == other.projections &&
self.wheres == other.wheres &&
self.groups == other.groups &&
self.havings == other.havings &&
- self.windows == other.windows
+ self.windows == other.windows &&
+ self.comment == other.comment
end
alias :== :eql?
end
diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb
index 00639304e4..6d1ac36b0e 100644
--- a/activerecord/lib/arel/nodes/unary.rb
+++ b/activerecord/lib/arel/nodes/unary.rb
@@ -35,6 +35,7 @@ module Arel # :nodoc: all
Not
Offset
On
+ OptimizerHints
Ordering
RollUp
}.each do |name|
diff --git a/activerecord/lib/arel/nodes/values.rb b/activerecord/lib/arel/nodes/values.rb
deleted file mode 100644
index 650248dc04..0000000000
--- a/activerecord/lib/arel/nodes/values.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Arel # :nodoc: all
- module Nodes
- class Values < Arel::Nodes::Binary
- alias :expressions :left
- alias :expressions= :left=
- alias :columns :right
- alias :columns= :right=
-
- def initialize(exprs, columns = [])
- super
- end
- end
- end
-end
diff --git a/activerecord/lib/arel/nodes/values_list.rb b/activerecord/lib/arel/nodes/values_list.rb
index 27109848e4..1a9d9ebf01 100644
--- a/activerecord/lib/arel/nodes/values_list.rb
+++ b/activerecord/lib/arel/nodes/values_list.rb
@@ -2,23 +2,8 @@
module Arel # :nodoc: all
module Nodes
- class ValuesList < Node
- attr_reader :rows
-
- def initialize(rows)
- @rows = rows
- super()
- end
-
- def hash
- @rows.hash
- end
-
- def eql?(other)
- self.class == other.class &&
- self.rows == other.rows
- end
- alias :== :eql?
+ class ValuesList < Unary
+ alias :rows :expr
end
end
end
diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb
index 77502dd199..895d394363 100644
--- a/activerecord/lib/arel/predications.rb
+++ b/activerecord/lib/arel/predications.rb
@@ -35,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 other.begin.nil? || 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))
@@ -81,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 other.begin.nil? || 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)
@@ -217,7 +221,6 @@ Passing a range to `#not_in` is deprecated. Call `#not_between`, instead.
end
private
-
def grouping_any(method_id, others, *extras)
nodes = others.map { |expr| send(method_id, expr, *extras) }
Nodes::Grouping.new nodes.inject { |memo, node|
@@ -238,12 +241,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/select_manager.rb b/activerecord/lib/arel/select_manager.rb
index a2b2838a3d..ddc9e394dd 100644
--- a/activerecord/lib/arel/select_manager.rb
+++ b/activerecord/lib/arel/select_manager.rb
@@ -146,6 +146,13 @@ module Arel # :nodoc: all
@ctx.projections = projections
end
+ def optimizer_hints(*hints)
+ unless hints.empty?
+ @ctx.optimizer_hints = Arel::Nodes::OptimizerHints.new(hints)
+ end
+ self
+ end
+
def distinct(value = true)
if value
@ctx.set_quantifier = Arel::Nodes::Distinct.new
@@ -237,16 +244,9 @@ module Arel # :nodoc: all
@ctx.source
end
- class Row < Struct.new(:data) # :nodoc:
- def id
- data["id"]
- end
-
- def method_missing(name, *args)
- name = name.to_s
- return data[name] if data.key?(name)
- super
- end
+ def comment(*values)
+ @ctx.comment = Nodes::Comment.new(values)
+ self
end
private
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
index 92d309453c..98c3f92cf1 100644
--- a/activerecord/lib/arel/visitors/depth_first.rb
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -9,8 +9,7 @@ module Arel # :nodoc: all
end
private
-
- def visit(o)
+ def visit(o, _ = nil)
super
@block.call o
end
@@ -35,6 +34,8 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Ascending :unary
alias :visit_Arel_Nodes_Descending :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_OptimizerHints :unary
+ alias :visit_Arel_Nodes_ValuesList :unary
def function(o)
visit o.expressions
@@ -102,7 +103,6 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Regexp :binary
alias :visit_Arel_Nodes_RightOuterJoin :binary
alias :visit_Arel_Nodes_TableAlias :binary
- alias :visit_Arel_Nodes_Values :binary
alias :visit_Arel_Nodes_When :binary
def visit_Arel_Nodes_StringJoin(o)
@@ -180,6 +180,10 @@ module Arel # :nodoc: all
visit o.limit
end
+ def visit_Arel_Nodes_Comment(o)
+ visit o.values
+ end
+
def visit_Array(o)
o.each { |i| visit i }
end
diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb
index 6389c875cb..c4ea07bcfe 100644
--- a/activerecord/lib/arel/visitors/dot.rb
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -31,7 +31,6 @@ module Arel # :nodoc: all
end
private
-
def visit_Arel_Nodes_Ordering(o)
visit_edge o, "expr"
end
@@ -46,8 +45,8 @@ module Arel # :nodoc: all
visit_edge o, "distinct"
end
- def visit_Arel_Nodes_Values(o)
- visit_edge o, "expressions"
+ def visit_Arel_Nodes_ValuesList(o)
+ visit_edge o, "rows"
end
def visit_Arel_Nodes_StringJoin(o)
@@ -82,6 +81,7 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Offset :unary
alias :visit_Arel_Nodes_On :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_OptimizerHints :unary
alias :visit_Arel_Nodes_Preceding :unary
alias :visit_Arel_Nodes_Following :unary
alias :visit_Arel_Nodes_Rows :unary
@@ -233,6 +233,10 @@ module Arel # :nodoc: all
end
alias :visit_Set :visit_Array
+ def visit_Arel_Nodes_Comment(o)
+ visit_edge(o, "values")
+ end
+
def visit_edge(o, method)
edge(method) { visit o.send(method) }
end
diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb
index 73166054da..5cf958f5f0 100644
--- a/activerecord/lib/arel/visitors/ibm_db.rb
+++ b/activerecord/lib/arel/visitors/ibm_db.rb
@@ -4,6 +4,15 @@ module Arel # :nodoc: all
module Visitors
class IBM_DB < Arel::Visitors::ToSql
private
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = super
+ maybe_visit o.optimizer_hints, collector
+ end
+
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join
+ collector << "/* <OPTGUIDELINES>#{hints}</OPTGUIDELINES> */"
+ end
def visit_Arel_Nodes_Limit(o, collector)
collector << "FETCH FIRST "
@@ -16,6 +25,10 @@ module Arel # :nodoc: all
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
+
+ def collect_optimizer_hints(o, collector)
+ collector
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb
index 0a9713794e..1a4ad1c8d8 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?
@@ -41,10 +42,16 @@ module Arel # :nodoc: all
collector
end
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join(", ")
+ collector << "/*+ #{hints} */"
+ end
+
def visit_Arel_Nodes_Offset(o, collector)
collector << "SKIP "
visit o.expr, collector
end
+
def visit_Arel_Nodes_Limit(o, collector)
collector << "FIRST "
visit o.expr, collector
diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb
index fdd864b40d..92eb94f802 100644
--- a/activerecord/lib/arel/visitors/mssql.rb
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -11,7 +11,6 @@ module Arel # :nodoc: all
end
private
-
def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
right = o.right
@@ -76,6 +75,16 @@ module Arel # :nodoc: all
end
end
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = super
+ maybe_visit o.optimizer_hints, collector
+ end
+
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join(", ")
+ collector << "OPTION (#{hints})"
+ end
+
def get_offset_limit_clause(o)
first_row = o.offset ? o.offset.expr.to_i + 1 : 1
last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil
@@ -97,12 +106,16 @@ module Arel # :nodoc: all
collector = visit o.relation, collector
if o.wheres.any?
collector << " WHERE "
- inject_join o.wheres, collector, AND
+ inject_join o.wheres, collector, " AND "
else
collector
end
end
+ def collect_optimizer_hints(o, collector)
+ collector
+ end
+
def determine_order_by(orders, x)
if orders.any?
orders
diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb
index f96bf65ee5..aab66301ef 100644
--- a/activerecord/lib/arel/visitors/oracle.rb
+++ b/activerecord/lib/arel/visitors/oracle.rb
@@ -4,7 +4,6 @@ module Arel # :nodoc: all
module Visitors
class Oracle < Arel::Visitors::ToSql
private
-
def visit_Arel_Nodes_SelectStatement(o, collector)
o = order_hacks(o)
diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb
index b092aa95e0..36783243b5 100644
--- a/activerecord/lib/arel/visitors/oracle12.rb
+++ b/activerecord/lib/arel/visitors/oracle12.rb
@@ -4,15 +4,13 @@ module Arel # :nodoc: all
module Visitors
class Oracle12 < Arel::Visitors::ToSql
private
-
def visit_Arel_Nodes_SelectStatement(o, collector)
# Oracle does not allow LIMIT clause with select for update
if o.limit && o.lock
- raise ArgumentError, <<-MSG
- 'Combination of limit and lock is not supported.
- because generated SQL statements
- `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.`
- MSG
+ raise ArgumentError, <<~MSG
+ Combination of limit and lock is not supported. Because generated SQL statements
+ `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.
+ MSG
end
super
end
@@ -20,7 +18,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)
diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb
index 920776b4dc..d4f21ff93e 100644
--- a/activerecord/lib/arel/visitors/postgresql.rb
+++ b/activerecord/lib/arel/visitors/postgresql.rb
@@ -3,13 +3,7 @@
module Arel # :nodoc: all
module Visitors
class PostgreSQL < Arel::Visitors::ToSql
- CUBE = "CUBE"
- ROLLUP = "ROLLUP"
- GROUPING_SETS = "GROUPING SETS"
- LATERAL = "LATERAL"
-
private
-
def visit_Arel_Nodes_Matches(o, collector)
op = o.case_sensitive ? " LIKE " : " ILIKE "
collector = infix_value o, collector, op
@@ -57,23 +51,22 @@ module Arel # :nodoc: all
end
def visit_Arel_Nodes_Cube(o, collector)
- collector << CUBE
+ collector << "CUBE"
grouping_array_or_grouping_element o, collector
end
def visit_Arel_Nodes_RollUp(o, collector)
- collector << ROLLUP
+ collector << "ROLLUP"
grouping_array_or_grouping_element o, collector
end
def visit_Arel_Nodes_GroupingSet(o, collector)
- collector << GROUPING_SETS
+ collector << "GROUPING SETS"
grouping_array_or_grouping_element o, collector
end
def visit_Arel_Nodes_Lateral(o, collector)
- collector << LATERAL
- collector << SPACE
+ collector << "LATERAL "
grouping_parentheses o, collector
end
diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb
index af6f7e856a..62ec74ad82 100644
--- a/activerecord/lib/arel/visitors/sqlite.rb
+++ b/activerecord/lib/arel/visitors/sqlite.rb
@@ -4,7 +4,6 @@ module Arel # :nodoc: all
module Visitors
class SQLite < Arel::Visitors::ToSql
private
-
# Locks are not supported in SQLite
def visit_Arel_Nodes_Lock(o, collector)
collector
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
index f9fe4404eb..eff7a0d036 100644
--- a/activerecord/lib/arel/visitors/to_sql.rb
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -9,59 +9,6 @@ module Arel # :nodoc: all
end
class ToSql < Arel::Visitors::Visitor
- ##
- # This is some roflscale crazy stuff. I'm roflscaling this because
- # building SQL queries is a hotspot. I will explain the roflscale so that
- # others will not rm this code.
- #
- # In YARV, string literals in a method body will get duped when the byte
- # code is executed. Let's take a look:
- #
- # > puts RubyVM::InstructionSequence.new('def foo; "bar"; end').disasm
- #
- # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>=====
- # 0000 trace 8
- # 0002 trace 1
- # 0004 putstring "bar"
- # 0006 trace 16
- # 0008 leave
- #
- # The `putstring` bytecode will dup the string and push it on the stack.
- # In many cases in our SQL visitor, that string is never mutated, so there
- # is no need to dup the literal.
- #
- # If we change to a constant lookup, the string will not be duped, and we
- # can reduce the objects in our system:
- #
- # > puts RubyVM::InstructionSequence.new('BAR = "bar"; def foo; BAR; end').disasm
- #
- # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>========
- # 0000 trace 8
- # 0002 trace 1
- # 0004 getinlinecache 11, <ic:0>
- # 0007 getconstant :BAR
- # 0009 setinlinecache <ic:0>
- # 0011 trace 16
- # 0013 leave
- #
- # `getconstant` should be a hash lookup, and no object is duped when the
- # value of the constant is pushed on the stack. Hence the crazy
- # constants below.
- #
- # `matches` and `doesNotMatch` operate case-insensitively via Visitor subclasses
- # specialized for specific databases when necessary.
- #
-
- WHERE = " WHERE " # :nodoc:
- SPACE = " " # :nodoc:
- COMMA = ", " # :nodoc:
- GROUP_BY = " GROUP BY " # :nodoc:
- ORDER_BY = " ORDER BY " # :nodoc:
- WINDOW = " WINDOW " # :nodoc:
- AND = " AND " # :nodoc:
-
- DISTINCT = "DISTINCT" # :nodoc:
-
def initialize(connection)
super()
@connection = connection
@@ -72,7 +19,6 @@ module Arel # :nodoc: all
end
private
-
def visit_Arel_Nodes_DeleteStatement(o, collector)
o = prepare_delete_statement(o)
@@ -105,10 +51,14 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_InsertStatement(o, collector)
collector << "INSERT INTO "
collector = visit o.relation, collector
- if o.columns.any?
- collector << " (#{o.columns.map { |x|
- quote_column_name x.name
- }.join ', '})"
+
+ unless o.columns.empty?
+ collector << " ("
+ o.columns.each_with_index do |x, i|
+ collector << ", " unless i == 0
+ collector << quote_column_name(x.name)
+ end
+ collector << ")"
end
if o.values
@@ -150,48 +100,27 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_ValuesList(o, collector)
collector << "VALUES "
- len = o.rows.length - 1
- o.rows.each_with_index { |row, i|
+ o.rows.each_with_index do |row, i|
+ collector << ", " unless i == 0
collector << "("
- row_len = row.length - 1
row.each_with_index do |value, k|
+ collector << ", " unless k == 0
case value
when Nodes::SqlLiteral, Nodes::BindParam
collector = visit(value, collector)
else
- collector << quote(value)
+ collector << quote(value).to_s
end
- collector << COMMA unless k == row_len
end
collector << ")"
- collector << COMMA unless i == len
- }
+ end
collector
end
- def visit_Arel_Nodes_Values(o, collector)
- collector << "VALUES ("
-
- len = o.expressions.length - 1
- o.expressions.each_with_index { |value, i|
- case value
- when Nodes::SqlLiteral, Nodes::BindParam
- collector = visit value, collector
- else
- collector << quote(value).to_s
- end
- unless i == len
- collector << COMMA
- end
- }
-
- collector << ")"
- end
-
def visit_Arel_Nodes_SelectStatement(o, collector)
if o.with
collector = visit o.with, collector
- collector << SPACE
+ collector << " "
end
collector = o.cores.inject(collector) { |c, x|
@@ -199,46 +128,53 @@ module Arel # :nodoc: all
}
unless o.orders.empty?
- collector << ORDER_BY
- len = o.orders.length - 1
- o.orders.each_with_index { |x, i|
+ collector << " ORDER BY "
+ o.orders.each_with_index do |x, i|
+ collector << ", " unless i == 0
collector = visit(x, collector)
- collector << COMMA unless len == i
- }
+ end
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)
collector << "SELECT"
+ collector = collect_optimizer_hints(o, collector)
collector = maybe_visit o.set_quantifier, collector
- collect_nodes_for o.projections, collector, SPACE
+ collect_nodes_for o.projections, collector, " "
if o.source && !o.source.empty?
collector << " FROM "
collector = visit o.source, collector
end
- collect_nodes_for o.wheres, collector, WHERE, AND
- collect_nodes_for o.groups, collector, GROUP_BY
- collect_nodes_for o.havings, collector, " HAVING ", AND
- collect_nodes_for o.windows, collector, WINDOW
+ collect_nodes_for o.wheres, collector, " WHERE ", " AND "
+ collect_nodes_for o.groups, collector, " GROUP BY "
+ collect_nodes_for o.havings, collector, " HAVING ", " AND "
+ collect_nodes_for o.windows, collector, " WINDOW "
+
+ maybe_visit o.comment, collector
+ end
- collector
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join(" ")
+ collector << "/*+ #{hints} */"
end
- def collect_nodes_for(nodes, collector, spacer, connector = COMMA)
+ def visit_Arel_Nodes_Comment(o, collector)
+ collector << o.values.map { |v| "/* #{sanitize_as_sql_comment(v)} */" }.join(" ")
+ end
+
+ def collect_nodes_for(nodes, collector, spacer, connector = ", ")
unless nodes.empty?
collector << spacer
inject_join nodes, collector, connector
@@ -250,7 +186,7 @@ module Arel # :nodoc: all
end
def visit_Arel_Nodes_Distinct(o, collector)
- collector << DISTINCT
+ collector << "DISTINCT"
end
def visit_Arel_Nodes_DistinctOn(o, collector)
@@ -259,12 +195,12 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_With(o, collector)
collector << "WITH "
- inject_join o.children, collector, COMMA
+ inject_join o.children, collector, ", "
end
def visit_Arel_Nodes_WithRecursive(o, collector)
collector << "WITH RECURSIVE "
- inject_join o.children, collector, COMMA
+ inject_join o.children, collector, ", "
end
def visit_Arel_Nodes_Union(o, collector)
@@ -297,13 +233,13 @@ module Arel # :nodoc: all
collect_nodes_for o.partitions, collector, "PARTITION BY "
if o.orders.any?
- collector << SPACE if o.partitions.any?
+ collector << " " if o.partitions.any?
collector << "ORDER BY "
collector = inject_join o.orders, collector, ", "
end
if o.framing
- collector << SPACE if o.partitions.any? || o.orders.any?
+ collector << " " if o.partitions.any? || o.orders.any?
collector = visit o.framing, collector
end
@@ -508,8 +444,8 @@ module Arel # :nodoc: all
collector = visit o.left, collector
end
if o.right.any?
- collector << SPACE if o.left
- collector = inject_join o.right, collector, SPACE
+ collector << " " if o.left
+ collector = inject_join o.right, collector, " "
end
collector
end
@@ -529,7 +465,7 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_FullOuterJoin(o, collector)
collector << "FULL OUTER JOIN "
collector = visit o.left, collector
- collector << SPACE
+ collector << " "
visit o.right, collector
end
@@ -543,7 +479,7 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_RightOuterJoin(o, collector)
collector << "RIGHT OUTER JOIN "
collector = visit o.left, collector
- collector << SPACE
+ collector << " "
visit o.right, collector
end
@@ -551,7 +487,7 @@ module Arel # :nodoc: all
collector << "INNER JOIN "
collector = visit o.left, collector
if o.right
- collector << SPACE
+ collector << " "
visit(o.right, collector)
else
collector
@@ -570,41 +506,73 @@ module Arel # :nodoc: all
def visit_Arel_Table(o, collector)
if o.table_alias
- collector << "#{quote_table_name o.name} #{quote_table_name o.table_alias}"
+ collector << quote_table_name(o.name) << " " << quote_table_name(o.table_alias)
else
collector << quote_table_name(o.name)
end
end
def visit_Arel_Nodes_In(o, collector)
- if Array === o.right && !o.right.empty?
- o.right.keep_if { |value| boundable?(value) }
+ unless Array === o.right
+ return collect_in_clause(o.left, o.right, collector)
end
- if Array === o.right && o.right.empty?
- collector << "1=0"
+ unless o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ return collector << "1=0" if o.right.empty?
+
+ in_clause_length = @connection.in_clause_length
+
+ if !in_clause_length || o.right.length <= in_clause_length
+ collect_in_clause(o.left, o.right, collector)
else
- collector = visit o.left, collector
- collector << " IN ("
- visit(o.right, collector) << ")"
+ collector << "("
+ o.right.each_slice(in_clause_length).each_with_index do |right, i|
+ collector << " OR " unless i == 0
+ collect_in_clause(o.left, right, collector)
+ end
+ collector << ")"
end
end
+ def collect_in_clause(left, right, collector)
+ collector = visit left, collector
+ collector << " IN ("
+ visit(right, collector) << ")"
+ end
+
def visit_Arel_Nodes_NotIn(o, collector)
- if Array === o.right && !o.right.empty?
- o.right.keep_if { |value| boundable?(value) }
+ unless Array === o.right
+ return collect_not_in_clause(o.left, o.right, collector)
end
- if Array === o.right && o.right.empty?
- collector << "1=1"
+ unless o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ return collector << "1=1" if o.right.empty?
+
+ in_clause_length = @connection.in_clause_length
+
+ if !in_clause_length || o.right.length <= in_clause_length
+ collect_not_in_clause(o.left, o.right, collector)
else
- collector = visit o.left, collector
- collector << " NOT IN ("
- collector = visit o.right, collector
- collector << ")"
+ o.right.each_slice(in_clause_length).each_with_index do |right, i|
+ collector << " AND " unless i == 0
+ collect_not_in_clause(o.left, right, collector)
+ end
+ collector
end
end
+ def collect_not_in_clause(left, right, collector)
+ collector = visit left, collector
+ collector << " NOT IN ("
+ visit(right, collector) << ")"
+ end
+
def visit_Arel_Nodes_And(o, collector)
inject_join o.children, collector, " AND "
end
@@ -631,6 +599,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?
@@ -664,6 +634,8 @@ module Arel # :nodoc: all
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?
@@ -710,20 +682,13 @@ module Arel # :nodoc: all
end
def visit_Arel_Nodes_UnqualifiedColumn(o, collector)
- collector << "#{quote_column_name o.name}"
- collector
+ collector << quote_column_name(o.name)
end
def visit_Arel_Attributes_Attribute(o, collector)
join_name = o.relation.table_alias || o.relation.name
- collector << "#{quote_table_name join_name}.#{quote_column_name o.name}"
+ collector << quote_table_name(join_name) << "." << quote_column_name(o.name)
end
- alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
- alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
- alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
- alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
- alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
- alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
def literal(o, collector); collector << o.to_s; end
@@ -797,6 +762,15 @@ module Arel # :nodoc: all
@connection.quote_column_name(name)
end
+ def sanitize_as_sql_comment(value)
+ return value if Arel::Nodes::SqlLiteral === value
+ @connection.sanitize_as_sql_comment(value)
+ end
+
+ def collect_optimizer_hints(o, collector)
+ maybe_visit o.optimizer_hints, collector
+ end
+
def maybe_visit(thing, collector)
return collector unless thing
collector << " "
@@ -804,18 +778,15 @@ module Arel # :nodoc: all
end
def inject_join(list, collector, join_str)
- len = list.length - 1
- list.each_with_index.inject(collector) { |c, (x, i)|
- if i == len
- visit x, c
- else
- visit(x, c) << join_str
- end
- }
+ list.each_with_index do |x, i|
+ collector << join_str unless i == 0
+ collector = visit(x, collector)
+ end
+ collector
end
- def boundable?(value)
- !value.respond_to?(:boundable?) || value.boundable?
+ def unboundable?(value)
+ value.respond_to?(:unboundable?) && value.unboundable?
end
def has_join_sources?(o)
diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb
index 1c17184e86..9066307aed 100644
--- a/activerecord/lib/arel/visitors/visitor.rb
+++ b/activerecord/lib/arel/visitors/visitor.rb
@@ -7,16 +7,15 @@ module Arel # :nodoc: all
@dispatch = get_dispatch_cache
end
- def accept(object, *args)
- visit object, *args
+ def accept(object, collector = nil)
+ visit object, collector
end
private
-
attr_reader :dispatch
def self.dispatch_cache
- Hash.new do |hash, klass|
+ @dispatch_cache ||= Hash.new do |hash, klass|
hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
end
end
@@ -25,9 +24,13 @@ module Arel # :nodoc: all
self.class.dispatch_cache
end
- def visit(object, *args)
+ def visit(object, collector = nil)
dispatch_method = dispatch[object.class]
- send dispatch_method, object, *args
+ if collector
+ send dispatch_method, object, collector
+ else
+ send dispatch_method, object
+ end
rescue NoMethodError => e
raise e if respond_to?(dispatch_method, true)
superklass = object.class.ancestors.find { |klass|
diff --git a/activerecord/lib/arel/visitors/where_sql.rb b/activerecord/lib/arel/visitors/where_sql.rb
index c6caf5e7c9..8fb299d1c8 100644
--- a/activerecord/lib/arel/visitors/where_sql.rb
+++ b/activerecord/lib/arel/visitors/where_sql.rb
@@ -9,7 +9,6 @@ module Arel # :nodoc: all
end
private
-
def visit_Arel_Nodes_SelectCore(o, collector)
collector << "WHERE "
wheres = o.wheres.map do |where|
diff --git a/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb b/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb
index 35d5664400..56b9628a92 100644
--- a/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb
@@ -13,7 +13,6 @@ module ActiveRecord
end
private
-
def application_record_file_name
@application_record_file_name ||=
if namespaced?
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
index cbb88d571d..af753071a9 100644
--- a/activerecord/lib/rails/generators/active_record/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration.rb
@@ -17,7 +17,6 @@ module ActiveRecord
end
private
-
def primary_key_type
key_type = options[:primary_key_type]
", id: :#{key_type}" if key_type
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index dd79bcf542..0620a515bd 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -7,8 +7,9 @@ module ActiveRecord
class MigrationGenerator < Base # :nodoc:
argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
+ class_option :timestamps, type: :boolean
class_option :primary_key_type, type: :string, desc: "The type for primary key"
- class_option :database, type: :string, aliases: %i(db), desc: "The database for your migration. By default, the current environment's primary database is used."
+ class_option :database, type: :string, aliases: %i(--db), desc: "The database for your migration. By default, the current environment's primary database is used."
def create_migration_file
set_local_assigns!
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
index 5f7201cfe1..562543f981 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
@@ -6,7 +6,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
t.string :password_digest<%= attribute.inject_options %>
<% elsif attribute.token? -%>
t.string :<%= attribute.name %><%= attribute.inject_options %>
-<% else -%>
+<% elsif !attribute.virtual? -%>
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
<% end -%>
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
index 481c70201b..c07380bec9 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
@@ -7,7 +7,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
<%- elsif attribute.token? -%>
add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
- <%- else -%>
+ <%- elsif !attribute.virtual? -%>
add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- if attribute.has_index? -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
@@ -21,7 +21,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
<%- attributes.each do |attribute| -%>
<%- if attribute.reference? -%>
t.references :<%= attribute.name %><%= attribute.inject_options %>
- <%- else -%>
+ <%- elsif !attribute.virtual? -%>
<%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
<%- end -%>
@@ -37,7 +37,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
<%- if attribute.has_index? -%>
remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
+ <%- if !attribute.virtual? %>
remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
+ <%- end -%>
<%- end -%>
<%- end -%>
<%- end -%>
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index eac504f9f1..d4733f948f 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -14,7 +14,7 @@ module ActiveRecord
class_option :parent, type: :string, desc: "The parent class for the generated model"
class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns"
class_option :primary_key_type, type: :string, desc: "The type for primary key"
- class_option :database, type: :string, aliases: %i(db), desc: "The database for your model's migration. By default, the current environment's primary database is used."
+ class_option :database, type: :string, aliases: %i(--db), desc: "The database for your model's migration. By default, the current environment's primary database is used."
# creates the migration file for the model.
def create_migration_file
@@ -35,7 +35,6 @@ module ActiveRecord
hook_for :test_framework
private
-
def attributes_with_index
attributes.select { |a| !a.reference? && a.has_index? }
end
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
index 55dc65c8ad..77b9ea1c86 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
@@ -1,7 +1,16 @@
<% module_namespacing do -%>
class <%= class_name %> < <%= parent_class_name.classify %>
<% attributes.select(&:reference?).each do |attribute| -%>
- belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %>
+ belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
+<% end -%>
+<% attributes.select(&:rich_text?).each do |attribute| -%>
+ has_rich_text :<%= attribute.name %>
+<% end -%>
+<% attributes.select(&:attachment?).each do |attribute| -%>
+ has_one_attached :<%= attribute.name %>
+<% end -%>
+<% attributes.select(&:attachments?).each do |attribute| -%>
+ has_many_attached :<%= attribute.name %>
<% end -%>
<% attributes.select(&:token?).each do |attribute| -%>
has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>