🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

activerecord

Package Overview
Dependencies
Maintainers
1
Versions
519
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

activerecord - rubygems Package Compare versions

Comparing version
8.0.5
to
8.1.0.beta1
+88
lib/active_record/associations/deprecation.rb
# frozen_string_literal: true
require "active_support/notifications"
require "active_support/core_ext/array/conversions"
module ActiveRecord::Associations::Deprecation # :nodoc:
EVENT = "deprecated_association.active_record"
private_constant :EVENT
MODES = [:warn, :raise, :notify].freeze
private_constant :MODES
class << self
attr_reader :mode, :backtrace
def mode=(value) # private setter
unless MODES.include?(value)
raise ArgumentError, "invalid deprecated associations mode #{value.inspect} (valid modes are #{MODES.map(&:inspect).to_sentence})"
end
@mode = value
end
def backtrace=(value)
@backtrace = !!value
end
def guard(reflection)
report(reflection, context: yield) if reflection.deprecated?
if reflection.through_reflection?
reflection.deprecated_nested_reflections.each do |deprecated_nested_reflection|
context = "referenced as nested association of the through #{reflection.active_record}##{reflection.name}"
report(deprecated_nested_reflection, context: context)
end
end
end
def report(reflection, context:)
reflection = user_facing_reflection(reflection)
message = +"The association #{reflection.active_record}##{reflection.name} is deprecated, #{context}"
message << " (#{backtrace_cleaner.first_clean_frame})"
case @mode
when :warn
message = [message, *clean_frames].join("\n\t") if @backtrace
ActiveRecord::Base.logger&.warn(message)
when :raise
error = ActiveRecord::DeprecatedAssociationError.new(message)
if set_backtrace_supports_array_of_locations?
error.set_backtrace(clean_locations)
else
error.set_backtrace(clean_frames)
end
raise error
else
payload = { reflection: reflection, message: message, location: backtrace_cleaner.first_clean_location }
payload[:backtrace] = clean_locations if @backtrace
ActiveSupport::Notifications.instrument(EVENT, payload)
end
end
private
def backtrace_cleaner
ActiveRecord::LogSubscriber.backtrace_cleaner
end
def clean_frames
backtrace_cleaner.clean(caller)
end
def clean_locations
backtrace_cleaner.clean_locations(caller_locations)
end
def set_backtrace_supports_array_of_locations?
@backtrace_supports_array_of_locations ||= Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
end
def user_facing_reflection(reflection)
reflection.active_record.reflect_on_association(reflection.name)
end
end
self.mode = :warn
self.backtrace = false
end
# frozen_string_literal: true
module ActiveRecord
class FilterAttributeHandler # :nodoc:
class << self
def on_sensitive_attribute_declared(&block)
@sensitive_attribute_declaration_listeners ||= Concurrent::Array.new
@sensitive_attribute_declaration_listeners << block
end
def sensitive_attribute_was_declared(klass, list)
@sensitive_attribute_declaration_listeners&.each do |block|
block.call(klass, list)
end
end
end
def initialize(app)
@app = app
@attributes_by_class = Concurrent::Map.new
@collecting = true
end
def enable
install_collecting_hook
apply_collected_attributes
@collecting = false
end
private
attr_reader :app
def install_collecting_hook
self.class.on_sensitive_attribute_declared do |klass, list|
attribute_was_declared(klass, list)
end
end
def attribute_was_declared(klass, list)
if collecting?
collect_for_later(klass, list)
else
apply_filter(klass, list)
end
end
def apply_collected_attributes
@attributes_by_class.each do |klass, list|
apply_filter(klass, list)
end
end
def collecting?
@collecting
end
def collect_for_later(klass, list)
@attributes_by_class[klass] ||= Concurrent::Array.new
@attributes_by_class[klass] += list
end
def apply_filter(klass, list)
list.each do |attribute|
next if klass.abstract_class? || klass == Base
klass_name = klass.name ? klass.model_name.element : nil
filter = [klass_name, attribute.to_s].compact.join(".")
app.config.filter_parameters << filter unless app.config.filter_parameters.include?(filter)
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
class Migration
# This class is used by the schema dumper to format versions information.
#
# The class receives the current +connection+ when initialized.
class DefaultSchemaVersionsFormatter # :nodoc:
def initialize(connection)
@connection = connection
end
def format(versions)
sm_table = connection.quote_table_name(connection.pool.schema_migration.table_name)
if versions.is_a?(Array)
sql = +"INSERT INTO #{sm_table} (version) VALUES\n"
sql << versions.reverse.map { |v| "(#{connection.quote(v)})" }.join(",\n")
sql << ";"
sql
else
"INSERT INTO #{sm_table} (version) VALUES (#{connection.quote(versions)});"
end
end
private
attr_reader :connection
end
end
end
# frozen_string_literal: true
module ActiveRecord
module Railties # :nodoc:
module JobCheckpoints # :nodoc:
def checkpoint!
if ActiveRecord.all_open_transactions.any?
raise ActiveJob::Continuation::CheckpointError, "Cannot checkpoint job with open transactions"
else
super
end
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
module Tasks # :nodoc:
class AbstractTasks # :nodoc:
def self.using_database_configurations?
true
end
def initialize(db_config)
@db_config = db_config
@configuration_hash = db_config.configuration_hash
end
def charset
connection.encoding
end
def collation
connection.collation
end
def check_current_protected_environment!(db_config, migration_class)
with_temporary_pool(db_config, migration_class) do |pool|
migration_context = pool.migration_context
current = migration_context.current_environment
stored = migration_context.last_stored_environment
if migration_context.protected_environment?
raise ActiveRecord::ProtectedEnvironmentError.new(stored)
end
if stored && stored != current
raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored)
end
rescue ActiveRecord::NoDatabaseError
end
end
private
attr_reader :db_config, :configuration_hash
def connection
ActiveRecord::Base.lease_connection
end
def establish_connection(config = db_config)
ActiveRecord::Base.establish_connection(config)
end
def configuration_hash_without_database
configuration_hash.merge(database: nil)
end
def run_cmd(cmd, *args, **opts)
fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, opts)
end
def run_cmd_error(cmd, args)
msg = +"failed to execute:\n"
msg << "#{cmd} #{args.join(' ')}\n\n"
msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
msg
end
def with_temporary_pool(db_config, migration_class, clobber: false)
original_db_config = migration_class.connection_db_config
pool = migration_class.connection_handler.establish_connection(db_config, clobber: clobber)
yield pool
ensure
migration_class.connection_handler.establish_connection(original_db_config, clobber: clobber)
end
end
end
end
+420
-651

@@ -1,188 +0,78 @@

## Rails 8.0.5 (March 24, 2026) ##
## Rails 8.1.0.beta1 (September 04, 2025) ##
* Fix `insert_all` and `upsert_all` log message when called on anonymous classes.
* Remove deprecated `:unsigned_float` and `:unsigned_decimal` column methods for MySQL.
*Gabriel Sobrinho*
*Rafael Mendonça França*
* Respect `ActiveRecord::SchemaDumper.ignore_tables` when dumping SQLite virtual tables.
* Remove deprecated `:retries` option for the SQLite3 adapter.
*Hans Schnedlitz*
*Rafael Mendonça França*
* Restore previous instrumenter after `execute_or_skip`
* Introduce new database configuration options `keepalive`, `max_age`, and
`min_connections` -- and rename `pool` to `max_connections` to match.
`FutureResult#execute_or_skip` replaces the thread's instrumenter with an
`EventBuffer` to collect events published during async query execution.
If the global async executor is saturated and the `caller_runs` fallback
executes the task on the calling thread, we need to make sure the previous
instrumenter is restored or the stale `EventBuffer` would stay in place and
permanently swallow all subsequent `sql.active_record` notifications on
that thread.
There are no changes to default behavior, but these allow for more specific
control over pool behavior.
*Rosa Gutierrez*
*Matthew Draper*, *Chris AtLee*, *Rachael Wright-Munn*
* Fix Ruby 4.0 delegator warning when calling inspect on ActiveRecord::Type::Serialized.
* Move `LIMIT` validation from query generation to when `limit()` is called.
*Hammad Khan*
*Hartley McGuire*, *Shuyang*
* Fix support for table names containing hyphens.
* Add `ActiveRecord::CheckViolation` error class for check constraint violations.
*Evgeniy Demin*
*Ryuta Kamizono*
* Fix column deduplication for SQLite3 and PostgreSQL virtual (generated) columns.
* Add `ActiveRecord::ExclusionViolation` error class for exclusion constraint violations.
`Column#==` and `Column#hash` now account for `virtual?` so that the
`Deduplicable` registry does not treat a generated column and a regular
column with the same name and type as identical. Previously, if a
generated column was registered first, a regular column on a different
table could be deduplicated to the generated instance, silently
excluding it from INSERT/UPDATE statements.
When an exclusion constraint is violated in PostgreSQL, the error will now be raised
as `ActiveRecord::ExclusionViolation` instead of the generic `ActiveRecord::StatementInvalid`,
making it easier to handle these specific constraint violations in application code.
*Jay Huber*
This follows the same pattern as other constraint violation error classes like
`RecordNotUnique` for unique constraint violations and `InvalidForeignKey` for
foreign key constraint violations.
* Fix merging relations with arel equality predicates with null relations.
*Ryuta Kamizono*
*fatkodima*
* Attributes filtered by `filter_attributes` will now also be filtered by `filter_parameters`
so sensitive information is not leaked.
* Fix SQLite3 schema dump for non-autoincrement integer primary keys.
*Jill Klang*
Previously, `schema.rb` should incorrectly restore that table with an auto incrementing
primary key.
* Add `connection.current_transaction.isolation` API to check current transaction's isolation level.
*Chris Hasiński*
Returns the isolation level if it was explicitly set via the `isolation:` parameter
or through `ActiveRecord.with_transaction_isolation_level`, otherwise returns `nil`.
Nested transactions return the parent transaction's isolation level.
* Fix PostgreSQL `schema_search_path` not being reapplied after `reset!` or `reconnect!`.
```ruby
# Returns nil when no transaction
User.connection.current_transaction.isolation # => nil
The `schema_search_path` configured in `database.yml` is now correctly
reapplied instead of falling back to PostgreSQL defaults.
# Returns explicitly set isolation level
User.transaction(isolation: :serializable) do
User.connection.current_transaction.isolation # => :serializable
end
*Tobias Egli*
# Returns nil when isolation not explicitly set
User.transaction do
User.connection.current_transaction.isolation # => nil
end
* Ensure batched preloaded associations accounts for klass when grouping to avoid issues with STI.
*zzak*, *Stjepan Hadjic*
* Fix `ActiveRecord::SoleRecordExceeded#record` to return the relation.
This was the case until Rails 7.2, but starting from 8.0 it
started mistakenly returning the model class.
*Jean Boussier*
* Improve PostgreSQLAdapter resilience to Timeout.timeout.
Better handle asynchronous exceptions being thrown inside
the `reconnect!` method.
This may fixes some deep errors such as:
# Nested transactions inherit parent's isolation
User.transaction(isolation: :read_committed) do
User.transaction do
User.connection.current_transaction.isolation # => :read_committed
end
end
```
undefined method `key?' for nil:NilClass (NoMethodError)
if !type_map.key?(oid)
```
*Jean Boussier*
*Kir Shatrov*
* Fix `eager_load` when loading `has_many` assocations with composite primary keys.
* Emit a warning for pg gem < 1.6.0 when using PostgreSQL 18+
This would result in some records being loaded multiple times.
*Yasuo Honda*
*Martin-Alexander*
## Rails 8.0.4.1 (March 23, 2026) ##
* No changes.
## Rails 8.0.4 (October 28, 2025) ##
* Fix SQLite3 data loss during table alterations with CASCADE foreign keys.
When altering a table in SQLite3 that is referenced by child tables with
`ON DELETE CASCADE` foreign keys, ActiveRecord would silently delete all
data from the child tables. This occurred because SQLite requires table
recreation for schema changes, and during this process the original table
is temporarily dropped, triggering CASCADE deletes on child tables.
The root cause was incorrect ordering of operations. The original code
wrapped `disable_referential_integrity` inside a transaction, but
`PRAGMA foreign_keys` cannot be modified inside a transaction in SQLite -
attempting to do so simply has no effect. This meant foreign keys remained
enabled during table recreation, causing CASCADE deletes to fire.
The fix reverses the order to follow the official SQLite 12-step ALTER TABLE
procedure: `disable_referential_integrity` now wraps the transaction instead
of being wrapped by it. This ensures foreign keys are properly disabled
before the transaction starts and re-enabled after it commits, preventing
CASCADE deletes while maintaining data integrity through atomic transactions.
*Ruy Rocha*
* Add support for bound SQL literals in CTEs.
*Nicolas Bachschmidt*
* Fix `belongs_to` associations not to clear the entire composite primary key.
When clearing a `belongs_to` association that references a model with composite primary key,
only the optional part of the key should be cleared.
*zzak*
* Fix invalid records being autosaved when distantly associated records are marked for deletion.
*Ian Terrell*, *axlekb AB*
## Rails 8.0.3 (September 22, 2025) ##
* Fix query cache for pinned connections in multi threaded transactional tests
When a pinned connection is used across separate threads, they now use a separate cache store
for each thread.
This improve accuracy of system tests, and any test using multiple threads.
*Heinrich Lee Yu*, *Jean Boussier*
* Don't add `id_value` attribute alias when attribute/column with that name already exists.
*Rob Lewis*
* Fix false positive change detection involving STI and polymorphic has one relationships.
Polymorphic `has_one` relationships would always be considered changed when defined in a STI child
class, causing nedless extra autosaves.
*David Fritsch*
* Skip calling `PG::Connection#cancel` in `cancel_any_running_query`
when using libpq >= 18 with pg < 1.6.0, due to incompatibility.
Rollback still runs, but may take longer.
*Yasuo Honda*, *Lars Kanis*
* Fix stale association detection for polymorphic `belongs_to`.
*Florent Beaurain*, *Thomas Crambert*
* Fix removal of PostgreSQL version comments in `structure.sql` for latest PostgreSQL versions which include `\restrict`
*Brendan Weibrecht*
* Allow setting `schema_format` in database configuration.
```
primary:
schema_format: ruby
```
Useful in multi-database setups to have different formats per-database.
*T S Vallender*
* Use ntuples to populate row_count instead of count for Postgres
*Jonathan Calvert*
* Fix `#merge` with `#or` or `#and` and a mixture of attributes and SQL strings resulting in an incorrect query.

@@ -226,720 +116,599 @@

