🎩 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
522
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.1.0.beta1
to
8.1.0.rc1
+85
lib/active_record/structured_event_subscriber.rb
# frozen_string_literal: true
require "active_support/structured_event_subscriber"
module ActiveRecord
class StructuredEventSubscriber < ActiveSupport::StructuredEventSubscriber # :nodoc:
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
def strict_loading_violation(event)
owner = event.payload[:owner]
reflection = event.payload[:reflection]
emit_debug_event("active_record.strict_loading_violation",
owner: owner.name,
class: reflection.klass.name,
name: reflection.name,
)
end
debug_only :strict_loading_violation
def sql(event)
payload = event.payload
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
binds = nil
if payload[:binds]&.any?
casted_params = type_casted_binds(payload[:type_casted_binds])
binds = []
payload[:binds].each_with_index do |attr, i|
attribute_name = if attr.respond_to?(:name)
attr.name
elsif attr.respond_to?(:[]) && attr[i].respond_to?(:name)
attr[i].name
else
nil
end
filtered_params = filter(attribute_name, casted_params[i])
binds << render_bind(attr, filtered_params)
end
end
emit_debug_event("active_record.sql",
async: payload[:async],
name: payload[:name],
sql: payload[:sql],
cached: payload[:cached],
lock_wait: payload[:lock_wait],
binds: binds,
duration_ms: event.duration.round(2),
)
end
debug_only :sql
private
def type_casted_binds(casted_binds)
casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds
end
def render_bind(attr, value)
case attr
when ActiveModel::Attribute
if attr.type.binary? && attr.value
value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
end
when Array
attr = attr.first
else
attr = nil
end
[attr&.name, value]
end
def filter(name, value)
ActiveRecord::Base.inspection_filter.filter_param(name, value)
end
end
end
ActiveRecord::StructuredEventSubscriber.attach_to :active_record
+105
-4

@@ -0,1 +1,106 @@

## Rails 8.1.0.rc1 (October 15, 2025) ##
* Add replicas to test database parallelization setup.
Setup and configuration of databases for parallel testing now includes replicas.
This fixes an issue when using a replica database, database selector middleware,
and non-transactional tests, where integration tests running in parallel would select
the base test database, i.e. `db_test`, instead of the numbered parallel worker database,
i.e. `db_test_{n}`.
*Adam Maas*
* Support virtual (not persisted) generated columns on PostgreSQL 18+
PostgreSQL 18 introduces virtual (not persisted) generated columns,
which are now the default unless the `stored: true` option is explicitly specified on PostgreSQL 18+.
```ruby
create_table :users do |t|
t.string :name
t.virtual :lower_name, type: :string, as: "LOWER(name)", stored: false
t.virtual :name_length, type: :integer, as: "LENGTH(name)"
end
```
*Yasuo Honda*
* Optimize schema dumping to prevent duplicate file generation.
`ActiveRecord::Tasks::DatabaseTasks.dump_all` now tracks which schema files
have already been dumped and skips dumping the same file multiple times.
This improves performance when multiple database configurations share the
same schema dump path.
*Mikey Gough*, *Hartley McGuire*
* Add structured events for Active Record:
- `active_record.strict_loading_violation`
- `active_record.sql`
*Gannon McGibbon*
* Add support for integer shard keys.
```ruby
# Now accepts symbols as shard keys.
ActiveRecord::Base.connects_to(shards: {
1: { writing: :primary_shard_one, reading: :primary_shard_one },
2: { writing: :primary_shard_two, reading: :primary_shard_two},
})
ActiveRecord::Base.connected_to(shard: 1) do
# ..
end
```
*Nony Dutton*
* Add `ActiveRecord::Base.only_columns`
Similar in use case to `ignored_columns` but listing columns to consider rather than the ones
to ignore.
Can be useful when working with a legacy or shared database schema, or to make safe schema change
in two deploys rather than three.
*Anton Kandratski*
* Use `PG::Connection#close_prepared` (protocol level Close) to deallocate
prepared statements when available.
To enable its use, you must have pg >= 1.6.0, libpq >= 17, and a PostgreSQL
database version >= 17.
*Hartley McGuire*, *Andrew Jackson*
* 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*
* Fix time attribute dirty tracking with timezone conversions.
Time-only attributes now maintain a fixed date of 2000-01-01 during timezone conversions,
preventing them from being incorrectly marked as changed due to date shifts.
This fixes an issue where time attributes would be marked as changed when setting the same time value
due to timezone conversion causing internal date shifts.
*Prateek Choudhary*
* 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*
* Don't add `id_value` attribute alias when attribute/column with that name already exists.
*Rob Lewis*
## Rails 8.1.0.beta1 (September 04, 2025) ##

@@ -74,6 +179,2 @@

