activerecord
Advanced tools
| # 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 |
@@ -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