* Fix inline `has_and_belongs_to_many` fixtures for tables with composite primary keys.
* Make schema dumper to account for `ActiveRecord.dump_schemas` when dumping in `:ruby` format.
*fatkodima*
* Fix migration log message for down operations.
* Add `:touch` option to `update_column`/`update_columns` methods.
*Bernardo Barreto*
```ruby
# Will update :updated_at/:updated_on alongside :nice column.
user.update_column(:nice, true, touch: true)
* Prepend `extra_flags` in postgres' `structure_load`
# Will update :updated_at/:updated_on alongside :last_ip column
user.update_columns(last_ip: request.remote_ip, touch: true)
```
When specifying `structure_load_flags` with a postgres adapter, the flags
were appended to the default flags, instead of prepended.
This caused issues with flags not being taken into account by postgres.
*Dmitrii Ivliev*
*Alice Loeser*
* Optimize Active Record batching further when using ranges.
* Fix `annotate` comments to propagate to `update_all`/`delete_all`.
Tested on a PostgreSQL table with 10M records and batches of 10k records, the generation
of relations for the 1000 batches was `4.8x` faster (`6.8s` vs. `1.4s`), used `900x`
less bandwidth (`180MB` vs. `0.2MB`) and allocated `45x` less memory (`490MB` vs. `11MB`).
*fatkodima*
*Maxime Réty*, *fatkodima*
* Fix checking whether an unpersisted record is `include?`d in a strictly
loaded `has_and_belongs_to_many` association.
* Include current character length in error messages for index and table name length validations.
*Hartley McGuire*
*Joshua Young*
* `create_or_find_by` will now correctly rollback a transaction.
* Add `rename_schema` method for PostgreSQL.
When using `create_or_find_by`, raising a ActiveRecord::Rollback error
in a `after_save` callback had no effect, the transaction was committed
and a record created.
*T S Vallender*
*Edouard Chin*
* Implement support for deprecating associations:
* Gracefully handle `Timeout.timeout` firing during connection configuration.
```ruby
has_many :posts, deprecated: true
```
Use of `Timeout.timeout` could result in improperly initialized database connection.
With that, Active Record will report any usage of the `posts` association.
This could lead to a partially configured connection being used, resulting in various exceptions,
the most common being with the PostgreSQLAdapter raising `undefined method 'key?' for nil`
or `TypeError: wrong argument type nil (expected PG::TypeMap)`.
Three reporting modes are supported (`:warn`, `:raise`, and `:notify`), and
backtraces can be enabled or disabled. Defaults are `:warn` mode and
disabled backtraces.
*Jean Boussier*
Please, check the docs for further details.
* Fix stale state for composite foreign keys in belongs_to associations.
*Xavier Noria*
*Varun Sharma*
* PostgreSQL adapter create DB now supports `locale_provider` and `locale`.
*Bengt-Ove Hollaender*
## Rails 8.0.2.1 (August 13, 2025) ##
* Use ntuples to populate row_count instead of count for Postgres
* Call inspect on ids in RecordNotFound error
*Jonathan Calvert*
[CVE-2025-55193]
* Fix checking whether an unpersisted record is `include?`d in a strictly
loaded `has_and_belongs_to_many` association.
*Gannon McGibbon*, *John Hawthorn*
## Rails 8.0.2 (March 12, 2025) ##
* Fix inverting `rename_enum_value` when `:from`/`:to` are provided.
*fatkodima*
* Prevent persisting invalid record.
*Edouard Chin*
* Fix inverting `drop_table` without options.
*fatkodima*
* Fix count with group by qualified name on loaded relation.
*Ryuta Kamizono*
* Fix `sum` with qualified name on loaded relation.
*Chris Gunther*
* The SQLite3 adapter quotes non-finite Numeric values like "Infinity" and "NaN".
*Mike Dalessio*
* Handle libpq returning a database version of 0 on no/bad connection in `PostgreSQLAdapter`.
Before, this version would be cached and an error would be raised during connection configuration when
comparing it with the minimum required version for the adapter. This meant that the connection could
never be successfully configured on subsequent reconnection attempts.
Now, this is treated as a connection failure consistent with libpq, raising a `ActiveRecord::ConnectionFailed`
and ensuring the version isn't cached, which allows the version to be retrieved on the next connection attempt.
*Joshua Young*, *Rian McGuire*
* Fix error handling during connection configuration.
Active Record wasn't properly handling errors during the connection configuration phase.
This could lead to a partially configured connection being used, resulting in various exceptions,
the most common being with the PostgreSQLAdapter raising `undefined method `key?' for nil`
or `TypeError: wrong argument type nil (expected PG::TypeMap)`.
*Jean Boussier*
* Fix a case where a non-retryable query could be marked retryable.
*Hartley McGuire*
* Handle circular references when autosaving associations.
* Add ability to change transaction isolation for all pools within a block.
*zzak*
This functionality is useful if your application needs to change the database
transaction isolation for a request or action.
* PoolConfig no longer keeps a reference to the connection class.
Calling `ActiveRecord.with_transaction_isolation_level(level) {}` in an around filter or
middleware will set the transaction isolation for all pools accessed within the block,
but not for the pools that aren't.
Keeping a reference to the class caused subtle issues when combined with reloading in
development. Fixes #54343.
This works with explicit and implicit transactions:
*Mike Dalessio*
* Fix SQL notifications sometimes not sent when using async queries.
```ruby
Post.async_count
ActiveSupport::Notifications.subscribed(->(*) { "Will never reach here" }) do
Post.count
ActiveRecord.with_transaction_isolation_level(:read_committed) do
Tag.transaction do # opens a transaction explicitly
Tag.create!
end
end
```
In rare circumstances and under the right race condition, Active Support notifications
would no longer be dispatched after using an asynchronous query.
This is now fixed.
*Edouard Chin*
* Fix support for PostgreSQL enum types with commas in their name.
*Arthur Hess*
* Fix inserts on MySQL with no RETURNING support for a table with multiple auto populated columns.
*Nikita Vasilevsky*
* Fix joining on a scoped association with string joins and bind parameters.
```ruby
class Instructor < ActiveRecord::Base
has_many :instructor_roles, -> { active }
ActiveRecord.with_transaction_isolation_level(:read_committed) do
Tag.create! # opens a transaction implicitly
end
class InstructorRole < ActiveRecord::Base
scope :active, -> {
joins("JOIN students ON instructor_roles.student_id = students.id")
.where(students { status: 1 })
}
end
Instructor.joins(:instructor_roles).first
```
The above example would result in `ActiveRecord::StatementInvalid` because the
`active` scope bind parameters would be lost.
*Eileen M. Uchitelle*
*Jean Boussier*
* Raise `ActiveRecord::MissingRequiredOrderError` when order dependent finder methods (e.g. `#first`, `#last`) are
called without `order` values on the relation, and the model does not have any order columns (`implicit_order_column`,
`query_constraints`, or `primary_key`) to fall back on.
* Fix a potential race condition with system tests and transactional fixtures.
This change will be introduced with a new framework default for Rails 8.1, and the current behavior of not raising
an error has been deprecated with the aim of removing the configuration option in Rails 8.2.
*Sjoerd Lagarde*
* Fix autosave associations to no longer validated unmodified associated records.
Active Record was incorrectly performing validation on associated record that
weren't created nor modified as part of the transaction:
```ruby
Post.create!(author: User.find(1)) # Fail if user is invalid
config.active_record.raise_on_missing_required_finder_order_columns = true
```
*Jean Boussier*
*Joshua Young*
* Remember when a database connection has recently been verified (for
two seconds, by default), to avoid repeated reverifications during a
single request.
* `:class_name` is now invalid in polymorphic `belongs_to` associations.
This should recreate a similar rate of verification as in Rails 7.1,
where connections are leased for the duration of a request, and thus
only verified once.
Reason is `:class_name` does not make sense in those associations because
the class name of target records is dynamic and stored in the type column.
*Matthew Draper*
Existing polymorphic associations setting this option can just delete it.
While it did not raise, it had no effect anyway.
*Xavier Noria*
## Rails 8.0.1 (December 13, 2024) ##
* Add support for multiple databases to `db:migrate:reset`.
* Fix removing foreign keys with :restrict action for MySQL.
*Joé Dupuis*
*fatkodima*
* Add `affected_rows` to `ActiveRecord::Result`.
* Fix a race condition in `ActiveRecord::Base#method_missing` when lazily defining attributes.
*Jenny Shen*
If multiple thread were concurrently triggering attribute definition on the same model,
it could result in a `NoMethodError` being raised.
* Enable passing retryable SqlLiterals to `#where`.
*Jean Boussier*
*Hartley McGuire*
* Fix MySQL default functions getting dropped when changing a column's nullability.
* Set default for primary keys in `insert_all`/`upsert_all`.
*Bastian Bartmann*
Previously in Postgres, updating and inserting new records in one upsert wasn't possible
due to null primary key values. `nil` primary key values passed into `insert_all`/`upsert_all`
are now implicitly set to the default insert value specified by adapter.
* Fix `add_unique_constraint`/`add_check_constraint`/`add_foreign_key` to be revertible when given invalid options.
*Jenny Shen*
*fatkodima*
* Add a load hook `active_record_database_configurations` for `ActiveRecord::DatabaseConfigurations`
* Fix asynchronous destroying of polymorphic `belongs_to` associations.
*Mike Dalessio*
*fatkodima*
* Use `TRUE` and `FALSE` for SQLite queries with boolean columns.
* Fix `insert_all` to not update existing records.
*Hartley McGuire*
*fatkodima*
* Bump minimum supported SQLite to 3.23.0.
* `NOT VALID` constraints should not dump in `create_table`.
*Hartley McGuire*
*Ryuta Kamizono*
* Allow allocated Active Records to lookup associations.
* Fix finding by nil composite primary key association.
Previously, the association cache isn't setup on allocated record objects, so association
lookups will crash. Test frameworks like mocha use allocate to check for stubbable instance
methods, which can trigger an association lookup.
*fatkodima*
*Gannon McGibbon*
* Properly reset composite primary key configuration when setting a primary key.
* Encryption now supports `support_unencrypted_data: true` being set per-attribute.
*fatkodima*
Previously this only worked if `ActiveRecord::Encryption.config.support_unencrypted_data == true`.
Now, if the global config is turned off, you can still opt in for a specific attribute.
* Fix Mysql2Adapter support for prepared statements
```ruby
# ActiveRecord::Encryption.config.support_unencrypted_data = true
class User < ActiveRecord::Base
encrypts :name, support_unencrypted_data: false # only supports encrypted data
encrypts :email # supports encrypted or unencrypted data
end
```
Using prepared statements with MySQL could result in a `NoMethodError` exception.
```ruby
# ActiveRecord::Encryption.config.support_unencrypted_data = false
class User < ActiveRecord::Base
encrypts :name, support_unencrypted_data: true # supports encrypted or unencrypted data
encrypts :email # only supports encrypted data
end
```
*Jean Boussier*, *Leo Arnold*, *zzak*
*Alex Ghiculescu*
* Fix parsing of SQLite foreign key names when they contain non-ASCII characters
* Model generator no longer needs a database connection to validate column types.
*Zacharias Knudsen*
* Fix parsing of MySQL 8.0.16+ CHECK constraints when they contain new lines.
*Steve Hill*
* Ensure normalized attribute queries use `IS NULL` consistently for `nil` and normalized `nil` values.
*Joshua Young*
* Fix `sum` when performing a grouped calculation.
`User.group(:friendly).sum` no longer worked. This is fixed.
*Edouard Chin*
* Restore back the ability to pass only database name to `DATABASE_URL`.
*fatkodima*
## Rails 8.0.0.1 (December 10, 2024) ##
* No changes.
## Rails 8.0.0 (November 07, 2024) ##
* Fix support for `query_cache: false` in `database.yml`.
`query_cache: false` would no longer entirely disable the Active Record query cache.
*zzak*
## Rails 8.0.0.rc2 (October 30, 2024) ##
* NULLS NOT DISTINCT works with UNIQUE CONSTRAINT as well as UNIQUE INDEX.
*Ryuta Kamizono*
* The `db:prepare` task no longer loads seeds when a non-primary database is created.
Previously, the `db:prepare` task would load seeds whenever a new database
is created, leading to potential loss of data if a database is added to an
existing environment.
Introduces a new database config property `seeds` to control whether seeds
are loaded during `db:prepare` which defaults to `true` for primary database
configs and `false` otherwise.
Fixes #53348.
*Mike Dalessio*
* `PG::UnableToSend: no connection to the server` is now retryable as a connection-related exception
* Allow signed ID verifiers to be configurable via `Rails.application.message_verifiers`
*Kazuma Watanabe*
Prior to this change, the primary way to configure signed ID verifiers was
to set `signed_id_verifier` on each model class:
* Fix strict loading propagation even if statement cache is not used.
```ruby
Post.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
Comment.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
```
*Ryuta Kamizono*
And if the developer did not set `signed_id_verifier`, a verifier would be
instantiated with a secret derived from `secret_key_base` and the following
options:
* Allow `rename_enum` accepts two from/to name arguments as `rename_table` does so.
```ruby
{ digest: "SHA256", serializer: JSON, url_safe: true }
```
*Ryuta Kamizono*
Thus it was cumbersome to rotate configuration for all verifiers.
This change defines a new Rails config: [`config.active_record.use_legacy_signed_id_verifier`][].
The default value is `:generate_and_verify`, which preserves the previous
behavior. However, when set to `:verify`, signed ID verifiers will use
configuration from `Rails.application.message_verifiers` (specifically,
`Rails.application.message_verifiers["active_record/signed_id"]`) to
generate and verify signed IDs, but will also verify signed IDs using the
older configuration.
## Rails 8.0.0.rc1 (October 19, 2024) ##
To avoid complication, the new behavior only applies when `signed_id_verifier_secret`
is not set on a model class or any of its ancestors. Additionally,
`signed_id_verifier_secret` is now deprecated. If you are currently setting
`signed_id_verifier_secret` on a model class, you can set `signed_id_verifier`
instead:
* Remove deprecated support to setting `ENV["SCHEMA_CACHE"]`.
```ruby
# BEFORE
Post.signed_id_verifier_secret = "my secret"
*Rafael Mendonça França*
# AFTER
Post.signed_id_verifier = ActiveSupport::MessageVerifier.new("my secret", digest: "SHA256", serializer: JSON, url_safe: true)
```
* Remove deprecated support to passing a database name to `cache_dump_filename`.
To ease migration, `signed_id_verifier` has also been changed to behave as a
`class_attribute` (i.e. inheritable), but _only when `signed_id_verifier_secret`
is not set_:
*Rafael Mendonça França*
```ruby
# BEFORE
ActiveRecord::Base.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => false
* Remove deprecated `ActiveRecord::ConnectionAdapters::ConnectionPool#connection`.
# AFTER
ActiveRecord::Base.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => true
*Rafael Mendonça França*
Post.signed_id_verifier_secret = "my secret" # => deprecation warning
Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => false
```
* Remove deprecated `config.active_record.sqlite3_deprecated_warning`.
Note, however, that it is recommended to eventually migrate from
model-specific verifiers to a unified configuration managed by
`Rails.application.message_verifiers`. `ActiveSupport::MessageVerifier#rotate`
can facilitate that transition. For example:
*Rafael Mendonça França*
```ruby
# BEFORE
# Generate and verify signed Post IDs using Post-specific configuration
Post.signed_id_verifier = ActiveSupport::MessageVerifier.new("post secret", ...)
* Remove deprecated `config.active_record.warn_on_records_fetched_greater_than`.
# AFTER
# Generate and verify signed Post IDs using the unified configuration
Post.signed_id_verifier = Post.signed_id_verifier.dup
# Fall back to Post-specific configuration when verifying signed IDs
Post.signed_id_verifier.rotate("post secret", ...)
```
*Rafael Mendonça França*
[`config.active_record.use_legacy_signed_id_verifier`]: https://guides.rubyonrails.org/v8.1/configuring.html#config-active-record-use-legacy-signed-id-verifier
* Remove deprecated support for defining `enum` with keyword arguments.
*Ali Sepehri*, *Jonathan Hefner*
*Rafael Mendonça França*
* Prepend `extra_flags` in postgres' `structure_load`
* Remove deprecated support to finding database adapters that aren't registered to Active Record.
When specifying `structure_load_flags` with a postgres adapter, the flags
were appended to the default flags, instead of prepended.
This caused issues with flags not being taken into account by postgres.
*Rafael Mendonça França*
*Alice Loeser*
* Remove deprecated `config.active_record.allow_deprecated_singular_associations_name`.
* Allow bypassing primary key/constraint addition in `implicit_order_column`
*Rafael Mendonça França*
When specifying multiple columns in an array for `implicit_order_column`, adding
`nil` as the last element will prevent appending the primary key to order
conditions. This allows more precise control of indexes used by
generated queries. It should be noted that this feature does introduce the risk
of API misbehavior if the specified columns are not fully unique.
* Remove deprecated `config.active_record.commit_transaction_on_non_local_return`.
*Issy Long*
*Rafael Mendonça França*
* Allow setting the `schema_format` via database configuration.
* Fix incorrect SQL query when passing an empty hash to `ActiveRecord::Base.insert`.
```
primary:
schema_format: ruby
```
*David Stosik*
Useful for multi-database setups when apps require different formats per-database.
* Allow to save records with polymorphic join tables that have `inverse_of`
specified.
*T S Vallender*
*Markus Doits*
* Support disabling indexes for MySQL v8.0.0+ and MariaDB v10.6.0+
* Fix association scopes applying on the incorrect join when using a polymorphic `has_many through:`.
MySQL 8.0.0 added an option to disable indexes from being used by the query
optimizer by making them "invisible". This allows the index to still be maintained
and updated but no queries will be permitted to use it. This can be useful for adding
new invisible indexes or making existing indexes invisible before dropping them
to ensure queries are not negatively affected.
See https://dev.mysql.com/blog-archive/mysql-8-0-invisible-indexes/ for more details.
*Joshua Young*
MariaDB 10.6.0 also added support for this feature by allowing indexes to be "ignored"
in queries. See https://mariadb.com/kb/en/ignored-indexes/ for more details.
* Allow `ActiveRecord::Base#pluck` to accept hash arguments with symbol and string values.
Active Record now supports this option for MySQL 8.0.0+ and MariaDB 10.6.0+ for
index creation and alteration where the new index option `enabled: true/false` can be
passed to column and index methods as below:
```ruby
Post.joins(:comments).pluck(:id, comments: :id)
Post.joins(:comments).pluck("id", "comments" => "id")
```
add_index :users, :email, enabled: false
enable_index :users, :email
add_column :users, :dob, :string, index: { enabled: false }
*Joshua Young*
change_table :users do |t|
t.index :name, enabled: false
t.index :dob
t.disable_index :dob
t.column :username, :string, index: { enabled: false }
t.references :account, index: { enabled: false }
end
* Make Float distinguish between `float4` and `float8` in PostgreSQL.
Fixes #52742
*Ryota Kitazawa*, *Takayuki Nagatomi*
## Rails 8.0.0.beta1 (September 26, 2024) ##
* Allow `drop_table` to accept an array of table names.
This will let you to drop multiple tables in a single call.
```ruby
ActiveRecord::Base.lease_connection.drop_table(:users, :posts)
create_table :users do |t|
t.string :name, index: { enabled: false }
t.string :email
t.index :email, enabled: false
end
```
*Gabriel Sobrinho*
*Merve Taner*
* Add support for PostgreSQL `IF NOT EXISTS` via the `:if_not_exists` option
on the `add_enum_value` method.
* Respect `implicit_order_column` in `ActiveRecord::Relation#reverse_order`.
*Ariel Rzezak*
*Joshua Young*
* When running `db:migrate` on a fresh database, load the databases schemas before running migrations.
* Add column types to `ActiveRecord::Result` for SQLite3.
*Andrew Novoselac*, *Marek Kasztelnik*
*Andrew Kane*
* Fix an issue where `.left_outer_joins` used with multiple associations that have
the same child association but different parents does not join all parents.
* Raise `ActiveRecord::ReadOnlyError` when pessimistically locking with a readonly role.
Previously, using `.left_outer_joins` with the same child association would only join one of the parents.
*Joshua Young*
Now it will correctly join both parents.
* Fix using the `SQLite3Adapter`'s `dbconsole` method outside of a Rails application.
Fixes #41498.
*Hartley McGuire*
*Garrett Blehm*
* Fix migrating multiple databases with `ActiveRecord::PendingMigration` action.
* Deprecate `unsigned_float` and `unsigned_decimal` short-hand column methods.
*Gannon McGibbon*
As of MySQL 8.0.17, the UNSIGNED attribute is deprecated for columns of type FLOAT, DOUBLE,
and DECIMAL. Consider using a simple CHECK constraint instead for such columns.
* Enable automatically retrying idempotent association queries on connection
errors.
https://dev.mysql.com/doc/refman/8.0/en/numeric-type-syntax.html
*Hartley McGuire*
*Ryuta Kamizono*
* Add `allow_retry` to `sql.active_record` instrumentation.
* Drop MySQL 5.5 support.
This enables identifying queries which queries are automatically retryable on connection errors.
MySQL 5.5 is the only version that does not support datetime with precision,
which we have supported in the core. Now we support MySQL 5.6.4 or later, which
is the first version to support datetime with precision.
*Hartley McGuire*
*Ryuta Kamizono*
* Better support UPDATE with JOIN for Postgresql and SQLite3
* Make Active Record asynchronous queries compatible with transactional fixtures.
Previously when generating update queries with one or more JOIN clauses,
Active Record would use a sub query which would prevent to reference the joined
tables in the `SET` clause, for instance:
Previously transactional fixtures would disable asynchronous queries, because transactional
fixtures impose all queries use the same connection.
```ruby
Comment.joins(:post).update_all("title = posts.title")
```
Now asynchronous queries will use the connection pinned by transactional fixtures, and behave
much closer to production.
This is now supported as long as the relation doesn't also use a `LIMIT`, `ORDER` or
`GROUP BY` clause. This was supported by the MySQL adapter for a long time.
*Jean Boussier*
* Deserialize binary data before decrypting
* Introduce a before-fork hook in `ActiveSupport::Testing::Parallelization` to clear existing
connections, to avoid fork-safety issues with the mysql2 adapter.
This ensures that we call `PG::Connection.unescape_bytea` on PostgreSQL before decryption.
Fixes #41776
*Donal McBreen*
*Mike Dalessio*, *Donal McBreen*
* Ensure `ActiveRecord::Encryption.config` is always ready before access.
* PoolConfig no longer keeps a reference to the connection class.
Previously, `ActiveRecord::Encryption` configuration was deferred until `ActiveRecord::Base`
was loaded. Therefore, accessing `ActiveRecord::Encryption.config` properties before
`ActiveRecord::Base` was loaded would give incorrect results.
Keeping a reference to the class caused subtle issues when combined with reloading in
development. Fixes #54343.
`ActiveRecord::Encryption` now has its own loading hook so that its configuration is set as
soon as needed.
*Mike Dalessio*
When `ActiveRecord::Base` is loaded, even lazily, it in turn triggers the loading of
`ActiveRecord::Encryption`, thus preserving the original behavior of having its config ready
before any use of `ActiveRecord::Base`.
* Fix SQL notifications sometimes not sent when using async queries.
*Maxime Réty*
* Add `TimeZoneConverter#==` method, so objects will be properly compared by
their type, scale, limit & precision.
Address #52699.
*Ruy Rocha*
* Add support for SQLite3 full-text-search and other virtual tables.
Previously, adding sqlite3 virtual tables messed up `schema.rb`.
Now, virtual tables can safely be added using `create_virtual_table`.
*Zacharias Knudsen*
* Support use of alternative database interfaces via the `database_cli` ActiveRecord configuration option.
```ruby
Rails.application.configure do
config.active_record.database_cli = { postgresql: "pgcli" }
Post.async_count
ActiveSupport::Notifications.subscribed(->(*) { "Will never reach here" }) do
Post.count
end
```
*T S Vallender*
In rare circumstances and under the right race condition, Active Support notifications
would no longer be dispatched after using an asynchronous query.
This is now fixed.
* Add support for dumping table inheritance and native partitioning table definitions for PostgeSQL adapter
*Edouard Chin*
*Justin Talbott*
* Eliminate queries loading dumped schema cache on Postgres
* Add support for `ActiveRecord::Point` type casts using `Hash` values
Improve resiliency by avoiding needing to open a database connection to load the
type map while defining attribute methods at boot when a schema cache file is
configured on PostgreSQL databases.
This allows `ActiveRecord::Point` to be cast or serialized from a hash
with `:x` and `:y` keys of numeric values, mirroring the functionality of
existing casts for string and array values. Both string and symbol keys are
supported.
*James Coleman*
* `ActiveRecord::Coder::JSON` can be instantiated
Options can now be passed to `ActiveRecord::Coder::JSON` when instantiating the coder. This allows:
```ruby
class PostgresqlPoint < ActiveRecord::Base
attribute :x, :point
attribute :y, :point
attribute :z, :point
end
val = PostgresqlPoint.new({
x: '(12.34, -43.21)',
y: [12.34, '-43.21'],
z: {x: '12.34', y: -43.21}
})
ActiveRecord::Point.new(12.32, -43.21) == val.x == val.y == val.z
serialize :config, coder: ActiveRecord::Coder::JSON.new(symbolize_names: true)
```
*matthaigh27*
*Stephen Drew*
* Deprecate using `insert_all`/`upsert_all` with unpersisted records in associations.
* Replace `SQLite3::Database#busy_timeout` with `#busy_handler_timeout=`.
Using these methods on associations containing unpersisted records will now
show a deprecation warning, as the unpersisted records will be lost after
the operation.
Provides a non-GVL-blocking, fair retry interval busy handler implementation.
*Nick Schwaderer*
*Stephen Margheim*
* Make column name optional for `index_exists?`.
* SQLite3Adapter: Translate `SQLite3::BusyException` into `ActiveRecord::StatementTimeout`.
This aligns well with `remove_index` signature as well, where
index name doesn't need to be derived from the column names.
*Matthew Nguyen*
*Ali Ismayiliov*
* Include schema name in `enable_extension` statements in `db/schema.rb`.
* Change the payload name of `sql.active_record` notification for eager
loading from "SQL" to "#{model.name} Eager Load".
The schema dumper will now include the schema name in generated
`enable_extension` statements if they differ from the current schema.
*zzak*
For example, if you have a migration:
* Enable automatically retrying idempotent `#exists?` queries on connection
errors.
```ruby
enable_extension "heroku_ext.pgcrypto"
enable_extension "pg_stat_statements"
```
*Hartley McGuire*, *classidied*
then the generated schema dump will also contain:
* Deprecate usage of unsupported methods in conjunction with `update_all`:
```ruby
enable_extension "heroku_ext.pgcrypto"
enable_extension "pg_stat_statements"
```
`update_all` will now print a deprecation message if a query includes either `WITH`,
`WITH RECURSIVE` or `DISTINCT` statements. Those were never supported and were ignored
when generating the SQL query.
*Tony Novak*
An error will be raised in a future Rails release. This behavior will be consistent
with `delete_all` which currently raises an error for unsupported statements.
* Fix `ActiveRecord::Encryption::EncryptedAttributeType#type` to return
actual cast type.
*Edouard Chin*
*Vasiliy Ermolovich*
* The table columns inside `schema.rb` are now sorted alphabetically.
* SQLite3Adapter: Bulk insert fixtures.
Previously they'd be sorted by creation order, which can cause merge conflicts when two
branches modify the same table concurrently.
Previously one insert command was executed for each fixture, now they are
aggregated in a single bulk insert command.
*John Duff*
*Lázaro Nixon*
* Introduce versions formatter for the schema dumper.
* PostgreSQLAdapter: Allow `disable_extension` to be called with schema-qualified name.
It is now possible to override how schema dumper formats versions information inside the
`structure.sql` file. Currently, the versions are simply sorted in the decreasing order.
Within large teams, this can potentially cause many merge conflicts near the top of the list.
For parity with `enable_extension`, the `disable_extension` method can be called with a schema-qualified
name (e.g. `disable_extension "myschema.pgcrypto"`). Note that PostgreSQL's `DROP EXTENSION` does not
actually take a schema name (unlike `CREATE EXTENSION`), so the resulting SQL statement will only name
the extension, e.g. `DROP EXTENSION IF EXISTS "pgcrypto"`.
Now, the custom formatter can be provided with a custom sorting logic (e.g. by hash values
of the versions), which can greatly reduce the number of conflicts.
*Tony Novak*
*fatkodima*
* Make `create_schema` / `drop_schema` reversible in migrations.
* Serialized attributes can now be marked as comparable.
Previously, `create_schema` and `drop_schema` were irreversible migration operations.
A not rare issue when working with serialized attributes is that the serialized representation of an object
can change over time. Either because you are migrating from one serializer to the other (e.g. YAML to JSON or to msgpack),
or because the serializer used subtly changed its output.
*Tony Novak*
One example is libyaml that used to have some extra trailing whitespaces, and recently fixed that.
When this sorts of thing happen, you end up with lots of records that report being changed even though
they aren't, which in the best case leads to a lot more writes to the database and in the worst case lead to nasty bugs.
* Support batching using custom columns.
The solution is to instead compare the deserialized representation of the object, however Active Record
can't assume the deserialized object has a working `==` method. Hence why this new functionality is opt-in.
```ruby
Product.in_batches(cursor: [:shop_id, :id]) do |relation|
# do something with relation
end
serialize :config, type: Hash, coder: JSON, comparable: true
```
*fatkodima*
*Jean Boussier*
* Use SQLite `IMMEDIATE` transactions when possible.
* Fix MySQL default functions getting dropped when changing a column's nullability.
Transactions run against the SQLite3 adapter default to IMMEDIATE mode to improve concurrency support and avoid busy exceptions.
*Bastian Bartmann*
*Stephen Margheim*
* SQLite extensions can be configured in `config/database.yml`.
* Raise specific exception when a connection is not defined.
The database configuration option `extensions:` allows an application to load SQLite extensions
when using `sqlite3` >= v2.4.0. The array members may be filesystem paths or the names of
modules that respond to `.to_path`:
The new `ConnectionNotDefined` exception provides connection name, shard and role accessors indicating the details of the connection that was requested.
``` yaml
development:
adapter: sqlite3
extensions:
- SQLean::UUID # module name responding to `.to_path`
- .sqlpkg/nalgeon/crypto/crypto.so # or a filesystem path
- <%= AppExtensions.location %> # or ruby code returning a path
```
*Hana Harencarova*, *Matthew Draper*
*Mike Dalessio*
* Delete the deprecated constant `ActiveRecord::ImmutableRelation`.
* `ActiveRecord::Middleware::ShardSelector` supports granular database connection switching.
*Xavier Noria*
A new configuration option, `class_name:`, is introduced to
`config.active_record.shard_selector` to allow an application to specify the abstract connection
class to be switched by the shard selection middleware. The default class is
`ActiveRecord::Base`.
* Fix duplicate callback execution when child autosaves parent with `has_one` and `belongs_to`.
For example, this configuration tells `ShardSelector` to switch shards using
`AnimalsRecord.connected_to`:
Before, persisting a new child record with a new associated parent record would run `before_validation`,
`after_validation`, `before_save` and `after_save` callbacks twice.
Now, these callbacks are only executed once as expected.
*Joshua Young*
* `ActiveRecord::Encryption::Encryptor` now supports a `:compressor` option to customize the compression algorithm used.
```ruby
module ZstdCompressor
def self.deflate(data)
Zstd.compress(data)
end
def self.inflate(data)
Zstd.decompress(data)
end
end
class User
encrypts :name, compressor: ZstdCompressor
end
```
You disable compression by passing `compress: false`.
```ruby
class User
encrypts :name, compress: false
end
config.active_record.shard_selector = { class_name: "AnimalsRecord" }
```
*heka1024*
*Mike Dalessio*
* Add condensed `#inspect` for `ConnectionPool`, `AbstractAdapter`, and
`DatabaseConfig`.
* Reset relations after `insert_all`/`upsert_all`.
*Hartley McGuire*
Bulk insert/upsert methods will now call `reset` if used on a relation, matching the behavior of `update_all`.
* Add `.shard_keys`, `.sharded?`, & `.connected_to_all_shards` methods.
*Milo Winningham*
```ruby
class ShardedBase < ActiveRecord::Base
self.abstract_class = true
* Use `_N` as a parallel tests databases suffixes
connects_to shards: {
shard_one: { writing: :shard_one },
shard_two: { writing: :shard_two }
}
end
Peviously, `-N` was used as a suffix. This can cause problems for RDBMSes
which do not support dashes in database names.
class ShardedModel < ShardedBase
end
*fatkodima*
ShardedModel.shard_keys => [:shard_one, :shard_two]
ShardedModel.sharded? => true
ShardedBase.connected_to_all_shards { ShardedModel.current_shard } => [:shard_one, :shard_two]
```
* Remember when a database connection has recently been verified (for
two seconds, by default), to avoid repeated reverifications during a
single request.
*Nony Dutton*
This should recreate a similar rate of verification as in Rails 7.1,
where connections are leased for the duration of a request, and thus
only verified once.
* Add a `filter` option to `in_order_of` to prioritize certain values in the sorting without filtering the results
by these values.
*Matthew Draper*
*Igor Depolli*
* Allow to reset cache counters for multiple records.
* Fix an issue where the IDs reader method did not return expected results
for preloaded associations in models using composite primary keys.
```
Aircraft.reset_counters([1, 2, 3], :wheels_count)
```
*Jay Ang*
It produces much fewer queries compared to the custom implementation using looping over ids.
Previously: `O(ids.size * counters.size)` queries, now: `O(ids.size + counters.size)` queries.
* Allow to configure `strict_loading_mode` globally or within a model.
*fatkodima*
Defaults to `:all`, can be changed to `:n_plus_one_only`.
* Add `affected_rows` to `sql.active_record` Notification.
*Garen Torikian*
*Hartley McGuire*
* Add `ActiveRecord::Relation#readonly?`.
* Fix `sum` when performing a grouped calculation.
Reflects if the relation has been marked as readonly.
`User.group(:friendly).sum` no longer worked. This is fixed.
*Theodor Tonum*
*Edouard Chin*
* Improve `ActiveRecord::Store` to raise a descriptive exception if the column is not either
structured (e.g., PostgreSQL +hstore+/+json+, or MySQL +json+) or declared serializable via
`ActiveRecord.store`.
* Add support for enabling or disabling transactional tests per database.
Previously, a `NoMethodError` would be raised when the accessor was read or written:
A test class can now override the default `use_transactional_tests` setting
for individual databases, which can be useful if some databases need their
current state to be accessible to an external process while tests are running.
NoMethodError: undefined method `accessor' for an instance of ActiveRecord::Type::Text
Now, a descriptive `ConfigurationError` is raised:
ActiveRecord::ConfigurationError: the column 'metadata' has not been configured as a store.
Please make sure the column is declared serializable via 'ActiveRecord.store' or, if your
database supports it, use a structured column type like hstore or json.
*Mike Dalessio*
* Fix inference of association model on nested models with the same demodularized name.
E.g. with the following setup:
```ruby
class Nested::Post < ApplicationRecord
has_one :post, through: :other
class MostlyTransactionalTest < ActiveSupport::TestCase
self.use_transactional_tests = true
skip_transactional_tests_for_database :shared
end
```
Before, `#post` would infer the model as `Nested::Post`, but now it correctly infers `Post`.
*Matthew Cheetham*, *Morgan Mareve*
*Joshua Young*
* Cast `query_cache` value when using URL configuration.
* Add public method for checking if a table is ignored by the schema cache.
*zzak*
Previously, an application would need to reimplement `ignored_table?` from the schema cache class to check if a table was set to be ignored. This adds a public method to support this and updates the schema cache to use that directly.
* NULLS NOT DISTINCT works with UNIQUE CONSTRAINT as well as UNIQUE INDEX.
```ruby
ActiveRecord.schema_cache_ignored_tables = ["developers"]
ActiveRecord.schema_cache_ignored_table?("developers")
=> true
```
*Ryuta Kamizono*
*Eileen M. Uchitelle*
* `PG::UnableToSend: no connection to the server` is now retryable as a connection-related exception
Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activerecord/CHANGELOG.md) for previous changes.
*Kazuma Watanabe*
Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/activerecord/CHANGELOG.md) for previous changes.

@@ -29,2 +29,3 @@ # frozen_string_literal: true

require "active_support/ordered_options"
require "active_support/core_ext/array/conversions"
require "active_model"

@@ -56,2 +57,3 @@ require "arel"

autoload :FixtureSet, "active_record/fixtures"
autoload :FilterAttributeHandler
autoload :Inheritance

@@ -67,3 +69,2 @@ autoload :Integration

autoload :NoTouching
autoload :Normalization
autoload :Persistence

@@ -180,3 +181,4 @@ autoload :QueryCache

autoload :DatabaseTasks
autoload :MySQLDatabaseTasks, "active_record/tasks/mysql_database_tasks"
autoload :AbstractTasks, "active_record/tasks/abstract_tasks"
autoload :MySQLDatabaseTasks, "active_record/tasks/mysql_database_tasks"
autoload :PostgreSQLDatabaseTasks, "active_record/tasks/postgresql_database_tasks"

@@ -266,2 +268,5 @@ autoload :SQLiteDatabaseTasks, "active_record/tasks/sqlite_database_tasks"

# Specify allowlist of database warnings.
# Can be a string, regular expression, or an error code from the database.
#
# ActiveRecord::Base.db_warnings_ignore = [/`SHOW WARNINGS` did not return the warnings/, "01000"]
singleton_class.attr_accessor :db_warnings_ignore

@@ -294,2 +299,3 @@ self.db_warnings_ignore = []