* Emit a warning for pg gem < 1.6.0 when using PostgreSQL 18+
*Yasuo Honda*
* Fix `#merge` with `#or` or `#and` and a mixture of attributes and SQL strings resulting in an incorrect query.

@@ -80,0 +181,0 @@

@@ -138,3 +138,5 @@ # 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]

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

+1
-1

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

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

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

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

begin
super(user_input_in_time_zone(value)) || super
result = super(user_input_in_time_zone(value)) || super
if result && type == :time
result = result.change(year: 2000, month: 1, day: 1)
end
result
rescue ArgumentError

@@ -45,3 +49,7 @@ nil

if value.acts_like?(:time)
value.in_time_zone
converted = value.in_time_zone
if type == :time && converted
converted = converted.change(year: 2000, month: 1, day: 1)
end
converted
elsif value.respond_to?(:infinite?) && value.infinite?

@@ -48,0 +56,0 @@ value

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

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

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

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

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

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

require "active_record/log_subscriber"
require "active_record/explain_subscriber"
require "active_record/relation/delegation"

@@ -260,3 +259,3 @@ require "active_record/attributes"

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

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

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

@@ -270,0 +269,0 @@ # AttributeAssignmentError

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

require "active_support/concurrency/load_interlock_aware_monitor"
require "active_support/concurrency/thread_monitor"
require "arel/collectors/bind"

@@ -46,3 +47,4 @@ require "arel/collectors/composite"

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

@@ -56,3 +58,7 @@

set_callback :checkin, :after, :enable_lazy_transactions!
def allow_preconnect=(value) # :nodoc:
@lock.synchronize do
@allow_preconnect = value
end
end

@@ -159,5 +165,6 @@ def self.type_cast_config_to_integer(config)

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

@@ -195,5 +202,5 @@ @statements = build_statement_pool

when Thread
ActiveSupport::Concurrency::ThreadLoadInterlockAwareMonitor.new
ActiveSupport::Concurrency::ThreadMonitor.new
when Fiber
ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
::Monitor.new
else

@@ -204,4 +211,4 @@ ActiveSupport::Concurrency::NullLock

def check_if_write_query(sql) # :nodoc:
if preventing_writes? && write_query?(sql)
def ensure_writes_are_allowed(sql) # :nodoc:
if preventing_writes?
raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"

@@ -336,4 +343,8 @@ end

@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) if update_idle
@owner = nil
_run_checkin_callbacks do
@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) if update_idle
@owner = nil
enable_lazy_transactions!
unset_query_cache!
end
else

@@ -835,4 +846,7 @@ raise ActiveRecordError, "Cannot expire connection, it is not currently leased."

def clean! # :nodoc:
@raw_connection_dirty = false
@verified = nil
_run_checkout_callbacks do
@raw_connection_dirty = false
@verified = nil
end
self
end

@@ -839,0 +853,0 @@

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

# HELPER METHODS ===========================================
# Must return the MySQL error number from the exception, if the exception has an

@@ -216,0 +214,0 @@ # error number.

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

module ConnectionAdapters
module AbstractPool # :nodoc:
end
class NullPool # :nodoc:
include ConnectionAdapters::AbstractPool
class NullConfig

@@ -40,2 +35,3 @@ def method_missing(...)

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

@@ -121,2 +117,3 @@ def checkin(_); end

# * +max_connections+: maximum number of connections the pool may manage (default 5).
# Set to +nil+ or -1 for unlimited connections.
# * +min_connections+: minimum number of connections the pool will open and maintain (default 0).

@@ -189,17 +186,26 @@ # * +pool_jitter+: maximum reduction factor to apply to +max_age+ and

class LeaseRegistry # :nodoc:
def initialize
@mutex = Mutex.new
@map = WeakThreadKeyMap.new
if RUBY_ENGINE == "ruby"
# Thanks to the GVL, the LeaseRegistry doesn't need to be synchronized on MRI
class LeaseRegistry < WeakThreadKeyMap # :nodoc:
def [](context)
super || (self[context] = Lease.new)
end
end
else
class LeaseRegistry # :nodoc:
def initialize
@mutex = Mutex.new
@map = WeakThreadKeyMap.new
end
def [](context)
@mutex.synchronize do
@map[context] ||= Lease.new
def [](context)
@mutex.synchronize do
@map[context] ||= Lease.new
end
end
end
def clear
@mutex.synchronize do
@map.clear
def clear
@mutex.synchronize do
@map.clear
end
end

@@ -236,3 +242,2 @@ end

prepend QueryCache::ConnectionPoolConfiguration
include ConnectionAdapters::AbstractPool

@@ -357,4 +362,5 @@ attr_accessor :automatic_reconnect, :checkout_timeout

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

@@ -377,2 +383,3 @@

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

@@ -400,2 +407,3 @@ @pinned_connection.begin_transaction joinable: false, _lazy: false

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

@@ -664,7 +672,3 @@ connection.lock_thread = nil

connection_lease.clear(conn)
conn._run_checkin_callbacks do
conn.expire
end
conn.expire
@available.add conn

@@ -796,2 +800,3 @@ end

while new_conn = try_to_checkout_new_connection { @connections.size < @min_connections }
new_conn.allow_preconnect = true
checkin(new_conn)

@@ -942,3 +947,3 @@ end

# a second call to this method starting to work through the list
# before the first call has completed. (Though regular pool behaviour
# before the first call has completed. (Though regular pool behavior
# will prevent two instances from working on the same specific

@@ -1234,3 +1239,3 @@ # connection at the same time.)

if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @max_connections && (!block_given? || yield)
if @threads_blocking_new_connections.zero? && (@max_connections.nil? || (@connections.size + @now_connecting) < @max_connections) && (!block_given? || yield)
if @connections.size > 0 || @original_context != ActiveSupport::IsolatedExecutionState.context

@@ -1282,6 +1287,3 @@ @activated = true

def checkout_and_verify(c)
c._run_checkout_callbacks do
c.clean!
end
c
c.clean!
rescue Exception

@@ -1288,0 +1290,0 @@ remove c

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

loop do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@cond.wait(timeout - elapsed)
end
@cond.wait(timeout - elapsed)

@@ -137,0 +135,0 @@ return remove if any?

@@ -356,2 +356,18 @@ # frozen_string_literal: true

def transaction(requires_new: nil, isolation: nil, joinable: true, &block)
# If we're running inside the single, non-joinable transaction that
# ActiveRecord::TestFixtures starts around each example (depth == 1),
# an `isolation:` hint must be validated then ignored so that the
# adapter isn't asked to change the isolation level mid-transaction.
if isolation && !requires_new && open_transactions == 1 && !current_transaction.joinable?
iso = isolation.to_sym
unless transaction_isolation_levels.include?(iso)
raise ActiveRecord::TransactionIsolationError,
"invalid transaction isolation level: #{iso.inspect}"
end
current_transaction.isolation = iso
isolation = nil
end
if !requires_new && current_transaction.joinable?

@@ -376,6 +392,6 @@ if isolation && current_transaction.isolation != isolation

def mark_transaction_written_if_write(sql) # :nodoc:
def mark_transaction_written # :nodoc:
transaction = current_transaction
if transaction.open?
transaction.written ||= write_query?(sql)
transaction.written ||= true
end

@@ -425,9 +441,12 @@ end

TRANSACTION_ISOLATION_LEVELS = {
read_uncommitted: "READ UNCOMMITTED",
read_committed: "READ COMMITTED",
repeatable_read: "REPEATABLE READ",
serializable: "SERIALIZABLE"
}.freeze
private_constant :TRANSACTION_ISOLATION_LEVELS
def transaction_isolation_levels
{
read_uncommitted: "READ UNCOMMITTED",
read_committed: "READ COMMITTED",
repeatable_read: "REPEATABLE READ",
serializable: "SERIALIZABLE"
}
TRANSACTION_ISOLATION_LEVELS
end

@@ -555,5 +574,3 @@

with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
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
result = perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch)
handle_warnings(result, sql)

@@ -582,4 +599,6 @@ result

def preprocess_query(sql)
check_if_write_query(sql)
mark_transaction_written_if_write(sql)
if write_query?(sql)
ensure_writes_are_allowed(sql)
mark_transaction_written
end

@@ -586,0 +605,0 @@ # We call tranformers after the write checks so we don't add extra parsing work.

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

:exec_insert_all
base.set_callback :checkin, :after, :unset_query_cache!
end

@@ -213,4 +211,2 @@

attr_accessor :query_cache
def initialize(*)

@@ -221,4 +217,17 @@ 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

@@ -262,3 +271,3 @@

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

@@ -287,3 +296,3 @@

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

@@ -307,3 +316,3 @@

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

@@ -310,0 +319,0 @@ yield

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

def user_transaction; ActiveRecord::Transaction::NULL_TRANSACTION; end
def isolation=(_); end
end

@@ -160,2 +161,6 @@

def isolation=(isolation) # :nodoc:
@isolation_level = isolation
end
def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)

@@ -431,2 +436,6 @@ super()

def isolation=(isolation) # :nodoc:
@parent_transaction.isolation = isolation
end
def materialize!

@@ -433,0 +442,0 @@ connection.create_savepoint(savepoint_name)

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

# = Active Record MySQL Adapter \Index Definition
class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition
class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition # :nodoc:
attr_accessor :enabled