@global_thread_pool_async_query_executor ||= Concurrent::ThreadPoolExecutor.new(
name: "ActiveRecord-global-async-query-executor",
min_threads: 0,

@@ -360,2 +366,5 @@ max_threads: concurrency,

singleton_class.attr_accessor :raise_on_missing_required_finder_order_columns
self.run_after_transaction_callbacks_in_order_defined = false
singleton_class.attr_accessor :application_record_class

@@ -412,2 +421,8 @@ self.application_record_class = nil

##
# :singleton-method: schema_versions_formatter
# Specify the formatter used by schema dumper to format versions information.
singleton_class.attr_accessor :schema_versions_formatter
self.schema_versions_formatter = Migration::DefaultSchemaVersionsFormatter
##
# :singleton-method: dump_schema_after_migration

@@ -472,2 +487,25 @@ # Specify whether schema dump should happen at the end of the

def self.deprecated_associations_options=(options)
raise ArgumentError, "deprecated_associations_options must be a hash" unless options.is_a?(Hash)
valid_keys = [:mode, :backtrace]
invalid_keys = options.keys - valid_keys
unless invalid_keys.empty?
inflected_key = invalid_keys.size == 1 ? "key" : "keys"
raise ArgumentError, "invalid deprecated_associations_options #{inflected_key} #{invalid_keys.map(&:inspect).to_sentence} (valid keys are #{valid_keys.map(&:inspect).to_sentence})"
end
options.each do |key, value|
ActiveRecord::Associations::Deprecation.send("#{key}=", value)
end
end
def self.deprecated_associations_options
{
mode: ActiveRecord::Associations::Deprecation.mode,
backtrace: ActiveRecord::Associations::Deprecation.backtrace
}
end
def self.marshalling_format_version

@@ -509,2 +547,9 @@ Marshalling.format_version

##
# :singleton-method: message_verifiers
#
# ActiveSupport::MessageVerifiers instance for Active Record. If you are using
# Rails, this will be set to +Rails.application.message_verifiers+.
singleton_class.attr_accessor :message_verifiers
def self.eager_load!

@@ -564,3 +609,3 @@ super

if current_transaction.open? && current_transaction.joinable? && !current_transaction.state.invalidated?
if current_transaction.open? && current_transaction.joinable?
open_transactions << current_transaction

@@ -572,2 +617,19 @@ end

end
def self.default_transaction_isolation_level=(isolation_level) # :nodoc:
ActiveSupport::IsolatedExecutionState[:active_record_transaction_isolation] = isolation_level
end
def self.default_transaction_isolation_level # :nodoc:
ActiveSupport::IsolatedExecutionState[:active_record_transaction_isolation]
end
# Sets a transaction isolation level for all connection pools within the block.
def self.with_transaction_isolation_level(isolation_level, &block)
original_level = self.default_transaction_isolation_level
self.default_transaction_isolation_level = isolation_level
yield
ensure
self.default_transaction_isolation_level = original_level
end
end

@@ -574,0 +636,0 @@

@@ -20,3 +20,3 @@ # frozen_string_literal: true

class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{method}(attributes, **kwargs)
def #{method}(...)
if @association.reflection.through_reflection?

@@ -23,0 +23,0 @@ raise ArgumentError, "Bulk insert or upsert is currently not supported for has_many through association"

@@ -235,3 +235,3 @@ # frozen_string_literal: true

# Whether the association represent a single record
# Whether the association represents a single record
# or a collection of records.

@@ -238,0 +238,0 @@ def collection?

@@ -138,5 +138,3 @@ # frozen_string_literal: true

if force || reflection_fk.map { |fk| owner._read_attribute(fk) } != target_key_values
owner_pk = Array(owner.class.primary_key)
reflection_fk.each_with_index do |key, index|
next if record.nil? && owner_pk.include?(key)
owner[key] = target_key_values[index]

@@ -143,0 +141,0 @@ end

@@ -22,3 +22,3 @@ # frozen_string_literal: true

VALID_OPTIONS = [
:class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :query_constraints
:anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :query_constraints, :deprecated
].freeze # :nodoc:

@@ -106,3 +106,5 @@

def #{name}
association(:#{name}).reader
association = association(:#{name})
deprecated_associations_api_guard(association, __method__)
association.reader
end

@@ -115,3 +117,5 @@ CODE

def #{name}=(value)
association(:#{name}).writer(value)
association = association(:#{name})
deprecated_associations_api_guard(association, __method__)
association.writer(value)
end

@@ -144,4 +148,11 @@ CODE

def self.add_destroy_callbacks(model, reflection)
name = reflection.name
model.before_destroy(->(o) { o.association(name).handle_dependency })
if reflection.deprecated?
# If :dependent is set, destroying the record has a side effect that
# would no longer happen if the association is removed.
model.before_destroy do
report_deprecated_association(reflection, context: ":dependent has a side effect here")
end
end
model.before_destroy(->(o) { o.association(reflection.name).handle_dependency })
end

@@ -148,0 +159,0 @@

@@ -11,4 +11,5 @@ # frozen_string_literal: true

valid = super + [:polymorphic, :counter_cache, :optional, :default]
valid += [:foreign_type] if options[:polymorphic]
valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async
valid << :class_name unless options[:polymorphic]
valid << :foreign_type if options[:polymorphic]
valid << :ensuring_owner_was if options[:dependent] == :destroy_async
valid

@@ -111,2 +112,10 @@ end

def self.add_destroy_callbacks(model, reflection)
if reflection.deprecated?
# If :dependent is set, destroying the record has some side effect that
# would no longer happen if the association is removed.
model.before_destroy do
report_deprecated_association(reflection, context: ":dependent has a side effect here")
end
end
model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }

@@ -149,7 +158,11 @@ end

def #{reflection.name}_changed?
association(:#{reflection.name}).target_changed?
association = association(:#{reflection.name})
deprecated_associations_api_guard(association, __method__)
association.target_changed?
end
def #{reflection.name}_previously_changed?
association(:#{reflection.name}).target_previously_changed?
association = association(:#{reflection.name})
deprecated_associations_api_guard(association, __method__)
association.target_previously_changed?
end

@@ -156,0 +169,0 @@ CODE

@@ -10,3 +10,3 @@ # frozen_string_literal: true

def self.valid_options(options)
super + [:before_add, :after_add, :before_remove, :after_remove, :extend]
super + [:class_name, :before_add, :after_add, :before_remove, :after_remove, :extend]
end

@@ -64,3 +64,5 @@

def #{name.to_s.singularize}_ids
association(:#{name}).ids_reader
association = association(:#{name})
deprecated_associations_api_guard(association, __method__)
association.ids_reader
end

@@ -75,3 +77,5 @@ CODE

def #{name.to_s.singularize}_ids=(ids)
association(:#{name}).ids_writer(ids)
association = association(:#{name})
deprecated_associations_api_guard(association, __method__)
association.ids_writer(ids)
end

@@ -78,0 +82,0 @@ CODE

@@ -10,3 +10,3 @@ # frozen_string_literal: true

def self.valid_options(options)
valid = super + [:as, :through]
valid = super + [:class_name, :as, :through]
valid += [:foreign_type] if options[:as]

@@ -13,0 +13,0 @@ valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async

@@ -20,7 +20,11 @@ # frozen_string_literal: true

def reload_#{name}
association(:#{name}).force_reload_reader
association = association(:#{name})
deprecated_associations_api_guard(association, __method__)
association.force_reload_reader
end
def reset_#{name}
association(:#{name}).reset
association = association(:#{name})
deprecated_associations_api_guard(association, __method__)
association.reset
end

@@ -34,11 +38,17 @@ CODE

def build_#{name}(*args, &block)
association(:#{name}).build(*args, &block)
association = association(:#{name})
deprecated_associations_api_guard(association, __method__)
association.build(*args, &block)
end
def create_#{name}(*args, &block)
association(:#{name}).create(*args, &block)
association = association(:#{name})
deprecated_associations_api_guard(association, __method__)
association.create(*args, &block)
end
def create_#{name}!(*args, &block)
association(:#{name}).create!(*args, &block)
association = association(:#{name})
deprecated_associations_api_guard(association, __method__)
association.create!(*args, &block)
end

@@ -48,4 +58,22 @@ CODE

def self.define_callbacks(model, reflection)
super
# If the record is saved or destroyed and `:touch` is set, the parent
# record gets a timestamp updated. We want to know about it, because
# deleting the association would change that side-effect and perhaps there
# is code relying on it.
if reflection.deprecated? && reflection.options[:touch]
model.before_save do
report_deprecated_association(reflection, context: ":touch has a side effect here")
end
model.before_destroy do
report_deprecated_association(reflection, context: ":touch has a side effect here")
end
end
end
private_class_method :valid_options, :define_accessors, :define_constructors
end
end

@@ -113,3 +113,3 @@ # frozen_string_literal: true

# Finds an object in the collection responding to the +id+. Uses the same
# rules as ActiveRecord::FinderMethods.find. Returns ActiveRecord::RecordNotFound
# rules as ActiveRecord::FinderMethods.find. Raises ActiveRecord::RecordNotFound
# error if the object cannot be found.

@@ -1129,2 +1129,22 @@ #

%w(insert insert_all insert! insert_all! upsert upsert_all).each do |method|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{method}(...)
if @association&.target&.any? { |r| r.new_record? }
association_name = @association.reflection.name
ActiveRecord.deprecator.warn(<<~MSG)
Using #{method} on association \#{association_name} with unpersisted records
is deprecated and will be removed in Rails 8.2.
The unpersisted records will be lost after this operation.
Please either persist your records first or store them separately before
calling #{method}.
MSG
scope.#{method}(...)
else
scope.#{method}(...).tap { reset }
end
end
RUBY
end
delegate_methods = [

@@ -1135,5 +1155,3 @@ QueryMethods,

klass.public_instance_methods(false)
} - self.public_instance_methods(false) - [:select] + [
:scoping, :values, :insert, :insert_all, :insert!, :insert_all!, :upsert, :upsert_all, :load_async
]
} - self.public_instance_methods(false) - [ :select ] + [ :scoping, :values, :load_async ]

@@ -1140,0 +1158,0 @@ delegate(*delegate_methods, to: :scope)

@@ -265,2 +265,5 @@ # frozen_string_literal: true

end
class DeprecatedAssociationError < ActiveRecordError
end
end

@@ -106,3 +106,3 @@ # frozen_string_literal: true

def instantiate(result_set, strict_loading_value, &block)
primary_key = Array(join_root.primary_key).map { |column| aliases.column_alias(join_root, column) }
primary_key = aliases.column_alias(join_root, join_root.primary_key)

@@ -145,3 +145,3 @@ seen = Hash.new { |i, parent|

result_set.each { |row_hash|
parent_key = primary_key.empty? ? row_hash : row_hash.values_at(*primary_key)
parent_key = primary_key ? row_hash[primary_key] : row_hash
parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, column_types, &block)

@@ -240,2 +240,4 @@ construct(parent, join_root, row_hash, seen, model_cache, strict_loading_value)

Deprecation.guard(reflection) { "referenced in query to join its table" }
JoinAssociation.new(reflection, build(right, reflection.klass))

@@ -242,0 +244,0 @@ end

@@ -41,9 +41,3 @@ # frozen_string_literal: true

def group_and_load_similar(loaders)
non_through = loaders.grep_v(ThroughAssociation)
grouped = non_through.group_by do |loader|
[loader.loader_query, loader.klass]
end
grouped.each do |(query, _klass), similar_loaders|
loaders.grep_v(ThroughAssociation).group_by(&:loader_query).each_pair do |query, similar_loaders|
query.load_records_in_batch(similar_loaders)

@@ -50,0 +44,0 @@ end

@@ -121,2 +121,3 @@ # frozen_string_literal: true

grouped_records.flat_map do |reflection, reflection_records|
Deprecation.guard(reflection) { "referenced in query to preload records" }
preloaders_for_reflection(reflection, reflection_records)

@@ -123,0 +124,0 @@ end

@@ -116,3 +116,3 @@ # frozen_string_literal: true

super(attribute_names)
alias_attribute :id_value, :id if _has_attribute?("id") && !_has_attribute?("id_value")
alias_attribute :id_value, :id if _has_attribute?("id")
end

@@ -119,0 +119,0 @@

@@ -54,2 +54,12 @@ # frozen_string_literal: true

# will set to +type.new+
# * +comparable+ - Specify whether the deserialized object is safely comparable
# for the purpose of detecting changes. Defaults to +false+
# When set to +false+ the old and new values will be compared by their serialized
# representation (e.g. JSON or YAML), which can sometimes cause two objects that are
# semantically equal to be considered different.
# For instance two hashes with the same keys and values but a different order have a
# different serialized representation, but are semantically equal once deserialized.
# If set to +true+ the comparison will be done on the deserialized object.
# This options should only be enabled if the +type+ is known to have
# a proper <tt>==</tt> method that deeply compare the objects.
# * +yaml+ - Optional. Yaml specific options. The allowed config is:

@@ -184,3 +194,3 @@ # * +:permitted_classes+ - +Array+ with the permitted classes.

#
def serialize(attr_name, coder: nil, type: Object, yaml: {}, **options)
def serialize(attr_name, coder: nil, type: Object, comparable: false, yaml: {}, **options)
coder ||= default_column_serializer

@@ -205,3 +215,3 @@ unless coder

cast_type = cast_type.subtype if Type::Serialized === cast_type
Type::Serialized.new(cast_type, column_serializer)
Type::Serialized.new(cast_type, column_serializer, comparable: comparable)
end

@@ -215,4 +225,7 @@ end

# using the #as_json hook.
coder = Coders::JSON if coder == ::JSON
if coder == ::JSON || coder == Coders::JSON
coder = Coders::JSON.new
end
if coder == ::YAML || coder == Coders::YAMLColumn

@@ -219,0 +232,0 @@ Coders::YAMLColumn.new(attr_name, type, **(yaml || {}))

@@ -10,2 +10,3 @@ # frozen_string_literal: true

include ActiveModel::AttributeRegistration
include ActiveModel::Attributes::Normalization

@@ -255,2 +256,3 @@ # = Active Record \Attributes

@default_attributes ||= begin
# TODO: Remove the need for a connection after we release 8.1.
attributes_hash = with_connection do |connection|

@@ -315,2 +317,3 @@ columns_hash.transform_values do |column|

def type_for_column(connection, column)
# TODO: Remove the need for a connection after we release 8.1.
hook_attribute_type(column.name, super)

@@ -317,0 +320,0 @@ end

@@ -377,3 +377,3 @@ # frozen_string_literal: true

if context || record.changed_for_autosave?
if record.changed? || record.new_record? || context
associated_errors = record.errors.objects

@@ -531,3 +531,3 @@ else

class_name = record._read_attribute(reflection.inverse_of.foreign_type)
reflection.active_record.polymorphic_name != class_name
reflection.active_record != record.class.polymorphic_class_for(class_name)
end

@@ -534,0 +534,0 @@

@@ -259,3 +259,3 @@ # frozen_string_literal: true

# * AttributeAssignmentError - An error occurred while doing a mass assignment through the
# {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=] method.
# {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
# You can inspect the +attribute+ property of the exception object to determine which attribute

@@ -266,3 +266,3 @@ # triggered the error.

# * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
# {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=] method.
# {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
# The +errors+ property of this exception contains an array of

@@ -333,3 +333,2 @@ # AttributeAssignmentError

include Suppressor
include Normalization
include Marshalling::Methods

@@ -336,0 +335,0 @@

# frozen_string_literal: true
require "active_support/json"
module ActiveRecord
module Coders # :nodoc:
module JSON # :nodoc:
def self.dump(obj)
ActiveSupport::JSON.encode(obj)
class JSON # :nodoc:
DEFAULT_OPTIONS = { escape: false }.freeze
def initialize(options = nil)
@options = options ? DEFAULT_OPTIONS.merge(options) : DEFAULT_OPTIONS
@encoder = ActiveSupport::JSON::Encoding.json_encoder.new(options)
end
def self.load(json)
ActiveSupport::JSON.decode(json) unless json.blank?
def dump(obj)
@encoder.encode(obj)
end
def load(json)
ActiveSupport::JSON.decode(json, @options) unless json.blank?
end
end
end
end

@@ -87,2 +87,3 @@ # frozen_string_literal: true

autoload :ColumnDefinition
autoload :ColumnMethods
autoload :ChangeColumnDefinition

@@ -89,0 +90,0 @@ autoload :ChangeColumnDefaultDefinition

@@ -45,3 +45,3 @@ # frozen_string_literal: true

attr_reader :visitor, :owner, :logger, :lock
attr_accessor :pinned # :nodoc:
attr_accessor :allow_preconnect
alias :in_use? :owner

@@ -124,3 +124,3 @@

def self.dbconsole(config, options = {})
raise NotImplementedError
raise NotImplementedError.new("#{self.class} should define `dbconsole` that accepts a db config and options to implement connecting to the db console")
end

@@ -133,2 +133,3 @@

@unconfigured_connection = nil
@connected_since = nil

@@ -146,2 +147,3 @@ if config_or_deprecated_connection.is_a?(Hash)

@unconfigured_connection = config_or_deprecated_connection
@connected_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@logger = deprecated_logger || ActiveRecord::Base.logger

@@ -158,5 +160,5 @@ if deprecated_config

@owner = nil
@pinned = false
@pool = ActiveRecord::ConnectionAdapters::NullPool.new
@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@allow_preconnect = true
@visitor = arel_visitor

@@ -179,2 +181,4 @@ @statements = build_statement_pool

@verified = false
@pool_jitter = rand * max_jitter
end

@@ -207,2 +211,7 @@

MAX_JITTER = 0.0..1.0 # :nodoc:
def max_jitter
(@config[:pool_jitter] || 0.2).to_f.clamp(MAX_JITTER)
end
def replica?

@@ -272,5 +281,9 @@ @config[:replica] || false

def valid_type?(type) # :nodoc:
!native_database_types[type].nil?
self.class.valid_type?(type)
end
def native_database_types # :nodoc:
self.class.native_database_types
end
# this method must only be called while holding connection pool's mutex

@@ -312,4 +325,8 @@ def lease

def pool_jitter(duration)
duration * (1.0 - @pool_jitter)
end
# this method must only be called while holding connection pool's mutex
def expire
def expire(update_idle = true) # :nodoc:
if in_use?

@@ -322,3 +339,3 @@ if @owner != ActiveSupport::IsolatedExecutionState.context

@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) if update_idle
@owner = nil

@@ -356,2 +373,17 @@ else

# Seconds since this connection was established. nil if not
# connected; infinity if the connection has been explicitly
# retired.
def connection_age # :nodoc:
if @raw_connection && @connected_since
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @connected_since
end
end
# Mark the connection as needing to be retired, as if the age has
# exceeded the maximum allowed.
def force_retirement # :nodoc:
@connected_since &&= -Float::INFINITY
end
def unprepared_statement

@@ -571,2 +603,6 @@ cache = prepared_statements_disabled_cache.add?(object_id) if @prepared_statements

def supports_disabling_indexes?
false
end
def return_value_after_insert?(column) # :nodoc:

@@ -681,29 +717,33 @@ column.auto_populated?

@lock.synchronize do
attempt_configure_connection do
reconnect
@allow_preconnect = false
enable_lazy_transactions!
@raw_connection_dirty = false
@last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@verified = true
reconnect
reset_transaction(restore: restore_transactions) do
clear_cache!(new_connection: true)
configure_connection
end
rescue => original_exception
translated_exception = translate_exception_class(original_exception, nil, nil)
retry_deadline_exceeded = deadline && deadline < Process.clock_gettime(Process::CLOCK_MONOTONIC)
enable_lazy_transactions!
@raw_connection_dirty = false
@last_activity = @connected_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@verified = true
@allow_preconnect = true
if !retry_deadline_exceeded && retries_available > 0
retries_available -= 1
reset_transaction(restore: restore_transactions) do
clear_cache!(new_connection: true)
attempt_configure_connection
end
rescue => original_exception
translated_exception = translate_exception_class(original_exception, nil, nil)
retry_deadline_exceeded = deadline && deadline < Process.clock_gettime(Process::CLOCK_MONOTONIC)
if retryable_connection_error?(translated_exception)
backoff(connection_retries - retries_available)
retry
end
if !retry_deadline_exceeded && retries_available > 0
retries_available -= 1
if retryable_connection_error?(translated_exception)
backoff(connection_retries - retries_available)
retry
end
end
raise translated_exception
end
@last_activity = nil
@verified = false
raise translated_exception
end

@@ -716,7 +756,6 @@ end

@lock.synchronize do
@last_activity = nil
@verified = false
clear_cache!(new_connection: true)
reset_transaction
@raw_connection_dirty = false
@connected_since = nil
end

@@ -744,7 +783,5 @@ end

def reset!
attempt_configure_connection do
clear_cache!(new_connection: true)
reset_transaction
configure_connection
end
clear_cache!(new_connection: true)
reset_transaction
attempt_configure_connection
end

@@ -783,9 +820,8 @@

if @unconfigured_connection
attempt_configure_connection do
@raw_connection = @unconfigured_connection
@unconfigured_connection = nil
configure_connection
@last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@verified = true
end
@raw_connection = @unconfigured_connection
@unconfigured_connection = nil
attempt_configure_connection
@last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@verified = true
@allow_preconnect = true
return

@@ -798,2 +834,3 @@ end

@last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@verified = true

@@ -812,2 +849,6 @@ end

def verified? # :nodoc:
@verified
end
# Provides access to the underlying database driver for this adapter. For

@@ -898,3 +939,3 @@ # example, this method returns a Mysql2::Client object in case of Mysql2Adapter,

precision = extract_precision(args.last)
klass.new(precision: precision, **kwargs)
klass.new(precision: precision, **kwargs).freeze
end

@@ -911,2 +952,6 @@ end

def valid_type?(type) # :nodoc:
!native_database_types[type].nil?
end
private

@@ -931,3 +976,3 @@ def initialize_type_map(m)

m.register_type %r(^json)i, Type::Json.new
m.register_type %r(^json)i, Type::Json.new.freeze

@@ -940,5 +985,5 @@ m.register_type(%r(decimal)i) do |sql_type|

# FIXME: Remove this class as well
Type::DecimalWithoutScale.new(precision: precision)
Type::DecimalWithoutScale.new(precision: precision).freeze
else
Type::Decimal.new(precision: precision, scale: scale)
Type::Decimal.new(precision: precision, scale: scale).freeze
end

@@ -951,3 +996,3 @@ end

limit = extract_limit(args.last)
klass.new(limit: limit)
klass.new(limit: limit).freeze
end

@@ -1113,3 +1158,3 @@ end

def reconnect
raise NotImplementedError
raise NotImplementedError.new("#{self.class} should define `reconnect` to implement adapter-specific logic for reconnecting to the database")
end

@@ -1165,3 +1210,3 @@

def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, &block) # :doc:
def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, allow_retry: false, &block) # :doc:
instrumenter.instrument(

@@ -1174,4 +1219,6 @@ "sql.active_record",

async: async,
allow_retry: allow_retry,
connection: self,
transaction: current_transaction.user_transaction.presence,
affected_rows: 0,
row_count: 0,

@@ -1250,3 +1297,3 @@ &block

def attempt_configure_connection
yield
configure_connection
rescue Exception # Need to handle things such as Timeout::ExitException

@@ -1253,0 +1300,0 @@ disconnect!

@@ -45,3 +45,3 @@ # frozen_string_literal: true

blob: { name: "blob" },
boolean: { name: "tinyint", limit: 1 },
boolean: { name: "boolean" },
json: { name: "json" },

@@ -85,2 +85,6 @@ }

end
def native_database_types # :nodoc:
NATIVE_DATABASE_TYPES
end
end

@@ -183,2 +187,12 @@

# See https://dev.mysql.com/doc/refman/8.0/en/invisible-indexes.html for more details on MySQL feature.
# See https://mariadb.com/kb/en/ignored-indexes/ for more details on the MariaDB feature.
def supports_disabling_indexes?
if mariadb?
database_version >= "10.6.0"
else
database_version >= "8.0.0"
end
end
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:

@@ -192,6 +206,2 @@ query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1

def native_database_types
NATIVE_DATABASE_TYPES
end
def index_algorithms

@@ -464,2 +474,20 @@ {

def enable_index(table_name, index_name) # :nodoc:
raise NotImplementedError unless supports_disabling_indexes?
query = <<~SQL
ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "NOT IGNORED" : "VISIBLE"}
SQL
execute(query)
end
def disable_index(table_name, index_name) # :nodoc:
raise NotImplementedError unless supports_disabling_indexes?
query = <<~SQL
ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "IGNORED" : "INVISIBLE"}
SQL
execute(query)
end
def add_sql_comment!(sql, comment) # :nodoc:

@@ -740,8 +768,8 @@ sql << " COMMENT #{quote(comment)}" if comment.present?

def register_integer_type(mapping, key, **options)
def register_integer_type(mapping, key, limit:)
mapping.register_type(key) do |sql_type|
if /\bunsigned\b/.match?(sql_type)
Type::UnsignedInteger.new(**options)
Type::UnsignedInteger.new(limit: limit)
else
Type::Integer.new(**options)
Type::Integer.new(limit: limit)
end

@@ -776,3 +804,3 @@ end

def handle_warnings(sql)
def handle_warnings(_initial_result, sql)
return if ActiveRecord.db_warnings_action.nil? || @raw_connection.warning_count == 0

@@ -783,3 +811,3 @@

result = [
["Warning", nil, "Query had warning_count=#{warning_count} but ‘SHOW WARNINGS’ did not return the warnings. Check MySQL logs or database configuration."],
["Warning", nil, "Query had warning_count=#{warning_count} but `SHOW WARNINGS` did not return the warnings. Check MySQL logs or database configuration."],
] if result.count == 0

@@ -821,2 +849,4 @@ result.each do |level, code, message|

ER_FK_INCOMPATIBLE_COLUMNS = 3780
ER_CHECK_CONSTRAINT_VIOLATED = 3819
ER_CONSTRAINT_FAILED = 4025
ER_CLIENT_INTERACTION_TIMEOUT = 4031

@@ -854,2 +884,4 @@

NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
when ER_CHECK_CONSTRAINT_VIOLATED, ER_CONSTRAINT_FAILED
CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
when ER_LOCK_DEADLOCK

@@ -973,3 +1005,3 @@ Deadlocked.new(message, sql: sql, binds: binds, connection_pool: @pool)

def column_definitions(table_name) # :nodoc:
internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA")
internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA", allow_retry: true)
end

@@ -976,0 +1008,0 @@

@@ -161,5 +161,3 @@ # frozen_string_literal: true

# Returns any connections in use by the current thread back to the pool,
# and also returns connections to the pool cached by threads that are no
# longer alive.
# Returns any connections in use by the current thread back to the pool.
def clear_active_connections!(role = nil)

@@ -166,0 +164,0 @@ each_connection_pool(role).each do |pool|

@@ -17,3 +17,3 @@ # frozen_string_literal: true

class NullConfig # :nodoc:
class NullConfig
def method_missing(...)

@@ -23,3 +23,3 @@ nil

end
NULL_CONFIG = NullConfig.new # :nodoc:
NULL_CONFIG = NullConfig.new

@@ -41,3 +41,2 @@ def initialize

def schema_cache; end
def query_cache; end
def connection_descriptor; end

@@ -55,2 +54,7 @@ def checkin(_); end

end
def pool_transaction_isolation_level; end
def pool_transaction_isolation_level=(isolation_level)
raise NotImplementedError, "This method should never be called"
end
end

@@ -108,9 +112,16 @@

#
# * +pool+: maximum number of connections the pool may manage (default 5).
# * +checkout_timeout+: number of seconds to wait for a connection to
# become available before giving up and raising a timeout error (default
# 5 seconds).
# * +idle_timeout+: number of seconds that a connection will be kept
# unused in the pool before it is automatically disconnected (default
# 300 seconds). Set this to zero to keep connections forever.
# * +checkout_timeout+: number of seconds to wait for a connection to
# become available before giving up and raising a timeout error (default
# 5 seconds).
# * +keepalive+: number of seconds between keepalive checks if the
# connection has been idle (default 600 seconds).
# * +max_age+: number of seconds the pool will allow the connection to
# exist before retiring it at next checkin. (default Float::INFINITY).
# * +max_connections+: maximum number of connections the pool may manage (default 5).
# * +min_connections+: minimum number of connections the pool will open and maintain (default 0).
# * +pool_jitter+: maximum reduction factor to apply to +max_age+ and
# +keepalive+ intervals (default 0.2; range 0.0-1.0).
#

@@ -123,2 +134,3 @@ #--

# * @now_connecting
# * @maintaining
# * private methods that require being called in a +synchronize+ blocks

@@ -129,3 +141,3 @@ # are now explicitly documented

# https://bugs.ruby-lang.org/issues/20688
if ObjectSpace.const_defined?(:WeakKeyMap) && Gem::Version.new(RUBY_VERSION) >= "3.3.5"
if ObjectSpace.const_defined?(:WeakKeyMap) && RUBY_VERSION >= "3.3.5"
WeakThreadKeyMap = ObjectSpace::WeakKeyMap

@@ -230,3 +242,4 @@ else

attr_accessor :automatic_reconnect, :checkout_timeout
attr_reader :db_config, :size, :reaper, :pool_config, :async_executor, :role, :shard
attr_reader :db_config, :max_connections, :min_connections, :max_age, :keepalive, :reaper, :pool_config, :async_executor, :role, :shard
alias :size :max_connections

@@ -251,3 +264,6 @@ delegate :schema_reflection, :server_version, to: :pool_config

@idle_timeout = db_config.idle_timeout
@size = db_config.pool
@max_connections = db_config.max_connections
@min_connections = db_config.min_connections
@max_age = db_config.max_age
@keepalive = db_config.keepalive

@@ -274,2 +290,8 @@ # This variable tracks the cache of threads mapped to reserved connections, with the

# Sometimes otherwise-idle connections are temporarily held by the Reaper for
# maintenance. This variable tracks the number of connections currently in that
# state -- if a thread requests a connection and there are none available, it
# will await any in-maintenance connections in preference to creating a new one.
@maintaining = 0
@threads_blocking_new_connections = 0

@@ -285,2 +307,6 @@

@activated = false
@original_context = ActiveSupport::IsolatedExecutionState.context
@reaper_lock = Monitor.new
@reaper = Reaper.new(self, db_config.reaping_frequency)

@@ -291,4 +317,4 @@ @reaper.run

def inspect # :nodoc:
name_field = " name=#{db_config.name.inspect}" unless db_config.name == "primary"
shard_field = " shard=#{@shard.inspect}" unless @shard == :default
name_field = " name=#{name_inspect}" if name_inspect
shard_field = " shard=#{shard_inspect}" if shard_inspect

@@ -323,2 +349,10 @@ "#<#{self.class.name} env_name=#{db_config.env_name.inspect}#{name_field} role=#{role.inspect}#{shard_field}>"

def activate
@activated = true
end
def activated?
@activated
end
# Retrieve the connection associated with the current thread, or call

@@ -331,5 +365,4 @@ # #checkout to obtain one if necessary.

lease = connection_lease
lease.sticky = true
lease.connection ||= checkout
lease.sticky = true
lease.connection
end

@@ -352,3 +385,2 @@

@pinned_connection.lock_thread = ActiveSupport::IsolatedExecutionState.context if lock_thread
@pinned_connection.pinned = true
@pinned_connection.verify! # eagerly validate the connection

@@ -376,3 +408,2 @@ @pinned_connection.begin_transaction joinable: false, _lazy: false

if @pinned_connection.nil?
connection.pinned = false
connection.steal!

@@ -409,2 +440,4 @@ connection.lock_thread = nil

def release_connection(existing_lease = nil)
return if self.discarded?
if conn = connection_lease.release

@@ -447,2 +480,20 @@ checkin conn

def with_pool_transaction_isolation_level(isolation_level, transaction_open, &block) # :nodoc:
if !ActiveRecord.default_transaction_isolation_level.nil?
begin
if transaction_open && self.pool_transaction_isolation_level != ActiveRecord.default_transaction_isolation_level
raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open"
end
old_level = self.pool_transaction_isolation_level
self.pool_transaction_isolation_level = isolation_level
yield
ensure
self.pool_transaction_isolation_level = old_level
end
else
yield
end
end
# Returns true if a connection has already been opened.

@@ -475,14 +526,22 @@ def connected?

def disconnect(raise_on_acquisition_timeout = true)
with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
synchronize do
@connections.each do |conn|
if conn.in_use?
conn.steal!
checkin conn
@reaper_lock.synchronize do
return if self.discarded?
with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
synchronize do
return if self.discarded?
@connections.each do |conn|
if conn.in_use?
conn.steal!
checkin conn
end
conn.disconnect!
end
conn.disconnect!
@connections = []
@leases.clear
@available.clear
# Stop maintaining the minimum size until reactivated
@activated = false
end
@connections = @pinned_connection ? [@pinned_connection] : []
@leases.clear
@available.clear
end

@@ -508,8 +567,10 @@ end

def discard! # :nodoc:
synchronize do
return if self.discarded?
@connections.each do |conn|
conn.discard!
@reaper_lock.synchronize do
synchronize do
return if self.discarded?
@connections.each do |conn|
conn.discard!
end
@connections = @available = @leases = nil
end
@connections = @available = @leases = nil
end

@@ -522,2 +583,12 @@ end

def maintainable? # :nodoc:
synchronize do
@connections&.size&.> 0 || (activated? && @min_connections > 0)
end
end
def reaper_lock(&block) # :nodoc:
@reaper_lock.synchronize(&block)
end
# Clears reloadable connections from the pool and re-connects connections that

@@ -628,3 +699,3 @@ # require reloading.

# @available.any_waiting? => true means that prior to removing this
# conn, the pool was at its max size (@connections.size == @size).
# conn, the pool was at its max size (@connections.size == @max_connections).
# This would mean that any threads stuck waiting in the queue wouldn't

@@ -637,3 +708,3 @@ # know they could checkout_new_connection, so let's do it for them.

# connections into the Queue.
needs_new_connection = @available.any_waiting?
needs_new_connection = @available.num_waiting > @maintaining
end

@@ -645,3 +716,3 @@

# stale, we can live with that (bulk_make_new_connections will make
# sure not to exceed the pool's @size limit).
# sure not to exceed the pool's @max_connections limit).
bulk_make_new_connections(1) if needs_new_connection

@@ -679,7 +750,23 @@ end

idle_connections = synchronize do
removed_connections = synchronize do
return if self.discarded?
@connections.select do |conn|
idle_connections = @connections.select do |conn|
!conn.in_use? && conn.seconds_idle >= minimum_idle
end.each do |conn|
end.sort_by { |conn| -conn.seconds_idle } # sort longest idle first
# Don't go below our configured pool minimum unless we're flushing
# everything
idles_to_retain =
if minimum_idle > 0
@min_connections - (@connections.size - idle_connections.size)
else
0
end
if idles_to_retain > 0
idle_connections.pop idles_to_retain
end
idle_connections.each do |conn|
conn.lease

@@ -692,3 +779,3 @@

idle_connections.each do |conn|
removed_connections.each do |conn|
conn.disconnect!

@@ -699,8 +786,90 @@ end

# Disconnect all currently idle connections. Connections currently checked
# out are unaffected.
# out are unaffected. The pool will stop maintaining its minimum size until
# it is reactivated (such as by a subsequent checkout).
def flush!
reap
flush(-1)
# Stop maintaining the minimum size until reactivated
@activated = false
end
# Ensure that the pool contains at least the configured minimum number of
# connections.
def prepopulate
need_new_connections = nil
synchronize do
return if self.discarded?
# We don't want to start prepopulating until we know the pool is wanted,
# so we can avoid maintaining full pools in one-off scripts etc.
return unless @activated
need_new_connections = @connections.size < @min_connections
end
if need_new_connections
while new_conn = try_to_checkout_new_connection { @connections.size < @min_connections }
checkin(new_conn)
end
end
end
def retire_old_connections(max_age = @max_age)
max_age ||= Float::INFINITY
sequential_maintenance -> c { c.connection_age&.>= c.pool_jitter(max_age) } do |conn|
# Disconnect, then return the adapter to the pool. Preconnect will
# handle the rest.
conn.disconnect!
end
end
# Preconnect all connections in the pool. This saves pool users from
# having to wait for a connection to be established when first using it
# after checkout.
def preconnect
sequential_maintenance -> c { (!c.connected? || !c.verified?) && c.allow_preconnect } do |conn|
conn.connect!
rescue
# Wholesale rescue: there's nothing we can do but move on. The
# connection will go back to the pool, and the next consumer will
# presumably try to connect again -- which will either work, or
# fail and they'll be able to report the exception.
end
end
# Prod any connections that have been idle for longer than the configured
# keepalive time. This will incidentally verify the connection is still
# alive, but the main purpose is to show the server (and any intermediate
# network hops) that we're still here and using the connection.
def keep_alive(threshold = @keepalive)
return if threshold.nil?
sequential_maintenance -> c { (c.seconds_since_last_activity || 0) > c.pool_jitter(threshold) } do |conn|
# conn.active? will cause some amount of network activity, which is all
# we need to provide a keepalive signal.
#
# If it returns false, the connection is already broken; disconnect,
# so it can be found and repaired.
conn.disconnect! unless conn.active?
end
end
# Immediately mark all current connections as due for replacement,
# equivalent to them having reached +max_age+ -- even if there is
# no +max_age+ configured.
def recycle!
synchronize do
return if self.discarded?
@connections.each do |conn|
conn.force_retirement
end
end
retire_old_connections
end
def num_waiting_in_queue # :nodoc:

@@ -710,2 +879,6 @@ @available.num_waiting

def num_available_in_queue # :nodoc:
@available.size
end
# Returns the connection pool's usage statistic.

@@ -741,2 +914,12 @@ #

def pool_transaction_isolation_level
isolation_level_key = "activerecord_pool_transaction_isolation_level_#{db_config.name}"
ActiveSupport::IsolatedExecutionState[isolation_level_key]
end
def pool_transaction_isolation_level=(isolation_level)
isolation_level_key = "activerecord_pool_transaction_isolation_level_#{db_config.name}"
ActiveSupport::IsolatedExecutionState[isolation_level_key] = isolation_level
end
private

@@ -751,3 +934,5 @@ def connection_lease

if @db_config.max_threads > 0
name_with_shard = [name_inspect, shard_inspect].join("-").tr("_", "-")
Concurrent::ThreadPoolExecutor.new(
name: "ActiveRecord-#{name_with_shard}-async-query-executor",
min_threads: @db_config.min_threads,

@@ -764,7 +949,105 @@ max_threads: @db_config.max_threads,

# Perform maintenance work on pool connections. This method will
# select a connection to work on by calling the +candidate_selector+
# proc while holding the pool lock. If a connection is selected, it
# will be checked out for maintenance and passed to the
# +maintenance_work+ proc. The connection will always be returned to
# the pool after the proc completes.
#
# If the pool has async threads, all work will be scheduled there.
# Otherwise, this method will block until all work is complete.
#
# Each connection will only be processed once per call to this method,
# but (particularly in the async case) there is no protection against
# a second call to this method starting to work through the list
# before the first call has completed. (Though regular pool behaviour
# will prevent two instances from working on the same specific
# connection at the same time.)
def sequential_maintenance(candidate_selector, &maintenance_work)
# This hash doesn't need to be synchronized, because it's only
# used by one thread at a time: the +perform_work+ block gives
# up its right to +connections_visited+ when it schedules the
# next iteration.
connections_visited = Hash.new(false)
connections_visited.compare_by_identity
perform_work = lambda do
connection_to_maintain = nil
synchronize do
unless self.discarded?
if connection_to_maintain = @connections.select { |conn| !conn.in_use? }.select(&candidate_selector).sort_by(&:seconds_idle).find { |conn| !connections_visited[conn] }
checkout_for_maintenance connection_to_maintain
end
end
end
if connection_to_maintain
connections_visited[connection_to_maintain] = true
# If we're running async, we can schedule the next round of work
# as soon as we've grabbed a connection to work on.
@async_executor&.post(&perform_work)
begin
maintenance_work.call connection_to_maintain
ensure
return_from_maintenance connection_to_maintain
end
true
end
end
if @async_executor
@async_executor.post(&perform_work)
else
nil while perform_work.call
end
end
# Directly check a specific connection out of the pool. Skips callbacks.
#
# The connection must later either #return_from_maintenance or
# #remove_from_maintenance, or the pool will hang.
def checkout_for_maintenance(conn)
synchronize do
@maintaining += 1
@available.delete(conn)
conn.lease
conn
end
end
# Return a connection to the pool after it has been checked out for
# maintenance. Does not update the connection's idle time, and skips
# callbacks.
#--
# We assume that a connection that has required maintenance is less
# desirable (either it's been idle for a long time, or it was just
# created and hasn't been used yet). We'll put it at the back of the
# queue.
def return_from_maintenance(conn)
synchronize do
conn.expire(false)
@available.add_back(conn)
@maintaining -= 1
end
end
# Remove a connection from the pool after it has been checked out for
# maintenance. It will be automatically replaced with a new connection if
# necessary.
def remove_from_maintenance(conn)
synchronize do
@maintaining -= 1
remove conn
end
end
#--
# this is unfortunately not concurrent
def bulk_make_new_connections(num_new_conns_needed)
num_new_conns_needed.times do
# try_to_checkout_new_connection will not exceed pool's @size limit
# try_to_checkout_new_connection will not exceed pool's @max_connections limit
if new_conn = try_to_checkout_new_connection

@@ -782,5 +1065,7 @@ # make the new_conn available to the starving threads stuck @available Queue

def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true)
with_new_connections_blocked do
attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout)
yield
@reaper_lock.synchronize do
with_new_connections_blocked do
attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout)
yield
end
end

@@ -905,3 +1190,3 @@ end

# of the said methods and avoid an additional +synchronize+ overhead.
if conn = @available.poll || try_to_checkout_new_connection
if conn = @available.poll || try_to_queue_for_background_connection(checkout_timeout) || try_to_checkout_new_connection
conn

@@ -912,3 +1197,3 @@ else

# remove an inactive connection, or both
if conn = @available.poll || try_to_checkout_new_connection
if conn = @available.poll || try_to_queue_for_background_connection(checkout_timeout) || try_to_checkout_new_connection
conn

@@ -924,2 +1209,27 @@ else

#--
# If new connections are already being established in the background,
# and there are fewer threads already waiting than the number of
# upcoming connections, we can just get in queue and wait to be handed a
# connection. This avoids us overshooting the required connection count
# by starting a new connection ourselves, and is likely to be faster
# too (because at least some of the time it takes to establish a new
# connection must have already passed).
#
# If background connections are available, this method will block and
# return a connection. If no background connections are available, it
# will immediately return +nil+.
def try_to_queue_for_background_connection(checkout_timeout)
return unless @maintaining > 0
synchronize do
return unless @maintaining > @available.num_waiting
# We are guaranteed the "maintaining" thread will return its promised
# connection within one maintenance-unit of time. Thus we can safely
# do a blocking wait with (functionally) no timeout.
@available.poll(100)
end
end
#--
# if owner_thread param is omitted, this must be called in synchronize block

@@ -933,4 +1243,7 @@ def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)

# If the pool is not at a <tt>@size</tt> limit, establish new connection. Connecting
# If the pool is not at a <tt>@max_connections</tt> limit, establish new connection. Connecting
# to the DB is done outside main synchronized section.
#
# If a block is supplied, it is an additional constraint (checked while holding the
# pool lock) on whether a new connection should be established.
#--

@@ -941,6 +1254,12 @@ # Implementation constraint: a newly established connection returned by this

# first in synchronized section check if establishing new conns is allowed
# and increment @now_connecting, to prevent overstepping this pool's @size
# and increment @now_connecting, to prevent overstepping this pool's @max_connections
# constraint
do_checkout = synchronize do
if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size
return if self.discarded?
if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @max_connections && (!block_given? || yield)
if @connections.size > 0 || @original_context != ActiveSupport::IsolatedExecutionState.context
@activated = true
end
@now_connecting += 1

@@ -956,8 +1275,12 @@ end

synchronize do
@now_connecting -= 1
if conn
adopt_connection(conn)
# returned conn needs to be already leased
conn.lease
if self.discarded?
conn.discard!
else
adopt_connection(conn)
# returned conn needs to be already leased
conn.lease
end
end
@now_connecting -= 1
end

@@ -994,4 +1317,12 @@ end

end
def name_inspect
db_config.name.inspect unless db_config.name == "primary"
end
def shard_inspect
shard.inspect unless shard == :default
end
end
end
end

@@ -43,2 +43,10 @@ # frozen_string_literal: true

# Add +element+ to the back of the queue. Never blocks.
def add_back(element)
synchronize do
@queue.unshift element
@cond.signal
end
end
# If +element+ is in the queue, remove and return it, or +nil+.

@@ -58,2 +66,9 @@ def delete(element)

# Number of elements in the queue.
def size
synchronize do
@queue.size
end
end
# Remove the head of the queue.

@@ -60,0 +75,0 @@ #

@@ -10,8 +10,25 @@ # frozen_string_literal: true

#
# Every +frequency+ seconds, the reaper will call +reap+ and +flush+ on
# +pool+. A reaper instantiated with a zero frequency will never reap
# the connection pool.
# The reaper is a singleton that exists in the background of the process
# and is responsible for general maintenance of all the connection pools.
#
# Configure the frequency by setting +reaping_frequency+ in your database
# YAML file (default 60 seconds).
# It will reclaim connections that are leased to now-dead threads,
# ensuring that a bad thread can't leak a pool slot forever. By definition,
# this involves touching currently-leased connections, but that is safe
# because the owning thread is known to be dead.
#
# Beyond that, it manages the health of available / unleased connections:
# * retiring connections that have been idle[1] for too long
# * creating occasional activity on inactive[1] connections
# * keeping the pool prepopulated up to its minimum size
# * proactively connecting to the target database from any pooled
# connections that had lazily deferred that step
# * resetting or replacing connections that are known to be broken
#
#
# [1]: "idle" and "inactive" here distinguish between connections that
# have not been requested by the application in a while (idle) and those
# that have not spoken to their remote server in a while (inactive). The
# former is a desirable opportunity to reduce our connection count
# (`idle_timeout`); the latter is a risk that the server or a firewall may
# drop a connection we still anticipate using (avoided by `keepalive`).
class Reaper

@@ -40,2 +57,11 @@ attr_reader :pool, :frequency

def pools(refs = nil) # :nodoc:
refs ||= @mutex.synchronize { @pools.values.flatten(1) }
refs.filter_map do |ref|
ref.__getobj__ if ref.weakref_alive?
rescue WeakRef::RefError
end.select(&:maintainable?)
end
private

@@ -51,14 +77,14 @@ def spawn_thread(frequency)

sleep t
refs = nil
@mutex.synchronize do
@pools[frequency].select! do |pool|
refs = @pools[frequency]
refs.select! do |pool|
pool.weakref_alive? && !pool.discarded?
end
@pools[frequency].each do |p|
p.reap
p.flush
rescue WeakRef::RefError
end
if @pools[frequency].empty?
if refs.empty?
@pools.delete(frequency)

@@ -69,2 +95,15 @@ @threads.delete(frequency)

end
if running
pools(refs).each do |pool|
pool.reaper_lock do
pool.reap
pool.flush
pool.prepopulate
pool.retire_old_connections
pool.keep_alive
pool.preconnect
end
end
end
end

@@ -71,0 +110,0 @@ end

@@ -58,8 +58,11 @@ # frozen_string_literal: true

if prepared_statements
collector = collector()
collector.retryable = true
sql, binds = visitor.compile(arel.ast, collector)
query = klass.query(sql)
query = klass.query(sql, retryable: collector.retryable)
else
collector = klass.partial_query_collector
collector.retryable = true
parts, binds = visitor.compile(arel.ast, collector)
query = klass.partial_query(parts)
query = klass.partial_query(parts, retryable: collector.retryable)
end

@@ -114,4 +117,4 @@ [query, binds]

def query(...) # :nodoc:
internal_exec_query(...).rows
def query(sql, name = nil, allow_retry: true, materialize_transactions: true) # :nodoc:
internal_exec_query(sql, name, allow_retry:, materialize_transactions:).rows
end

@@ -356,3 +359,3 @@

if !requires_new && current_transaction.joinable?
if isolation
if isolation && current_transaction.isolation != isolation
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"

@@ -505,16 +508,2 @@ end

# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
#
# The +limit+ may be anything that can evaluate to a string via #to_s. It
# should look like an integer, or an Arel SQL literal.
#
# Returns Integer and Arel::Nodes::SqlLiteral limits as is.
def sanitize_limit(limit)
if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral)
limit
else
Integer(limit)
end
end
# Fixture value is quoted by Arel, however scalar values

@@ -554,9 +543,20 @@ # are not quotable. In this case we want to convert

def default_insert_value(column) # :nodoc:
DEFAULT_INSERT_VALUE
end
private
DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
private_constant :DEFAULT_INSERT_VALUE
# Lowest level way to execute a query. Doesn't check for illegal writes, doesn't annotate queries, yields a native result object.
def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, batch: false)
type_casted_binds = type_casted_binds(binds)
log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
log(sql, name, binds, type_casted_binds, async: async, allow_retry: allow_retry) do |notification_payload|
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch)
result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch)
end
handle_warnings(result, sql)
result
end

@@ -570,2 +570,5 @@ end

def handle_warnings(raw_result, sql)
end
# Receive a native adapter result object and returns an ActiveRecord::Result object.

@@ -606,9 +609,2 @@ def cast_result(raw_result)

DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
private_constant :DEFAULT_INSERT_VALUE
def default_insert_value(column)
DEFAULT_INSERT_VALUE
end
def build_fixture_sql(fixtures, table_name)

@@ -627,4 +623,4 @@ columns = schema_cache.columns_hash(table_name).reject { |_, column| supports_virtual_columns? && column.virtual? }

if fixture.key?(name)
type = lookup_cast_type_from_column(column)
with_yaml_fallback(type.serialize(fixture[name]))
# TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
with_yaml_fallback(column.fetch_cast_type(self).serialize(fixture[name]))
else

@@ -749,3 +745,3 @@ default_insert_value(column)

def extract_table_ref_from_insert_sql(sql)
if sql =~ /into\s("[-A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im
if sql =~ /into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im
$1.delete('"').strip

@@ -752,0 +748,0 @@ end

@@ -34,2 +34,14 @@ # frozen_string_literal: true

# This is the actual query cache store.
#
# It has an internal hash whose keys are either SQL strings, or arrays of
# two elements [SQL string, binds], if there are binds. The hash values
# are their corresponding ActiveRecord::Result objects.
#
# Keeping the hash size under max size is achieved with LRU eviction.
#
# The store gets passed a version object, which is shared among the query
# cache stores of a given connection pool (see ConnectionPoolConfiguration
# down below). The version value may be externally changed as a way to
# signal cache invalidation, that is why all methods have a guard for it.
class Store # :nodoc:

@@ -98,2 +110,8 @@ attr_accessor :enabled, :dirties

# Each connection pool has one of these registries. They map execution
# contexts to query cache stores.
#
# The keys of the internal map are threads or fibers (whatever
# ActiveSupport::IsolatedExecutionState.context returns), and their
# associated values are their respective query cache stores.
class QueryCacheRegistry # :nodoc:

@@ -196,2 +214,4 @@ def initialize

attr_accessor :query_cache
def initialize(*)

@@ -202,17 +222,4 @@ super

attr_writer :query_cache
def query_cache
if @pinned && @owner != ActiveSupport::IsolatedExecutionState.context
# With transactional tests, if the connection is pinned, any thread
# other than the one that pinned the connection need to go through the
# query cache pool, so each thread get a different cache.
pool.query_cache
else
@query_cache
end
end
def query_cache_enabled
query_cache&.enabled?
@query_cache&.enabled?
end

@@ -256,4 +263,4 @@

# Such queries should not be cached.
if query_cache_enabled && !(arel.respond_to?(:locked) && arel.locked)
sql, binds, preparable, allow_retry = to_sql_and_binds(arel, binds, preparable)
if @query_cache&.enabled? && !(arel.respond_to?(:locked) && arel.locked)
sql, binds, preparable, allow_retry = to_sql_and_binds(arel, binds, preparable, allow_retry)

@@ -281,3 +288,3 @@ if async

@lock.synchronize do
result = query_cache[key]
result = @query_cache[key]
end

@@ -301,3 +308,3 @@

@lock.synchronize do
result = query_cache.compute_if_absent(key) do
result = @query_cache.compute_if_absent(key) do
hit = false

@@ -304,0 +311,0 @@ yield

# frozen_string_literal: true
require "active_support/core_ext/big_decimal/conversions"
require "active_support/multibyte/chars"

@@ -87,3 +86,4 @@ module ActiveRecord

when Class then "'#{value}'"
else raise TypeError, "can't quote #{value.class.name}"
else
raise TypeError, "can't quote #{value.class.name}"
end

@@ -97,3 +97,3 @@ end

case value
when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
when Symbol, Type::Binary::Data, ActiveSupport::Multibyte::Chars
value.to_s

@@ -107,3 +107,4 @@ when true then unquoted_true

when Date, Time then quoted_date(value)
else raise TypeError, "can't cast #{value.class.name}"
else
raise TypeError, "can't cast #{value.class.name}"
end

@@ -119,15 +120,2 @@ end

# If you are having to call this function, you are likely doing something
# wrong. The column does not have sufficient type information if the user
# provided a custom type on the class level either explicitly (via
# Attributes::ClassMethods#attribute) or implicitly (via
# AttributeMethods::Serialization::ClassMethods#serialize, +time_zone_aware_attributes+).
# In almost all cases, the sql type should only be used to change quoting behavior, when the primitive to
# represent the type doesn't sufficiently reflect the differences
# (varchar vs binary) for example. The type used to get this primitive
# should have been provided before reaching the connection adapter.
def lookup_cast_type_from_column(column) # :nodoc:
lookup_cast_type(column.sql_type)
end
# Quotes a string, escaping any ' (single quote) and \ (backslash)

@@ -165,3 +153,5 @@ # characters.

else
value = lookup_cast_type(column.sql_type).serialize(value)
# TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
cast_type = column.fetch_cast_type(self)
value = cast_type.serialize(value)
quote(value)

@@ -216,6 +206,6 @@ end

def sanitize_as_sql_comment(value) # :nodoc:
# Sanitize a string to appear within a SQL comment
# Sanitize a string to appear within an SQL comment
# For compatibility, this also surrounding "/*+", "/*", and "*/"
# charcacters, possibly with single surrounding space.
# Then follows that by replacing any internal "*/" or "/ *" with
# Then follows that by replacing any internal "*/" or "/*" with
# "* /" or "/ *"

@@ -229,2 +219,7 @@ comment = value.to_s.dup

def lookup_cast_type(sql_type) # :nodoc:
# TODO: Make this method private after we release 8.1.
type_map.lookup(sql_type)
end
private

@@ -240,8 +235,4 @@ def type_casted_binds(binds)

end
def lookup_cast_type(sql_type)
type_map.lookup(sql_type)
end
end
end
end

@@ -20,3 +20,3 @@ # frozen_string_literal: true

:supports_index_include?, :supports_exclusion_constraints?, :supports_unique_constraints?,
:supports_nulls_not_distinct?,
:supports_nulls_not_distinct?, :lookup_cast_type,
to: :@conn, private: true

@@ -152,3 +152,3 @@

def add_column_options!(sql, options)
sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
sql << " DEFAULT #{quote_default_expression_for_column_definition(options[:default], options[:column])}" if options_include_default?(options)
# must explicitly check for :null to allow change_column to work on migrations

@@ -167,2 +167,7 @@ if options[:null] == false

def quote_default_expression_for_column_definition(default, column_definition)
column_definition.cast_type = lookup_cast_type(column_definition.sql_type)
quote_default_expression(default, column_definition)
end
def to_sql(sql)

@@ -169,0 +174,0 @@ sql = sql.to_sql if sql.respond_to?(:to_sql)

@@ -78,3 +78,3 @@ # frozen_string_literal: true

# for generating a number of table creation or table changing SQL statements.
ColumnDefinition = Struct.new(:name, :type, :options, :sql_type) do # :nodoc:
ColumnDefinition = Struct.new(:name, :type, :options, :sql_type, :cast_type) do # :nodoc:
self::OPTION_NAMES = [

@@ -112,2 +112,6 @@ :limit,

end
def fetch_cast_type(connection)
cast_type
end
end

@@ -308,31 +312,7 @@

# 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
included do
define_column_methods :bigint, :binary, :boolean, :date, :datetime, :decimal,
:float, :integer, :json, :string, :text, :time, :timestamp, :virtual
alias :blob :binary
alias :numeric :decimal
end
class_methods do
def define_column_methods(*column_types) # :nodoc:
column_types.each do |column_type|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
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)

@@ -342,7 +322,19 @@ raise ArgumentError, "Missing column name(s) for #{column_type}" if names.empty?

end
RUBY
RUBY
end
end
end
private :define_column_methods
end
extend ClassMethods
# 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, primary_key: true)
end
define_column_methods :bigint, :binary, :boolean, :date, :datetime, :decimal,
:float, :integer, :json, :string, :text, :time, :timestamp, :virtual
alias :blob :binary
alias :numeric :decimal
end

@@ -358,3 +350,3 @@

#
# class SomeMigration < ActiveRecord::Migration[8.0]
# class SomeMigration < ActiveRecord::Migration[8.1]
# def up

@@ -773,3 +765,3 @@ # create_table :foo do |t|

# See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?]
def index_exists?(column_name, **options)
def index_exists?(column_name = nil, **options)
@base.index_exists?(name, column_name, **options)

@@ -776,0 +768,0 @@ end

@@ -88,3 +88,4 @@ # frozen_string_literal: true

return unless column.has_default?
type = @connection.lookup_cast_type_from_column(column)
# TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
type = column.fetch_cast_type(@connection)
default = type.deserialize(column.default)

@@ -91,0 +92,0 @@ if default.nil?

@@ -115,2 +115,3 @@ # frozen_string_literal: true

def joinable?; false; end
def isolation; nil; end
def add_record(record, _ = true); end

@@ -154,2 +155,7 @@ def restartable?; false; end

# Returns the isolation level if it was explicitly set, nil otherwise
def isolation
@isolation_level
end
def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)

@@ -180,7 +186,7 @@ super()

def open?
true
!closed?
end
def closed?
false
@state.finalized?
end

@@ -392,3 +398,3 @@

delegate :materialize!, :materialized?, :restart, to: :@parent
delegate :materialize!, :materialized?, :restart, :isolation, to: :@parent

@@ -412,2 +418,3 @@ def rollback

@parent_transaction = parent_transaction
parent_transaction.state.add_child(@state)

@@ -422,2 +429,7 @@

# Delegates to parent transaction's isolation level
def isolation
@parent_transaction.isolation
end
def materialize!

@@ -629,2 +641,3 @@ connection.create_savepoint(savepoint_name)

def within_new_transaction(isolation: nil, joinable: true)
isolation ||= @connection.pool.pool_transaction_isolation_level
@connection.lock.synchronize do

@@ -631,0 +644,0 @@ transaction = begin_transaction(isolation: isolation, joinable: joinable)

@@ -20,7 +20,8 @@ # frozen_string_literal: true

# +null+ determines if this column allows +NULL+ values.
def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **)
def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **)
@name = name.freeze
@cast_type = cast_type
@sql_type_metadata = sql_type_metadata
@null = null
@default = default
@default = default.nil? || cast_type.mutable? ? default : cast_type.deserialize(default)
@default_function = default_function

@@ -31,2 +32,7 @@ @collation = collation

def fetch_cast_type(connection) # :nodoc:
# TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
@cast_type || connection.lookup_cast_type(sql_type)
end
def has_default?

@@ -50,2 +56,3 @@ !default.nil? || default_function

@name = coder["name"]
@cast_type = coder["cast_type"]
@sql_type_metadata = coder["sql_type_metadata"]

@@ -61,2 +68,3 @@ @null = coder["null"]

coder["name"] = @name
coder["cast_type"] = @cast_type
coder["sql_type_metadata"] = @sql_type_metadata

@@ -82,2 +90,3 @@ coder["null"] = @null

name == other.name &&
cast_type == other.cast_type &&
default == other.default &&

@@ -96,2 +105,3 @@ sql_type_metadata == other.sql_type_metadata &&

name.encoding.hash ^
cast_type.hash ^
default.hash ^

@@ -109,2 +119,5 @@ sql_type_metadata.hash ^

protected
attr_reader :cast_type
private

@@ -114,3 +127,3 @@ def deduplicated

@sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
@default = -default if default
@default = -default if String === default
@default_function = -default_function if default_function

@@ -125,3 +138,3 @@ @collation = -collation if collation

def initialize(name, **)
super(name, nil)
super(name, nil, nil)
end

@@ -128,0 +141,0 @@ end

@@ -48,2 +48,6 @@ # frozen_string_literal: true

def default_insert_value(column) # :nodoc:
super unless column.auto_increment?
end
private

@@ -55,6 +59,2 @@ # https://mariadb.com/kb/en/analyze-statement/

def default_insert_value(column)
super unless column.auto_increment?
end
def returning_column_values(result)

@@ -61,0 +61,0 @@ if supports_insert_returning?

@@ -52,2 +52,4 @@ # frozen_string_literal: true

sql << "(#{quoted_columns(o)})"
sql << "INVISIBLE" if o.disabled? && !mariadb?
sql << "IGNORED" if o.disabled? && mariadb?

@@ -54,0 +56,0 @@ add_sql_comment!(sql.join(" "), o.comment)

@@ -8,2 +8,3 @@ # frozen_string_literal: true

extend ActiveSupport::Concern
extend ConnectionAdapters::ColumnMethods::ClassMethods

@@ -46,9 +47,23 @@ ##

included do
define_column_methods :blob, :tinyblob, :mediumblob, :longblob,
:tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint,
:unsigned_float, :unsigned_decimal
define_column_methods :blob, :tinyblob, :mediumblob, :longblob,
:tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint
end
deprecate :unsigned_float, :unsigned_decimal, deprecator: ActiveRecord.deprecator
# = Active Record MySQL Adapter \Index Definition
class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition
attr_accessor :enabled
def initialize(*args, **kwargs)
@enabled = kwargs.key?(:enabled) ? kwargs.delete(:enabled) : true
super
end
def defined_for?(columns = nil, name: nil, unique: nil, valid: nil, include: nil, nulls_not_distinct: nil, enabled: nil, **options)
super(columns, name:, unique:, valid:, include:, nulls_not_distinct:, **options) &&
(enabled.nil? || self.enabled == enabled)
end
def disabled?
!@enabled
end
end

@@ -105,2 +120,24 @@

include ColumnMethods
# Enables an index to be used by query optimizers.
#
# t.enable_index(:email)
#
# Note: only supported by MySQL version 8.0.0 and greater, and MariaDB version 10.6.0 and greater.
#
# See {connection.enable_index}[rdoc-ref:SchemaStatements#enable_index]
def enable_index(index_name)
@base.enable_index(name, index_name)
end
# Disables an index not to be used by query optimizers.
#
# t.disable_index(:email)
#
# Note: only supported by MySQL version 8.0.0 and greater, and MariaDB version 10.6.0 and greater.
#
# See {connection.disable_index}[rdoc-ref:SchemaStatements#disable_index]
def disable_index(index_name)
@base.disable_index(name, index_name)
end
end

@@ -107,0 +144,0 @@ end

@@ -24,3 +24,3 @@ # frozen_string_literal: true

indexes << [
index = [
row["Table"],

@@ -34,4 +34,10 @@ row["Key_name"],

using: index_using,
comment: row["Index_comment"].presence
comment: row["Index_comment"].presence,
]
if supports_disabling_indexes?
index[-1][:enabled] = mariadb? ? row["Ignored"] == "NO" : row["Visible"] == "YES"
end
indexes << index
end

@@ -68,4 +74,3 @@

end
IndexDefinition.new(*index, **options)
MySQL::IndexDefinition.new(*index, **options)
end

@@ -80,2 +85,12 @@ rescue StatementInvalid => e

def create_index_definition(table_name, name, unique, columns, **options)
MySQL::IndexDefinition.new(table_name, name, unique, columns, **options)
end
def add_index_options(table_name, column_name, name: nil, if_not_exists: false, internal: false, **options) # :nodoc:
index, algorithm, if_not_exists = super
index.enabled = options[:enabled] unless options[:enabled].nil?
[index, algorithm, if_not_exists]
end
def remove_column(table_name, column_name, type = nil, **options)

@@ -216,2 +231,3 @@ if foreign_key_exists?(table_name, column: column_name)

field["Field"],
lookup_cast_type(type_metadata.sql_type),
default,

@@ -241,2 +257,8 @@ type_metadata,

def valid_index_options
index_options = super
index_options << :enabled if supports_disabling_indexes?
index_options
end
def add_options_for_index_columns(quoted_columns, **options)

@@ -243,0 +265,0 @@ quoted_columns = add_index_length(quoted_columns, **options)

@@ -16,2 +16,3 @@ # frozen_string_literal: true

ER_ACCESS_DENIED_ERROR = 1045
ER_UNKNOWN_STMT_HANDLER = 1243
ER_CONN_HOST_ERROR = 2003

@@ -18,0 +19,0 @@ ER_UNKNOWN_HOST_ERROR = 2005

@@ -53,20 +53,28 @@ # frozen_string_literal: true

if binds.nil? || binds.empty?
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
result = raw_connection.query(sql)
# Ref: https://github.com/brianmario/mysql2/pull/1383
# As of mysql2 0.5.6 `#affected_rows` might raise Mysql2::Error if a prepared statement
# from that same connection was GCed while `#query` released the GVL.
# By avoiding to call `#affected_rows` when we have a result, we reduce the likeliness
# of hitting the bug.
@affected_rows_before_warnings = result&.size || raw_connection.affected_rows
end
result = raw_connection.query(sql)
# Ref: https://github.com/brianmario/mysql2/pull/1383
# As of mysql2 0.5.6 `#affected_rows` might raise Mysql2::Error if a prepared statement
# from that same connection was GCed while `#query` released the GVL.
# By avoiding to call `#affected_rows` when we have a result, we reduce the likeliness
# of hitting the bug.
@affected_rows_before_warnings = result&.size || raw_connection.affected_rows
elsif prepare
stmt = @statements[sql] ||= raw_connection.prepare(sql)
retry_count = 1
begin
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
result = stmt.execute(*type_casted_binds)
@affected_rows_before_warnings = stmt.affected_rows
stmt = @statements[sql] ||= raw_connection.prepare(sql)
result = stmt.execute(*type_casted_binds)
@affected_rows_before_warnings = stmt.affected_rows
rescue ::Mysql2::Error => error
@statements.delete(sql)
# Sometimes for an unknown reason, we get that error.
# It suggest somehow that the prepared statement was deallocated
# but the client doesn't know it.
# But we know that this error is safe to retry, so we do so after
# getting rid of the originally cached statement.
if error.error_number == Mysql2Adapter::ER_UNKNOWN_STMT_HANDLER
if retry_count.positive?
retry_count -= 1
retry
end
end
rescue ::Mysql2::Error
@statements.delete(sql)
raise

@@ -78,6 +86,4 @@ end

begin
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
result = stmt.execute(*type_casted_binds)
@affected_rows_before_warnings = stmt.affected_rows
end
result = stmt.execute(*type_casted_binds)
@affected_rows_before_warnings = stmt.affected_rows

@@ -105,3 +111,2 @@ # Ref: https://github.com/brianmario/mysql2/pull/1383

verified!
handle_warnings(sql)
result

@@ -115,3 +120,3 @@ ensure

def cast_result(raw_result)
return ActiveRecord::Result.empty if raw_result.nil?
return ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings) if raw_result.nil?

@@ -121,3 +126,3 @@ fields = raw_result.fields

result = if fields.empty?
ActiveRecord::Result.empty
ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings)
else

@@ -124,0 +129,0 @@ ActiveRecord::Result.new(fields, raw_result.to_a)

@@ -384,7 +384,2 @@ # frozen_string_literal: true

def clear_cache!(new_connection: false)
super
@schema_search_path = nil if new_connection
end
# Disconnects from the database if already connected. Otherwise, this

@@ -406,6 +401,2 @@ # method does nothing.

def native_database_types # :nodoc:
self.class.native_database_types
end
def self.native_database_types # :nodoc:

@@ -646,3 +637,3 @@ @native_database_types ||= begin

if version == 0
raise ActiveRecord::ConnectionFailed, "Could not determine PostgreSQL version"
raise ActiveRecord::ConnectionNotEstablished, "Could not determine PostgreSQL version"
end

@@ -681,2 +672,5 @@ version

end
if database_version >= 18_00_00 && Gem::Version.new(PG::VERSION) < Gem::Version.new("1.6.0")
warn "pg gem version #{PG::VERSION} is known to be incompatible with PostgreSQL 18+. Please upgrade to pg 1.6.0 or later."
end
end

@@ -804,2 +798,4 @@

UNIQUE_VIOLATION = "23505"
CHECK_VIOLATION = "23514"
EXCLUSION_VIOLATION = "23P01"
SERIALIZATION_FAILURE = "40001"

@@ -836,2 +832,6 @@ DEADLOCK_DETECTED = "40P01"

InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
when CHECK_VIOLATION
CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
when EXCLUSION_VIOLATION
ExclusionViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
when VALUE_LIMIT_VIOLATION

@@ -1007,4 +1007,2 @@ ValueTooLong.new(message, sql: sql, binds: binds, connection_pool: @pool)

schema_search_path # populate cache
reload_type_map

@@ -1011,0 +1009,0 @@ end

@@ -68,4 +68,3 @@ # frozen_string_literal: true

identity? == other.identity? &&
serial? == other.serial? &&
virtual? == other.virtual?
serial? == other.serial?
end

@@ -78,4 +77,3 @@ alias :eql? :==

identity?.hash ^
serial?.hash ^
virtual?.hash
serial?.hash
end

@@ -82,0 +80,0 @@ end

@@ -14,4 +14,4 @@ # frozen_string_literal: true

# Queries the database and returns the results in an Array-like object
def query(sql, name = nil) # :nodoc:
result = internal_execute(sql, name)
def query(sql, name = nil, allow_retry: true, materialize_transactions: true) # :nodoc:
result = internal_execute(sql, name, allow_retry:, materialize_transactions:)
result.map_types!(@type_map_for_results).values

@@ -131,10 +131,3 @@ end

# Skip @raw_connection.cancel (PG::Connection#cancel) when using libpq >= 18 with pg < 1.6.0,
# because the pg gem cannot obtain the backend_key in that case.
# This method is only called from exec_rollback_db_transaction and exec_restart_db_transaction.
# Even without cancel, rollback will still run. However, since any running
# query must finish first, the rollback may take longer.
if !(PG.library_version >= 18_00_00 && Gem::Version.new(PG::VERSION) < Gem::Version.new("1.6.0"))
@raw_connection.cancel
end
@raw_connection.cancel
@raw_connection.block

@@ -175,3 +168,4 @@ rescue PG::Error

verified!
handle_warnings(result)
notification_payload[:affected_rows] = result.cmd_tuples
notification_payload[:row_count] = result.ntuples

@@ -182,15 +176,16 @@ result

def cast_result(result)
if result.fields.empty?
result.clear
return ActiveRecord::Result.empty
ar_result = if result.fields.empty?
ActiveRecord::Result.empty(affected_rows: result.cmd_tuples)
else
fields = result.fields
types = Array.new(fields.size)
fields.size.times do |index|
ftype = result.ftype(index)
fmod = result.fmod(index)
types[index] = get_oid_type(ftype, fmod, fields[index])
end
ActiveRecord::Result.new(fields, result.values, types.freeze, affected_rows: result.cmd_tuples)
end
types = {}
fields = result.fields
fields.each_with_index do |fname, i|
ftype = result.ftype i
fmod = result.fmod i
types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
end
ar_result = ActiveRecord::Result.new(fields, result.values, types.freeze)
result.clear

@@ -227,3 +222,3 @@ ar_result

def handle_warnings(sql)
def handle_warnings(result, sql)
@notice_receiver_sql_warnings.each do |warning|

@@ -230,0 +225,0 @@ next if warning_ignored?(warning)

@@ -19,4 +19,4 @@ # frozen_string_literal: true

@pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter
@pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter
@pg_encoder = PG::TextEncoder::Array.new(name: "#{type}[]".freeze, delimiter: delimiter).freeze
@pg_decoder = PG::TextDecoder::Array.new(name: "#{type}[]".freeze, delimiter: delimiter).freeze
end

@@ -23,0 +23,0 @@

@@ -71,3 +71,3 @@ # frozen_string_literal: true

register_with_subtype(row["oid"], row["typelem"].to_i) do |subtype|
OID::Array.new(subtype, row["typdelim"])
OID::Array.new(subtype, row["typdelim"].freeze)
end

@@ -74,0 +74,0 @@ end

@@ -156,2 +156,3 @@ # frozen_string_literal: true

# `column` may be either an instance of Column or ColumnDefinition.
def quote_default_expression(value, column) # :nodoc:

@@ -163,4 +164,4 @@ if value.is_a?(Proc)

elsif column.respond_to?(:array?)
type = lookup_cast_type_from_column(column)
quote(type.serialize(value))
# TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
quote(column.fetch_cast_type(self).serialize(value))
else

@@ -191,12 +192,8 @@ super

def lookup_cast_type_from_column(column) # :nodoc:
verify! if type_map.nil?
type_map.lookup(column.oid, column.fmod, column.sql_type)
# TODO: Make this method private after we release 8.1.
def lookup_cast_type(sql_type) # :nodoc:
super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
end
private
def lookup_cast_type(sql_type)
super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
end
def encode_array(array_data)

@@ -214,3 +211,13 @@ encoder = array_data.encoder

def encode_range(range)
"[#{type_cast_range_value(range.begin)},#{type_cast_range_value(range.end)}#{range.exclude_end? ? ')' : ']'}"
lower_bound = type_cast_range_value(range.begin)
upper_bound = if date_or_time_range?(range)
# Postgres will convert `[today,]` to `[today,)`, making it exclusive.
# We can use the special timestamp value `infinity` to force inclusion.
# https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-INFINITE
range.end.nil? ? "infinity" : type_cast(range.end)
else
type_cast_range_value(range.end)
end
"[#{lower_bound},#{upper_bound}#{range.exclude_end? ? ')' : ']'}"
end

@@ -239,2 +246,6 @@

end
def date_or_time_range?(range)
[range.begin.class, range.end.class].intersect?([Date, DateTime, Time])
end
end

@@ -241,0 +252,0 @@ end

@@ -102,3 +102,3 @@ # frozen_string_literal: true

else
quoted_default = quote_default_expression(options[:default], column)
quoted_default = quote_default_expression_for_column_definition(options[:default], column)
change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}"

@@ -105,0 +105,0 @@ end

@@ -8,2 +8,3 @@ # frozen_string_literal: true

extend ActiveSupport::Concern
extend ConnectionAdapters::ColumnMethods::ClassMethods

@@ -19,19 +20,7 @@ # Defines the primary key field.

#
# By default, this will use the <tt>gen_random_uuid()</tt> function from the
# +pgcrypto+ extension. As that extension is only available in
# PostgreSQL 9.4+, for earlier versions an explicit default can be set
# to use <tt>uuid_generate_v4()</tt> from the +uuid-ossp+ extension instead:
# By default, this will use the <tt>gen_random_uuid()</tt> function.
#
# create_table :stuffs, id: false do |t|
# t.primary_key :id, :uuid, default: "uuid_generate_v4()"
# t.uuid :foo_id
# t.timestamps
# end
# To use a UUID primary key without any defaults, set the +:default+
# option to +nil+:
#
# To enable the appropriate extension, which is a requirement, use
# the +enable_extension+ method in your migrations.
#
# To use a UUID primary key without any of the extensions, set the
# +:default+ option to +nil+:
#
# create_table :stuffs, id: false do |t|

@@ -186,8 +175,6 @@ # t.primary_key :id, :uuid, default: nil

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, :timestamptz, :enum
end
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, :timestamptz, :enum
end

@@ -194,0 +181,0 @@

@@ -8,2 +8,19 @@ # frozen_string_literal: true

private
attr_accessor :schema_name
def initialize(connection, options = {})
super
@dump_schemas =
case ActiveRecord.dump_schemas
when :schema_search_path
connection.current_schemas
when String
schema_names = ActiveRecord.dump_schemas.split(",").map(&:strip)
schema_names & connection.schema_names
else
connection.schema_names
end
end
def extensions(stream)

@@ -21,10 +38,11 @@ extensions = @connection.extensions

def types(stream)
types = @connection.enum_types
if types.any?
stream.puts " # Custom types defined in this database."
stream.puts " # Note that some types may not work with other database engines. Be careful if changing database."
types.sort.each do |name, values|
stream.puts " create_enum #{name.inspect}, #{values.inspect}"
within_each_schema do
types = @connection.enum_types
if types.any?
stream.puts " # Custom types defined in this database."
stream.puts " # Note that some types may not work with other database engines. Be careful if changing database."
types.sort.each do |name, values|
stream.puts " create_enum #{relation_name(name).inspect}, #{values.inspect}"
end
end
stream.puts
end

@@ -34,3 +52,3 @@ end

def schemas(stream)
schema_names = @connection.schema_names - ["public"]
schema_names = @dump_schemas - ["public"]

@@ -45,21 +63,24 @@ if schema_names.any?

def tables(stream)
previous_schema_had_tables = false
within_each_schema do
stream.puts if previous_schema_had_tables
super
previous_schema_had_tables = @connection.tables.any?
end
end
def exclusion_constraints_in_create(table, stream)
if (exclusion_constraints = @connection.exclusion_constraints(table)).any?
add_exclusion_constraint_statements = exclusion_constraints.map do |exclusion_constraint|
parts = [
"t.exclusion_constraint #{exclusion_constraint.expression.inspect}"
]
exclusion_constraint_statements = exclusion_constraints.map do |exclusion_constraint|
parts = [ exclusion_constraint.expression.inspect ]
parts << "where: #{exclusion_constraint.where.inspect}" if exclusion_constraint.where
parts << "using: #{exclusion_constraint.using.inspect}" if exclusion_constraint.using
parts << "deferrable: #{exclusion_constraint.deferrable.inspect}" if exclusion_constraint.deferrable
parts << "name: #{exclusion_constraint.name.inspect}" if exclusion_constraint.export_name_on_schema_dump?
if exclusion_constraint.export_name_on_schema_dump?
parts << "name: #{exclusion_constraint.name.inspect}"
end
" #{parts.join(', ')}"
" t.exclusion_constraint #{parts.join(', ')}"
end
stream.puts add_exclusion_constraint_statements.sort.join("\n")
stream.puts exclusion_constraint_statements.sort.join("\n")
end

@@ -70,18 +91,12 @@ end

if (unique_constraints = @connection.unique_constraints(table)).any?
add_unique_constraint_statements = unique_constraints.map do |unique_constraint|
parts = [
"t.unique_constraint #{unique_constraint.column.inspect}"
]
unique_constraint_statements = unique_constraints.map do |unique_constraint|
parts = [ unique_constraint.column.inspect ]
parts << "nulls_not_distinct: #{unique_constraint.nulls_not_distinct.inspect}" if unique_constraint.nulls_not_distinct
parts << "deferrable: #{unique_constraint.deferrable.inspect}" if unique_constraint.deferrable
parts << "name: #{unique_constraint.name.inspect}" if unique_constraint.export_name_on_schema_dump?
if unique_constraint.export_name_on_schema_dump?
parts << "name: #{unique_constraint.name.inspect}"
end
" #{parts.join(', ')}"
" t.unique_constraint #{parts.join(', ')}"
end
stream.puts add_unique_constraint_statements.sort.join("\n")
stream.puts unique_constraint_statements.sort.join("\n")
end

@@ -130,2 +145,22 @@ end

end
def within_each_schema
@dump_schemas.each do |schema_name|
old_search_path = @connection.schema_search_path
@connection.schema_search_path = schema_name
self.schema_name = schema_name
yield
ensure
self.schema_name = nil
@connection.schema_search_path = old_search_path
end
end
def relation_name(name)
if @dump_schemas.size == 1
name
else
"#{schema_name}.#{name}"
end
end
end

@@ -132,0 +167,0 @@ end

@@ -15,5 +15,6 @@ # frozen_string_literal: true

# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
# <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
# <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
# <tt>:encoding</tt> (defaults to utf8), <tt>:locale_provider</tt>, <tt>:locale</tt>,
# <tt>:collation</tt>, <tt>:ctype</tt>, <tt>:tablespace</tt>, and
# <tt>:connection_limit</tt> (note that MySQL uses <tt>:charset</tt> while PostgreSQL
# uses <tt>:encoding</tt>).
#

@@ -34,2 +35,6 @@ # Example:

" ENCODING = '#{value}'"
when :locale_provider
" LOCALE_PROVIDER = '#{value}'"
when :locale
" LOCALE = '#{value}'"
when :collation

@@ -92,4 +97,9 @@ " LC_COLLATE = '#{value}'"

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, d.indisvalid
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid),
pg_catalog.obj_description(i.oid, 'pg_class') AS comment, d.indisvalid,
ARRAY(
SELECT pg_get_indexdef(d.indexrelid, k + 1, true)
FROM generate_subscripts(d.indkey, 1) AS k
ORDER BY k
) AS columns
FROM pg_class t

@@ -111,5 +121,6 @@ INNER JOIN pg_index d ON t.oid = d.indrelid

inddef = row[3]
oid = row[4]
comment = row[5]
valid = row[6]
comment = row[4]
valid = row[5]
columns = decode_string_array(row[6]).map { |c| Utils.unquote_identifier(c.strip.gsub('""', '"')) }
using, expressions, include, nulls_not_distinct, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: INCLUDE \((.+?)\))?( NULLS NOT DISTINCT)?(?: WHERE (.+))?\z/m).flatten

@@ -124,4 +135,2 @@

else
columns = column_names_from_column_numbers(oid, indkey)
# prevent INCLUDE columns from being matched

@@ -234,2 +243,10 @@ columns.reject! { |c| include_columns.include?(c) }

# Returns an array of the names of all schemas presently in the effective search path,
# in their priority order.
def current_schemas # :nodoc:
schemas = query_value("SELECT current_schemas(false)", "SCHEMA")
decoder = PG::TextDecoder::Array.new
decoder.decode(schemas)
end
# Returns the current database encoding format.

@@ -279,2 +296,7 @@ def encoding

# Renames the schema for the given schema name.
def rename_schema(schema_name, new_name)
execute "ALTER SCHEMA #{quote_schema_name(schema_name)} RENAME TO #{quote_schema_name(new_name)}"
end
# Sets the schema search path to a string of comma-separated schema names.

@@ -286,2 +308,3 @@ # Names beginning with $ have to be quoted (e.g. $user => '$user').

def schema_search_path=(schema_csv)
return if schema_csv == @schema_search_path
if schema_csv

@@ -331,3 +354,3 @@ internal_execute("SET search_path TO #{schema_csv}")

query_value("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
internal_execute("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
else

@@ -363,3 +386,3 @@ @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger

query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA")
internal_execute("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA")
end

@@ -597,12 +620,28 @@ end

fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conkey, c.confkey, c.conrelid, c.confrelid
SELECT t2.oid::regclass::text AS to_table, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conrelid, c.confrelid,
(
SELECT array_agg(a.attname ORDER BY idx)
FROM (
SELECT idx, c.conkey[idx] AS conkey_elem
FROM generate_subscripts(c.conkey, 1) AS idx
) indexed_conkeys
JOIN pg_attribute a ON a.attrelid = t1.oid
AND a.attnum = indexed_conkeys.conkey_elem
) AS conkey_names,
(
SELECT array_agg(a.attname ORDER BY idx)
FROM (
SELECT idx, c.confkey[idx] AS confkey_elem
FROM generate_subscripts(c.confkey, 1) AS idx
) indexed_confkeys
JOIN pg_attribute a ON a.attrelid = t2.oid
AND a.attnum = indexed_confkeys.confkey_elem
) AS confkey_names
FROM pg_constraint c
JOIN pg_class t1 ON c.conrelid = t1.oid
JOIN pg_class t2 ON c.confrelid = t2.oid
JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
JOIN pg_namespace t3 ON c.connamespace = t3.oid
JOIN pg_namespace n ON c.connamespace = n.oid
WHERE c.contype = 'f'
AND t1.relname = #{scope[:name]}
AND t3.nspname = #{scope[:schema]}
AND n.nspname = #{scope[:schema]}
ORDER BY c.conname

@@ -613,17 +652,10 @@ SQL

to_table = Utils.unquote_identifier(row["to_table"])
conkey = row["conkey"].scan(/\d+/).map(&:to_i)
confkey = row["confkey"].scan(/\d+/).map(&:to_i)
if conkey.size > 1
column = column_names_from_column_numbers(row["conrelid"], conkey)
primary_key = column_names_from_column_numbers(row["confrelid"], confkey)
else
column = Utils.unquote_identifier(row["column"])
primary_key = row["primary_key"]
end
column = decode_string_array(row["conkey_names"])
primary_key = decode_string_array(row["confkey_names"])
options = {
column: column,
column: column.size == 1 ? column.first : column,
name: row["name"],
primary_key: primary_key
primary_key: primary_key.size == 1 ? primary_key.first : primary_key
}

@@ -713,3 +745,12 @@

unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef
SELECT c.conname, c.conrelid, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef,
(
SELECT array_agg(a.attname ORDER BY idx)
FROM (
SELECT idx, c.conkey[idx] AS conkey_elem
FROM generate_subscripts(c.conkey, 1) AS idx
) indexed_conkeys
JOIN pg_attribute a ON a.attrelid = t.oid
AND a.attnum = indexed_conkeys.conkey_elem
) AS conkey_names
FROM pg_constraint c

@@ -724,4 +765,3 @@ JOIN pg_class t ON c.conrelid = t.oid

unique_info.map do |row|
conkey = row["conkey"].delete("{}").split(",").map(&:to_i)
columns = column_names_from_column_numbers(row["conrelid"], conkey)
columns = decode_string_array(row["conkey_names"])

@@ -997,2 +1037,3 @@ nulls_not_distinct = row["constraintdef"].start_with?("UNIQUE NULLS NOT DISTINCT")

column_name,
get_oid_type(oid.to_i, fmod.to_i, column_name, type),
default_value,

@@ -1167,9 +1208,4 @@ type_metadata,

def column_names_from_column_numbers(table_oid, column_numbers)
Hash[query(<<~SQL, "SCHEMA")].values_at(*column_numbers).compact
SELECT a.attnum, a.attname
FROM pg_attribute a
WHERE a.attrelid = #{table_oid}
AND a.attnum IN (#{column_numbers.join(", ")})
SQL
def decode_string_array(value)
PG::TextDecoder::Array.new.decode(value)
end

@@ -1176,0 +1212,0 @@ end

@@ -274,6 +274,6 @@ # frozen_string_literal: true

def encode_with(coder) # :nodoc:
coder["columns"] = @columns.sort.to_h
coder["columns"] = @columns.sort.to_h.transform_values { _1.sort_by(&:name) }
coder["primary_keys"] = @primary_keys.sort.to_h
coder["data_sources"] = @data_sources.sort.to_h
coder["indexes"] = @indexes.sort.to_h
coder["indexes"] = @indexes.sort.to_h.transform_values { _1.sort_by(&:name) }
coder["version"] = @version

@@ -280,0 +280,0 @@ end

@@ -22,10 +22,26 @@ # frozen_string_literal: true

module ConnectionAdapters # :nodoc:
# = Active Record SQLite3 Adapter
# = Active Record \SQLite3 Adapter
#
# The SQLite3 adapter works with the sqlite3-ruby drivers
# (available as gem from https://rubygems.org/gems/sqlite3).
# The \SQLite3 adapter works with the sqlite3[https://sparklemotion.github.io/sqlite3-ruby/]
# driver.
#
# ==== Options
#
# * <tt>:database</tt> - Path to the database file.
# * +:database+ (String): Filesystem path to the database file.
# * +:statement_limit+ (Integer): Maximum number of prepared statements to cache per database connection. (default: 1000)
# * +:timeout+ (Integer): Timeout in milliseconds to use when waiting for a lock. (default: no wait)
# * +:strict+ (Boolean): Enable or disable strict mode. When enabled, this will
# {disallow double-quoted string literals in SQL
# statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted].
# (default: see strict_strings_by_default)
# * +:extensions+ (Array): (<b>requires sqlite3 v2.4.0</b>) Each entry specifies a sqlite extension
# to load for this database. The entry may be a filesystem path, or the name of a class that
# responds to +.to_path+ to provide the filesystem path for the extension. See {sqlite3-ruby
# documentation}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#class-SQLite3::Database-label-SQLite+Extensions]
# for more information.
#
# There may be other options available specific to the SQLite3 driver. Please read the
# documentation for
# {SQLite3::Database.new}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#method-c-new]
#
class SQLite3Adapter < AbstractAdapter

@@ -50,6 +66,10 @@ ADAPTER_NAME = "SQLite"

args << "-header" if options[:header]
args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil)
args << File.expand_path(config.database, defined?(Rails.root) ? Rails.root : nil)
find_cmd_and_exec(ActiveRecord.database_cli[:sqlite], *args)
end
def native_database_types # :nodoc:
NATIVE_DATABASE_TYPES
end
end

@@ -63,8 +83,15 @@

# :singleton-method:
# Configure the SQLite3Adapter to be used in a strict strings mode.
# This will disable double-quoted string literals, because otherwise typos can silently go unnoticed.
# For example, it is possible to create an index for a non existing column.
#
# Configure the SQLite3Adapter to be used in a "strict strings" mode. When enabled, this will
# {disallow double-quoted string literals in SQL
# statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted],
# which may prevent some typographical errors like creating an index for a non-existent
# column. The default is +false+.
#
# If you wish to enable this mode you can add the following line to your application.rb file:
#
# config.active_record.sqlite3_adapter_strict_strings_by_default = true
#
# This can also be configured on individual databases by setting the +strict:+ option.
#
class_attribute :strict_strings_by_default, default: false

@@ -128,5 +155,9 @@

@last_affected_rows = nil
@previous_read_uncommitted = nil
@config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
extensions = @config.fetch(:extensions, []).map do |extension|
extension.safe_constantize || extension
end
@connection_parameters = @config.merge(

@@ -136,2 +167,3 @@ database: @config[:database].to_s,

default_transaction_mode: :immediate,
extensions: extensions
)

@@ -161,3 +193,3 @@ end

def supports_expression_index?
database_version >= "3.9.0"
true
end

@@ -190,3 +222,3 @@

def supports_common_table_expressions?
database_version >= "3.8.3"
true
end

@@ -239,6 +271,2 @@

def native_database_types # :nodoc:
NATIVE_DATABASE_TYPES
end
# Returns the current database encoding format as a string, e.g. 'UTF-8'

@@ -302,3 +330,3 @@ def encoding

VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.*)\)/i
VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.+)\)/i

@@ -490,4 +518,4 @@ # Returns a list of defined virtual tables

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."
if database_version < "3.23.0"
raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.23.0."
end

@@ -548,2 +576,4 @@ end

[ $1 ].pack("H*")
when "TRUE", "FALSE"
default
else

@@ -597,4 +627,4 @@ # Anything else is blank or some function

disable_referential_integrity do
transaction do
transaction do
disable_referential_integrity do
move_table(table_name, altered_table_name, options.merge(temporary: true))

@@ -640,4 +670,4 @@ move_table(altered_table_name, table_name, &caller)

elsif column.has_default?
type = lookup_cast_type_from_column(column)
default = type.deserialize(column.default)
# TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
default = column.fetch_cast_type(self).deserialize(column.default)
default = -> { column.default_function } if default.nil?

@@ -716,2 +746,4 @@

InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
elsif exception.message.match?(/CHECK constraint failed: .*/i)
CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
elsif exception.message.match?(/called on a closed database/i)

@@ -806,5 +838,5 @@ ConnectionNotEstablished.new(exception, connection_pool: @pool)

if supports_virtual_columns?
internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA", allow_retry: true)
else
internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA", allow_retry: true)
end

@@ -836,14 +868,6 @@ end

def configure_connection
if @config[:timeout] && @config[:retries]
raise ArgumentError, "Cannot specify both timeout and retries arguments"
elsif @config[:timeout]
if @config[:timeout]
timeout = self.class.type_cast_config_to_integer(@config[:timeout])
raise TypeError, "timeout must be integer, not #{timeout}" unless timeout.is_a?(Integer)
@raw_connection.busy_handler_timeout = timeout
elsif @config[:retries]
ActiveRecord.deprecator.warn(<<~MSG)
The retries option is deprecated and will be removed in Rails 8.1. Use timeout instead.
MSG
retries = self.class.type_cast_config_to_integer(@config[:retries])
raw_connection.busy_handler { |count| count <= retries }
end

@@ -850,0 +874,0 @@

@@ -49,5 +49,3 @@ # frozen_string_literal: true

super &&
auto_increment? == other.auto_increment? &&
rowid == other.rowid &&
generated_type == other.generated_type
auto_increment? == other.auto_increment?
end

@@ -60,8 +58,4 @@ alias :eql? :==

auto_increment?.hash ^
rowid.hash ^
virtual?.hash
rowid.hash
end
protected
attr_reader :generated_type
end

@@ -68,0 +62,0 @@ end

@@ -64,2 +64,10 @@ # frozen_string_literal: true

def default_insert_value(column) # :nodoc:
if column.default_function
Arel.sql(column.default_function)
else
column.default
end
end
private

@@ -80,18 +88,15 @@ def internal_begin_transaction(mode, isolation)

def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
total_changes_before_query = raw_connection.total_changes
affected_rows = nil
if batch
raw_connection.execute_batch2(sql)
elsif prepare
stmt = @statements[sql] ||= raw_connection.prepare(sql)
stmt.reset!
stmt.bind_params(type_casted_binds)
result = if stmt.column_count.zero? # No return
stmt.step
ActiveRecord::Result.empty
else
stmt = if prepare
@statements[sql] ||= raw_connection.prepare(sql)
@statements[sql].reset!
else
ActiveRecord::Result.new(stmt.columns, stmt.to_a)
# Don't cache statements if they are not prepared.
raw_connection.prepare(sql)
end
else
# Don't cache statements if they are not prepared.
stmt = raw_connection.prepare(sql)
begin

@@ -103,13 +108,28 @@ unless binds.nil? || binds.empty?

stmt.step
ActiveRecord::Result.empty
affected_rows = if raw_connection.total_changes > total_changes_before_query
raw_connection.changes
else
0
end
ActiveRecord::Result.empty(affected_rows: affected_rows)
else
ActiveRecord::Result.new(stmt.columns, stmt.to_a)
rows = stmt.to_a
affected_rows = if raw_connection.total_changes > total_changes_before_query
raw_connection.changes
else
0
end
ActiveRecord::Result.new(stmt.columns, rows, stmt.types.map { |t| type_map.lookup(t) }, affected_rows: affected_rows)
end
ensure
stmt.close
stmt.close unless prepare
end
end
@last_affected_rows = raw_connection.changes
verified!
notification_payload[:affected_rows] = affected_rows
notification_payload[:row_count] = result&.length || 0

@@ -126,3 +146,3 @@ result

def affected_rows(result)
@last_affected_rows
result.affected_rows
end

@@ -142,10 +162,2 @@

end
def default_insert_value(column)
if column.default_function
Arel.sql(column.default_function)
else
column.default
end
end
end

@@ -152,0 +164,0 @@ end

@@ -83,6 +83,2 @@ # frozen_string_literal: true

def quoted_true
"1"
end
def unquoted_true

@@ -92,6 +88,2 @@ 1

def quoted_false
"0"
end
def unquoted_false

@@ -98,0 +90,0 @@ 0

@@ -9,3 +9,3 @@ # frozen_string_literal: true

def virtual_tables(stream)
virtual_tables = @connection.virtual_tables.reject { |name, _| ignored?(name) }
virtual_tables = @connection.virtual_tables
if virtual_tables.any?

@@ -23,21 +23,9 @@ stream.puts

def default_primary_key?(column)
schema_type(column) == :integer && primary_key_has_autoincrement?
schema_type(column) == :integer
end
def explicit_primary_key_default?(column)
column.bigint? || (column.type == :integer && !primary_key_has_autoincrement?)
column.bigint?
end
def primary_key_has_autoincrement?
return false unless table_name
table_sql = @connection.query_value(<<~SQL, "SCHEMA")
SELECT sql FROM sqlite_master WHERE name = #{@connection.quote(table_name)} AND type = 'table'
UNION ALL
SELECT sql FROM sqlite_temp_master WHERE name = #{@connection.quote(table_name)} AND type = 'table'
SQL
table_sql.to_s.match?(/\bAUTOINCREMENT\b/i)
end
def prepare_column_options(column)

@@ -44,0 +32,0 @@ spec = super

@@ -66,19 +66,9 @@ # frozen_string_literal: true

def remove_foreign_key(from_table, to_table = nil, **options)
return if options.delete(:if_exists) == true && !foreign_key_exists?(from_table, to_table)
return if options.delete(:if_exists) && !foreign_key_exists?(from_table, to_table, **options.slice(:column))
to_table ||= options[:to_table]
options = options.except(:name, :to_table, :validate)
fkey = foreign_key_for!(from_table, to_table: to_table, **options)
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)
options = options.slice(*fk.options.keys)
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)

@@ -161,2 +151,3 @@ alter_table(from_table, foreign_keys)

field["name"],
lookup_cast_type(field["type"]),
default_value,

@@ -163,0 +154,0 @@ type_metadata,

@@ -184,3 +184,3 @@ # frozen_string_literal: true

case exception
when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError
when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError, ::Trilogy::SSLError
return ConnectionFailed.new(message, connection_pool: @pool)

@@ -187,0 +187,0 @@ when ::Trilogy::Error

@@ -32,3 +32,4 @@ # frozen_string_literal: true

verified!
handle_warnings(sql)
notification_payload[:affected_rows] = result.affected_rows
notification_payload[:row_count] = result.count

@@ -44,5 +45,5 @@ result

if result.fields.empty?
ActiveRecord::Result.empty
ActiveRecord::Result.empty(affected_rows: result.affected_rows)
else
ActiveRecord::Result.new(result.fields, result.rows)
ActiveRecord::Result.new(result.fields, result.rows, affected_rows: result.affected_rows)
end

@@ -49,0 +50,0 @@ end

@@ -175,7 +175,5 @@ # frozen_string_literal: true

append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: classes)
begin
yield
ensure
connected_to_stack.pop
end
yield
ensure
connected_to_stack.pop
end

@@ -401,9 +399,7 @@