@@ -54,0 +54,0 @@

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

# HELPER METHODS ===========================================
def error_number(exception)

@@ -99,0 +97,0 @@ exception.error_number if exception.respond_to?(:error_number)

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

if PG::Connection.method_defined?(:close_prepared) # pg 1.6.0 & libpq 17
def supports_close_prepared? # :nodoc:
database_version >= 17_00_00
end
else
def supports_close_prepared? # :nodoc:
false
end
end
def index_algorithms

@@ -313,4 +323,8 @@ { concurrently: "CONCURRENTLY" }

# a reconnect would invalidate the entire statement pool.)
if conn = @connection.instance_variable_get(:@raw_connection)
conn.query "DEALLOCATE #{key}" if conn.status == PG::CONNECTION_OK
if (conn = @connection.instance_variable_get(:@raw_connection)) && conn.status == PG::CONNECTION_OK
if @connection.supports_close_prepared?
conn.close_prepared key
else
conn.query "DEALLOCATE #{key}"
end
end

@@ -671,5 +685,2 @@ rescue PG::Error

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

@@ -676,0 +687,0 @@

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

def virtual_stored?
@generated == "s"
end
def has_default?

@@ -35,0 +39,0 @@ super && !virtual?

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

@raw_connection.cancel
# 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.block

@@ -133,0 +140,0 @@ rescue PG::Error

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

delegate :quoted_include_columns_for_index, to: :@conn
delegate :database_version, to: :@conn

@@ -130,12 +131,13 @@ def visit_AlterTable(o)

if as = options[:as]
sql << " GENERATED ALWAYS AS (#{as})"
stored = options[:stored]
if options[:stored]
sql << " STORED"
else
if stored != true && database_version < 18_00_00
raise ArgumentError, <<~MSG
PostgreSQL currently does not support VIRTUAL (not persisted) generated columns.
PostgreSQL versions before 18 do not support VIRTUAL (not persisted) generated columns.
Specify 'stored: true' option for '#{options[:column].name}'
MSG
end
sql << " GENERATED ALWAYS AS (#{as})"
sql << (stored ? " STORED" : " VIRTUAL")
end

@@ -142,0 +144,0 @@ super

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

spec[:as] = extract_expression_for_virtual_column(column)
spec[:stored] = true
spec[:stored] = "true" if column.virtual_stored?
spec = { type: schema_type(column).inspect }.merge!(spec)

@@ -109,0 +109,0 @@ end

@@ -438,12 +438,9 @@ # frozen_string_literal: true

SELECT a.attname
FROM (
SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
FROM pg_index
WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass
AND indisprimary
) i
JOIN pg_attribute a
ON a.attrelid = i.indrelid
AND a.attnum = i.indkey[i.idx]
ORDER BY i.idx
FROM pg_index i
JOIN pg_attribute a
ON a.attrelid = i.indrelid
AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = #{quote(quote_table_name(table_name))}::regclass
AND i.indisprimary
ORDER BY array_position(i.indkey, a.attnum)
SQL

@@ -450,0 +447,0 @@ end

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

self.connection_class = true
connections << connection_handler.establish_connection(db_config, owner_name: self, role: role, shard: shard.to_sym)
shard = shard.to_sym unless shard.is_a? Integer
connections << connection_handler.establish_connection(db_config, owner_name: self, role: role, shard: shard)
end

@@ -106,0 +107,0 @@ end

@@ -74,3 +74,6 @@ # frozen_string_literal: true

def max_connections
(configuration_hash[:max_connections] || configuration_hash[:pool] || 5).to_i
max_connections = configuration_hash.fetch(:max_connections) {
configuration_hash.fetch(:pool, 5)
}&.to_i
max_connections if max_connections && max_connections >= 0
end

@@ -90,3 +93,3 @@

def max_threads
(configuration_hash[:max_threads] || max_connections).to_i
(configuration_hash[:max_threads] || (max_connections || 5).clamp(0, 5)).to_i
end

@@ -93,0 +96,0 @@

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

ENCODING_ERRORS = [EncodingError, Errors::Encoding]
# This threshold cannot be changed.
#
# Users can search for attributes encrypted with `deterministic: true`.
# That is possible because we are able to generate the message for the
# given clear text deterministically, and with that perform a regular
# string lookup in SQL.
#
# Problem is, messages may have a "c" header that is present or not
# depending on whether compression was applied on encryption. If this
# threshold was modified, the message generated for lookup could vary
# for the same clear text, and searches on exisiting data could fail.
THRESHOLD_TO_JUSTIFY_COMPRESSION = 140.bytes

@@ -99,0 +111,0 @@

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

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

@@ -455,3 +455,3 @@ class SubclassNotFound < ActiveRecordError

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

@@ -469,3 +469,3 @@ class AttributeAssignmentError < ActiveRecordError

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

@@ -472,0 +472,0 @@ # objects, each corresponding to the error while assigning to an attribute.

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

class ExplainRegistry # :nodoc:
class Subscriber
MUTEX = Mutex.new
@subscribed = false
class << self
def ensure_subscribed
return if @subscribed
MUTEX.synchronize do
return if @subscribed
ActiveSupport::Notifications.subscribe("sql.active_record", new)
@subscribed = true
end
end
end
def start(name, id, payload)
# unused
end
def finish(name, id, payload)
if ExplainRegistry.collect? && !ignore_payload?(payload)
ExplainRegistry.queries << payload.values_at(:sql, :binds)
end
end
def silenced?(_name)
!ExplainRegistry.collect?
end
# SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
# our own EXPLAINs no matter how loopingly beautiful that would be.
#
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN)
EXPLAINED_SQLS = /\A\s*(\/\*.*\*\/)?\s*(with|select|update|delete|insert)\b/i
def ignore_payload?(payload)
payload[:exception] ||
payload[:cached] ||
IGNORED_PAYLOADS.include?(payload[:name]) ||
!payload[:sql].match?(EXPLAINED_SQLS)
end
end
class << self
delegate :reset, :collect, :collect=, :collect?, :queries, to: :instance
delegate :start, :reset, :collect, :collect=, :collect?, :queries, to: :instance

@@ -28,2 +73,7 @@ private

def start
Subscriber.ensure_subscribed
@collect = true
end
def collect?

@@ -30,0 +80,0 @@ @collect

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

def collecting_queries_for_explain # :nodoc:
ExplainRegistry.collect = true
ExplainRegistry.start
yield

@@ -13,0 +13,0 @@ ExplainRegistry.queries

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

TINY = 0
PRE = "beta1"
PRE = "rc1"

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

# frozen_string_literal: true
module ActiveRecord
class LogSubscriber < ActiveSupport::LogSubscriber
class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]

@@ -6,0 +6,0 @@

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

module RemoveForeignKeyColumnMatch
def remove_foreign_key(from_table, to_table = nil, **options)
def remove_foreign_key(*args, **options)
options[:_skip_column_match] = true

@@ -39,0 +39,0 @@ super

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

#
# If you are organising your models within modules you can add a prefix to the models within
# If you are organizing your models within modules you can add a prefix to the models within
# a namespace by defining a singleton method in the parent module called table_name_prefix which

@@ -69,3 +69,3 @@ # returns your chosen prefix.

#
# If you are organising your models within modules, you can add a suffix to the models within
# If you are organizing your models within modules, you can add a suffix to the models within
# a namespace by defining a singleton method in the parent module called table_name_suffix which

@@ -186,2 +186,3 @@ # returns your chosen suffix.

self.ignored_columns = [].freeze
self.only_columns = [].freeze

@@ -340,2 +341,8 @@ delegate :type_for_attribute, :column_for_attribute, to: :class

# The list of columns names the model should allow. Only columns are used to define
# attribute accessors, and are referenced in SQL queries.
def only_columns
@only_columns || superclass.only_columns
end
# Sets the columns names the model should ignore. Ignored columns won't have attribute

@@ -373,2 +380,3 @@ # accessors defined, and won't be referenced in SQL queries.

def ignored_columns=(columns)
check_model_columns(@only_columns.present?)
reload_schema_from_cache

@@ -378,2 +386,8 @@ @ignored_columns = columns.map(&:to_s).freeze

def only_columns=(columns)
check_model_columns(@ignored_columns.present?)
reload_schema_from_cache
@only_columns = columns.map(&:to_s).freeze
end
def sequence_name

@@ -588,2 +602,3 @@ if base_class?

@ignored_columns = nil
@only_columns = nil
end

@@ -602,3 +617,7 @@ end

columns_hash = schema_cache.columns_hash(table_name)
columns_hash = columns_hash.except(*ignored_columns) unless ignored_columns.empty?
if only_columns.present?
columns_hash = columns_hash.slice(*only_columns)
elsif ignored_columns.present?
columns_hash = columns_hash.except(*ignored_columns)
end
@columns_hash = columns_hash.freeze

@@ -642,4 +661,8 @@

end
def check_model_columns(columns_present)
raise ArgumentError, "You can not use both only_columns and ignored_columns in the same model." if columns_present
end
end
end
end

@@ -44,7 +44,10 @@ # frozen_string_literal: true