append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: [self])
begin
return_value = yield
return_value.load if return_value.is_a? ActiveRecord::Relation
return_value
ensure
self.connected_to_stack.pop
end
return_value = yield
return_value.load if return_value.is_a? ActiveRecord::Relation
return_value
ensure
self.connected_to_stack.pop
end

@@ -410,0 +406,0 @@

# frozen_string_literal: true
require "active_support/core_ext/enumerable"
require "active_support/core_ext/module/delegation"
require "active_support/parameter_filter"

@@ -115,3 +114,3 @@ require "concurrent/map"

#
# When set to `:all` inspect will list all the record's attributes:
# When set to +:all+ inspect will list all the record's attributes:
#

@@ -362,2 +361,4 @@ # Post.attributes_for_inspect = :all

@filter_attributes = filter_attributes
FilterAttributeHandler.sensitive_attribute_was_declared(self, filter_attributes)
end

@@ -456,3 +457,3 @@

statement.execute(values.flatten, connection, allow_retry: true).then do |r|
statement.execute(values.flatten, connection).then do |r|
r.first

@@ -647,3 +648,3 @@ rescue TypeError

if primary_key_values_present?
if self.class.composite_primary_key? ? primary_key_values_present? : id
self.class.hash ^ id.hash

@@ -650,0 +651,0 @@ else

@@ -20,3 +20,3 @@ # frozen_string_literal: true

#
# * +id+ - The id of the object you wish to reset a counter on.
# * +id+ - The id of the object you wish to reset a counter on or an array of ids.
# * +counters+ - One or more association counters to reset. Association name or counter name can be given.

@@ -32,2 +32,5 @@ # * <tt>:touch</tt> - Touch timestamp columns when updating.

#
# # For posts with ids #1 and #2, reset the comments_count
# Post.reset_counters([1, 2], :comments)
#
# # Like above, but also touch the updated_at and/or updated_on

@@ -37,5 +40,14 @@ # # attributes.

def reset_counters(id, *counters, touch: nil)
object = find(id)
ids = if composite_primary_key?
if id.first.is_a?(Array)
id
else
[id]
end
else
Array(id)
end
updates = {}
updates = Hash.new { |h, k| h[k] = {} }
counters.each do |counter_association|

@@ -54,2 +66,3 @@ has_many_association = _reflect_on_association(counter_association)

counter_association = counter_association.to_sym
foreign_key = has_many_association.foreign_key.to_s

@@ -60,5 +73,12 @@ child_class = has_many_association.klass

count_was = object.send(counter_name)
count = object.send(counter_association).count(:all)
updates[counter_name] = count if count != count_was
counts =
unscoped
.joins(counter_association)
.where(primary_key => ids)
.group(primary_key)
.count(:all)
ids.each do |id|
updates[id].merge!(counter_name => counts[id] || 0)
end
end

@@ -71,6 +91,11 @@

touch_updates = touch_attributes_with_time(*names, **options)
updates.merge!(touch_updates)
updates.each_value do |record_updates|
record_updates.merge!(touch_updates)
end
end
unscoped.where(primary_key => [object.id]).update_all(updates) if updates.any?
updates.each do |id, record_updates|
unscoped.where(primary_key => [id]).update_all(record_updates)
end

@@ -77,0 +102,0 @@ true

@@ -39,5 +39,7 @@ # frozen_string_literal: true

#
# ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
# next unless config.key?(:vitess)
# VitessConfig.new(env_name, name, config)
# ActiveSupport.on_load(:active_record_database_configurations) do
# ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
# next unless config.key?(:vitess)
# VitessConfig.new(env_name, name, config)
# end
# end

@@ -310,2 +312,4 @@ #

end
ActiveSupport.run_load_hooks(:active_record_database_configurations, DatabaseConfigurations)
end

@@ -51,6 +51,10 @@ # frozen_string_literal: true

def pool
def min_connections
raise NotImplementedError
end
def max_connections
raise NotImplementedError
end
def min_threads

@@ -57,0 +61,0 @@ raise NotImplementedError

@@ -41,2 +41,3 @@ # frozen_string_literal: true

@configuration_hash = configuration_hash.symbolize_keys.freeze
validate_configuration!
end

@@ -73,6 +74,13 @@

def pool
(configuration_hash[:pool] || 5).to_i
def max_connections
(configuration_hash[:max_connections] || configuration_hash[:pool] || 5).to_i
end
def min_connections
(configuration_hash[:min_connections] || 0).to_i
end
alias :pool :max_connections
deprecate pool: :max_connections, deprecator: ActiveRecord.deprecator
def min_threads

@@ -83,5 +91,14 @@ (configuration_hash[:min_threads] || 0).to_i

def max_threads
(configuration_hash[:max_threads] || pool).to_i
(configuration_hash[:max_threads] || max_connections).to_i
end
def max_age
v = configuration_hash[:max_age]&.to_i
if v && v > 0
v
else
Float::INFINITY
end
end
def query_cache

@@ -99,6 +116,4 @@ configuration_hash[:query_cache]

# `reaping_frequency` is configurable mostly for historical reasons, but it
# could also be useful if someone wants a very low `idle_timeout`.
def reaping_frequency
configuration_hash.fetch(:reaping_frequency, 60)&.to_f
def reaping_frequency # :nodoc:
configuration_hash.fetch(:reaping_frequency, default_reaping_frequency)&.to_f
end

@@ -111,2 +126,7 @@

def keepalive
keepalive = (configuration_hash[:keepalive] || 600).to_f
keepalive if keepalive > 0
end
def adapter

@@ -167,4 +187,4 @@ configuration_hash[:adapter]&.to_s

def schema_format # :nodoc:
format = configuration_hash[:schema_format]&.to_sym || ActiveRecord.schema_format
raise "Invalid schema format" unless [ :ruby, :sql ].include? format
format = configuration_hash.fetch(:schema_format, ActiveRecord.schema_format).to_sym
raise "Invalid schema format" unless [:ruby, :sql].include?(format)
format