if logger && logger.info?
db_rt_before_render = ActiveRecord::RuntimeRegistry.reset_runtimes
runtime_stats = ActiveRecord::RuntimeRegistry.stats
db_rt_before_render = runtime_stats.reset_runtimes
self.db_runtime = (db_runtime || 0) + db_rt_before_render
runtime = super
queries_rt = ActiveRecord::RuntimeRegistry.sql_runtime - ActiveRecord::RuntimeRegistry.async_sql_runtime
db_rt_after_render = ActiveRecord::RuntimeRegistry.reset_runtimes
queries_rt = runtime_stats.sql_runtime - runtime_stats.async_sql_runtime
db_rt_after_render = runtime_stats.reset_runtimes
self.db_runtime += db_rt_after_render

@@ -60,5 +63,7 @@ runtime - queries_rt

payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::RuntimeRegistry.reset_runtimes
payload[:queries_count] = ActiveRecord::RuntimeRegistry.reset_queries_count
payload[:cached_queries_count] = ActiveRecord::RuntimeRegistry.reset_cached_queries_count
runtime_stats = ActiveRecord::RuntimeRegistry.stats
payload[:db_runtime] = (db_runtime || 0) + runtime_stats.sql_runtime
payload[:queries_count] = runtime_stats.queries_count
payload[:cached_queries_count] = runtime_stats.cached_queries_count
runtime_stats.reset
end

@@ -65,0 +70,0 @@ end

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

task load: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(nil, ENV["SCHEMA"])
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(ENV["SCHEMA_FORMAT"], ENV["SCHEMA"])
end

@@ -472,0 +472,0 @@

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

super(operation, payload) do
db_runtime_before_perform = ActiveRecord::RuntimeRegistry.sql_runtime
db_runtime_before_perform = ActiveRecord::RuntimeRegistry.stats.sql_runtime
result = block.call
payload[:db_runtime] = ActiveRecord::RuntimeRegistry.sql_runtime - db_runtime_before_perform
payload[:db_runtime] = ActiveRecord::RuntimeRegistry.stats.sql_runtime - db_runtime_before_perform
result

@@ -16,0 +16,0 @@ end

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

# 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].

@@ -313,0 +313,0 @@ def find_or_initialize_by(attributes, &block)

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

values_last = values.last
yielded_relation = where(cursor => values).order(batch_orders.to_h)
yielded_relation = rewhere(cursor => values)
yielded_relation.load_records(records)

@@ -459,3 +459,3 @@ elsif (empty_scope && use_ranges != false) || use_ranges

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 = yielded_relation.except(:limit, :order)
yielded_relation.skip_query_cache!(false)

@@ -467,3 +467,3 @@ end

values_last = values.last
yielded_relation = where(cursor => values).order(batch_orders.to_h)
yielded_relation = rewhere(cursor => values)
end

@@ -470,0 +470,0 @@

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

if other.model == relation.model
relation.select_values += other.select_values if relation.select_values != other.select_values
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

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

.predicate_builder.expand_from_hash(value.stringify_keys)
elsif table.associated_with?(key)
elsif (associated_reflection = table.associated_with(key))
# Find the foreign key when using queries such as:

@@ -109,7 +109,9 @@ # Post.where(author: author)