@@ -190,4 +210,25 @@ end

end
def default_reaping_frequency
# Reap every 20 seconds by default, but run more often as necessary to
# meet other configured timeouts.
[20, idle_timeout, max_age, keepalive].compact.min
end
def validate_configuration!
if configuration_hash[:pool] && configuration_hash[:max_connections]
pool_val = configuration_hash[:pool].to_i
max_conn_val = configuration_hash[:max_connections].to_i
if pool_val != max_conn_val
raise "Ambiguous configuration: 'pool' (#{pool_val}) and 'max_connections' (#{max_conn_val}) are set to different values. Prefer just 'max_connections'."
end
end
if configuration_hash[:pool] && configuration_hash[:min_connections]
raise "Ambiguous configuration: when setting 'min_connections', use 'max_connections' instead of 'pool'."
end
end
end
end
end

@@ -50,5 +50,4 @@ # frozen_string_literal: true

if @configuration_hash[:query_cache] == "false"
@configuration_hash[:query_cache] = false
end
query_cache = parse_query_cache
@configuration_hash[:query_cache] = query_cache unless query_cache.nil?

@@ -62,2 +61,13 @@ to_boolean!(@configuration_hash, :replica)

private
def parse_query_cache
case value = @configuration_hash[:query_cache]
when /\A\d+\z/
value.to_i
when "false"
false
else
value
end
end
def to_boolean!(configuration_hash, key)

@@ -64,0 +74,0 @@ if configuration_hash[key].is_a?(String)

@@ -232,3 +232,3 @@ # frozen_string_literal: true

def delegated_type(role, types:, **options)
belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
belongs_to role, options.delete(:scope), **options, polymorphic: true
define_delegated_type_methods role, types: types, options: options

@@ -235,0 +235,0 @@ end

@@ -10,4 +10,6 @@ # frozen_string_literal: true

else
match = Method.match(self, name)
match && match.valid? || super
super || begin
match = Method.match(name)
match && match.valid?(self, name)
end
end

@@ -17,6 +19,6 @@ end

def method_missing(name, ...)
match = Method.match(self, name)
match = Method.match(name)
if match && match.valid?
match.define
if match && match.valid?(self, name)
match.define(self, name)
send(name, ...)

@@ -29,93 +31,76 @@ else

class Method
@matchers = []
class << self
attr_reader :matchers
def match(model, name)
klass = matchers.find { |k| k.pattern.match?(name) }
klass.new(model, name) if klass
def match(name)
FindBy.match?(name) || FindByBang.match?(name)
end
def pattern
@pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
def valid?(model, name)
attribute_names(model, name.to_s).all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
end
def prefix
raise NotImplementedError
def define(model, name)
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
def self.#{name}(#{signature(model, name)})
#{body(model, name)}
end
CODE
end
def suffix
""
end
end
private
def make_pattern(prefix, suffix)
/\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end
attr_reader :model, :name, :attribute_names
def attribute_names(model, name)
attribute_names = name.match(pattern)[1].split("_and_")
attribute_names.map! { |name| model.attribute_aliases[name] || name }
end
def initialize(model, method_name)
@model = model
@name = method_name.to_s
@attribute_names = @name.match(self.class.pattern)[1].split("_and_")
@attribute_names.map! { |name| @model.attribute_aliases[name] || name }
end
def body(model, method_name)
"#{finder}(#{attributes_hash(model, method_name)})"
end
def valid?
attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
end
# The parameters in the signature may have reserved Ruby words, in order
# to prevent errors, we start each param name with `_`.
def signature(model, method_name)
attribute_names(model, method_name.to_s).map { |name| "_#{name}" }.join(", ")
end
def define
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
def self.#{name}(#{signature})
#{body}
# Given that the parameters starts with `_`, the finder needs to use the
# same parameter name.
def attributes_hash(model, method_name)
"{" + attribute_names(model, method_name).map { |name| ":#{name} => _#{name}" }.join(",") + "}"
end
CODE
end
end
private
def body
"#{finder}(#{attributes_hash})"
end
class FindBy < Method
@pattern = make_pattern("find_by", "")
# The parameters in the signature may have reserved Ruby words, in order
# to prevent errors, we start each param name with `_`.
def signature
attribute_names.map { |name| "_#{name}" }.join(", ")
end
class << self
attr_reader :pattern
# Given that the parameters starts with `_`, the finder needs to use the
# same parameter name.
def attributes_hash
"{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}"
def match?(name)
pattern.match?(name) && self
end
def finder
raise NotImplementedError
"find_by"
end
end
class FindBy < Method
Method.matchers << self
def self.prefix
"find_by"
end
def finder
"find_by"
end
end
class FindByBang < Method
Method.matchers << self
@pattern = make_pattern("find_by", "!")
def self.prefix
"find_by"
end
class << self
attr_reader :pattern
def self.suffix
"!"
end
def match?(name)
pattern.match?(name) && self
end
def finder
"find_by!"
def finder
"find_by!"
end
end

@@ -122,0 +107,0 @@ end

@@ -33,6 +33,6 @@ # frozen_string_literal: true

# data.
# * <tt>:support_unencrypted_data</tt> - If `config.active_record.encryption.support_unencrypted_data` is +true+,
# you can set this to +false+ to opt out of unencrypted data support for this attribute. This is useful for
# scenarios where you encrypt one column, and want to disable support for unencrypted data without having to tweak
# the global setting.
# * <tt>:support_unencrypted_data</tt> - When true, unencrypted data can be read normally. When false, it will raise errors.
# Falls back to +config.active_record.encryption.support_unencrypted_data+ if no value is provided.
# This is useful for scenarios where you encrypt one column, and want to disable support for unencrypted data
# without having to tweak the global setting.
# * <tt>:downcase</tt> - When true, it converts the encrypted content to downcase automatically. This allows to

@@ -39,0 +39,0 @@ # effectively ignore case when querying data. Notice that the case is lost. Use +:ignore_case+ if you are interested

@@ -62,3 +62,3 @@ # frozen_string_literal: true

def support_unencrypted_data?
ActiveRecord::Encryption.config.support_unencrypted_data && scheme.support_unencrypted_data? && !previous_type?
scheme.support_unencrypted_data? && !previous_type?
end

@@ -65,0 +65,0 @@

@@ -62,3 +62,3 @@ # frozen_string_literal: true

def merge(other_scheme)
self.class.new(**to_h.merge(other_scheme.to_h))
self.class.new(**to_h, **other_scheme.to_h)
end

@@ -65,0 +65,0 @@

@@ -224,3 +224,3 @@ # frozen_string_literal: true

def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, validate: false, **options)
assert_valid_enum_definition_values(values)
values = assert_valid_enum_definition_values(values)
assert_valid_enum_options(options)

@@ -346,2 +346,16 @@

end
values = values.transform_values do |value|
value.is_a?(Symbol) ? value.name : value
end
values.each_value do |value|
case value
when String, Integer, true, false, nil
# noop
else
raise ArgumentError, "Enum values #{values} must be only booleans, integers, symbols or strings, got: #{value.class}"
end
end
when Array

@@ -362,2 +376,4 @@ if values.empty?

end
values
end

@@ -374,3 +390,3 @@

"You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
"this will generate a %{type} method \"%{method}\", which is already defined " \
"this will generate %{type} method \"%{method}\", which is already defined " \
"by %{source}."

@@ -381,15 +397,15 @@ private_constant :ENUM_CONFLICT_MESSAGE

if klass_method && dangerous_class_method?(method_name)
raise_conflict_error(enum_name, method_name, type: "class")
raise_conflict_error(enum_name, method_name, "a class")
elsif klass_method && method_defined_within?(method_name, Relation)
raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
raise_conflict_error(enum_name, method_name, "a class", source: Relation.name)
elsif klass_method && method_name.to_sym == :id
raise_conflict_error(enum_name, method_name)
raise_conflict_error(enum_name, method_name, "an instance")
elsif !klass_method && dangerous_attribute_method?(method_name)
raise_conflict_error(enum_name, method_name)
raise_conflict_error(enum_name, method_name, "an instance")
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
raise_conflict_error(enum_name, method_name, source: "another enum")
raise_conflict_error(enum_name, method_name, "an instance", source: "another enum")
end
end
def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Record")
def raise_conflict_error(enum_name, method_name, type, source: "Active Record")
raise ArgumentError, ENUM_CONFLICT_MESSAGE % {

@@ -396,0 +412,0 @@ enum: enum_name,

# frozen_string_literal: true
require "active_support/deprecation"

@@ -16,3 +15,3 @@ module ActiveRecord

# (for example due to improper usage of column that
# {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema.inheritance_column]
# {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema::ClassMethods#inheritance_column]
# points to).

@@ -297,2 +296,10 @@ class SubclassNotFound < ActiveRecordError

# Raised when a record cannot be inserted or updated because it would violate a check constraint.
class CheckViolation < StatementInvalid
end
# Raised when a record cannot be inserted or updated because it would violate an exclusion constraint.
class ExclusionViolation < StatementInvalid
end
# Raised when a record cannot be inserted or updated because a value too long for a column type.

@@ -344,11 +351,11 @@ class ValueTooLong < StatementInvalid

NoDatabaseError.new(<<~MSG)
We could not find your database: #{db_name}. Available database configurations can be found in config/database.yml.
Database not found: #{db_name}. Available database configurations can be found in config/database.yml.
To resolve this error:
- Did you not create the database, or did you delete it? To create the database, run:
- Create the database by running:
bin/rails db:create
- Has the database name changed? Verify that config/database.yml contains the correct database name.
- Verify that config/database.yml contains the correct database name.
MSG

@@ -450,3 +457,3 @@ end

# Raised when an error occurred while doing a mass assignment to an attribute through the
# {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=] method.
# {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
# The exception has an +attribute+ property that is the name of the offending attribute.

@@ -464,3 +471,3 @@ class AttributeAssignmentError < ActiveRecordError

# Raised when there are multiple errors while doing a mass assignment through the
# {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=]
# {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=]
# method. The exception has an +errors+ property that contains an array of AttributeAssignmentError

@@ -498,2 +505,3 @@ # objects, each corresponding to the error while assigning to an attribute.

# relation = Task.all
# relation.load
# relation.loaded? # => true

@@ -561,2 +569,7 @@ #

# MissingRequiredOrderError is raised when a relation requires ordering but
# lacks any +order+ values in scope or any model order columns to use.
class MissingRequiredOrderError < ActiveRecordError
end
# IrreversibleOrderError is raised when a relation's order is too complex for

@@ -619,4 +632,7 @@ # +reverse_order+ to automatically reverse.

end
class DeprecatedAssociationError < ActiveRecordError
end
end
require "active_record/associations/errors"
# frozen_string_literal: true
require "active_support/core_ext/module/delegation"

@@ -5,0 +4,0 @@ module ActiveRecord

@@ -245,6 +245,6 @@ # frozen_string_literal: true

#
# george: # generated id: 503576764
# george: # generated id: 380982691
# name: George the Monkey
#
# reginald: # generated id: 324201669
# reginald: # generated id: 41001176
# name: Reginald the Pirate

@@ -251,0 +251,0 @@ #

@@ -108,3 +108,2 @@ # frozen_string_literal: true

return unless @mutex.try_lock
previous_instrumenter = ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]
begin

@@ -118,3 +117,2 @@ if pending?

ensure
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = previous_instrumenter
@mutex.unlock

@@ -121,0 +119,0 @@ end

@@ -11,5 +11,5 @@ # frozen_string_literal: true

MAJOR = 8
MINOR = 0
TINY = 5
PRE = nil
MINOR = 1
TINY = 0
PRE = "beta1"

@@ -16,0 +16,0 @@ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")

@@ -100,3 +100,3 @@ # frozen_string_literal: true

#
# Consider the following behaviour:
# Consider the following behavior:
#

@@ -103,0 +103,0 @@ # class ApplicationRecord < ActiveRecord::Base

@@ -14,3 +14,3 @@ # frozen_string_literal: true

new(relation, c, ...).execute
end
end.tap { relation.reset }
end

@@ -43,3 +43,3 @@ end

@unique_by = find_unique_index_for(@unique_by) if @on_duplicate != :raise
@unique_by = find_unique_index_for(@unique_by)

@@ -53,3 +53,3 @@ configure_on_duplicate_update_logic

message = +"#{model.name} "
message = +"#{model} "
message << "Bulk " if inserts.many?

@@ -231,3 +231,3 @@ message << (on_duplicate == :update ? "Upsert" : "Insert")

delegate :skip_duplicates?, :update_duplicates?, :keys, :keys_including_timestamps, :record_timestamps?, to: :insert_all
delegate :skip_duplicates?, :update_duplicates?, :keys, :keys_including_timestamps, :record_timestamps?, :primary_keys, to: :insert_all

@@ -243,7 +243,12 @@ def initialize(insert_all)

def values_list
types = extract_types_from_columns_on(model.table_name, keys: keys_including_timestamps)
types = extract_types_for(keys_including_timestamps)
values_list = insert_all.map_key_with_value do |key, value|
next value if Arel::Nodes::SqlLiteral === value
ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
if Arel::Nodes::SqlLiteral === value
value
elsif primary_keys.include?(key) && value.nil?
connection.default_insert_value(model.columns_hash[key])
else
ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
end
end

@@ -311,4 +316,4 @@

def extract_types_from_columns_on(table_name, keys:)
columns = @model.schema_cache.columns_hash(table_name)
def extract_types_for(keys)
columns = @model.columns_hash

@@ -315,0 +320,0 @@ unknown_column = (keys - columns.keys).first

@@ -104,2 +104,9 @@ # frozen_string_literal: true

if self[locking_column].nil?
raise(<<-MSG.squish)
For optimistic locking, locking_column ('#{locking_column}') can't be nil.
Are you missing a default value or validation on '#{locking_column}'?
MSG
end
self[locking_column] += 1

@@ -106,0 +113,0 @@

@@ -70,2 +70,6 @@ # frozen_string_literal: true

def lock!(lock = true)
if self.class.current_preventing_writes
raise ActiveRecord::ReadOnlyError, "Lock query attempted while in readonly mode"
end
if persisted?

@@ -83,2 +87,3 @@ if has_changes_to_save?

end
self

@@ -85,0 +90,0 @@ end

@@ -130,7 +130,3 @@ # frozen_string_literal: true

def query_source_location
Thread.each_caller_location do |location|
frame = backtrace_cleaner.clean_frame(location)
return frame if frame
end
nil
backtrace_cleaner.first_clean_frame
end

@@ -137,0 +133,0 @@

@@ -12,21 +12,36 @@ # frozen_string_literal: true

#
# The ShardSelector takes a set of options (currently only +lock+ is supported)
# that can be used by the middleware to alter behavior. +lock+ is
# true by default and will prohibit the request from switching shards once
# inside the block. If +lock+ is false, then shard swapping will be allowed.
# For tenant based sharding, +lock+ should always be true to prevent application
# code from mistakenly switching between tenants.
# == Setup
#
# Options can be set in the config:
# Applications must provide a resolver that will provide application-specific logic for
# selecting the appropriate shard. Setting +config.active_record.shard_resolver+ will cause
# Rails to add ShardSelector to the default middleware stack.
#
# config.active_record.shard_selector = { lock: true }
# The resolver, along with any configuration options, can be set in the application
# configuration using an initializer like so:
#
# Applications must also provide the code for the resolver as it depends on application
# specific models. An example resolver would look like this:
# Rails.application.configure do
# config.active_record.shard_selector = { lock: false, class_name: "AnimalsRecord" }
# config.active_record.shard_resolver = ->(request) {
# subdomain = request.subdomain
# tenant = Tenant.find_by_subdomain!(subdomain)
# tenant.shard
# }
# end
#
# config.active_record.shard_resolver = ->(request) {
# subdomain = request.subdomain
# tenant = Tenant.find_by_subdomain!(subdomain)
# tenant.shard
# }
# == Configuration
#
# The behavior of ShardSelector can be altered through some configuration options.
#
# [+lock:+]
# +lock+ is true by default and will prohibit the request from switching shards once inside
# the block. If +lock+ is false, then shard switching will be allowed. For tenant based
# sharding, +lock+ should always be true to prevent application code from mistakenly switching
# between tenants.
#
# [+class_name:+]
# +class_name+ is the name of the abstract connection class to switch. By
# default, the ShardSelector will use ActiveRecord::Base, but if the
# application has multiple databases, then this option should be set to
# the name of the sharded database's abstract connection class.
#
class ShardSelector

@@ -57,4 +72,6 @@ def initialize(app, resolver, options = {})

def set_shard(shard, &block)
ActiveRecord::Base.connected_to(shard: shard.to_sym) do
ActiveRecord::Base.prohibit_shard_swapping(options.fetch(:lock, true), &block)
klass = options[:class_name]&.constantize || ActiveRecord::Base
klass.connected_to(shard: shard.to_sym) do
klass.prohibit_shard_swapping(options.fetch(:lock, true), &block)
end

@@ -61,0 +78,0 @@ end

@@ -21,3 +21,3 @@ # frozen_string_literal: true

#
# class IrreversibleMigrationExample < ActiveRecord::Migration[8.0]
# class IrreversibleMigrationExample < ActiveRecord::Migration[8.1]
# def change

@@ -40,3 +40,3 @@ # create_table :distributors do |t|

#
# class ReversibleMigrationExample < ActiveRecord::Migration[8.0]
# class ReversibleMigrationExample < ActiveRecord::Migration[8.1]
# def up

@@ -66,3 +66,3 @@ # create_table :distributors do |t|

#
# class ReversibleMigrationExample < ActiveRecord::Migration[8.0]
# class ReversibleMigrationExample < ActiveRecord::Migration[8.1]
# def change

@@ -252,3 +252,3 @@ # create_table :distributors do |t|

#
# class AddSsl < ActiveRecord::Migration[8.0]
# class AddSsl < ActiveRecord::Migration[8.1]
# def up

@@ -273,3 +273,3 @@ # add_column :accounts, :ssl_enabled, :boolean, default: true

#
# class AddSystemSettings < ActiveRecord::Migration[8.0]
# class AddSystemSettings < ActiveRecord::Migration[8.1]
# def up

@@ -403,3 +403,3 @@ # create_table :system_settings do |t|

# This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this:
# class AddFieldnameToTablename < ActiveRecord::Migration[8.0]
# class AddFieldnameToTablename < ActiveRecord::Migration[8.1]
# def change

@@ -430,3 +430,3 @@ # add_column :tablenames, :fieldname, :string

#
# class RemoveEmptyTags < ActiveRecord::Migration[8.0]
# class RemoveEmptyTags < ActiveRecord::Migration[8.1]
# def up

@@ -444,3 +444,3 @@ # Tag.all.each { |tag| tag.destroy if tag.pages.empty? }

#
# class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[8.0]
# class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[8.1]
# def up

@@ -459,3 +459,3 @@ # remove_column :items, :incomplete_items_count

#
# class MakeJoinUnique < ActiveRecord::Migration[8.0]
# class MakeJoinUnique < ActiveRecord::Migration[8.1]
# def up

@@ -477,3 +477,3 @@ # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"

#
# class AddPeopleSalary < ActiveRecord::Migration[8.0]
# class AddPeopleSalary < ActiveRecord::Migration[8.1]
# def up

@@ -540,3 +540,3 @@ # add_column :people, :salary, :integer

#
# class TenderloveMigration < ActiveRecord::Migration[8.0]
# class TenderloveMigration < ActiveRecord::Migration[8.1]
# def change

@@ -571,3 +571,3 @@ # create_table(:horses) do |t|

#
# class ChangeEnum < ActiveRecord::Migration[8.0]
# class ChangeEnum < ActiveRecord::Migration[8.1]
# disable_ddl_transaction!

@@ -585,2 +585,3 @@ #

autoload :Compatibility, "active_record/migration/compatibility"
autoload :DefaultSchemaVersionsFormatter, "active_record/migration/default_schema_versions_formatter"
autoload :JoinTable, "active_record/migration/join_table"

@@ -797,2 +798,7 @@ autoload :ExecutionStrategy, "active_record/migration/execution_strategy"

end
def respond_to_missing?(method, include_private = false)
return false if nearest_delegate == delegate
nearest_delegate.respond_to?(method, include_private)
end
end

@@ -835,3 +841,3 @@

#
# class FixTLMigration < ActiveRecord::Migration[8.0]
# class FixTLMigration < ActiveRecord::Migration[8.1]
# def change

@@ -855,3 +861,3 @@ # revert do

#
# class FixupTLMigration < ActiveRecord::Migration[8.0]
# class FixupTLMigration < ActiveRecord::Migration[8.1]
# def change

@@ -907,3 +913,3 @@ # revert TenderloveMigration

#
# class SplitNameMigration < ActiveRecord::Migration[8.0]
# class SplitNameMigration < ActiveRecord::Migration[8.1]
# def change

@@ -936,3 +942,3 @@ # add_column :users, :first_name, :string

#
# class AddPublishedToPosts < ActiveRecord::Migration[8.0]
# class AddPublishedToPosts < ActiveRecord::Migration[8.1]
# def change

@@ -1190,2 +1196,6 @@ # add_column :posts, :published, :boolean, default: false

end
def respond_to_missing?(method, include_private = false)
execution_strategy.respond_to?(method, include_private) || super
end
end

@@ -1192,0 +1202,0 @@

@@ -47,2 +47,4 @@ # frozen_string_literal: true

# * rename_table
# * enable_index
# * disable_index
class CommandRecorder

@@ -62,3 +64,4 @@ ReversibleAndIrreversibleMethods = [

:create_schema, :drop_schema,
:create_virtual_table, :drop_virtual_table
:create_virtual_table, :drop_virtual_table,
:enable_index, :disable_index
]

@@ -144,3 +147,3 @@ include JoinTable

commands = recorder.commands
@commands << [:change_table, [table_name], -> t { bulk_change_table(t.name, commands.reverse) }]
@commands << [:change_table, [table_name], -> t { bulk_change_table(table_name, commands) }]
else

@@ -189,2 +192,12 @@ yield delegate.update_table_definition(table_name, self)

def invert_enable_index(args)
table_name, index_name = args
[:disable_index, [table_name, index_name]]
end
def invert_disable_index(args)
table_name, index_name = args
[:enable_index, [table_name, index_name]]
end
def invert_transaction(args, &block)

@@ -191,0 +204,0 @@ sub_recorder = CommandRecorder.new(delegate)

@@ -24,3 +24,3 @@ # frozen_string_literal: true

# There are classes for each prior Rails version. Each class descends from the *next* Rails version, so:
# 5.2 < 6.0 < 6.1 < 7.0 < 7.1 < 7.2 < 8.0
# 5.2 < 6.0 < 6.1 < 7.0 < 7.1 < 7.2 < 8.0 < 8.1
#

@@ -33,4 +33,28 @@ # If you are introducing new migration functionality that should only apply from Rails 7 onward, then you should

# for migrations written for 5.2, but will for migrations written for 6.0.
V8_0 = Current
V8_1 = Current
class V8_0 < V8_1
module RemoveForeignKeyColumnMatch
def remove_foreign_key(from_table, to_table = nil, **options)
options[:_skip_column_match] = true
super
end
end
module TableDefinition
def remove_foreign_key(to_table = nil, **options)
options[:_skip_column_match] = true
super
end
end
include RemoveForeignKeyColumnMatch
private
def compatible_table_definition(t)
t.singleton_class.prepend(TableDefinition)
super
end
end
class V7_2 < V8_0

@@ -159,5 +183,3 @@ end

def compatible_table_definition(t)
class << t
prepend TableDefinition
end
t.singleton_class.prepend(TableDefinition)
super

@@ -223,5 +245,3 @@ end

def compatible_table_definition(t)
class << t
prepend TableDefinition
end
t.singleton_class.prepend(TableDefinition)
super

@@ -267,5 +287,3 @@ end

def compatible_table_definition(t)
class << t
prepend TableDefinition
end
t.singleton_class.prepend(TableDefinition)
super

@@ -316,5 +334,3 @@ end

def compatible_table_definition(t)
class << t
prepend TableDefinition
end
t.singleton_class.prepend(TableDefinition)
super

@@ -325,5 +341,3 @@ end

recorder = super
class << recorder
prepend CommandRecorder
end
recorder.singleton_class.prepend(CommandRecorder)
recorder

@@ -416,5 +430,3 @@ end

def compatible_table_definition(t)
class << t
prepend TableDefinition
end
t.singleton_class.prepend(TableDefinition)
super

@@ -453,3 +465,3 @@ end

def index_exists?(table_name, column_name, **options)
def index_exists?(table_name, column_name = nil, **options)
column_names = Array(column_name).map(&:to_s)

@@ -472,5 +484,3 @@ options[:name] =

def compatible_table_definition(t)
class << t
prepend TableDefinition
end
t.singleton_class.prepend(TableDefinition)
super

@@ -477,0 +487,0 @@ end

@@ -116,3 +116,3 @@ # frozen_string_literal: true

#
# The name of the column records are ordered by if no explicit order clause
# The name of the column(s) records are ordered by if no explicit order clause
# is used during an ordered finder call. If not set the primary key is used.

@@ -124,6 +124,8 @@

#
# 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. Records are subsorted
# by the primary key if it exists to ensure deterministic results.
# Sets the column(s) to sort records by when no explicit order clause is used
# during an ordered finder call. Useful for models where the primary key isn't an
# auto-incrementing integer (such as UUID).
#
# By default, records are subsorted by primary key to ensure deterministic results.
# To disable this subsort behavior, set `implicit_order_column` to `["column_name", nil]`.

@@ -506,3 +508,3 @@ ##

#
# class CreateJobLevels < ActiveRecord::Migration[8.0]
# class CreateJobLevels < ActiveRecord::Migration[8.1]
# def up

@@ -626,3 +628,4 @@ # create_table :job_levels do |t|

def type_for_column(connection, column)
type = connection.lookup_cast_type_from_column(column)
# TODO: Remove the need for a connection after we release 8.1.
type = column.fetch_cast_type(connection)

@@ -629,0 +632,0 @@ if immutable_strings_by_default && type.respond_to?(:to_immutable_string)

@@ -390,2 +390,4 @@ # frozen_string_literal: true

def #{association_name}_attributes=(attributes)
association = association(:#{association_name})
deprecated_associations_api_guard(association, __method__)
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)

@@ -392,0 +394,0 @@ end

@@ -495,2 +495,3 @@ # frozen_string_literal: true

becoming.instance_variable_set(:@new_record, new_record?)
becoming.instance_variable_set(:@previously_new_record, previously_new_record?)
becoming.instance_variable_set(:@destroyed, destroyed?)

@@ -585,4 +586,4 @@ becoming.errors.copy!(errors)

# Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
update_columns(name => value)
def update_column(name, value, touch: nil)
update_columns(name => value, touch: touch)
end

@@ -601,3 +602,3 @@

# * \Callbacks are skipped.
# * +updated_at+/+updated_on+ are not updated.
# * +updated_at+/+updated_on+ are updated if the +touch+ option is set to +true+.
# * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all

@@ -607,2 +608,16 @@ #

# objects, or when at least one of the attributes is marked as readonly.
#
# ==== Parameters
#
# * <tt>:touch</tt> option - Touch the timestamp columns when updating.
# * If attribute names are passed, they are updated along with +updated_at+/+updated_on+ attributes.
#
# ==== Examples
#
# # Update a single attribute.
# user.update_columns(last_request_at: Time.current)
#
# # Update with touch option.
# user.update_columns(last_request_at: Time.current, touch: true)
def update_columns(attributes)

@@ -619,2 +634,11 @@ raise ActiveRecordError, "cannot update a new record" if new_record?

touch = attributes.delete("touch")
if touch
names = touch if touch != true
names = Array.wrap(names)
options = names.extract_options!
touch_updates = self.class.touch_attributes_with_time(*names, **options)
attributes.with_defaults!(touch_updates) unless touch_updates.empty?
end
update_constraints = _query_constraints_hash

@@ -648,4 +672,11 @@ attributes = attributes.each_with_object({}) do |(k, v), h|

# +update_counters+, see that for more.
#
# This method raises an ActiveRecord::ActiveRecordError when called on new
# objects, or when at least one of the attributes is marked as readonly.
#
# Returns +self+.
def increment!(attribute, by = 1, touch: nil)
raise ActiveRecordError, "cannot update a new record" if new_record?
raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
increment(attribute, by)

@@ -652,0 +683,0 @@ change = public_send(attribute) - (public_send(:"#{attribute}_in_database") || 0)

@@ -6,2 +6,3 @@ # frozen_string_literal: true

class QueryCache
# ActiveRecord::Base extends this module, so these methods are available in models.
module ClassMethods

@@ -24,7 +25,11 @@ # Enable the query cache within the block if Active Record is configured.

# Disable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
# Runs the block with the query cache disabled.
#
# Set <tt>dirties: false</tt> to prevent query caches on all connections from being cleared by write operations.
# (By default, write operations dirty all connections' query caches in case they are replicas whose cache would now be outdated.)
# If the query cache was enabled before the block was executed, it is
# enabled again after it.
#
# Set <tt>dirties: false</tt> to prevent query caches on all connections
# from being cleared by write operations. (By default, write operations
# dirty all connections' query caches in case they are replicas whose
# cache would now be outdated.)
def uncached(dirties: true, &block)

@@ -39,20 +44,22 @@ if connected? || !configurations.empty?

def self.run
ActiveRecord::Base.connection_handler.each_connection_pool.reject(&:query_cache_enabled).each do |pool|
next if pool.db_config&.query_cache == false
pool.enable_query_cache!
module ExecutorHooks # :nodoc:
def self.run
ActiveRecord::Base.connection_handler.each_connection_pool.reject(&:query_cache_enabled).each do |pool|
next if pool.db_config&.query_cache == false
pool.enable_query_cache!
end
end
end
def self.complete(pools)
pools.each do |pool|
pool.disable_query_cache!
pool.clear_query_cache
def self.complete(pools)
pools.each do |pool|
pool.disable_query_cache!
pool.clear_query_cache
end
end
end
def self.install_executor_hooks(executor = ActiveSupport::Executor)
executor.register_hook(self)
def self.install_executor_hooks(executor = ActiveSupport::Executor) # :nodoc:
executor.register_hook(ExecutorHooks)
end
end
end

@@ -72,3 +72,3 @@ # frozen_string_literal: true

#
# ActiveRecord::QueryLogs.prepend_comment = true
# config.active_record.query_log_tags_prepend_comment = true
#

@@ -161,7 +161,3 @@ # For applications where the content will not change during the lifetime of

def query_source_location # :nodoc:
Thread.each_caller_location do |location|
frame = LogSubscriber.backtrace_cleaner.clean_frame(location)
return frame if frame
end
nil
LogSubscriber.backtrace_cleaner.first_clean_frame
end

@@ -220,3 +216,3 @@

def escape_sql_comment(content)
# Sanitize a string to appear within a SQL comment
# Sanitize a string to appear within an SQL comment
# For compatibility, this also surrounding "/*+", "/*", and "*/"

@@ -223,0 +219,0 @@ # characters, possibly with single surrounding space.

@@ -38,5 +38,8 @@ # frozen_string_literal: true

config.active_record.cache_query_log_tags = false
config.active_record.query_log_tags_prepend_comment = false
config.active_record.raise_on_assign_to_attr_readonly = false
config.active_record.belongs_to_required_validates_foreign_key = true
config.active_record.generate_secure_token_on = :create
config.active_record.use_legacy_signed_id_verifier = :generate_and_verify
config.active_record.deprecated_associations_options = { mode: :warn, backtrace: false }

@@ -233,2 +236,3 @@ config.active_record.queues = ActiveSupport::InheritableOptions.new

:cache_query_log_tags,
:query_log_tags_prepend_comment,
:sqlite3_adapter_strict_strings_by_default,

@@ -238,2 +242,3 @@ :check_schema_cache_dump_version,

:postgresql_adapter_decode_dates,
:use_legacy_signed_id_verifier,
)

@@ -280,2 +285,9 @@

initializer "active_record.job_checkpoints" do
require "active_record/railties/job_checkpoints"
ActiveSupport.on_load(:active_job_continuable) do
prepend ActiveRecord::Railties::JobCheckpoints
end
end
initializer "active_record.set_reloader_hooks" do

@@ -326,5 +338,18 @@ ActiveSupport.on_load(:active_record) do

initializer "active_record.set_signed_id_verifier_secret" do
ActiveSupport.on_load(:active_record) do
self.signed_id_verifier_secret ||= -> { Rails.application.key_generator.generate_key("active_record/signed_id") }
initializer "active_record.filter_attributes_as_log_parameters" do |app|
ActiveRecord::FilterAttributeHandler.new(app).enable
end
initializer "active_record.configure_message_verifiers" do |app|
ActiveRecord.message_verifiers = app.message_verifiers
use_legacy_signed_id_verifier = app.config.active_record.use_legacy_signed_id_verifier
legacy_options = { digest: "SHA256", serializer: JSON, url_safe: true }
if use_legacy_signed_id_verifier == :generate_and_verify
app.message_verifiers.prepend { |salt| legacy_options if salt == "active_record/signed_id" }
elsif use_legacy_signed_id_verifier == :verify
app.message_verifiers.rotate { |salt| legacy_options if salt == "active_record/signed_id" }
elsif use_legacy_signed_id_verifier
raise ArgumentError, "Unrecognized value for config.active_record.use_legacy_signed_id_verifier: #{use_legacy_signed_id_verifier.inspect}"
end

@@ -395,2 +420,6 @@ end

end
if app.config.active_record.query_log_tags_prepend_comment
ActiveRecord::QueryLogs.prepend_comment = true
end
end

@@ -397,0 +426,0 @@ end

@@ -166,2 +166,14 @@ # frozen_string_literal: true

namespace :reset do
ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |name|
desc "Drop and recreate the #{name} database using migrations"
task name => :load_config do
db_namespace["drop:#{name}"].invoke
db_namespace["create:#{name}"].invoke
db_namespace["schema:dump:#{name}"].invoke
db_namespace["migrate:#{name}"].invoke
end
end
end
desc 'Run the "up" for a given migration VERSION.'

@@ -337,3 +349,3 @@ task up: :load_config do

pending_migrations = pending_migrations.flatten!
pending_migrations.flatten!

@@ -459,3 +471,3 @@ if pending_migrations.any?

task load: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(ENV["SCHEMA_FORMAT"], ENV["SCHEMA"])
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(nil, ENV["SCHEMA"])
end

@@ -465,3 +477,3 @@

ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |name|
desc "Create a database schema file (either db/schema.rb or db/structure.sql, depending on `ENV['SCHEMA_FORMAT']` or `config.active_record.schema_format`) for #{name} database"
desc "Create a database schema file (either db/schema.rb or db/structure.sql, depending on configuration) for #{name} database"
task name => :load_config do

@@ -481,3 +493,3 @@ ActiveRecord::Tasks::DatabaseTasks.with_temporary_pool_for_each(name: name) do |pool|

desc "Load a database schema file (either db/schema.rb or db/structure.sql, depending on configuration) into the #{name} database"
task name => "db:test:purge:#{name}" do
task name => [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.with_temporary_pool_for_each(name: name) do |pool|

@@ -484,0 +496,0 @@ db_config = pool.db_config

@@ -8,17 +8,16 @@ # frozen_string_literal: true

module JobRuntime # :nodoc:
private
def instrument(operation, payload = {}, &block)
if operation == :perform && block
super(operation, payload) do
db_runtime_before_perform = ActiveRecord::RuntimeRegistry.sql_runtime
result = block.call
payload[:db_runtime] = ActiveRecord::RuntimeRegistry.sql_runtime - db_runtime_before_perform
result
end
else
super
def instrument(operation, payload = {}, &block) # :nodoc:
if operation == :perform && block
super(operation, payload) do
db_runtime_before_perform = ActiveRecord::RuntimeRegistry.sql_runtime
result = block.call
payload[:db_runtime] = ActiveRecord::RuntimeRegistry.sql_runtime - db_runtime_before_perform
result
end
else
super
end
end
end
end
end

@@ -428,6 +428,10 @@ # frozen_string_literal: true

if active_record.name.demodulize == class_name
return compute_class("::#{class_name}") rescue NameError
begin
compute_class("::#{class_name}")
rescue NameError
compute_class(class_name)
end
else
compute_class(class_name)
end
compute_class(class_name)
end

@@ -520,2 +524,4 @@

super
@validated = false
@type = -(options[:foreign_type]&.to_s || "#{options[:as]}_type") if options[:as]

@@ -539,2 +545,4 @@ @foreign_type = -(options[:foreign_type]&.to_s || "#{name}_type") if options[:polymorphic]

@deprecated = !!options[:deprecated]
ensure_option_not_given_as_class!(:class_name)

@@ -622,2 +630,4 @@ end

def check_validity!
return if @validated
check_validity_of_inverse!

@@ -632,2 +642,4 @@

end
@validated = true
end

@@ -750,2 +762,6 @@

def deprecated?
@deprecated
end
private

@@ -984,2 +1000,4 @@ # Attempts to find the inverse association name automatically.

super()
@validated = false
@delegate_reflection = delegate_reflection

@@ -1148,2 +1166,4 @@ @klass = delegate_reflection.options[:anonymous_class]

def check_validity!
return if @validated
if through_reflection.nil?

@@ -1186,2 +1206,4 @@ raise HasManyThroughAssociationNotFoundError.new(active_record, self)

check_validity_of_inverse!
@validated = true
end

@@ -1207,2 +1229,6 @@

def deprecated_nested_reflections
@deprecated_nested_reflections ||= collect_deprecated_nested_reflections
end
protected

@@ -1232,2 +1258,15 @@ def actual_source_reflection # FIXME: this is a horrible name

def collect_deprecated_nested_reflections
result = []
[through_reflection, source_reflection].each do |reflection|
result << reflection if reflection.deprecated?
# Both the through and the source reflections could be through
# themselves. Nesting can go an arbitrary number of levels down.
if reflection.through_reflection?
result.concat(reflection.deprecated_nested_reflections)
end
end
result
end
delegate_methods = AssociationReflection.public_instance_methods -

@@ -1234,0 +1273,0 @@ public_instance_methods

@@ -63,3 +63,3 @@ # frozen_string_literal: true

CLAUSE_METHODS = [:where, :having, :from]
INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :with, :with_recursive]
INVALID_METHODS_FOR_UPDATE_AND_DELETE_ALL = [:distinct, :with, :with_recursive]

@@ -311,3 +311,3 @@ VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS

# Like #find_or_create_by, but calls {new}[rdoc-ref:Core.new]
# Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
# instead of {create}[rdoc-ref:Persistence::ClassMethods#create].

@@ -605,2 +605,14 @@ def find_or_initialize_by(attributes, &block)

invalid_methods = INVALID_METHODS_FOR_UPDATE_AND_DELETE_ALL.select do |method|
value = @values[method]
method == :distinct ? value : value&.any?
end
if invalid_methods.any?
ActiveRecord.deprecator.warn <<~MESSAGE
`#{invalid_methods.join(', ')}` is not supported by `update_all` and was never included in the generated query.
Calling `#{invalid_methods.join(', ')}` with `update_all` will raise an error in Rails 8.2.
MESSAGE
end
if updates.is_a?(Hash)

@@ -619,7 +631,5 @@ if model.locking_enabled? &&

model.with_connection do |c|
arel = eager_loading? ? apply_join_dependency.arel : build_arel(c)
arel = eager_loading? ? apply_join_dependency.arel : arel()
arel.source.left = table
group_values_arel_columns = arel_columns(group_values.uniq)
having_clause_ast = having_clause.ast unless having_clause.empty?
key = if model.composite_primary_key?

@@ -630,3 +640,3 @@ primary_key.map { |pk| table[pk] }

end
stmt = arel.compile_update(values, key, having_clause_ast, group_values_arel_columns)
stmt = arel.compile_update(values, key)
c.update(stmt, "#{model} Update All").tap { reset }

@@ -1018,3 +1028,3 @@ end

#
# This call deletes the affected posts all at once with a single DELETE statement.
# Both calls delete the affected posts all at once with a single DELETE statement.
# If you need to destroy dependent associations or call your <tt>before_*</tt> or

@@ -1030,3 +1040,3 @@ # +after_destroy+ callbacks, use the #destroy_all method instead.

invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
invalid_methods = INVALID_METHODS_FOR_UPDATE_AND_DELETE_ALL.select do |method|
value = @values[method]

@@ -1040,7 +1050,5 @@ method == :distinct ? value : value&.any?

model.with_connection do |c|
arel = eager_loading? ? apply_join_dependency.arel : build_arel(c)
arel = eager_loading? ? apply_join_dependency.arel : arel()
arel.source.left = table
group_values_arel_columns = arel_columns(group_values.uniq)
having_clause_ast = having_clause.ast unless having_clause.empty?
key = if model.composite_primary_key?

@@ -1051,3 +1059,3 @@ primary_key.map { |pk| table[pk] }

end
stmt = arel.compile_delete(key, having_clause_ast, group_values_arel_columns)
stmt = arel.compile_delete(key)

@@ -1417,3 +1425,3 @@ c.delete(stmt, "#{model} Delete All").tap { reset }

bind = predicate_builder.build_bind_attribute(attribute.name, value.abs)
expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attribute), 0)
expr = table.coalesce(attribute, 0)
expr = value < 0 ? expr - bind : expr + bind

@@ -1424,2 +1432,6 @@ expr.expr

def exec_queries(&block)
if lock_value && model.current_preventing_writes
raise ActiveRecord::ReadOnlyError, "Lock query attempted while in readonly mode"
end
skip_query_cache_if_necessary do

@@ -1464,3 +1476,3 @@ rows = if scheduled?

@_join_dependency = join_dependency
c.select_all(relation.arel, "SQL", async: async)
c.select_all(relation.arel, "#{model.name} Eager Load", async: async)
end

@@ -1467,0 +1479,0 @@ end

@@ -438,11 +438,23 @@ # frozen_string_literal: true

values = records.pluck(*cursor)
yielded_relation = where(cursor => values)
values_size = values.size
values_last = values.last
yielded_relation = where(cursor => values).order(batch_orders.to_h)
yielded_relation.load_records(records)
elsif (empty_scope && use_ranges != false) || use_ranges
values = batch_relation.pluck(*cursor)
# Efficiently peak at the last value for the next batch using offset and limit.
values_size = batch_limit
values_last = batch_relation.offset(batch_limit - 1).pick(*cursor)
finish = values.last
if finish
yielded_relation = apply_finish_limit(batch_relation, cursor, finish, batch_orders)
yielded_relation = yielded_relation.except(:limit, :order)
# If the last value is not found using offset, there is at most one more batch of size < batch_limit.
# Retry by getting the whole list of remaining values so that we have the exact size and last value.
unless values_last
values = batch_relation.pluck(*cursor)
values_size = values.size
values_last = values.last
end
# Finally, build the yielded relation if at least one value found.
if values_last
yielded_relation = apply_finish_limit(batch_relation, cursor, values_last, batch_orders)
yielded_relation = yielded_relation.except(:limit).reorder(batch_orders.to_h)
yielded_relation.skip_query_cache!(false)

@@ -452,8 +464,10 @@ end

values = batch_relation.pluck(*cursor)
yielded_relation = where(cursor => values)
values_size = values.size
values_last = values.last
yielded_relation = where(cursor => values).order(batch_orders.to_h)
end
break if values.empty?
break if values_size == 0
if values.flatten.any?(nil)
if [values_last].flatten.any?(nil)
raise ArgumentError, "Not all of the batch cursor columns were included in the custom select clause "\

@@ -465,6 +479,6 @@ "or some columns contain nil."

break if values.length < batch_limit
break if values_size < batch_limit
if limit_value
remaining -= values.length
remaining -= values_size

@@ -487,3 +501,3 @@ if remaining == 0

cursor_value = values.last
cursor_value = values_last
batch_relation = batch_condition(relation, cursor, cursor_value, operators)

@@ -490,0 +504,0 @@ end

@@ -289,2 +289,7 @@ # frozen_string_literal: true

#
# Be aware that #pluck ignores any previous select clauses
#
# Person.select(:name).pluck(:id)
# # SELECT people.id FROM people
#
# See also #ids.

@@ -318,3 +323,3 @@ def pluck(*column_names)

result = skip_query_cache_if_necessary do
if where_clause.contradiction?
if where_clause.contradiction? && !possible_aggregation?(column_names)
ActiveRecord::Result.empty(async: @async)

@@ -422,3 +427,3 @@ else

else
arel_column(column_name)
arel_column(column_name.to_s)
end

@@ -467,2 +472,12 @@ end

def possible_aggregation?(column_names)
column_names.all? do |column_name|
if column_name.is_a?(String)
column_name.include?("(")
else
Arel.arel_node?(column_name)
end
end
end
def operation_over_aggregate_column(column, operation, distinct)

@@ -519,3 +534,2 @@ operation == "count" ? column.count(distinct) : column.public_send(operation)

group_fields = group_values
group_fields = group_fields.uniq if group_fields.size > 1

@@ -543,3 +557,3 @@ if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)

select_value = operation_over_aggregate_column(column, operation, distinct)
select_value.as(model.adapter_class.quote_column_name(column_alias))
select_value = select_value.as(model.adapter_class.quote_column_name(column_alias))

@@ -671,2 +685,3 @@ select_values = [select_value]

relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
relation.unscope!(:order)
else

@@ -680,9 +695,5 @@ column_alias = Arel.sql("count_column")

if column_name == :all
relation.unscope(:order).build_subquery(subquery_alias, select_value)
else
relation.build_subquery(subquery_alias, select_value)
end
relation.build_subquery(subquery_alias, select_value)
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/module/delegation"

@@ -5,0 +4,0 @@ module ActiveRecord

@@ -144,3 +144,3 @@ # frozen_string_literal: true

def sole
found, undesired = first(2)
found, undesired = take(2)

@@ -152,3 +152,3 @@ if found.nil?

else
raise ActiveRecord::SoleRecordExceeded.new(self)
raise ActiveRecord::SoleRecordExceeded.new(model)
end

@@ -447,3 +447,3 @@ end

else
relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
relation = except(:select, :distinct, :order)._select!(Arel.sql(ONE_AS_ONE, retryable: true)).limit!(1)
end

@@ -645,4 +645,23 @@

def ordered_relation
if order_values.empty? && (model.implicit_order_column || !model.query_constraints_list.nil? || primary_key)
order(_order_columns.map { |column| table[column].asc })
if order_values.empty?
if !_order_columns.empty?
return order(_order_columns.map { |column| table[column].asc })
end
if ActiveRecord.raise_on_missing_required_finder_order_columns
raise MissingRequiredOrderError, <<~MSG.squish
Relation has no order values, and #{model} has no order columns to use as a default.
Set at least one of `implicit_order_column`, `query_constraints` or `primary_key` on
the model when no `order `is specified on the relation.
MSG
else
ActiveRecord.deprecator.warn(<<~MSG)
Calling order dependent finder methods (e.g. `#first`, `#second`) without `order` values on the relation,
and on a model (#{model}) that does not have any order columns (`implicit_order_column`, `query_constraints`,
or `primary_key`) to fall back on is deprecated and will raise `ActiveRecord::MissingRequiredOrderError`
in Rails 8.2.
MSG
self
end
else

@@ -654,14 +673,11 @@ self

def _order_columns
oc = []
columns = Array(model.implicit_order_column)
oc << model.implicit_order_column if model.implicit_order_column
oc << model.query_constraints_list if model.query_constraints_list
return columns.compact if columns.length.positive? && columns.last.nil?
if model.primary_key && model.query_constraints_list.nil?
oc << model.primary_key
end
columns += Array(model.query_constraints_list || model.primary_key)
oc.flatten.uniq.compact
columns.uniq.compact
end
end
end

@@ -88,5 +88,5 @@ # frozen_string_literal: true

if other.model == relation.model
relation.select_values |= other.select_values
relation.select_values += other.select_values if relation.select_values != other.select_values
else
relation.select_values |= other.instance_eval do
relation.select_values += other.instance_eval do
arel_columns(select_values)

@@ -93,0 +93,0 @@ end

@@ -40,3 +40,3 @@ # frozen_string_literal: true

# The handler can be any object that responds to +call+, and will be used
# for any value that +===+ the class given. For example:
# for any value that <tt>===</tt> the class given. For example:
#

@@ -86,3 +86,3 @@ # MyCustomDateRange = Struct.new(:start, :end)

def expand_from_hash(attributes, &block)
return ["1=0"] if attributes.empty?
return [Arel.sql("1=0", retryable: true)] if attributes.empty?

@@ -89,0 +89,0 @@ attributes.flat_map do |key, value|

@@ -34,5 +34,3 @@ # frozen_string_literal: true

array_predicates = ranges.map! { |range| predicate_builder.build(attribute, range) }
values_predicate.or(
Arel::Nodes::Grouping.new Arel::Nodes::Or.new(array_predicates)
)
array_predicates.inject(values_predicate, &:or)
end

@@ -39,0 +37,0 @@ end

@@ -18,3 +18,5 @@ # frozen_string_literal: true

elsif @type.mutable? # If the type is simply mutable, we deep_dup it.
@value_before_type_cast = @value_before_type_cast.deep_dup
unless @value_before_type_cast.frozen?
@value_before_type_cast = @value_before_type_cast.deep_dup
end
end

@@ -21,0 +23,0 @@ end

@@ -179,4 +179,2 @@ # frozen_string_literal: true

def except_predicates(columns)
return predicates if columns.empty?
attrs = columns.extract! { |node| node.is_a?(Arel::Attribute) }

@@ -186,3 +184,3 @@ non_attrs = columns.extract! { |node| node.is_a?(Arel::Predications) }

predicates.reject do |node|
if !non_attrs.empty? && equality_node?(node) && node.left.is_a?(Arel::Predications)
if !non_attrs.empty? && node.equality? && node.left.is_a?(Arel::Predications)
non_attrs.include?(node.left)

@@ -199,3 +197,3 @@ end || Arel.fetch_attribute(node) do |attr|

when Arel::Nodes::SqlLiteral, ::String
wrap_sql_literal(node)
Arel::Nodes::Grouping.new(node)
else node

@@ -211,9 +209,2 @@ end

def wrap_sql_literal(node)
if ::String === node
node = Arel.sql(node)
end
Arel::Nodes::Grouping.new(node)
end
def extract_node_value(node)

@@ -220,0 +211,0 @@ if node.respond_to?(:value_before_type_cast)

@@ -32,2 +32,7 @@ # frozen_string_literal: true

#
# # Get the number of rows affected by the query:
# result = ActiveRecord::Base.lease_connection.exec_query('INSERT INTO posts (title, body) VALUES ("title_3", "body_3"), ("title_4", "body_4")')
# result.affected_rows
# # => 2
#
# # ActiveRecord::Result also includes Enumerable.

@@ -93,13 +98,13 @@ # result.each do |row|

attr_reader :columns, :rows, :column_types
attr_reader :columns, :rows, :affected_rows
def self.empty(async: false) # :nodoc:
def self.empty(async: false, affected_rows: nil) # :nodoc:
if async
EMPTY_ASYNC
FutureResult.wrap(new(EMPTY_ARRAY, EMPTY_ARRAY, EMPTY_HASH, affected_rows: affected_rows)).freeze
else
EMPTY
new(EMPTY_ARRAY, EMPTY_ARRAY, EMPTY_HASH, affected_rows: affected_rows).freeze
end
end
def initialize(columns, rows, column_types = nil)
def initialize(columns, rows, column_types = nil, affected_rows: nil)
# We freeze the strings to prevent them getting duped when

@@ -110,4 +115,6 @@ # used as keys in ActiveRecord::Base's @attributes hash

@hash_rows = nil
@column_types = column_types || EMPTY_HASH
@column_types = column_types.freeze
@types_hash = nil
@column_indexes = nil
@affected_rows = affected_rows
end

@@ -160,2 +167,20 @@

# Returns the +ActiveRecord::Type+ type of all columns.
# Note that not all database adapters return the result types,
# so the hash may be empty.
def column_types
if @column_types
@types_hash ||= begin
types = {}
@columns.each_with_index do |name, index|
type = @column_types[index] || Type.default_value
types[name] = types[index] = type
end
types.freeze
end
else
EMPTY_HASH
end
end
def result # :nodoc:

@@ -169,3 +194,3 @@ self

def cast_values(type_overrides = {}) # :nodoc:
def cast_values(type_overrides = nil) # :nodoc:
if columns.one?

@@ -198,3 +223,2 @@ # Separated to avoid allocating an array per row

@rows = rows.dup
@column_types = column_types.dup
@hash_rows = nil

@@ -205,3 +229,4 @@ end

hash_rows.freeze
indexed_rows.freeze
indexed_rows
column_types
super

@@ -214,3 +239,3 @@ end

hash = {}
length = columns.length
length = columns.length
while index < length

@@ -233,6 +258,10 @@ hash[columns[index]] = index

def column_type(name, index, type_overrides)
type_overrides.fetch(name) do
column_types.fetch(index) do
column_types.fetch(name, Type.default_value)
if type_overrides
type_overrides.fetch(name) do
column_type(name, index, nil)
end
elsif @column_types
@column_types[index] || Type.default_value
else
Type.default_value
end

@@ -249,12 +278,6 @@ end

empty_array = [].freeze
EMPTY_ARRAY = [].freeze
EMPTY_HASH = {}.freeze
private_constant :EMPTY_HASH
EMPTY = new(empty_array, empty_array, EMPTY_HASH).freeze
private_constant :EMPTY
EMPTY_ASYNC = FutureResult.wrap(EMPTY).freeze
private_constant :EMPTY_ASYNC
private_constant :EMPTY_ARRAY, :EMPTY_HASH
end
end

@@ -164,2 +164,4 @@ # frozen_string_literal: true

# # => "role = '0'"
#
# Before using this method, please consider if Arel.sql would be better for your use-case
def sanitize_sql_array(ary)

@@ -166,0 +168,0 @@ statement, *values = ary

@@ -168,3 +168,3 @@ # frozen_string_literal: true

tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
tbl.print " create_table #{relation_name(remove_prefix_and_suffix(table)).inspect}"

@@ -196,3 +196,3 @@ case pk

# then dump all non-primary key columns
columns.each do |column|
columns.sort_by(&:name).each do |column|
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)

@@ -238,3 +238,3 @@ next if column.name == pk

table_name = remove_prefix_and_suffix(index.table).inspect
" add_index #{([table_name] + index_parts(index)).join(', ')}"
" add_index #{([relation_name(table_name)] + index_parts(index)).join(', ')}"
end

@@ -283,2 +283,3 @@

index_parts << "comment: #{index.comment.inspect}" if index.comment
index_parts << "enabled: #{index.enabled.inspect}" if @connection.supports_disabling_indexes? && index.disabled?
index_parts

@@ -324,4 +325,4 @@ end

parts = [
"add_foreign_key #{remove_prefix_and_suffix(foreign_key.from_table).inspect}",
remove_prefix_and_suffix(foreign_key.to_table).inspect,
relation_name(remove_prefix_and_suffix(foreign_key.from_table)).inspect,
relation_name(remove_prefix_and_suffix(foreign_key.to_table)).inspect,
]

@@ -337,6 +338,3 @@

if foreign_key.export_name_on_schema_dump?
parts << "name: #{foreign_key.name.inspect}"
end
parts << "name: #{foreign_key.name.inspect}" if foreign_key.export_name_on_schema_dump?
parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update

@@ -347,3 +345,3 @@ parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete

" #{parts.join(', ')}"
" add_foreign_key #{parts.join(', ')}"
end

@@ -373,2 +371,6 @@

def relation_name(name)
name
end
def remove_prefix_and_suffix(table)

@@ -375,0 +377,0 @@ # This method appears at the top when profiling active_record test cases run.

# frozen_string_literal: true
require "active_support/core_ext/module/delegation"

@@ -5,0 +4,0 @@ module ActiveRecord

@@ -9,2 +9,4 @@ # frozen_string_literal: true

included do
class_attribute :_signed_id_verifier, instance_accessor: false, instance_predicate: false
##

@@ -15,2 +17,16 @@ # :singleton-method:

class_attribute :signed_id_verifier_secret, instance_writer: false
module DeprecateSignedIdVerifierSecret
def signed_id_verifier_secret=(secret)
ActiveRecord.deprecator.warn(<<~MSG)
ActiveRecord::Base.signed_id_verifier_secret is deprecated and will be removed in Rails 8.2.
If the secret is model-specific, set Model.signed_id_verifier instead.
Otherwise, configure Rails.application.message_verifiers (or ActiveRecord.message_verifiers) with the secret.
MSG
super
end
end
singleton_class.prepend DeprecateSignedIdVerifierSecret
end

@@ -54,6 +70,7 @@

# User.find_signed signed_id, purpose: :password_reset # => User.first
def find_signed(signed_id, purpose: nil)
def find_signed(signed_id, purpose: nil, on_rotation: nil)
raise UnknownPrimaryKey.new(self) if primary_key.nil?
if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose))
options = { on_rotation: on_rotation }.compact
if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
find_by primary_key => id

@@ -75,4 +92,5 @@ end

# User.find_signed! signed_id # => ActiveRecord::RecordNotFound
def find_signed!(signed_id, purpose: nil)
if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose))
def find_signed!(signed_id, purpose: nil, on_rotation: nil)
options = { on_rotation: on_rotation }.compact
if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
find(id)

@@ -82,16 +100,22 @@ end

# The verifier instance that all signed ids are generated and verified from. By default, it'll be initialized
# with the class-level +signed_id_verifier_secret+, which within Rails comes from
# {Rails.application.key_generator}[rdoc-ref:Rails::Application#key_generator].
# By default, it's SHA256 for the digest and JSON for the serialization.
def signed_id_verifier
@signed_id_verifier ||= begin
secret = signed_id_verifier_secret
secret = secret.call if secret.respond_to?(:call)
if signed_id_verifier_secret
@signed_id_verifier ||= begin
secret = signed_id_verifier_secret
secret = secret.call if secret.respond_to?(:call)
if secret.nil?
raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed ids"
else
if secret.nil?
raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed IDs"
end
ActiveSupport::MessageVerifier.new secret, digest: "SHA256", serializer: JSON, url_safe: true
end
else
return _signed_id_verifier if _signed_id_verifier
if ActiveRecord.message_verifiers.nil?
raise "You must set ActiveRecord.message_verifiers to use signed IDs"
end
ActiveRecord.message_verifiers["active_record/signed_id"]
end

@@ -104,3 +128,7 @@ end

def signed_id_verifier=(verifier)
@signed_id_verifier = verifier
if signed_id_verifier_secret
@signed_id_verifier = verifier
else
self._signed_id_verifier = verifier
end
end

@@ -107,0 +135,0 @@

@@ -34,4 +34,7 @@ # frozen_string_literal: true

class Query # :nodoc:
def initialize(sql)
attr_reader :retryable
def initialize(sql, retryable:)
@sql = sql
@retryable = retryable
end

@@ -45,3 +48,3 @@

class PartialQuery < Query # :nodoc:
def initialize(values)
def initialize(values, retryable:)
@values = values

@@ -51,2 +54,3 @@ @indexes = values.each_with_index.find_all { |thing, i|

}.map(&:last)
@retryable = retryable
end

@@ -100,8 +104,8 @@

def self.query(sql)
Query.new(sql)
def self.query(...)
Query.new(...)
end
def self.partial_query(values)
PartialQuery.new(values)
def self.partial_query(...)
PartialQuery.new(...)
end

@@ -149,3 +153,3 @@

def execute(params, connection, allow_retry: false, async: false, &block)
def execute(params, connection, async: false, &block)
bind_values = @bind_map.bind params

@@ -155,5 +159,5 @@ sql = @query_builder.sql_for bind_values, connection

if async
@model.async_find_by_sql(sql, bind_values, preparable: true, allow_retry: allow_retry, &block)
@model.async_find_by_sql(sql, bind_values, preparable: true, allow_retry: @query_builder.retryable, &block)
else
@model.find_by_sql(sql, bind_values, preparable: true, allow_retry: allow_retry, &block)
@model.find_by_sql(sql, bind_values, preparable: true, allow_retry: @query_builder.retryable, &block)
end

@@ -160,0 +164,0 @@ rescue ::RangeError

@@ -149,3 +149,4 @@ # frozen_string_literal: true

prev_store, new_store = changes[store_attribute]
prev_store&.dig(key) != new_store&.dig(key)
accessor = store_accessor_for(store_attribute)
accessor.get(prev_store, key) != accessor.get(new_store, key)
end

@@ -156,3 +157,4 @@

prev_store, new_store = changes[store_attribute]
[prev_store&.dig(key), new_store&.dig(key)]
accessor = store_accessor_for(store_attribute)
[accessor.get(prev_store, key), accessor.get(new_store, key)]
end

@@ -163,3 +165,4 @@

prev_store, _new_store = changes[store_attribute]
prev_store&.dig(key)
accessor = store_accessor_for(store_attribute)
accessor.get(prev_store, key)
end

@@ -170,3 +173,4 @@

prev_store, new_store = saved_changes[store_attribute]
prev_store&.dig(key) != new_store&.dig(key)
accessor = store_accessor_for(store_attribute)
accessor.get(prev_store, key) != accessor.get(new_store, key)
end

@@ -177,3 +181,4 @@

prev_store, new_store = saved_changes[store_attribute]
[prev_store&.dig(key), new_store&.dig(key)]
accessor = store_accessor_for(store_attribute)
[accessor.get(prev_store, key), accessor.get(new_store, key)]
end

@@ -184,3 +189,4 @@

prev_store, _new_store = saved_changes[store_attribute]
prev_store&.dig(key)
accessor = store_accessor_for(store_attribute)
accessor.get(prev_store, key)
end

@@ -234,14 +240,27 @@ end

class HashAccessor # :nodoc:
def self.get(store_object, key)
if store_object
store_object[key]
end
end
def self.read(object, attribute, key)
prepare(object, attribute)
object.public_send(attribute)[key]
store_object = prepare(object, attribute)
store_object[key]
end
def self.write(object, attribute, key, value)
prepare(object, attribute)
object.public_send(attribute)[key] = value if value != read(object, attribute, key)
store_object = prepare(object, attribute)
store_object[key] = value if value != store_object[key]
end
def self.prepare(object, attribute)
object.public_send :"#{attribute}=", {} unless object.send(attribute)
store_object = object.public_send(attribute)
if store_object.nil?
store_object = {}
object.public_send(:"#{attribute}=", store_object)
end
store_object
end

@@ -251,8 +270,12 @@ end