# PriceEstimate.where(estimate_of: treasure)
associated_table = table.associated_table(key)
if associated_table.polymorphic_association?
if associated_reflection.polymorphic?
value = [value] unless value.is_a?(Array)
klass = PolymorphicArrayValue
elsif associated_table.through_association?
elsif associated_reflection.through_reflection?
associated_table = table.associated_table(key)
next associated_table.predicate_builder.expand_from_hash(

@@ -121,3 +123,3 @@ associated_table.primary_key => value

klass ||= AssociationQueryValue
queries = klass.new(associated_table, value).queries.map! do |query|
queries = klass.new(associated_reflection, value).queries.map! do |query|
# If the query produced is identical to attributes don't go any deeper.

@@ -124,0 +126,0 @@ # Prevents stack level too deep errors when association and foreign_key are identical.

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

class AssociationQueryValue # :nodoc:
def initialize(associated_table, value)
@associated_table = associated_table
def initialize(reflection, value)
@reflection = reflection
@value = value

@@ -13,9 +13,9 @@ end

def queries
if associated_table.join_foreign_key.is_a?(Array)
if reflection.join_foreign_key.is_a?(Array)
id_list = ids
id_list = id_list.pluck(primary_key) if id_list.is_a?(Relation)
id_list.map { |ids_set| associated_table.join_foreign_key.zip(ids_set).to_h }
id_list.map { |ids_set| reflection.join_foreign_key.zip(ids_set).to_h }
else
[ associated_table.join_foreign_key => ids ]
[ reflection.join_foreign_key => ids ]
end

@@ -25,3 +25,3 @@ end

private
attr_reader :associated_table, :value
attr_reader :reflection, :value

@@ -43,11 +43,11 @@ def ids

def primary_key
associated_table.join_primary_key
reflection.join_primary_key
end
def primary_type
associated_table.join_primary_type
reflection.join_primary_type
end
def polymorphic_name
associated_table.polymorphic_name_association
reflection.polymorphic_name
end

@@ -54,0 +54,0 @@

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

class PolymorphicArrayValue # :nodoc:
def initialize(associated_table, values)
@associated_table = associated_table
def initialize(reflection, values)
@reflection = reflection
@values = values

@@ -13,8 +13,8 @@ end

def queries
return [ associated_table.join_foreign_key => values ] if values.empty?
return [ reflection.join_foreign_key => values ] if values.empty?
type_to_ids_mapping.map do |type, ids|
query = {}
query[associated_table.join_foreign_type] = type if type
query[associated_table.join_foreign_key] = ids
query[reflection.join_foreign_type] = type if type
query[reflection.join_foreign_key] = ids
query

@@ -25,3 +25,3 @@ end

private
attr_reader :associated_table, :values
attr_reader :reflection, :values

@@ -36,3 +36,3 @@ def type_to_ids_mapping

def primary_key(value)
associated_table.join_primary_key(klass(value))
reflection.join_primary_key(klass(value))
end

@@ -39,0 +39,0 @@

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

def except_predicates(columns)
return predicates if columns.empty?
attrs = columns.extract! { |node| node.is_a?(Arel::Attribute) }

@@ -181,0 +183,0 @@ non_attrs = columns.extract! { |node| node.is_a?(Arel::Predications) }

@@ -6,78 +6,61 @@ # frozen_string_literal: true

#
# ActiveRecord::RuntimeRegistry.sql_runtime
# ActiveRecord::RuntimeRegistry.stats.sql_runtime
#
# returns the connection handler local to the current unit of execution (either thread of fiber).
module RuntimeRegistry # :nodoc:
extend self
class Stats
attr_accessor :sql_runtime, :async_sql_runtime, :queries_count, :cached_queries_count
def sql_runtime
ActiveSupport::IsolatedExecutionState[:active_record_sql_runtime] ||= 0.0
end
def initialize
@sql_runtime = 0.0
@async_sql_runtime = 0.0
@queries_count = 0
@cached_queries_count = 0
end
def sql_runtime=(runtime)
ActiveSupport::IsolatedExecutionState[:active_record_sql_runtime] = runtime
end
def reset_runtimes
sql_runtime_was = @sql_runtime
@sql_runtime = 0.0
@async_sql_runtime = 0.0
sql_runtime_was
end
def async_sql_runtime
ActiveSupport::IsolatedExecutionState[:active_record_async_sql_runtime] ||= 0.0
public alias_method :reset, :initialize
end
def async_sql_runtime=(runtime)
ActiveSupport::IsolatedExecutionState[:active_record_async_sql_runtime] = runtime
end
extend self
def queries_count
ActiveSupport::IsolatedExecutionState[:active_record_queries_count] ||= 0
def call(name, start, finish, id, payload)
record(
payload[:name],
(finish - start) * 1_000.0,
async: payload[:async],
lock_wait: payload[:lock_wait],
)
end
def queries_count=(count)
ActiveSupport::IsolatedExecutionState[:active_record_queries_count] = count
end
def record(query_name, runtime, cached: false, async: false, lock_wait: nil)
stats = self.stats
def cached_queries_count
ActiveSupport::IsolatedExecutionState[:active_record_cached_queries_count] ||= 0
unless query_name == "TRANSACTION" || query_name == "SCHEMA"
stats.queries_count += 1
stats.cached_queries_count += 1 if cached
end
if async
stats.async_sql_runtime += (runtime - lock_wait)
end
stats.sql_runtime += runtime
end
def cached_queries_count=(count)
ActiveSupport::IsolatedExecutionState[:active_record_cached_queries_count] = count
def stats
ActiveSupport::IsolatedExecutionState[:active_record_runtime] ||= Stats.new
end
def reset
reset_runtimes
reset_queries_count
reset_cached_queries_count
stats.reset
end
def reset_runtimes
rt, self.sql_runtime = sql_runtime, 0.0
self.async_sql_runtime = 0.0
rt
end
def reset_queries_count
qc = queries_count
self.queries_count = 0
qc
end
def reset_cached_queries_count
qc = cached_queries_count
self.cached_queries_count = 0
qc
end
end
end
ActiveSupport::Notifications.monotonic_subscribe("sql.active_record") do |name, start, finish, id, payload|
unless ["SCHEMA", "TRANSACTION"].include?(payload[:name])
ActiveRecord::RuntimeRegistry.queries_count += 1
ActiveRecord::RuntimeRegistry.cached_queries_count += 1 if payload[:cached]
end
runtime = (finish - start) * 1_000.0
if payload[:async]
ActiveRecord::RuntimeRegistry.async_sql_runtime += (runtime - payload[:lock_wait])
end
ActiveRecord::RuntimeRegistry.sql_runtime += runtime
end
ActiveSupport::Notifications.monotonic_subscribe("sql.active_record", ActiveRecord::RuntimeRegistry)

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

class TableMetadata # :nodoc:
delegate :join_primary_key, :join_primary_type, :join_foreign_key, :join_foreign_type, to: :reflection
def initialize(klass, arel_table, reflection = nil)
def initialize(klass, arel_table)
@klass = klass
@arel_table = arel_table
@reflection = reflection
end

@@ -26,3 +23,3 @@

def associated_with?(table_name)
def associated_with(table_name)
klass&._reflect_on_association(table_name)

@@ -47,22 +44,10 @@ end

arel_table = arel_table.alias(table_name) if arel_table.name != table_name
TableMetadata.new(association_klass, arel_table, reflection)
TableMetadata.new(association_klass, arel_table)
else
type_caster = TypeCaster::Connection.new(klass, table_name)
arel_table = Arel::Table.new(table_name, type_caster: type_caster)
TableMetadata.new(nil, arel_table, reflection)
TableMetadata.new(nil, arel_table)
end
end
def polymorphic_association?
reflection&.polymorphic?
end
def polymorphic_name_association
reflection&.polymorphic_name
end
def through_association?
reflection&.through_reflection?
end
def reflect_on_aggregation(aggregation_name)

@@ -84,4 +69,4 @@ klass&.reflect_on_aggregation(aggregation_name)

private
attr_reader :klass, :reflection
attr_reader :klass
end
end

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

def dump_all
with_temporary_pool_for_each do |pool|
db_config = pool.db_config
seen_schemas = []
ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
schema_path = schema_dump_path(db_config, ENV["SCHEMA_FORMAT"] || db_config.schema_format)
next if seen_schemas.include?(schema_path)
ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config, ENV["SCHEMA_FORMAT"] || db_config.schema_format)
seen_schemas << schema_path
end

@@ -445,15 +451,19 @@ end

FileUtils.mkdir_p(db_dir)
case format.to_sym
when :ruby
File.open(filename, "w:utf-8") do |file|
ActiveRecord::SchemaDumper.dump(migration_connection_pool, file)
end
when :sql
structure_dump(db_config, filename)
if migration_connection_pool.schema_migration.table_exists?
File.open(filename, "a") do |f|
f.puts migration_connection.dump_schema_versions
f.print "\n"
with_temporary_pool(db_config) do |pool|
FileUtils.mkdir_p(db_dir)
case format.to_sym
when :ruby
File.open(filename, "w:utf-8") do |file|
ActiveRecord::SchemaDumper.dump(pool, file)
end
when :sql
structure_dump(db_config, filename)
if pool.schema_migration.table_exists?
File.open(filename, "a") do |f|
pool.with_connection do |connection|
f.puts connection.dump_schema_versions
end
f.print "\n"
end
end
end

@@ -460,0 +470,0 @@ end

@@ -22,6 +22,8 @@ # frozen_string_literal: true

ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config|
ActiveRecord::Base.configurations.configs_for(env_name: env_name, include_hidden: true).each do |db_config|
db_config._database = "#{db_config.database}_#{i}"
ActiveRecord::Tasks::DatabaseTasks.reconstruct_from_schema(db_config, nil)
if db_config.database_tasks?
ActiveRecord::Tasks::DatabaseTasks.reconstruct_from_schema(db_config, nil)
end
end

@@ -28,0 +30,0 @@ ensure

@@ -8,2 +8,2 @@ Description:

This generates the base class. A test is not generated because no
behaviour is included in `ApplicationRecord` by default.
behavior is included in `ApplicationRecord` by default.
# frozen_string_literal: true
require "active_support/notifications"
require "active_record/explain_registry"
module ActiveRecord
class ExplainSubscriber # :nodoc:
def start(name, id, payload)
# unused
end
def finish(name, id, payload)
if ExplainRegistry.collect? && !ignore_payload?(payload)
ExplainRegistry.queries << payload.values_at(:sql, :binds)
end
end
# SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
# our own EXPLAINs no matter how loopingly beautiful that would be.
#
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN)
EXPLAINED_SQLS = /\A\s*(\/\*.*\*\/)?\s*(with|select|update|delete|insert)\b/i
def ignore_payload?(payload)
payload[:exception] ||
payload[:cached] ||
IGNORED_PAYLOADS.include?(payload[:name]) ||
!payload[:sql].match?(EXPLAINED_SQLS)
end
ActiveSupport::Notifications.subscribe("sql.active_record", new)
end
end

Sorry, the diff of this file is too big to display