class StringKeyedHashAccessor < HashAccessor # :nodoc:
def self.get(store_object, key)
super store_object, Symbol === key ? key.name : key.to_s
end
def self.read(object, attribute, key)
super object, attribute, key.to_s
super object, attribute, Symbol === key ? key.name : key.to_s
end
def self.write(object, attribute, key, value)
super object, attribute, key.to_s, value
super object, attribute, Symbol === key ? key.name : key.to_s, value
end

@@ -262,9 +285,11 @@ end

class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc:
def self.prepare(object, store_attribute)
attribute = object.send(store_attribute)
unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
attribute = IndifferentCoder.as_indifferent_hash(attribute)
object.public_send :"#{store_attribute}=", attribute
def self.prepare(object, attribute)
store_object = object.public_send(attribute)
unless store_object.is_a?(ActiveSupport::HashWithIndifferentAccess)
store_object = IndifferentCoder.as_indifferent_hash(store_object)
object.public_send :"#{attribute}=", store_object
end
attribute
store_object
end

@@ -271,0 +296,0 @@ end

@@ -69,3 +69,3 @@ # frozen_string_literal: true

configs_for(env_name: environment).each do |db_config|
check_current_protected_environment!(db_config)
database_adapter_for(db_config).check_current_protected_environment!(db_config, migration_class)
end

@@ -151,4 +151,2 @@ end

database_configs.each do |db_config|
next unless db_config.database_tasks?
yield db_config.name

@@ -458,3 +456,3 @@ end

File.open(filename, "a") do |f|
f.puts migration_connection.dump_schema_information
f.puts migration_connection.dump_schema_versions
f.print "\n"

@@ -646,19 +644,2 @@ end

def check_current_protected_environment!(db_config)
with_temporary_pool(db_config) do |pool|
migration_context = pool.migration_context
current = migration_context.current_environment
stored = migration_context.last_stored_environment
if migration_context.protected_environment?
raise ActiveRecord::ProtectedEnvironmentError.new(stored)
end
if stored && stored != current
raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored)
end
rescue ActiveRecord::NoDatabaseError
end
end
def initialize_database(db_config)

@@ -665,0 +646,0 @@ with_temporary_pool(db_config) do

@@ -5,12 +5,3 @@ # frozen_string_literal: true

module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
def self.using_database_configurations?
true
end
def initialize(db_config)
@db_config = db_config
@configuration_hash = db_config.configuration_hash
end
class MySQLDatabaseTasks < AbstractTasks # :nodoc:
def create

@@ -37,6 +28,2 @@ establish_connection(configuration_hash_without_database)

def collation
connection.collation
end
def structure_dump(filename, extra_flags)

@@ -58,3 +45,3 @@ args = prepare_command_options

run_cmd("mysqldump", args, "dumping")
run_cmd("mysqldump", *args)
end

@@ -68,20 +55,6 @@

run_cmd("mysql", args, "loading")
run_cmd("mysql", *args)
end
private
attr_reader :db_config, :configuration_hash
def connection
ActiveRecord::Base.lease_connection
end
def establish_connection(config = db_config)
ActiveRecord::Base.establish_connection(config)
end
def configuration_hash_without_database
configuration_hash.merge(database: nil)
end
def creation_options

@@ -112,14 +85,4 @@ Hash.new.tap do |options|

end
def run_cmd(cmd, args, action)
fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
end
def run_cmd_error(cmd, args, action)
msg = +"failed to execute: `#{cmd}`\n"
msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
msg
end
end
end
end

@@ -7,3 +7,3 @@ # frozen_string_literal: true

module Tasks # :nodoc:
class PostgreSQLDatabaseTasks # :nodoc:
class PostgreSQLDatabaseTasks < AbstractTasks # :nodoc:
DEFAULT_ENCODING = ENV["CHARSET"] || "utf8"

@@ -13,11 +13,2 @@ ON_ERROR_STOP_1 = "ON_ERROR_STOP=1"

def self.using_database_configurations?
true
end
def initialize(db_config)
@db_config = db_config
@configuration_hash = db_config.configuration_hash
end
def create(connection_already_established = false)

@@ -34,10 +25,2 @@ establish_connection(public_schema_config) unless connection_already_established

def charset
connection.encoding
end
def collation
connection.collation
end
def purge

@@ -78,3 +61,3 @@ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)

args << db_config.database
run_cmd("pg_dump", args, "dumping")
run_cmd("pg_dump", *args)
remove_sql_header_comments(filename)

@@ -89,16 +72,6 @@ File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" }

args << db_config.database
run_cmd("psql", args, "loading")
run_cmd("psql", *args)
end
private
attr_reader :db_config, :configuration_hash
def connection
ActiveRecord::Base.lease_connection
end
def establish_connection(config = db_config)
ActiveRecord::Base.establish_connection(config)
end
def encoding

@@ -125,13 +98,6 @@ configuration_hash[:encoding] || DEFAULT_ENCODING

def run_cmd(cmd, args, action)
fail run_cmd_error(cmd, args, action) unless Kernel.system(psql_env, cmd, *args)
def run_cmd(cmd, *args, **opts)
fail run_cmd_error(cmd, args) unless Kernel.system(psql_env, cmd, *args, **opts)
end
def run_cmd_error(cmd, args, action)
msg = +"failed to execute:\n"
msg << "#{cmd} #{args.join(' ')}\n\n"
msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
msg
end
def remove_sql_header_comments(filename)

@@ -138,0 +104,0 @@ removing_comments = true

@@ -5,7 +5,3 @@ # frozen_string_literal: true

module Tasks # :nodoc:
class SQLiteDatabaseTasks # :nodoc:
def self.using_database_configurations?
true
end
class SQLiteDatabaseTasks < AbstractTasks # :nodoc:
def initialize(db_config, root = ActiveRecord::Tasks::DatabaseTasks.root)

@@ -41,6 +37,2 @@ @db_config = db_config

def charset
connection.encoding
end
def structure_dump(filename, extra_flags)

@@ -59,3 +51,4 @@ args = []

end
run_cmd("sqlite3", args, filename)
run_cmd("sqlite3", *args, out: filename)
end

@@ -68,9 +61,15 @@

def check_current_protected_environment!(db_config, migration_class)
super
rescue ActiveRecord::StatementInvalid => e
case e.cause
when SQLite3::ReadOnlyException
else
raise e
end
end
private
attr_reader :db_config, :root
attr_reader :root
def connection
ActiveRecord::Base.lease_connection
end
def establish_connection(config = db_config)

@@ -80,15 +79,4 @@ ActiveRecord::Base.establish_connection(config)

end
def run_cmd(cmd, args, out)
fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out)
end
def run_cmd_error(cmd, args)
msg = +"failed to execute:\n"
msg << "#{cmd} #{args.join(' ')}\n\n"
msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
msg
end
end
end
end

@@ -7,4 +7,12 @@ # frozen_string_literal: true

module TestDatabases # :nodoc:
ActiveSupport::Testing::Parallelization.before_fork_hook do
if ActiveSupport.parallelize_test_databases
ActiveRecord::Base.connection_handler.clear_all_connections!
end
end
ActiveSupport::Testing::Parallelization.after_fork_hook do |i|
create_and_load_schema(i, env_name: ActiveRecord::ConnectionHandling::DEFAULT_ENV.call)
if ActiveSupport.parallelize_test_databases
create_and_load_schema(i, env_name: ActiveRecord::ConnectionHandling::DEFAULT_ENV.call)
end
end

@@ -16,3 +24,3 @@

ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config|
db_config._database = "#{db_config.database}-#{i}"
db_config._database = "#{db_config.database}_#{i}"

@@ -19,0 +27,0 @@ ActiveRecord::Tasks::DatabaseTasks.reconstruct_from_schema(db_config, nil)

@@ -39,2 +39,3 @@ # frozen_string_literal: true

class_attribute :fixture_sets, default: {}
class_attribute :database_transactions_config, default: {}

@@ -45,2 +46,16 @@ ActiveSupport.run_load_hooks(:active_record_fixtures, self)

module ClassMethods
# Do not use transactional tests for the given database. This overrides
# the default setting as defined by `use_transactional_tests`, which
# applies to all database connection pools not explicitly configured here.
def skip_transactional_tests_for_database(database_name)
use_transactional_tests_for_database(database_name, false)
end
# Enable or disable transactions per database. This overrides the default
# setting as defined by `use_transactional_tests`, which applies to all
# database connection pools not explicitly configured here.
def use_transactional_tests_for_database(database_name, enabled = true)
self.database_transactions_config = database_transactions_config.merge(database_name => enabled)
end
# Sets the model class for a fixture when the class name cannot be inferred from the fixture name.

@@ -111,3 +126,4 @@ #

def run_in_transaction?
use_transactional_tests &&
has_explicit_config = database_transactions_config.any? { |_, enabled| enabled }
(use_transactional_tests || has_explicit_config) &&
!self.class.uses_transaction?(name)

@@ -175,2 +191,6 @@ end

def transactional_tests_for_pool?(pool)
database_transactions_config.fetch(pool.db_config.name.to_sym, use_transactional_tests)
end
def setup_transactional_fixtures

@@ -181,2 +201,6 @@ setup_shared_connection_pool

@fixture_connection_pools = ActiveRecord::Base.connection_handler.connection_pool_list(:writing)
# Filter to pools that want to use transactions
@fixture_connection_pools.select! { |pool| transactional_tests_for_pool?(pool) }
@fixture_connection_pools.each do |pool|

@@ -197,3 +221,4 @@ pool.pin_connection!(lock_threads)

unless @fixture_connection_pools.include?(pool)
# Don't begin a transaction if we've already done so, or are not using them for this pool
if !@fixture_connection_pools.include?(pool) && transactional_tests_for_pool?(pool)
pool.pin_connection!(lock_threads)

@@ -200,0 +225,0 @@ pool.lease_connection

@@ -14,8 +14,14 @@ # frozen_string_literal: true

#
# If the +:include_schema+ option is provided, any queries (including schema related) are counted.
# Any unmaterialized transactions will be materialized to ensure only
# queries attempted intside the block are counted.
#
# If the +:include_schema+ option is provided, any queries (including
# schema related) are counted. Setting this option also skips leasing a
# connection to materialize pending transactions since we want to count
# queries executed at connection open (e.g., type map).
#
# assert_queries_count(1, include_schema: true) { Post.columns }
#
def assert_queries_count(count = nil, include_schema: false, &block)
ActiveRecord::Base.lease_connection.materialize_transactions
ActiveRecord::Base.lease_connection.materialize_transactions unless include_schema

@@ -22,0 +28,0 @@ counter = SQLCounter.new

@@ -172,4 +172,6 @@ # frozen_string_literal: true

all_timestamp_attributes_in_model.each do |attribute_name|
self[attribute_name] = nil
clear_attribute_change(attribute_name)
if self[attribute_name]
self[attribute_name] = nil
clear_attribute_change(attribute_name)
end
end

@@ -176,0 +178,0 @@ end

@@ -29,3 +29,3 @@ # frozen_string_literal: true

#
# Closed transactions are `blank?` too.
# Closed transactions are +blank?+ too.
#

@@ -105,5 +105,2 @@ # == Callbacks

# the block is never called.
#
# If the transaction is already finalized, attempting to register a callback
# will raise ActiveRecord::ActiveRecordError.
def after_rollback(&block)

@@ -120,3 +117,3 @@ @internal_transaction&.after_rollback(&block)

def closed?
@internal_transaction.nil? || @internal_transaction.state.finalized?
@internal_transaction.nil? || @internal_transaction.closed?
end

@@ -123,0 +120,0 @@

@@ -233,6 +233,26 @@ # frozen_string_literal: true

with_connection do |connection|
connection.transaction(**options, &block)
connection.pool.with_pool_transaction_isolation_level(ActiveRecord.default_transaction_isolation_level, connection.transaction_open?) do
connection.transaction(**options, &block)
end
end
end
# Makes all transactions the current pool use the isolation level initiated within the block.
def with_pool_transaction_isolation_level(isolation_level, &block)
if current_transaction.open?
raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open"
end
old_level = connection_pool.pool_transaction_isolation_level
connection_pool.pool_transaction_isolation_level = isolation_level
yield
ensure
connection_pool.pool_transaction_isolation_level = old_level
end
# Returns the default isolation level for the connection pool, set earlier by #with_pool_transaction_isolation_level.
def pool_transaction_isolation_level
connection_pool.pool_transaction_isolation_level
end
# Returns a representation of the current transaction state,

@@ -411,14 +431,16 @@ # which can be a top level transaction, a savepoint, or the absence of a transaction.

self.class.with_connection do |connection|
status = nil
ensure_finalize = !connection.transaction_open?
connection.pool.with_pool_transaction_isolation_level(ActiveRecord.default_transaction_isolation_level, connection.transaction_open?) do
status = nil
ensure_finalize = !connection.transaction_open?
connection.transaction do
add_to_transaction(ensure_finalize || has_transactional_callbacks?)
remember_transaction_record_state
connection.transaction do
add_to_transaction(ensure_finalize || has_transactional_callbacks?)
remember_transaction_record_state
status = yield
raise ActiveRecord::Rollback unless status
status = yield
raise ActiveRecord::Rollback unless status
end
@_last_transaction_return_status = status
status
end
@_last_transaction_return_status = status
status
end

@@ -425,0 +447,0 @@ end

@@ -22,3 +22,4 @@ # frozen_string_literal: true

if column
type = @klass.with_connection { |connection| connection.lookup_cast_type_from_column(column) }
# TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
type = column.fetch_cast_type(@klass.lease_connection)
end

@@ -25,0 +26,0 @@ end

@@ -29,2 +29,3 @@ # frozen_string_literal: true

else
value.freeze
@mapping[key] = proc { value }

@@ -54,3 +55,3 @@ end

def perform_fetch(type, *args, &block)
@mapping.fetch(type, block).call(type, *args)
@mapping.fetch(type, block).call(type, *args).freeze
end

@@ -57,0 +58,0 @@ end

@@ -19,2 +19,9 @@ # frozen_string_literal: true

end
def ==(other)
super(other) && timezone == other.timezone
end
protected
attr_reader :timezone
end

@@ -21,0 +28,0 @@ end

# frozen_string_literal: true
require "active_support/json"
module ActiveRecord

@@ -14,7 +16,18 @@ module Type

return value unless value.is_a?(::String)
ActiveSupport::JSON.decode(value) rescue nil
begin
ActiveSupport::JSON.decode(value)
rescue JSON::ParserError => e
# NOTE: This may hide json with duplicate keys. We don't really want to just ignore it
# but it's the best we can do in order to still allow updating columns that somehow already
# contain invalid json from some other source.
# See https://github.com/rails/rails/pull/55536
ActiveSupport.error_reporter.report(e, source: "application.active_record")
nil
end
end
JSON_ENCODER = ActiveSupport::JSON::Encoding.json_encoder.new(escape: false)
def serialize(value)
ActiveSupport::JSON.encode(value) unless value.nil?
JSON_ENCODER.encode(value) unless value.nil?
end

@@ -21,0 +34,0 @@

@@ -12,5 +12,6 @@ # frozen_string_literal: true

def initialize(subtype, coder)
def initialize(subtype, coder, comparable: false)
@subtype = subtype
@coder = coder
@comparable = comparable
super(subtype)

@@ -38,5 +39,11 @@ end

return false if value.nil?
raw_new_value = encoded(value)
raw_old_value.nil? != raw_new_value.nil? ||
subtype.changed_in_place?(raw_old_value, raw_new_value)
if @comparable
old_value = deserialize(raw_old_value)
old_value != value
else
raw_new_value = encoded(value)
raw_old_value.nil? != raw_new_value.nil? ||
subtype.changed_in_place?(raw_old_value, raw_new_value)
end
end

@@ -63,7 +70,2 @@

private
# Prevent Ruby 4.0 "delegator does not forward private method" warning.
# Kernel#inspect calls instance_variables_to_inspect which, without this,
# triggers Delegator#respond_to_missing? for a private method.
define_method(:instance_variables_to_inspect, Kernel.instance_method(:instance_variables))
def default_value?(value)

@@ -70,0 +72,0 @@ value == coder.load(nil)

@@ -49,3 +49,3 @@ # frozen_string_literal: true

if matching_pair
matching_pair.last.call(lookup_key)
matching_pair.last.call(lookup_key).freeze
elsif @parent

@@ -52,0 +52,0 @@ @parent.perform_fetch(lookup_key, &block)

@@ -10,3 +10,3 @@ # frozen_string_literal: true

if Array(value).reject { |association| valid_object?(association, context) }.any?
record.errors.add(attribute, :invalid, **options.merge(value: value))
record.errors.add(attribute, :invalid, **options, value: value)
end

@@ -13,0 +13,0 @@ end

@@ -53,3 +53,5 @@ # frozen_string_literal: true

def self.sql(sql_string, *positional_binds, retryable: false, **named_binds)
if positional_binds.empty? && named_binds.empty?
if Arel::Nodes::SqlLiteral === sql_string
sql_string
elsif positional_binds.empty? && named_binds.empty?
Arel::Nodes::SqlLiteral.new(sql_string, retryable: retryable)

@@ -56,0 +58,0 @@ else

@@ -6,2 +6,4 @@ # frozen_string_literal: true

def as(other)
other = other.name if other.is_a?(Symbol)
Nodes::As.new self, Nodes::SqlLiteral.new(other, retryable: true)

@@ -8,0 +10,0 @@ end

@@ -17,8 +17,3 @@ # frozen_string_literal: true

def compile_update(
values,
key = nil,
having_clause = nil,
group_values_columns = []
)
def compile_update(values, key = nil)
um = UpdateManager.new(source)

@@ -33,8 +28,8 @@ um.set(values)

um.group(group_values_columns) unless group_values_columns.empty?
um.having(having_clause) unless having_clause.nil?
um.ast.groups = @ctx.groups
@ctx.havings.each { |h| um.having(h) }
um
end
def compile_delete(key = nil, having_clause = nil, group_values_columns = [])
def compile_delete(key = nil)
dm = DeleteManager.new(source)

@@ -47,4 +42,4 @@ dm.take(limit)

dm.key = key
dm.group(group_values_columns) unless group_values_columns.empty?
dm.having(having_clause) unless having_clause.nil?
dm.ast.groups = @ctx.groups
@ctx.havings.each { |h| dm.having(h) }
dm

@@ -51,0 +46,0 @@ end

@@ -48,4 +48,2 @@ # frozen_string_literal: true

# function
# FIXME: Function + Alias can be rewritten as a Function and Alias node.
# We should make Function a Unary node and deprecate the use of "aliaz"
require "arel/nodes/function"

@@ -52,0 +50,0 @@ require "arel/nodes/count"

@@ -6,4 +6,4 @@ # frozen_string_literal: true

class Count < Arel::Nodes::Function
def initialize(expr, distinct = false, aliaz = nil)
super(expr, aliaz)
def initialize(expr, distinct = false)
super(expr)
@distinct = distinct

@@ -10,0 +10,0 @@ end

@@ -8,18 +8,13 @@ # frozen_string_literal: true

include Arel::FilterPredications
attr_accessor :expressions, :alias, :distinct
def initialize(expr, aliaz = nil)
attr_accessor :expressions, :distinct
def initialize(expr)
super()
@expressions = expr
@alias = aliaz && SqlLiteral.new(aliaz)
@distinct = false
end
def as(aliaz)
self.alias = SqlLiteral.new(aliaz)
self
end
def hash
[@expressions, @alias, @distinct].hash
[@expressions, @distinct].hash
end

@@ -30,3 +25,2 @@

self.expressions == other.expressions &&
self.alias == other.alias &&
self.distinct == other.distinct

@@ -33,0 +27,0 @@ end

@@ -8,4 +8,4 @@ # frozen_string_literal: true

def initialize(name, expr, aliaz = nil)
super(expr, aliaz)
def initialize(name, expr)
super(expr)
@name = name

@@ -12,0 +12,0 @@ end

@@ -9,3 +9,3 @@ # frozen_string_literal: true

# abstract syntax tree (AST) of the statement using various types of Arel::Nodes::Node. Each node represents a
# fragment of a SQL statement.
# fragment of an SQL statement.
#

@@ -12,0 +12,0 @@ # The intermediate representation allows Arel to compile the statement into the database's specific SQL dialect

@@ -234,3 +234,5 @@ # frozen_string_literal: true

nodes = others.map { |expr| send(method_id, expr, *extras) }
Nodes::Grouping.new Nodes::Or.new(nodes)
Nodes::Grouping.new nodes.inject { |memo, node|
Nodes::Or.new([memo, node])
}
end

@@ -237,0 +239,0 @@

@@ -77,4 +77,9 @@ # frozen_string_literal: true

# FIXME: backwards compat
column = Nodes::SqlLiteral.new(column) if String === column
column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column
case column
when Nodes::SqlLiteral
when String
column = Nodes::SqlLiteral.new(column)
when Symbol
column = Nodes::SqlLiteral.new(column.name)
end

@@ -81,0 +86,0 @@ @ctx.groups.push Nodes::Group.new column

@@ -37,3 +37,2 @@ # frozen_string_literal: true

visit_edge o, "distinct"
visit_edge o, "alias"
end

@@ -112,3 +111,2 @@

visit_edge o, "expressions"
visit_edge o, "alias"
end

@@ -120,3 +118,2 @@

visit_edge o, "distinct"
visit_edge o, "alias"
end

@@ -123,0 +120,0 @@

@@ -7,2 +7,51 @@ # frozen_string_literal: true

private
def visit_Arel_Nodes_UpdateStatement(o, collector)
collector.retryable = false
o = prepare_update_statement(o)
collector << "UPDATE "
# UPDATE with JOIN is in the form of:
#
# UPDATE t1 AS __active_record_update_alias
# SET ..
# FROM t1 JOIN t2 ON t2.join_id = t1.join_id ..
# WHERE t1.id = __active_record_update_alias.id AND ..
if has_join_sources?(o)
collector = visit o.relation.left, collector
collect_nodes_for o.values, collector, " SET "
collector << " FROM "
collector = inject_join o.relation.right, collector, " "
else
collector = visit o.relation, collector
collect_nodes_for o.values, collector, " SET "
end
collect_nodes_for o.wheres, collector, " WHERE ", " AND "
collect_nodes_for o.orders, collector, " ORDER BY "
maybe_visit o.limit, collector
maybe_visit o.comment, collector
end
# In the simple case, PostgreSQL allows us to place FROM or JOINs directly into the UPDATE
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
# these, we must use a subquery.
def prepare_update_statement(o)
if o.key && has_join_sources?(o) && !has_group_by_and_having?(o) && !has_limit_or_offset_or_orders?(o)
# Join clauses cannot reference the target table, so alias the
# updated table, place the entire relation in the FROM clause, and
# add a self-join (which requires the primary key)
stmt = o.clone
stmt.relation, stmt.wheres = o.relation.clone, o.wheres.clone
stmt.relation.right = [stmt.relation.left, *stmt.relation.right]
stmt.relation.left = stmt.relation.left.alias("__active_record_update_alias")
Array.wrap(o.key).each do |key|
stmt.wheres << key.eq(stmt.relation.left[key.name])
end
stmt
else
super
end
end
def visit_Arel_Nodes_Matches(o, collector)

@@ -70,2 +119,8 @@ op = o.case_sensitive ? " LIKE " : " ILIKE "

def visit_Arel_Nodes_InnerJoin(o, collector)
return super if o.right
collector << "CROSS JOIN "
visit o.left, collector
end
def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)

@@ -72,0 +127,0 @@ collector = visit o.left, collector

@@ -7,2 +7,57 @@ # frozen_string_literal: true

private
def visit_Arel_Nodes_UpdateStatement(o, collector)
collector.retryable = false
o = prepare_update_statement(o)
collector << "UPDATE "
# UPDATE with JOIN is in the form of:
#
# UPDATE t1 AS __active_record_update_alias
# SET ..
# FROM t1 JOIN t2 ON t2.join_id = t1.join_id ..
# WHERE t1.id = __active_record_update_alias.id AND ..
if has_join_sources?(o)
collector = visit o.relation.left, collector
collect_nodes_for o.values, collector, " SET "
collector << " FROM "
collector = inject_join o.relation.right, collector, " "
else
collector = visit o.relation, collector
collect_nodes_for o.values, collector, " SET "
end
collect_nodes_for o.wheres, collector, " WHERE ", " AND "
collect_nodes_for o.orders, collector, " ORDER BY "
maybe_visit o.limit, collector
maybe_visit o.comment, collector
end
def prepare_update_statement(o)
# Sqlite need to be built with the SQLITE_ENABLE_UPDATE_DELETE_LIMIT compile-time option
# to support LIMIT/OFFSET/ORDER in UPDATE and DELETE statements.
if o.key && has_join_sources?(o) && !has_group_by_and_having?(o) && !has_limit_or_offset_or_orders?(o)
# Join clauses cannot reference the target table, so alias the
# updated table, place the entire relation in the FROM clause, and
# add a self-join (which requires the primary key)
stmt = o.clone
stmt.relation, stmt.wheres = o.relation.clone, o.wheres.clone
stmt.relation.right = [stmt.relation.left, *stmt.relation.right]
stmt.relation.left = stmt.relation.left.alias("__active_record_update_alias")
Array.wrap(o.key).each do |key|
stmt.wheres << key.eq(stmt.relation.left[key.name])
end
stmt
else
super
end
end
def visit_Arel_Nodes_TableAlias(o, collector)
# "AS" is not optional in "{UPDATE | DELETE} table AS alias ..."
collector = visit o.relation, collector
collector << " AS "
collector << quote_table_name(o.name)
end
# Locks are not supported in SQLite

@@ -18,10 +73,2 @@ def visit_Arel_Nodes_Lock(o, collector)

def visit_Arel_Nodes_True(o, collector)
collector << "1"
end
def visit_Arel_Nodes_False(o, collector)
collector << "0"
end
def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)

@@ -28,0 +75,0 @@ collector = visit o.left, collector

@@ -80,9 +80,3 @@ # frozen_string_literal: true

collector << "EXISTS ("
collector = visit(o.expressions, collector) << ")"
if o.alias
collector << " AS "
visit o.alias, collector
else
collector
end
visit(o.expressions, collector) << ")"
end

@@ -394,9 +388,3 @@

collector << "DISTINCT " if o.distinct
collector = inject_join(o.expressions, collector, ", ") << ")"
if o.alias
collector << " AS "
visit o.alias, collector
else
collector
end
inject_join(o.expressions, collector, ", ") << ")"
end

@@ -1005,9 +993,3 @@

end
collector = inject_join(o.expressions, collector, ", ") << ")"
if o.alias
collector << " AS "
visit o.alias, collector
else
collector
end
inject_join(o.expressions, collector, ", ") << ")"
end

@@ -1014,0 +996,0 @@

@@ -142,3 +142,3 @@ = Active Record -- Object-relational mapping in \Rails

class AddSystemSettings < ActiveRecord::Migration[8.0]
class AddSystemSettings < ActiveRecord::Migration[8.1]
def up

@@ -145,0 +145,0 @@ create_table :system_settings do |t|

# frozen_string_literal: true
module ActiveRecord # :nodoc:
module Normalization
extend ActiveSupport::Concern
included do
class_attribute :normalized_attributes, default: Set.new
before_validation :normalize_changed_in_place_attributes
end
# Normalizes a specified attribute using its declared normalizations.
#
# ==== Examples
#
# class User < ActiveRecord::Base
# normalizes :email, with: -> email { email.strip.downcase }
# end
#
# legacy_user = User.find(1)
# legacy_user.email # => " CRUISE-CONTROL@EXAMPLE.COM\n"
# legacy_user.normalize_attribute(:email)
# legacy_user.email # => "cruise-control@example.com"
# legacy_user.save
def normalize_attribute(name)
# Treat the value as a new, unnormalized value.
self[name] = self[name]
end
module ClassMethods
# Declares a normalization for one or more attributes. The normalization
# is applied when the attribute is assigned or updated, and the normalized
# value will be persisted to the database. The normalization is also
# applied to the corresponding keyword argument of query methods. This
# allows a record to be created and later queried using unnormalized
# values.
#
# However, to prevent confusion, the normalization will not be applied
# when the attribute is fetched from the database. This means that if a
# record was persisted before the normalization was declared, the record's
# attribute will not be normalized until either it is assigned a new
# value, or it is explicitly migrated via Normalization#normalize_attribute.
#
# Because the normalization may be applied multiple times, it should be
# _idempotent_. In other words, applying the normalization more than once
# should have the same result as applying it only once.
#
# By default, the normalization will not be applied to +nil+ values. This
# behavior can be changed with the +:apply_to_nil+ option.
#
# Be aware that if your app was created before Rails 7.1, and your app
# marshals instances of the targeted model (for example, when caching),
# then you should set ActiveRecord.marshalling_format_version to +7.1+ or
# higher via either <tt>config.load_defaults 7.1</tt> or
# <tt>config.active_record.marshalling_format_version = 7.1</tt>.
# Otherwise, +Marshal+ may attempt to serialize the normalization +Proc+
# and raise +TypeError+.
#
# ==== Options
#
# * +:with+ - Any callable object that accepts the attribute's value as
# its sole argument, and returns it normalized.
# * +:apply_to_nil+ - Whether to apply the normalization to +nil+ values.
# Defaults to +false+.
#
# ==== Examples
#
# class User < ActiveRecord::Base
# normalizes :email, with: -> email { email.strip.downcase }
# normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
# end
#
# user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
# user.email # => "cruise-control@example.com"
#
# user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")
# user.email # => "cruise-control@example.com"
# user.email_before_type_cast # => "cruise-control@example.com"
#
# User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count # => 1
# User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0
#
# User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true
# User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false
#
# User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
def normalizes(*names, with:, apply_to_nil: false)
decorate_attributes(names) do |name, cast_type|
NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil)
end
self.normalized_attributes += names.map(&:to_sym)
end
# Normalizes a given +value+ using normalizations declared for +name+.
#
# ==== Examples
#
# class User < ActiveRecord::Base
# normalizes :email, with: -> email { email.strip.downcase }
# end
#
# User.normalize_value_for(:email, " CRUISE-CONTROL@EXAMPLE.COM\n")
# # => "cruise-control@example.com"
def normalize_value_for(name, value)
type_for_attribute(name).cast(value)
end
end
private
def normalize_changed_in_place_attributes
self.class.normalized_attributes.each do |name|
normalize_attribute(name) if attribute_changed_in_place?(name)
end
end
class NormalizedValueType < DelegateClass(ActiveModel::Type::Value) # :nodoc:
include ActiveModel::Type::SerializeCastValue
attr_reader :cast_type, :normalizer, :normalize_nil
alias :normalize_nil? :normalize_nil
def initialize(cast_type:, normalizer:, normalize_nil:)
@cast_type = cast_type
@normalizer = normalizer
@normalize_nil = normalize_nil
super(cast_type)
end
def cast(value)
normalize(super(value))
end
def serialize(value)
serialize_cast_value(cast(value))
end
def serialize_cast_value(value)
ActiveModel::Type::SerializeCastValue.serialize(cast_type, value)
end
def ==(other)
self.class == other.class &&
normalize_nil? == other.normalize_nil? &&
normalizer == other.normalizer &&
cast_type == other.cast_type
end
alias eql? ==
def hash
[self.class, cast_type, normalizer, normalize_nil?].hash
end
define_method(:inspect, Kernel.instance_method(:inspect))
private
def normalize(value)
normalizer.call(value) unless value.nil? && !normalize_nil?
end
end
end
end

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display