activerecord
Advanced tools
| # frozen_string_literal: true | ||
| require "active_support/notifications" | ||
| require "active_support/core_ext/array/conversions" | ||
| module ActiveRecord::Associations::Deprecation # :nodoc: | ||
| EVENT = "deprecated_association.active_record" | ||
| private_constant :EVENT | ||
| MODES = [:warn, :raise, :notify].freeze | ||
| private_constant :MODES | ||
| class << self | ||
| attr_reader :mode, :backtrace | ||
| def mode=(value) # private setter | ||
| unless MODES.include?(value) | ||
| raise ArgumentError, "invalid deprecated associations mode #{value.inspect} (valid modes are #{MODES.map(&:inspect).to_sentence})" | ||
| end | ||
| @mode = value | ||
| end | ||
| def backtrace=(value) | ||
| @backtrace = !!value | ||
| end | ||
| def guard(reflection) | ||
| report(reflection, context: yield) if reflection.deprecated? | ||
| if reflection.through_reflection? | ||
| reflection.deprecated_nested_reflections.each do |deprecated_nested_reflection| | ||
| context = "referenced as nested association of the through #{reflection.active_record}##{reflection.name}" | ||
| report(deprecated_nested_reflection, context: context) | ||
| end | ||
| end | ||
| end | ||
| def report(reflection, context:) | ||
| reflection = user_facing_reflection(reflection) | ||
| message = +"The association #{reflection.active_record}##{reflection.name} is deprecated, #{context}" | ||
| message << " (#{backtrace_cleaner.first_clean_frame})" | ||
| case @mode | ||
| when :warn | ||
| message = [message, *clean_frames].join("\n\t") if @backtrace | ||
| ActiveRecord::Base.logger&.warn(message) | ||
| when :raise | ||
| error = ActiveRecord::DeprecatedAssociationError.new(message) | ||
| if set_backtrace_supports_array_of_locations? | ||
| error.set_backtrace(clean_locations) | ||
| else | ||
| error.set_backtrace(clean_frames) | ||
| end | ||
| raise error | ||
| else | ||
| payload = { reflection: reflection, message: message, location: backtrace_cleaner.first_clean_location } | ||
| payload[:backtrace] = clean_locations if @backtrace | ||
| ActiveSupport::Notifications.instrument(EVENT, payload) | ||
| end | ||
| end | ||
| private | ||
| def backtrace_cleaner | ||
| ActiveRecord::LogSubscriber.backtrace_cleaner | ||
| end | ||
| def clean_frames | ||
| backtrace_cleaner.clean(caller) | ||
| end | ||
| def clean_locations | ||
| backtrace_cleaner.clean_locations(caller_locations) | ||
| end | ||
| def set_backtrace_supports_array_of_locations? | ||
| @backtrace_supports_array_of_locations ||= Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0") | ||
| end | ||
| def user_facing_reflection(reflection) | ||
| reflection.active_record.reflect_on_association(reflection.name) | ||
| end | ||
| end | ||
| self.mode = :warn | ||
| self.backtrace = false | ||
| end |
| # frozen_string_literal: true | ||
| module ActiveRecord | ||
| class FilterAttributeHandler # :nodoc: | ||
| class << self | ||
| def on_sensitive_attribute_declared(&block) | ||
| @sensitive_attribute_declaration_listeners ||= Concurrent::Array.new | ||
| @sensitive_attribute_declaration_listeners << block | ||
| end | ||
| def sensitive_attribute_was_declared(klass, list) | ||
| @sensitive_attribute_declaration_listeners&.each do |block| | ||
| block.call(klass, list) | ||
| end | ||
| end | ||
| end | ||
| def initialize(app) | ||
| @app = app | ||
| @attributes_by_class = Concurrent::Map.new | ||
| @collecting = true | ||
| end | ||
| def enable | ||
| install_collecting_hook | ||
| apply_collected_attributes | ||
| @collecting = false | ||
| end | ||
| private | ||
| attr_reader :app | ||
| def install_collecting_hook | ||
| self.class.on_sensitive_attribute_declared do |klass, list| | ||
| attribute_was_declared(klass, list) | ||
| end | ||
| end | ||
| def attribute_was_declared(klass, list) | ||
| if collecting? | ||
| collect_for_later(klass, list) | ||
| else | ||
| apply_filter(klass, list) | ||
| end | ||
| end | ||
| def apply_collected_attributes | ||
| @attributes_by_class.each do |klass, list| | ||
| apply_filter(klass, list) | ||
| end | ||
| end | ||
| def collecting? | ||
| @collecting | ||
| end | ||
| def collect_for_later(klass, list) | ||
| @attributes_by_class[klass] ||= Concurrent::Array.new | ||
| @attributes_by_class[klass] += list | ||
| end | ||
| def apply_filter(klass, list) | ||
| list.each do |attribute| | ||
| next if klass.abstract_class? || klass == Base | ||
| klass_name = klass.name ? klass.model_name.element : nil | ||
| filter = [klass_name, attribute.to_s].compact.join(".") | ||
| app.config.filter_parameters << filter unless app.config.filter_parameters.include?(filter) | ||
| end | ||
| end | ||
| end | ||
| end |
| # frozen_string_literal: true | ||
| module ActiveRecord | ||
| class Migration | ||
| # This class is used by the schema dumper to format versions information. | ||
| # | ||
| # The class receives the current +connection+ when initialized. | ||
| class DefaultSchemaVersionsFormatter # :nodoc: | ||
| def initialize(connection) | ||
| @connection = connection | ||
| end | ||
| def format(versions) | ||
| sm_table = connection.quote_table_name(connection.pool.schema_migration.table_name) | ||
| if versions.is_a?(Array) | ||
| sql = +"INSERT INTO #{sm_table} (version) VALUES\n" | ||
| sql << versions.reverse.map { |v| "(#{connection.quote(v)})" }.join(",\n") | ||
| sql << ";" | ||
| sql | ||
| else | ||
| "INSERT INTO #{sm_table} (version) VALUES (#{connection.quote(versions)});" | ||
| end | ||
| end | ||
| private | ||
| attr_reader :connection | ||
| end | ||
| end | ||
| end |
| # frozen_string_literal: true | ||
| module ActiveRecord | ||
| module Railties # :nodoc: | ||
| module JobCheckpoints # :nodoc: | ||
| def checkpoint! | ||
| if ActiveRecord.all_open_transactions.any? | ||
| raise ActiveJob::Continuation::CheckpointError, "Cannot checkpoint job with open transactions" | ||
| else | ||
| super | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
| # frozen_string_literal: true | ||
| module ActiveRecord | ||
| module Tasks # :nodoc: | ||
| class AbstractTasks # :nodoc: | ||
| def self.using_database_configurations? | ||
| true | ||
| end | ||
| def initialize(db_config) | ||
| @db_config = db_config | ||
| @configuration_hash = db_config.configuration_hash | ||
| end | ||
| def charset | ||
| connection.encoding | ||
| end | ||
| def collation | ||
| connection.collation | ||
| end | ||
| def check_current_protected_environment!(db_config, migration_class) | ||
| with_temporary_pool(db_config, migration_class) do |pool| | ||
| migration_context = pool.migration_context | ||
| current = migration_context.current_environment | ||
| stored = migration_context.last_stored_environment | ||
| if migration_context.protected_environment? | ||
| raise ActiveRecord::ProtectedEnvironmentError.new(stored) | ||
| end | ||
| if stored && stored != current | ||
| raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored) | ||
| end | ||
| rescue ActiveRecord::NoDatabaseError | ||
| end | ||
| end | ||
| private | ||
| attr_reader :db_config, :configuration_hash | ||
| def connection | ||
| ActiveRecord::Base.lease_connection | ||
| end | ||
| def establish_connection(config = db_config) | ||
| ActiveRecord::Base.establish_connection(config) | ||
| end | ||
| def configuration_hash_without_database | ||
| configuration_hash.merge(database: nil) | ||
| end | ||
| def run_cmd(cmd, *args, **opts) | ||
| fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, opts) | ||
| end | ||
| def run_cmd_error(cmd, args) | ||
| msg = +"failed to execute:\n" | ||
| msg << "#{cmd} #{args.join(' ')}\n\n" | ||
| msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" | ||
| msg | ||
| end | ||
| def with_temporary_pool(db_config, migration_class, clobber: false) | ||
| original_db_config = migration_class.connection_db_config | ||
| pool = migration_class.connection_handler.establish_connection(db_config, clobber: clobber) | ||
| yield pool | ||
| ensure | ||
| migration_class.connection_handler.establish_connection(original_db_config, clobber: clobber) | ||
| end | ||
| end | ||
| end | ||
| end |
+420
-651
@@ -1,188 +0,78 @@ | ||
| ## Rails 8.0.5 (March 24, 2026) ## | ||
| ## Rails 8.1.0.beta1 (September 04, 2025) ## | ||
| * Fix `insert_all` and `upsert_all` log message when called on anonymous classes. | ||
| * Remove deprecated `:unsigned_float` and `:unsigned_decimal` column methods for MySQL. | ||
| *Gabriel Sobrinho* | ||
| *Rafael Mendonça França* | ||
| * Respect `ActiveRecord::SchemaDumper.ignore_tables` when dumping SQLite virtual tables. | ||
| * Remove deprecated `:retries` option for the SQLite3 adapter. | ||
| *Hans Schnedlitz* | ||
| *Rafael Mendonça França* | ||
| * Restore previous instrumenter after `execute_or_skip` | ||
| * Introduce new database configuration options `keepalive`, `max_age`, and | ||
| `min_connections` -- and rename `pool` to `max_connections` to match. | ||
| `FutureResult#execute_or_skip` replaces the thread's instrumenter with an | ||
| `EventBuffer` to collect events published during async query execution. | ||
| If the global async executor is saturated and the `caller_runs` fallback | ||
| executes the task on the calling thread, we need to make sure the previous | ||
| instrumenter is restored or the stale `EventBuffer` would stay in place and | ||
| permanently swallow all subsequent `sql.active_record` notifications on | ||
| that thread. | ||
| There are no changes to default behavior, but these allow for more specific | ||
| control over pool behavior. | ||
| *Rosa Gutierrez* | ||
| *Matthew Draper*, *Chris AtLee*, *Rachael Wright-Munn* | ||
| * Fix Ruby 4.0 delegator warning when calling inspect on ActiveRecord::Type::Serialized. | ||
| * Move `LIMIT` validation from query generation to when `limit()` is called. | ||
| *Hammad Khan* | ||
| *Hartley McGuire*, *Shuyang* | ||
| * Fix support for table names containing hyphens. | ||
| * Add `ActiveRecord::CheckViolation` error class for check constraint violations. | ||
| *Evgeniy Demin* | ||
| *Ryuta Kamizono* | ||
| * Fix column deduplication for SQLite3 and PostgreSQL virtual (generated) columns. | ||
| * Add `ActiveRecord::ExclusionViolation` error class for exclusion constraint violations. | ||
| `Column#==` and `Column#hash` now account for `virtual?` so that the | ||
| `Deduplicable` registry does not treat a generated column and a regular | ||
| column with the same name and type as identical. Previously, if a | ||
| generated column was registered first, a regular column on a different | ||
| table could be deduplicated to the generated instance, silently | ||
| excluding it from INSERT/UPDATE statements. | ||
| When an exclusion constraint is violated in PostgreSQL, the error will now be raised | ||
| as `ActiveRecord::ExclusionViolation` instead of the generic `ActiveRecord::StatementInvalid`, | ||
| making it easier to handle these specific constraint violations in application code. | ||
| *Jay Huber* | ||
| This follows the same pattern as other constraint violation error classes like | ||
| `RecordNotUnique` for unique constraint violations and `InvalidForeignKey` for | ||
| foreign key constraint violations. | ||
| * Fix merging relations with arel equality predicates with null relations. | ||
| *Ryuta Kamizono* | ||
| *fatkodima* | ||
| * Attributes filtered by `filter_attributes` will now also be filtered by `filter_parameters` | ||
| so sensitive information is not leaked. | ||
| * Fix SQLite3 schema dump for non-autoincrement integer primary keys. | ||
| *Jill Klang* | ||
| Previously, `schema.rb` should incorrectly restore that table with an auto incrementing | ||
| primary key. | ||
| * Add `connection.current_transaction.isolation` API to check current transaction's isolation level. | ||
| *Chris Hasiński* | ||
| Returns the isolation level if it was explicitly set via the `isolation:` parameter | ||
| or through `ActiveRecord.with_transaction_isolation_level`, otherwise returns `nil`. | ||
| Nested transactions return the parent transaction's isolation level. | ||
| * Fix PostgreSQL `schema_search_path` not being reapplied after `reset!` or `reconnect!`. | ||
| ```ruby | ||
| # Returns nil when no transaction | ||
| User.connection.current_transaction.isolation # => nil | ||
| The `schema_search_path` configured in `database.yml` is now correctly | ||
| reapplied instead of falling back to PostgreSQL defaults. | ||
| # Returns explicitly set isolation level | ||
| User.transaction(isolation: :serializable) do | ||
| User.connection.current_transaction.isolation # => :serializable | ||
| end | ||
| *Tobias Egli* | ||
| # Returns nil when isolation not explicitly set | ||
| User.transaction do | ||
| User.connection.current_transaction.isolation # => nil | ||
| end | ||
| * Ensure batched preloaded associations accounts for klass when grouping to avoid issues with STI. | ||
| *zzak*, *Stjepan Hadjic* | ||
| * Fix `ActiveRecord::SoleRecordExceeded#record` to return the relation. | ||
| This was the case until Rails 7.2, but starting from 8.0 it | ||
| started mistakenly returning the model class. | ||
| *Jean Boussier* | ||
| * Improve PostgreSQLAdapter resilience to Timeout.timeout. | ||
| Better handle asynchronous exceptions being thrown inside | ||
| the `reconnect!` method. | ||
| This may fixes some deep errors such as: | ||
| # Nested transactions inherit parent's isolation | ||
| User.transaction(isolation: :read_committed) do | ||
| User.transaction do | ||
| User.connection.current_transaction.isolation # => :read_committed | ||
| end | ||
| end | ||
| ``` | ||
| undefined method `key?' for nil:NilClass (NoMethodError) | ||
| if !type_map.key?(oid) | ||
| ``` | ||
| *Jean Boussier* | ||
| *Kir Shatrov* | ||
| * Fix `eager_load` when loading `has_many` assocations with composite primary keys. | ||
| * Emit a warning for pg gem < 1.6.0 when using PostgreSQL 18+ | ||
| This would result in some records being loaded multiple times. | ||
| *Yasuo Honda* | ||
| *Martin-Alexander* | ||
| ## Rails 8.0.4.1 (March 23, 2026) ## | ||
| * No changes. | ||
| ## Rails 8.0.4 (October 28, 2025) ## | ||
| * Fix SQLite3 data loss during table alterations with CASCADE foreign keys. | ||
| When altering a table in SQLite3 that is referenced by child tables with | ||
| `ON DELETE CASCADE` foreign keys, ActiveRecord would silently delete all | ||
| data from the child tables. This occurred because SQLite requires table | ||
| recreation for schema changes, and during this process the original table | ||
| is temporarily dropped, triggering CASCADE deletes on child tables. | ||
| The root cause was incorrect ordering of operations. The original code | ||
| wrapped `disable_referential_integrity` inside a transaction, but | ||
| `PRAGMA foreign_keys` cannot be modified inside a transaction in SQLite - | ||
| attempting to do so simply has no effect. This meant foreign keys remained | ||
| enabled during table recreation, causing CASCADE deletes to fire. | ||
| The fix reverses the order to follow the official SQLite 12-step ALTER TABLE | ||
| procedure: `disable_referential_integrity` now wraps the transaction instead | ||
| of being wrapped by it. This ensures foreign keys are properly disabled | ||
| before the transaction starts and re-enabled after it commits, preventing | ||
| CASCADE deletes while maintaining data integrity through atomic transactions. | ||
| *Ruy Rocha* | ||
| * Add support for bound SQL literals in CTEs. | ||
| *Nicolas Bachschmidt* | ||
| * Fix `belongs_to` associations not to clear the entire composite primary key. | ||
| When clearing a `belongs_to` association that references a model with composite primary key, | ||
| only the optional part of the key should be cleared. | ||
| *zzak* | ||
| * Fix invalid records being autosaved when distantly associated records are marked for deletion. | ||
| *Ian Terrell*, *axlekb AB* | ||
| ## Rails 8.0.3 (September 22, 2025) ## | ||
| * Fix query cache for pinned connections in multi threaded transactional tests | ||
| When a pinned connection is used across separate threads, they now use a separate cache store | ||
| for each thread. | ||
| This improve accuracy of system tests, and any test using multiple threads. | ||
| *Heinrich Lee Yu*, *Jean Boussier* | ||
| * Don't add `id_value` attribute alias when attribute/column with that name already exists. | ||
| *Rob Lewis* | ||
| * Fix false positive change detection involving STI and polymorphic has one relationships. | ||
| Polymorphic `has_one` relationships would always be considered changed when defined in a STI child | ||
| class, causing nedless extra autosaves. | ||
| *David Fritsch* | ||
| * Skip calling `PG::Connection#cancel` in `cancel_any_running_query` | ||
| when using libpq >= 18 with pg < 1.6.0, due to incompatibility. | ||
| Rollback still runs, but may take longer. | ||
| *Yasuo Honda*, *Lars Kanis* | ||
| * Fix stale association detection for polymorphic `belongs_to`. | ||
| *Florent Beaurain*, *Thomas Crambert* | ||
| * Fix removal of PostgreSQL version comments in `structure.sql` for latest PostgreSQL versions which include `\restrict` | ||
| *Brendan Weibrecht* | ||
| * Allow setting `schema_format` in database configuration. | ||
| ``` | ||
| primary: | ||
| schema_format: ruby | ||
| ``` | ||
| Useful in multi-database setups to have different formats per-database. | ||
| *T S Vallender* | ||
| * Use ntuples to populate row_count instead of count for Postgres | ||
| *Jonathan Calvert* | ||
| * Fix `#merge` with `#or` or `#and` and a mixture of attributes and SQL strings resulting in an incorrect query. | ||
@@ -226,720 +116,599 @@ | ||
| * Fix inline `has_and_belongs_to_many` fixtures for tables with composite primary keys. | ||
| * Make schema dumper to account for `ActiveRecord.dump_schemas` when dumping in `:ruby` format. | ||
| *fatkodima* | ||
| * Fix migration log message for down operations. | ||
| * Add `:touch` option to `update_column`/`update_columns` methods. | ||
| *Bernardo Barreto* | ||
| ```ruby | ||
| # Will update :updated_at/:updated_on alongside :nice column. | ||
| user.update_column(:nice, true, touch: true) | ||
| * Prepend `extra_flags` in postgres' `structure_load` | ||
| # Will update :updated_at/:updated_on alongside :last_ip column | ||
| user.update_columns(last_ip: request.remote_ip, touch: true) | ||
| ``` | ||
| When specifying `structure_load_flags` with a postgres adapter, the flags | ||
| were appended to the default flags, instead of prepended. | ||
| This caused issues with flags not being taken into account by postgres. | ||
| *Dmitrii Ivliev* | ||
| *Alice Loeser* | ||
| * Optimize Active Record batching further when using ranges. | ||
| * Fix `annotate` comments to propagate to `update_all`/`delete_all`. | ||
| Tested on a PostgreSQL table with 10M records and batches of 10k records, the generation | ||
| of relations for the 1000 batches was `4.8x` faster (`6.8s` vs. `1.4s`), used `900x` | ||
| less bandwidth (`180MB` vs. `0.2MB`) and allocated `45x` less memory (`490MB` vs. `11MB`). | ||
| *fatkodima* | ||
| *Maxime Réty*, *fatkodima* | ||
| * Fix checking whether an unpersisted record is `include?`d in a strictly | ||
| loaded `has_and_belongs_to_many` association. | ||
| * Include current character length in error messages for index and table name length validations. | ||
| *Hartley McGuire* | ||
| *Joshua Young* | ||
| * `create_or_find_by` will now correctly rollback a transaction. | ||
| * Add `rename_schema` method for PostgreSQL. | ||
| When using `create_or_find_by`, raising a ActiveRecord::Rollback error | ||
| in a `after_save` callback had no effect, the transaction was committed | ||
| and a record created. | ||
| *T S Vallender* | ||
| *Edouard Chin* | ||
| * Implement support for deprecating associations: | ||
| * Gracefully handle `Timeout.timeout` firing during connection configuration. | ||
| ```ruby | ||
| has_many :posts, deprecated: true | ||
| ``` | ||
| Use of `Timeout.timeout` could result in improperly initialized database connection. | ||
| With that, Active Record will report any usage of the `posts` association. | ||
| This could lead to a partially configured connection being used, resulting in various exceptions, | ||
| the most common being with the PostgreSQLAdapter raising `undefined method 'key?' for nil` | ||
| or `TypeError: wrong argument type nil (expected PG::TypeMap)`. | ||
| Three reporting modes are supported (`:warn`, `:raise`, and `:notify`), and | ||
| backtraces can be enabled or disabled. Defaults are `:warn` mode and | ||
| disabled backtraces. | ||
| *Jean Boussier* | ||
| Please, check the docs for further details. | ||
| * Fix stale state for composite foreign keys in belongs_to associations. | ||
| *Xavier Noria* | ||
| *Varun Sharma* | ||
| * PostgreSQL adapter create DB now supports `locale_provider` and `locale`. | ||
| *Bengt-Ove Hollaender* | ||
| ## Rails 8.0.2.1 (August 13, 2025) ## | ||
| * Use ntuples to populate row_count instead of count for Postgres | ||
| * Call inspect on ids in RecordNotFound error | ||
| *Jonathan Calvert* | ||
| [CVE-2025-55193] | ||
| * Fix checking whether an unpersisted record is `include?`d in a strictly | ||
| loaded `has_and_belongs_to_many` association. | ||
| *Gannon McGibbon*, *John Hawthorn* | ||
| ## Rails 8.0.2 (March 12, 2025) ## | ||
| * Fix inverting `rename_enum_value` when `:from`/`:to` are provided. | ||
| *fatkodima* | ||
| * Prevent persisting invalid record. | ||
| *Edouard Chin* | ||
| * Fix inverting `drop_table` without options. | ||
| *fatkodima* | ||
| * Fix count with group by qualified name on loaded relation. | ||
| *Ryuta Kamizono* | ||
| * Fix `sum` with qualified name on loaded relation. | ||
| *Chris Gunther* | ||
| * The SQLite3 adapter quotes non-finite Numeric values like "Infinity" and "NaN". | ||
| *Mike Dalessio* | ||
| * Handle libpq returning a database version of 0 on no/bad connection in `PostgreSQLAdapter`. | ||
| Before, this version would be cached and an error would be raised during connection configuration when | ||
| comparing it with the minimum required version for the adapter. This meant that the connection could | ||
| never be successfully configured on subsequent reconnection attempts. | ||
| Now, this is treated as a connection failure consistent with libpq, raising a `ActiveRecord::ConnectionFailed` | ||
| and ensuring the version isn't cached, which allows the version to be retrieved on the next connection attempt. | ||
| *Joshua Young*, *Rian McGuire* | ||
| * Fix error handling during connection configuration. | ||
| Active Record wasn't properly handling errors during the connection configuration phase. | ||
| This could lead to a partially configured connection being used, resulting in various exceptions, | ||
| the most common being with the PostgreSQLAdapter raising `undefined method `key?' for nil` | ||
| or `TypeError: wrong argument type nil (expected PG::TypeMap)`. | ||
| *Jean Boussier* | ||
| * Fix a case where a non-retryable query could be marked retryable. | ||
| *Hartley McGuire* | ||
| * Handle circular references when autosaving associations. | ||
| * Add ability to change transaction isolation for all pools within a block. | ||
| *zzak* | ||
| This functionality is useful if your application needs to change the database | ||
| transaction isolation for a request or action. | ||
| * PoolConfig no longer keeps a reference to the connection class. | ||
| Calling `ActiveRecord.with_transaction_isolation_level(level) {}` in an around filter or | ||
| middleware will set the transaction isolation for all pools accessed within the block, | ||
| but not for the pools that aren't. | ||
| Keeping a reference to the class caused subtle issues when combined with reloading in | ||
| development. Fixes #54343. | ||
| This works with explicit and implicit transactions: | ||
| *Mike Dalessio* | ||
| * Fix SQL notifications sometimes not sent when using async queries. | ||
| ```ruby | ||
| Post.async_count | ||
| ActiveSupport::Notifications.subscribed(->(*) { "Will never reach here" }) do | ||
| Post.count | ||
| ActiveRecord.with_transaction_isolation_level(:read_committed) do | ||
| Tag.transaction do # opens a transaction explicitly | ||
| Tag.create! | ||
| end | ||
| end | ||
| ``` | ||
| In rare circumstances and under the right race condition, Active Support notifications | ||
| would no longer be dispatched after using an asynchronous query. | ||
| This is now fixed. | ||
| *Edouard Chin* | ||
| * Fix support for PostgreSQL enum types with commas in their name. | ||
| *Arthur Hess* | ||
| * Fix inserts on MySQL with no RETURNING support for a table with multiple auto populated columns. | ||
| *Nikita Vasilevsky* | ||
| * Fix joining on a scoped association with string joins and bind parameters. | ||
| ```ruby | ||
| class Instructor < ActiveRecord::Base | ||
| has_many :instructor_roles, -> { active } | ||
| ActiveRecord.with_transaction_isolation_level(:read_committed) do | ||
| Tag.create! # opens a transaction implicitly | ||
| end | ||
| class InstructorRole < ActiveRecord::Base | ||
| scope :active, -> { | ||
| joins("JOIN students ON instructor_roles.student_id = students.id") | ||
| .where(students { status: 1 }) | ||
| } | ||
| end | ||
| Instructor.joins(:instructor_roles).first | ||
| ``` | ||
| The above example would result in `ActiveRecord::StatementInvalid` because the | ||
| `active` scope bind parameters would be lost. | ||
| *Eileen M. Uchitelle* | ||
| *Jean Boussier* | ||
| * Raise `ActiveRecord::MissingRequiredOrderError` when order dependent finder methods (e.g. `#first`, `#last`) are | ||
| called without `order` values on the relation, and the model does not have any order columns (`implicit_order_column`, | ||
| `query_constraints`, or `primary_key`) to fall back on. | ||
| * Fix a potential race condition with system tests and transactional fixtures. | ||
| This change will be introduced with a new framework default for Rails 8.1, and the current behavior of not raising | ||
| an error has been deprecated with the aim of removing the configuration option in Rails 8.2. | ||
| *Sjoerd Lagarde* | ||
| * Fix autosave associations to no longer validated unmodified associated records. | ||
| Active Record was incorrectly performing validation on associated record that | ||
| weren't created nor modified as part of the transaction: | ||
| ```ruby | ||
| Post.create!(author: User.find(1)) # Fail if user is invalid | ||
| config.active_record.raise_on_missing_required_finder_order_columns = true | ||
| ``` | ||
| *Jean Boussier* | ||
| *Joshua Young* | ||
| * Remember when a database connection has recently been verified (for | ||
| two seconds, by default), to avoid repeated reverifications during a | ||
| single request. | ||
| * `:class_name` is now invalid in polymorphic `belongs_to` associations. | ||
| This should recreate a similar rate of verification as in Rails 7.1, | ||
| where connections are leased for the duration of a request, and thus | ||
| only verified once. | ||
| Reason is `:class_name` does not make sense in those associations because | ||
| the class name of target records is dynamic and stored in the type column. | ||
| *Matthew Draper* | ||
| Existing polymorphic associations setting this option can just delete it. | ||
| While it did not raise, it had no effect anyway. | ||
| *Xavier Noria* | ||
| ## Rails 8.0.1 (December 13, 2024) ## | ||
| * Add support for multiple databases to `db:migrate:reset`. | ||
| * Fix removing foreign keys with :restrict action for MySQL. | ||
| *Joé Dupuis* | ||
| *fatkodima* | ||
| * Add `affected_rows` to `ActiveRecord::Result`. | ||
| * Fix a race condition in `ActiveRecord::Base#method_missing` when lazily defining attributes. | ||
| *Jenny Shen* | ||
| If multiple thread were concurrently triggering attribute definition on the same model, | ||
| it could result in a `NoMethodError` being raised. | ||
| * Enable passing retryable SqlLiterals to `#where`. | ||
| *Jean Boussier* | ||
| *Hartley McGuire* | ||
| * Fix MySQL default functions getting dropped when changing a column's nullability. | ||
| * Set default for primary keys in `insert_all`/`upsert_all`. | ||
| *Bastian Bartmann* | ||
| Previously in Postgres, updating and inserting new records in one upsert wasn't possible | ||
| due to null primary key values. `nil` primary key values passed into `insert_all`/`upsert_all` | ||
| are now implicitly set to the default insert value specified by adapter. | ||
| * Fix `add_unique_constraint`/`add_check_constraint`/`add_foreign_key` to be revertible when given invalid options. | ||
| *Jenny Shen* | ||
| *fatkodima* | ||
| * Add a load hook `active_record_database_configurations` for `ActiveRecord::DatabaseConfigurations` | ||
| * Fix asynchronous destroying of polymorphic `belongs_to` associations. | ||
| *Mike Dalessio* | ||
| *fatkodima* | ||
| * Use `TRUE` and `FALSE` for SQLite queries with boolean columns. | ||
| * Fix `insert_all` to not update existing records. | ||
| *Hartley McGuire* | ||
| *fatkodima* | ||
| * Bump minimum supported SQLite to 3.23.0. | ||
| * `NOT VALID` constraints should not dump in `create_table`. | ||
| *Hartley McGuire* | ||
| *Ryuta Kamizono* | ||
| * Allow allocated Active Records to lookup associations. | ||
| * Fix finding by nil composite primary key association. | ||
| Previously, the association cache isn't setup on allocated record objects, so association | ||
| lookups will crash. Test frameworks like mocha use allocate to check for stubbable instance | ||
| methods, which can trigger an association lookup. | ||
| *fatkodima* | ||
| *Gannon McGibbon* | ||
| * Properly reset composite primary key configuration when setting a primary key. | ||
| * Encryption now supports `support_unencrypted_data: true` being set per-attribute. | ||
| *fatkodima* | ||
| Previously this only worked if `ActiveRecord::Encryption.config.support_unencrypted_data == true`. | ||
| Now, if the global config is turned off, you can still opt in for a specific attribute. | ||
| * Fix Mysql2Adapter support for prepared statements | ||
| ```ruby | ||
| # ActiveRecord::Encryption.config.support_unencrypted_data = true | ||
| class User < ActiveRecord::Base | ||
| encrypts :name, support_unencrypted_data: false # only supports encrypted data | ||
| encrypts :email # supports encrypted or unencrypted data | ||
| end | ||
| ``` | ||
| Using prepared statements with MySQL could result in a `NoMethodError` exception. | ||
| ```ruby | ||
| # ActiveRecord::Encryption.config.support_unencrypted_data = false | ||
| class User < ActiveRecord::Base | ||
| encrypts :name, support_unencrypted_data: true # supports encrypted or unencrypted data | ||
| encrypts :email # only supports encrypted data | ||
| end | ||
| ``` | ||
| *Jean Boussier*, *Leo Arnold*, *zzak* | ||
| *Alex Ghiculescu* | ||
| * Fix parsing of SQLite foreign key names when they contain non-ASCII characters | ||
| * Model generator no longer needs a database connection to validate column types. | ||
| *Zacharias Knudsen* | ||
| * Fix parsing of MySQL 8.0.16+ CHECK constraints when they contain new lines. | ||
| *Steve Hill* | ||
| * Ensure normalized attribute queries use `IS NULL` consistently for `nil` and normalized `nil` values. | ||
| *Joshua Young* | ||
| * Fix `sum` when performing a grouped calculation. | ||
| `User.group(:friendly).sum` no longer worked. This is fixed. | ||
| *Edouard Chin* | ||
| * Restore back the ability to pass only database name to `DATABASE_URL`. | ||
| *fatkodima* | ||
| ## Rails 8.0.0.1 (December 10, 2024) ## | ||
| * No changes. | ||
| ## Rails 8.0.0 (November 07, 2024) ## | ||
| * Fix support for `query_cache: false` in `database.yml`. | ||
| `query_cache: false` would no longer entirely disable the Active Record query cache. | ||
| *zzak* | ||
| ## Rails 8.0.0.rc2 (October 30, 2024) ## | ||
| * NULLS NOT DISTINCT works with UNIQUE CONSTRAINT as well as UNIQUE INDEX. | ||
| *Ryuta Kamizono* | ||
| * The `db:prepare` task no longer loads seeds when a non-primary database is created. | ||
| Previously, the `db:prepare` task would load seeds whenever a new database | ||
| is created, leading to potential loss of data if a database is added to an | ||
| existing environment. | ||
| Introduces a new database config property `seeds` to control whether seeds | ||
| are loaded during `db:prepare` which defaults to `true` for primary database | ||
| configs and `false` otherwise. | ||
| Fixes #53348. | ||
| *Mike Dalessio* | ||
| * `PG::UnableToSend: no connection to the server` is now retryable as a connection-related exception | ||
| * Allow signed ID verifiers to be configurable via `Rails.application.message_verifiers` | ||
| *Kazuma Watanabe* | ||
| Prior to this change, the primary way to configure signed ID verifiers was | ||
| to set `signed_id_verifier` on each model class: | ||
| * Fix strict loading propagation even if statement cache is not used. | ||
| ```ruby | ||
| Post.signed_id_verifier = ActiveSupport::MessageVerifier.new(...) | ||
| Comment.signed_id_verifier = ActiveSupport::MessageVerifier.new(...) | ||
| ``` | ||
| *Ryuta Kamizono* | ||
| And if the developer did not set `signed_id_verifier`, a verifier would be | ||
| instantiated with a secret derived from `secret_key_base` and the following | ||
| options: | ||
| * Allow `rename_enum` accepts two from/to name arguments as `rename_table` does so. | ||
| ```ruby | ||
| { digest: "SHA256", serializer: JSON, url_safe: true } | ||
| ``` | ||
| *Ryuta Kamizono* | ||
| Thus it was cumbersome to rotate configuration for all verifiers. | ||
| This change defines a new Rails config: [`config.active_record.use_legacy_signed_id_verifier`][]. | ||
| The default value is `:generate_and_verify`, which preserves the previous | ||
| behavior. However, when set to `:verify`, signed ID verifiers will use | ||
| configuration from `Rails.application.message_verifiers` (specifically, | ||
| `Rails.application.message_verifiers["active_record/signed_id"]`) to | ||
| generate and verify signed IDs, but will also verify signed IDs using the | ||
| older configuration. | ||
| ## Rails 8.0.0.rc1 (October 19, 2024) ## | ||
| To avoid complication, the new behavior only applies when `signed_id_verifier_secret` | ||
| is not set on a model class or any of its ancestors. Additionally, | ||
| `signed_id_verifier_secret` is now deprecated. If you are currently setting | ||
| `signed_id_verifier_secret` on a model class, you can set `signed_id_verifier` | ||
| instead: | ||
| * Remove deprecated support to setting `ENV["SCHEMA_CACHE"]`. | ||
| ```ruby | ||
| # BEFORE | ||
| Post.signed_id_verifier_secret = "my secret" | ||
| *Rafael Mendonça França* | ||
| # AFTER | ||
| Post.signed_id_verifier = ActiveSupport::MessageVerifier.new("my secret", digest: "SHA256", serializer: JSON, url_safe: true) | ||
| ``` | ||
| * Remove deprecated support to passing a database name to `cache_dump_filename`. | ||
| To ease migration, `signed_id_verifier` has also been changed to behave as a | ||
| `class_attribute` (i.e. inheritable), but _only when `signed_id_verifier_secret` | ||
| is not set_: | ||
| *Rafael Mendonça França* | ||
| ```ruby | ||
| # BEFORE | ||
| ActiveRecord::Base.signed_id_verifier = ActiveSupport::MessageVerifier.new(...) | ||
| Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => false | ||
| * Remove deprecated `ActiveRecord::ConnectionAdapters::ConnectionPool#connection`. | ||
| # AFTER | ||
| ActiveRecord::Base.signed_id_verifier = ActiveSupport::MessageVerifier.new(...) | ||
| Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => true | ||
| *Rafael Mendonça França* | ||
| Post.signed_id_verifier_secret = "my secret" # => deprecation warning | ||
| Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => false | ||
| ``` | ||
| * Remove deprecated `config.active_record.sqlite3_deprecated_warning`. | ||
| Note, however, that it is recommended to eventually migrate from | ||
| model-specific verifiers to a unified configuration managed by | ||
| `Rails.application.message_verifiers`. `ActiveSupport::MessageVerifier#rotate` | ||
| can facilitate that transition. For example: | ||
| *Rafael Mendonça França* | ||
| ```ruby | ||
| # BEFORE | ||
| # Generate and verify signed Post IDs using Post-specific configuration | ||
| Post.signed_id_verifier = ActiveSupport::MessageVerifier.new("post secret", ...) | ||
| * Remove deprecated `config.active_record.warn_on_records_fetched_greater_than`. | ||
| # AFTER | ||
| # Generate and verify signed Post IDs using the unified configuration | ||
| Post.signed_id_verifier = Post.signed_id_verifier.dup | ||
| # Fall back to Post-specific configuration when verifying signed IDs | ||
| Post.signed_id_verifier.rotate("post secret", ...) | ||
| ``` | ||
| *Rafael Mendonça França* | ||
| [`config.active_record.use_legacy_signed_id_verifier`]: https://guides.rubyonrails.org/v8.1/configuring.html#config-active-record-use-legacy-signed-id-verifier | ||
| * Remove deprecated support for defining `enum` with keyword arguments. | ||
| *Ali Sepehri*, *Jonathan Hefner* | ||
| *Rafael Mendonça França* | ||
| * Prepend `extra_flags` in postgres' `structure_load` | ||
| * Remove deprecated support to finding database adapters that aren't registered to Active Record. | ||
| When specifying `structure_load_flags` with a postgres adapter, the flags | ||
| were appended to the default flags, instead of prepended. | ||
| This caused issues with flags not being taken into account by postgres. | ||
| *Rafael Mendonça França* | ||
| *Alice Loeser* | ||
| * Remove deprecated `config.active_record.allow_deprecated_singular_associations_name`. | ||
| * Allow bypassing primary key/constraint addition in `implicit_order_column` | ||
| *Rafael Mendonça França* | ||
| When specifying multiple columns in an array for `implicit_order_column`, adding | ||
| `nil` as the last element will prevent appending the primary key to order | ||
| conditions. This allows more precise control of indexes used by | ||
| generated queries. It should be noted that this feature does introduce the risk | ||
| of API misbehavior if the specified columns are not fully unique. | ||
| * Remove deprecated `config.active_record.commit_transaction_on_non_local_return`. | ||
| *Issy Long* | ||
| *Rafael Mendonça França* | ||
| * Allow setting the `schema_format` via database configuration. | ||
| * Fix incorrect SQL query when passing an empty hash to `ActiveRecord::Base.insert`. | ||
| ``` | ||
| primary: | ||
| schema_format: ruby | ||
| ``` | ||
| *David Stosik* | ||
| Useful for multi-database setups when apps require different formats per-database. | ||
| * Allow to save records with polymorphic join tables that have `inverse_of` | ||
| specified. | ||
| *T S Vallender* | ||
| *Markus Doits* | ||
| * Support disabling indexes for MySQL v8.0.0+ and MariaDB v10.6.0+ | ||
| * Fix association scopes applying on the incorrect join when using a polymorphic `has_many through:`. | ||
| MySQL 8.0.0 added an option to disable indexes from being used by the query | ||
| optimizer by making them "invisible". This allows the index to still be maintained | ||
| and updated but no queries will be permitted to use it. This can be useful for adding | ||
| new invisible indexes or making existing indexes invisible before dropping them | ||
| to ensure queries are not negatively affected. | ||
| See https://dev.mysql.com/blog-archive/mysql-8-0-invisible-indexes/ for more details. | ||
| *Joshua Young* | ||
| MariaDB 10.6.0 also added support for this feature by allowing indexes to be "ignored" | ||
| in queries. See https://mariadb.com/kb/en/ignored-indexes/ for more details. | ||
| * Allow `ActiveRecord::Base#pluck` to accept hash arguments with symbol and string values. | ||
| Active Record now supports this option for MySQL 8.0.0+ and MariaDB 10.6.0+ for | ||
| index creation and alteration where the new index option `enabled: true/false` can be | ||
| passed to column and index methods as below: | ||
| ```ruby | ||
| Post.joins(:comments).pluck(:id, comments: :id) | ||
| Post.joins(:comments).pluck("id", "comments" => "id") | ||
| ``` | ||
| add_index :users, :email, enabled: false | ||
| enable_index :users, :email | ||
| add_column :users, :dob, :string, index: { enabled: false } | ||
| *Joshua Young* | ||
| change_table :users do |t| | ||
| t.index :name, enabled: false | ||
| t.index :dob | ||
| t.disable_index :dob | ||
| t.column :username, :string, index: { enabled: false } | ||
| t.references :account, index: { enabled: false } | ||
| end | ||
| * Make Float distinguish between `float4` and `float8` in PostgreSQL. | ||
| Fixes #52742 | ||
| *Ryota Kitazawa*, *Takayuki Nagatomi* | ||
| ## Rails 8.0.0.beta1 (September 26, 2024) ## | ||
| * Allow `drop_table` to accept an array of table names. | ||
| This will let you to drop multiple tables in a single call. | ||
| ```ruby | ||
| ActiveRecord::Base.lease_connection.drop_table(:users, :posts) | ||
| create_table :users do |t| | ||
| t.string :name, index: { enabled: false } | ||
| t.string :email | ||
| t.index :email, enabled: false | ||
| end | ||
| ``` | ||
| *Gabriel Sobrinho* | ||
| *Merve Taner* | ||
| * Add support for PostgreSQL `IF NOT EXISTS` via the `:if_not_exists` option | ||
| on the `add_enum_value` method. | ||
| * Respect `implicit_order_column` in `ActiveRecord::Relation#reverse_order`. | ||
| *Ariel Rzezak* | ||
| *Joshua Young* | ||
| * When running `db:migrate` on a fresh database, load the databases schemas before running migrations. | ||
| * Add column types to `ActiveRecord::Result` for SQLite3. | ||
| *Andrew Novoselac*, *Marek Kasztelnik* | ||
| *Andrew Kane* | ||
| * Fix an issue where `.left_outer_joins` used with multiple associations that have | ||
| the same child association but different parents does not join all parents. | ||
| * Raise `ActiveRecord::ReadOnlyError` when pessimistically locking with a readonly role. | ||
| Previously, using `.left_outer_joins` with the same child association would only join one of the parents. | ||
| *Joshua Young* | ||
| Now it will correctly join both parents. | ||
| * Fix using the `SQLite3Adapter`'s `dbconsole` method outside of a Rails application. | ||
| Fixes #41498. | ||
| *Hartley McGuire* | ||
| *Garrett Blehm* | ||
| * Fix migrating multiple databases with `ActiveRecord::PendingMigration` action. | ||
| * Deprecate `unsigned_float` and `unsigned_decimal` short-hand column methods. | ||
| *Gannon McGibbon* | ||
| As of MySQL 8.0.17, the UNSIGNED attribute is deprecated for columns of type FLOAT, DOUBLE, | ||
| and DECIMAL. Consider using a simple CHECK constraint instead for such columns. | ||
| * Enable automatically retrying idempotent association queries on connection | ||
| errors. | ||
| https://dev.mysql.com/doc/refman/8.0/en/numeric-type-syntax.html | ||
| *Hartley McGuire* | ||
| *Ryuta Kamizono* | ||
| * Add `allow_retry` to `sql.active_record` instrumentation. | ||
| * Drop MySQL 5.5 support. | ||
| This enables identifying queries which queries are automatically retryable on connection errors. | ||
| MySQL 5.5 is the only version that does not support datetime with precision, | ||
| which we have supported in the core. Now we support MySQL 5.6.4 or later, which | ||
| is the first version to support datetime with precision. | ||
| *Hartley McGuire* | ||
| *Ryuta Kamizono* | ||
| * Better support UPDATE with JOIN for Postgresql and SQLite3 | ||
| * Make Active Record asynchronous queries compatible with transactional fixtures. | ||
| Previously when generating update queries with one or more JOIN clauses, | ||
| Active Record would use a sub query which would prevent to reference the joined | ||
| tables in the `SET` clause, for instance: | ||
| Previously transactional fixtures would disable asynchronous queries, because transactional | ||
| fixtures impose all queries use the same connection. | ||
| ```ruby | ||
| Comment.joins(:post).update_all("title = posts.title") | ||
| ``` | ||
| Now asynchronous queries will use the connection pinned by transactional fixtures, and behave | ||
| much closer to production. | ||
| This is now supported as long as the relation doesn't also use a `LIMIT`, `ORDER` or | ||
| `GROUP BY` clause. This was supported by the MySQL adapter for a long time. | ||
| *Jean Boussier* | ||
| * Deserialize binary data before decrypting | ||
| * Introduce a before-fork hook in `ActiveSupport::Testing::Parallelization` to clear existing | ||
| connections, to avoid fork-safety issues with the mysql2 adapter. | ||
| This ensures that we call `PG::Connection.unescape_bytea` on PostgreSQL before decryption. | ||
| Fixes #41776 | ||
| *Donal McBreen* | ||
| *Mike Dalessio*, *Donal McBreen* | ||
| * Ensure `ActiveRecord::Encryption.config` is always ready before access. | ||
| * PoolConfig no longer keeps a reference to the connection class. | ||
| Previously, `ActiveRecord::Encryption` configuration was deferred until `ActiveRecord::Base` | ||
| was loaded. Therefore, accessing `ActiveRecord::Encryption.config` properties before | ||
| `ActiveRecord::Base` was loaded would give incorrect results. | ||
| Keeping a reference to the class caused subtle issues when combined with reloading in | ||
| development. Fixes #54343. | ||
| `ActiveRecord::Encryption` now has its own loading hook so that its configuration is set as | ||
| soon as needed. | ||
| *Mike Dalessio* | ||
| When `ActiveRecord::Base` is loaded, even lazily, it in turn triggers the loading of | ||
| `ActiveRecord::Encryption`, thus preserving the original behavior of having its config ready | ||
| before any use of `ActiveRecord::Base`. | ||
| * Fix SQL notifications sometimes not sent when using async queries. | ||
| *Maxime Réty* | ||
| * Add `TimeZoneConverter#==` method, so objects will be properly compared by | ||
| their type, scale, limit & precision. | ||
| Address #52699. | ||
| *Ruy Rocha* | ||
| * Add support for SQLite3 full-text-search and other virtual tables. | ||
| Previously, adding sqlite3 virtual tables messed up `schema.rb`. | ||
| Now, virtual tables can safely be added using `create_virtual_table`. | ||
| *Zacharias Knudsen* | ||
| * Support use of alternative database interfaces via the `database_cli` ActiveRecord configuration option. | ||
| ```ruby | ||
| Rails.application.configure do | ||
| config.active_record.database_cli = { postgresql: "pgcli" } | ||
| Post.async_count | ||
| ActiveSupport::Notifications.subscribed(->(*) { "Will never reach here" }) do | ||
| Post.count | ||
| end | ||
| ``` | ||
| *T S Vallender* | ||
| In rare circumstances and under the right race condition, Active Support notifications | ||
| would no longer be dispatched after using an asynchronous query. | ||
| This is now fixed. | ||
| * Add support for dumping table inheritance and native partitioning table definitions for PostgeSQL adapter | ||
| *Edouard Chin* | ||
| *Justin Talbott* | ||
| * Eliminate queries loading dumped schema cache on Postgres | ||
| * Add support for `ActiveRecord::Point` type casts using `Hash` values | ||
| Improve resiliency by avoiding needing to open a database connection to load the | ||
| type map while defining attribute methods at boot when a schema cache file is | ||
| configured on PostgreSQL databases. | ||
| This allows `ActiveRecord::Point` to be cast or serialized from a hash | ||
| with `:x` and `:y` keys of numeric values, mirroring the functionality of | ||
| existing casts for string and array values. Both string and symbol keys are | ||
| supported. | ||
| *James Coleman* | ||
| * `ActiveRecord::Coder::JSON` can be instantiated | ||
| Options can now be passed to `ActiveRecord::Coder::JSON` when instantiating the coder. This allows: | ||
| ```ruby | ||
| class PostgresqlPoint < ActiveRecord::Base | ||
| attribute :x, :point | ||
| attribute :y, :point | ||
| attribute :z, :point | ||
| end | ||
| val = PostgresqlPoint.new({ | ||
| x: '(12.34, -43.21)', | ||
| y: [12.34, '-43.21'], | ||
| z: {x: '12.34', y: -43.21} | ||
| }) | ||
| ActiveRecord::Point.new(12.32, -43.21) == val.x == val.y == val.z | ||
| serialize :config, coder: ActiveRecord::Coder::JSON.new(symbolize_names: true) | ||
| ``` | ||
| *matthaigh27* | ||
| *Stephen Drew* | ||
| * Deprecate using `insert_all`/`upsert_all` with unpersisted records in associations. | ||
| * Replace `SQLite3::Database#busy_timeout` with `#busy_handler_timeout=`. | ||
| Using these methods on associations containing unpersisted records will now | ||
| show a deprecation warning, as the unpersisted records will be lost after | ||
| the operation. | ||
| Provides a non-GVL-blocking, fair retry interval busy handler implementation. | ||
| *Nick Schwaderer* | ||
| *Stephen Margheim* | ||
| * Make column name optional for `index_exists?`. | ||
| * SQLite3Adapter: Translate `SQLite3::BusyException` into `ActiveRecord::StatementTimeout`. | ||
| This aligns well with `remove_index` signature as well, where | ||
| index name doesn't need to be derived from the column names. | ||
| *Matthew Nguyen* | ||
| *Ali Ismayiliov* | ||
| * Include schema name in `enable_extension` statements in `db/schema.rb`. | ||
| * Change the payload name of `sql.active_record` notification for eager | ||
| loading from "SQL" to "#{model.name} Eager Load". | ||
| The schema dumper will now include the schema name in generated | ||
| `enable_extension` statements if they differ from the current schema. | ||
| *zzak* | ||
| For example, if you have a migration: | ||
| * Enable automatically retrying idempotent `#exists?` queries on connection | ||
| errors. | ||
| ```ruby | ||
| enable_extension "heroku_ext.pgcrypto" | ||
| enable_extension "pg_stat_statements" | ||
| ``` | ||
| *Hartley McGuire*, *classidied* | ||
| then the generated schema dump will also contain: | ||
| * Deprecate usage of unsupported methods in conjunction with `update_all`: | ||
| ```ruby | ||
| enable_extension "heroku_ext.pgcrypto" | ||
| enable_extension "pg_stat_statements" | ||
| ``` | ||
| `update_all` will now print a deprecation message if a query includes either `WITH`, | ||
| `WITH RECURSIVE` or `DISTINCT` statements. Those were never supported and were ignored | ||
| when generating the SQL query. | ||
| *Tony Novak* | ||
| An error will be raised in a future Rails release. This behavior will be consistent | ||
| with `delete_all` which currently raises an error for unsupported statements. | ||
| * Fix `ActiveRecord::Encryption::EncryptedAttributeType#type` to return | ||
| actual cast type. | ||
| *Edouard Chin* | ||
| *Vasiliy Ermolovich* | ||
| * The table columns inside `schema.rb` are now sorted alphabetically. | ||
| * SQLite3Adapter: Bulk insert fixtures. | ||
| Previously they'd be sorted by creation order, which can cause merge conflicts when two | ||
| branches modify the same table concurrently. | ||
| Previously one insert command was executed for each fixture, now they are | ||
| aggregated in a single bulk insert command. | ||
| *John Duff* | ||
| *Lázaro Nixon* | ||
| * Introduce versions formatter for the schema dumper. | ||
| * PostgreSQLAdapter: Allow `disable_extension` to be called with schema-qualified name. | ||
| It is now possible to override how schema dumper formats versions information inside the | ||
| `structure.sql` file. Currently, the versions are simply sorted in the decreasing order. | ||
| Within large teams, this can potentially cause many merge conflicts near the top of the list. | ||
| For parity with `enable_extension`, the `disable_extension` method can be called with a schema-qualified | ||
| name (e.g. `disable_extension "myschema.pgcrypto"`). Note that PostgreSQL's `DROP EXTENSION` does not | ||
| actually take a schema name (unlike `CREATE EXTENSION`), so the resulting SQL statement will only name | ||
| the extension, e.g. `DROP EXTENSION IF EXISTS "pgcrypto"`. | ||
| Now, the custom formatter can be provided with a custom sorting logic (e.g. by hash values | ||
| of the versions), which can greatly reduce the number of conflicts. | ||
| *Tony Novak* | ||
| *fatkodima* | ||
| * Make `create_schema` / `drop_schema` reversible in migrations. | ||
| * Serialized attributes can now be marked as comparable. | ||
| Previously, `create_schema` and `drop_schema` were irreversible migration operations. | ||
| A not rare issue when working with serialized attributes is that the serialized representation of an object | ||
| can change over time. Either because you are migrating from one serializer to the other (e.g. YAML to JSON or to msgpack), | ||
| or because the serializer used subtly changed its output. | ||
| *Tony Novak* | ||
| One example is libyaml that used to have some extra trailing whitespaces, and recently fixed that. | ||
| When this sorts of thing happen, you end up with lots of records that report being changed even though | ||
| they aren't, which in the best case leads to a lot more writes to the database and in the worst case lead to nasty bugs. | ||
| * Support batching using custom columns. | ||
| The solution is to instead compare the deserialized representation of the object, however Active Record | ||
| can't assume the deserialized object has a working `==` method. Hence why this new functionality is opt-in. | ||
| ```ruby | ||
| Product.in_batches(cursor: [:shop_id, :id]) do |relation| | ||
| # do something with relation | ||
| end | ||
| serialize :config, type: Hash, coder: JSON, comparable: true | ||
| ``` | ||
| *fatkodima* | ||
| *Jean Boussier* | ||
| * Use SQLite `IMMEDIATE` transactions when possible. | ||
| * Fix MySQL default functions getting dropped when changing a column's nullability. | ||
| Transactions run against the SQLite3 adapter default to IMMEDIATE mode to improve concurrency support and avoid busy exceptions. | ||
| *Bastian Bartmann* | ||
| *Stephen Margheim* | ||
| * SQLite extensions can be configured in `config/database.yml`. | ||
| * Raise specific exception when a connection is not defined. | ||
| The database configuration option `extensions:` allows an application to load SQLite extensions | ||
| when using `sqlite3` >= v2.4.0. The array members may be filesystem paths or the names of | ||
| modules that respond to `.to_path`: | ||
| The new `ConnectionNotDefined` exception provides connection name, shard and role accessors indicating the details of the connection that was requested. | ||
| ``` yaml | ||
| development: | ||
| adapter: sqlite3 | ||
| extensions: | ||
| - SQLean::UUID # module name responding to `.to_path` | ||
| - .sqlpkg/nalgeon/crypto/crypto.so # or a filesystem path | ||
| - <%= AppExtensions.location %> # or ruby code returning a path | ||
| ``` | ||
| *Hana Harencarova*, *Matthew Draper* | ||
| *Mike Dalessio* | ||
| * Delete the deprecated constant `ActiveRecord::ImmutableRelation`. | ||
| * `ActiveRecord::Middleware::ShardSelector` supports granular database connection switching. | ||
| *Xavier Noria* | ||
| A new configuration option, `class_name:`, is introduced to | ||
| `config.active_record.shard_selector` to allow an application to specify the abstract connection | ||
| class to be switched by the shard selection middleware. The default class is | ||
| `ActiveRecord::Base`. | ||
| * Fix duplicate callback execution when child autosaves parent with `has_one` and `belongs_to`. | ||
| For example, this configuration tells `ShardSelector` to switch shards using | ||
| `AnimalsRecord.connected_to`: | ||
| Before, persisting a new child record with a new associated parent record would run `before_validation`, | ||
| `after_validation`, `before_save` and `after_save` callbacks twice. | ||
| Now, these callbacks are only executed once as expected. | ||
| *Joshua Young* | ||
| * `ActiveRecord::Encryption::Encryptor` now supports a `:compressor` option to customize the compression algorithm used. | ||
| ```ruby | ||
| module ZstdCompressor | ||
| def self.deflate(data) | ||
| Zstd.compress(data) | ||
| end | ||
| def self.inflate(data) | ||
| Zstd.decompress(data) | ||
| end | ||
| end | ||
| class User | ||
| encrypts :name, compressor: ZstdCompressor | ||
| end | ||
| ``` | ||
| You disable compression by passing `compress: false`. | ||
| ```ruby | ||
| class User | ||
| encrypts :name, compress: false | ||
| end | ||
| config.active_record.shard_selector = { class_name: "AnimalsRecord" } | ||
| ``` | ||
| *heka1024* | ||
| *Mike Dalessio* | ||
| * Add condensed `#inspect` for `ConnectionPool`, `AbstractAdapter`, and | ||
| `DatabaseConfig`. | ||
| * Reset relations after `insert_all`/`upsert_all`. | ||
| *Hartley McGuire* | ||
| Bulk insert/upsert methods will now call `reset` if used on a relation, matching the behavior of `update_all`. | ||
| * Add `.shard_keys`, `.sharded?`, & `.connected_to_all_shards` methods. | ||
| *Milo Winningham* | ||
| ```ruby | ||
| class ShardedBase < ActiveRecord::Base | ||
| self.abstract_class = true | ||
| * Use `_N` as a parallel tests databases suffixes | ||
| connects_to shards: { | ||
| shard_one: { writing: :shard_one }, | ||
| shard_two: { writing: :shard_two } | ||
| } | ||
| end | ||
| Peviously, `-N` was used as a suffix. This can cause problems for RDBMSes | ||
| which do not support dashes in database names. | ||
| class ShardedModel < ShardedBase | ||
| end | ||
| *fatkodima* | ||
| ShardedModel.shard_keys => [:shard_one, :shard_two] | ||
| ShardedModel.sharded? => true | ||
| ShardedBase.connected_to_all_shards { ShardedModel.current_shard } => [:shard_one, :shard_two] | ||
| ``` | ||
| * Remember when a database connection has recently been verified (for | ||
| two seconds, by default), to avoid repeated reverifications during a | ||
| single request. | ||
| *Nony Dutton* | ||
| This should recreate a similar rate of verification as in Rails 7.1, | ||
| where connections are leased for the duration of a request, and thus | ||
| only verified once. | ||
| * Add a `filter` option to `in_order_of` to prioritize certain values in the sorting without filtering the results | ||
| by these values. | ||
| *Matthew Draper* | ||
| *Igor Depolli* | ||
| * Allow to reset cache counters for multiple records. | ||
| * Fix an issue where the IDs reader method did not return expected results | ||
| for preloaded associations in models using composite primary keys. | ||
| ``` | ||
| Aircraft.reset_counters([1, 2, 3], :wheels_count) | ||
| ``` | ||
| *Jay Ang* | ||
| It produces much fewer queries compared to the custom implementation using looping over ids. | ||
| Previously: `O(ids.size * counters.size)` queries, now: `O(ids.size + counters.size)` queries. | ||
| * Allow to configure `strict_loading_mode` globally or within a model. | ||
| *fatkodima* | ||
| Defaults to `:all`, can be changed to `:n_plus_one_only`. | ||
| * Add `affected_rows` to `sql.active_record` Notification. | ||
| *Garen Torikian* | ||
| *Hartley McGuire* | ||
| * Add `ActiveRecord::Relation#readonly?`. | ||
| * Fix `sum` when performing a grouped calculation. | ||
| Reflects if the relation has been marked as readonly. | ||
| `User.group(:friendly).sum` no longer worked. This is fixed. | ||
| *Theodor Tonum* | ||
| *Edouard Chin* | ||
| * Improve `ActiveRecord::Store` to raise a descriptive exception if the column is not either | ||
| structured (e.g., PostgreSQL +hstore+/+json+, or MySQL +json+) or declared serializable via | ||
| `ActiveRecord.store`. | ||
| * Add support for enabling or disabling transactional tests per database. | ||
| Previously, a `NoMethodError` would be raised when the accessor was read or written: | ||
| A test class can now override the default `use_transactional_tests` setting | ||
| for individual databases, which can be useful if some databases need their | ||
| current state to be accessible to an external process while tests are running. | ||
| NoMethodError: undefined method `accessor' for an instance of ActiveRecord::Type::Text | ||
| Now, a descriptive `ConfigurationError` is raised: | ||
| ActiveRecord::ConfigurationError: the column 'metadata' has not been configured as a store. | ||
| Please make sure the column is declared serializable via 'ActiveRecord.store' or, if your | ||
| database supports it, use a structured column type like hstore or json. | ||
| *Mike Dalessio* | ||
| * Fix inference of association model on nested models with the same demodularized name. | ||
| E.g. with the following setup: | ||
| ```ruby | ||
| class Nested::Post < ApplicationRecord | ||
| has_one :post, through: :other | ||
| class MostlyTransactionalTest < ActiveSupport::TestCase | ||
| self.use_transactional_tests = true | ||
| skip_transactional_tests_for_database :shared | ||
| end | ||
| ``` | ||
| Before, `#post` would infer the model as `Nested::Post`, but now it correctly infers `Post`. | ||
| *Matthew Cheetham*, *Morgan Mareve* | ||
| *Joshua Young* | ||
| * Cast `query_cache` value when using URL configuration. | ||
| * Add public method for checking if a table is ignored by the schema cache. | ||
| *zzak* | ||
| Previously, an application would need to reimplement `ignored_table?` from the schema cache class to check if a table was set to be ignored. This adds a public method to support this and updates the schema cache to use that directly. | ||
| * NULLS NOT DISTINCT works with UNIQUE CONSTRAINT as well as UNIQUE INDEX. | ||
| ```ruby | ||
| ActiveRecord.schema_cache_ignored_tables = ["developers"] | ||
| ActiveRecord.schema_cache_ignored_table?("developers") | ||
| => true | ||
| ``` | ||
| *Ryuta Kamizono* | ||
| *Eileen M. Uchitelle* | ||
| * `PG::UnableToSend: no connection to the server` is now retryable as a connection-related exception | ||
| Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activerecord/CHANGELOG.md) for previous changes. | ||
| *Kazuma Watanabe* | ||
| Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/activerecord/CHANGELOG.md) for previous changes. |
+65
-3
@@ -29,2 +29,3 @@ # frozen_string_literal: true | ||
| require "active_support/ordered_options" | ||
| require "active_support/core_ext/array/conversions" | ||
| require "active_model" | ||
@@ -56,2 +57,3 @@ require "arel" | ||
| autoload :FixtureSet, "active_record/fixtures" | ||
| autoload :FilterAttributeHandler | ||
| autoload :Inheritance | ||
@@ -67,3 +69,2 @@ autoload :Integration | ||
| autoload :NoTouching | ||
| autoload :Normalization | ||
| autoload :Persistence | ||
@@ -180,3 +181,4 @@ autoload :QueryCache | ||
| autoload :DatabaseTasks | ||
| autoload :MySQLDatabaseTasks, "active_record/tasks/mysql_database_tasks" | ||
| autoload :AbstractTasks, "active_record/tasks/abstract_tasks" | ||
| autoload :MySQLDatabaseTasks, "active_record/tasks/mysql_database_tasks" | ||
| autoload :PostgreSQLDatabaseTasks, "active_record/tasks/postgresql_database_tasks" | ||
@@ -266,2 +268,5 @@ autoload :SQLiteDatabaseTasks, "active_record/tasks/sqlite_database_tasks" | ||
| # Specify allowlist of database warnings. | ||
| # Can be a string, regular expression, or an error code from the database. | ||
| # | ||
| # ActiveRecord::Base.db_warnings_ignore = [/`SHOW WARNINGS` did not return the warnings/, "01000"] | ||
| singleton_class.attr_accessor :db_warnings_ignore | ||
@@ -294,2 +299,3 @@ self.db_warnings_ignore = [] | ||
| @global_thread_pool_async_query_executor ||= Concurrent::ThreadPoolExecutor.new( | ||
| name: "ActiveRecord-global-async-query-executor", | ||
| min_threads: 0, | ||
@@ -360,2 +366,5 @@ max_threads: concurrency, | ||
| singleton_class.attr_accessor :raise_on_missing_required_finder_order_columns | ||
| self.run_after_transaction_callbacks_in_order_defined = false | ||
| singleton_class.attr_accessor :application_record_class | ||
@@ -412,2 +421,8 @@ self.application_record_class = nil | ||
| ## | ||
| # :singleton-method: schema_versions_formatter | ||
| # Specify the formatter used by schema dumper to format versions information. | ||
| singleton_class.attr_accessor :schema_versions_formatter | ||
| self.schema_versions_formatter = Migration::DefaultSchemaVersionsFormatter | ||
| ## | ||
| # :singleton-method: dump_schema_after_migration | ||
@@ -472,2 +487,25 @@ # Specify whether schema dump should happen at the end of the | ||
| def self.deprecated_associations_options=(options) | ||
| raise ArgumentError, "deprecated_associations_options must be a hash" unless options.is_a?(Hash) | ||
| valid_keys = [:mode, :backtrace] | ||
| invalid_keys = options.keys - valid_keys | ||
| unless invalid_keys.empty? | ||
| inflected_key = invalid_keys.size == 1 ? "key" : "keys" | ||
| raise ArgumentError, "invalid deprecated_associations_options #{inflected_key} #{invalid_keys.map(&:inspect).to_sentence} (valid keys are #{valid_keys.map(&:inspect).to_sentence})" | ||
| end | ||
| options.each do |key, value| | ||
| ActiveRecord::Associations::Deprecation.send("#{key}=", value) | ||
| end | ||
| end | ||
| def self.deprecated_associations_options | ||
| { | ||
| mode: ActiveRecord::Associations::Deprecation.mode, | ||
| backtrace: ActiveRecord::Associations::Deprecation.backtrace | ||
| } | ||
| end | ||
| def self.marshalling_format_version | ||
@@ -509,2 +547,9 @@ Marshalling.format_version | ||
| ## | ||
| # :singleton-method: message_verifiers | ||
| # | ||
| # ActiveSupport::MessageVerifiers instance for Active Record. If you are using | ||
| # Rails, this will be set to +Rails.application.message_verifiers+. | ||
| singleton_class.attr_accessor :message_verifiers | ||
| def self.eager_load! | ||
@@ -564,3 +609,3 @@ super | ||
| if current_transaction.open? && current_transaction.joinable? && !current_transaction.state.invalidated? | ||
| if current_transaction.open? && current_transaction.joinable? | ||
| open_transactions << current_transaction | ||
@@ -572,2 +617,19 @@ end | ||
| end | ||
| def self.default_transaction_isolation_level=(isolation_level) # :nodoc: | ||
| ActiveSupport::IsolatedExecutionState[:active_record_transaction_isolation] = isolation_level | ||
| end | ||
| def self.default_transaction_isolation_level # :nodoc: | ||
| ActiveSupport::IsolatedExecutionState[:active_record_transaction_isolation] | ||
| end | ||
| # Sets a transaction isolation level for all connection pools within the block. | ||
| def self.with_transaction_isolation_level(isolation_level, &block) | ||
| original_level = self.default_transaction_isolation_level | ||
| self.default_transaction_isolation_level = isolation_level | ||
| yield | ||
| ensure | ||
| self.default_transaction_isolation_level = original_level | ||
| end | ||
| end | ||
@@ -574,0 +636,0 @@ |
@@ -20,3 +20,3 @@ # frozen_string_literal: true | ||
| class_eval <<~RUBY, __FILE__, __LINE__ + 1 | ||
| def #{method}(attributes, **kwargs) | ||
| def #{method}(...) | ||
| if @association.reflection.through_reflection? | ||
@@ -23,0 +23,0 @@ raise ArgumentError, "Bulk insert or upsert is currently not supported for has_many through association" |
@@ -235,3 +235,3 @@ # frozen_string_literal: true | ||
| # Whether the association represent a single record | ||
| # Whether the association represents a single record | ||
| # or a collection of records. | ||
@@ -238,0 +238,0 @@ def collection? |
@@ -138,5 +138,3 @@ # frozen_string_literal: true | ||
| if force || reflection_fk.map { |fk| owner._read_attribute(fk) } != target_key_values | ||
| owner_pk = Array(owner.class.primary_key) | ||
| reflection_fk.each_with_index do |key, index| | ||
| next if record.nil? && owner_pk.include?(key) | ||
| owner[key] = target_key_values[index] | ||
@@ -143,0 +141,0 @@ end |
@@ -22,3 +22,3 @@ # frozen_string_literal: true | ||
| VALID_OPTIONS = [ | ||
| :class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :query_constraints | ||
| :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :query_constraints, :deprecated | ||
| ].freeze # :nodoc: | ||
@@ -106,3 +106,5 @@ | ||
| def #{name} | ||
| association(:#{name}).reader | ||
| association = association(:#{name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.reader | ||
| end | ||
@@ -115,3 +117,5 @@ CODE | ||
| def #{name}=(value) | ||
| association(:#{name}).writer(value) | ||
| association = association(:#{name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.writer(value) | ||
| end | ||
@@ -144,4 +148,11 @@ CODE | ||
| def self.add_destroy_callbacks(model, reflection) | ||
| name = reflection.name | ||
| model.before_destroy(->(o) { o.association(name).handle_dependency }) | ||
| if reflection.deprecated? | ||
| # If :dependent is set, destroying the record has a side effect that | ||
| # would no longer happen if the association is removed. | ||
| model.before_destroy do | ||
| report_deprecated_association(reflection, context: ":dependent has a side effect here") | ||
| end | ||
| end | ||
| model.before_destroy(->(o) { o.association(reflection.name).handle_dependency }) | ||
| end | ||
@@ -148,0 +159,0 @@ |
@@ -11,4 +11,5 @@ # frozen_string_literal: true | ||
| valid = super + [:polymorphic, :counter_cache, :optional, :default] | ||
| valid += [:foreign_type] if options[:polymorphic] | ||
| valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async | ||
| valid << :class_name unless options[:polymorphic] | ||
| valid << :foreign_type if options[:polymorphic] | ||
| valid << :ensuring_owner_was if options[:dependent] == :destroy_async | ||
| valid | ||
@@ -111,2 +112,10 @@ end | ||
| def self.add_destroy_callbacks(model, reflection) | ||
| if reflection.deprecated? | ||
| # If :dependent is set, destroying the record has some side effect that | ||
| # would no longer happen if the association is removed. | ||
| model.before_destroy do | ||
| report_deprecated_association(reflection, context: ":dependent has a side effect here") | ||
| end | ||
| end | ||
| model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency } | ||
@@ -149,7 +158,11 @@ end | ||
| def #{reflection.name}_changed? | ||
| association(:#{reflection.name}).target_changed? | ||
| association = association(:#{reflection.name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.target_changed? | ||
| end | ||
| def #{reflection.name}_previously_changed? | ||
| association(:#{reflection.name}).target_previously_changed? | ||
| association = association(:#{reflection.name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.target_previously_changed? | ||
| end | ||
@@ -156,0 +169,0 @@ CODE |
@@ -10,3 +10,3 @@ # frozen_string_literal: true | ||
| def self.valid_options(options) | ||
| super + [:before_add, :after_add, :before_remove, :after_remove, :extend] | ||
| super + [:class_name, :before_add, :after_add, :before_remove, :after_remove, :extend] | ||
| end | ||
@@ -64,3 +64,5 @@ | ||
| def #{name.to_s.singularize}_ids | ||
| association(:#{name}).ids_reader | ||
| association = association(:#{name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.ids_reader | ||
| end | ||
@@ -75,3 +77,5 @@ CODE | ||
| def #{name.to_s.singularize}_ids=(ids) | ||
| association(:#{name}).ids_writer(ids) | ||
| association = association(:#{name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.ids_writer(ids) | ||
| end | ||
@@ -78,0 +82,0 @@ CODE |
@@ -10,3 +10,3 @@ # frozen_string_literal: true | ||
| def self.valid_options(options) | ||
| valid = super + [:as, :through] | ||
| valid = super + [:class_name, :as, :through] | ||
| valid += [:foreign_type] if options[:as] | ||
@@ -13,0 +13,0 @@ valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async |
@@ -20,7 +20,11 @@ # frozen_string_literal: true | ||
| def reload_#{name} | ||
| association(:#{name}).force_reload_reader | ||
| association = association(:#{name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.force_reload_reader | ||
| end | ||
| def reset_#{name} | ||
| association(:#{name}).reset | ||
| association = association(:#{name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.reset | ||
| end | ||
@@ -34,11 +38,17 @@ CODE | ||
| def build_#{name}(*args, &block) | ||
| association(:#{name}).build(*args, &block) | ||
| association = association(:#{name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.build(*args, &block) | ||
| end | ||
| def create_#{name}(*args, &block) | ||
| association(:#{name}).create(*args, &block) | ||
| association = association(:#{name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.create(*args, &block) | ||
| end | ||
| def create_#{name}!(*args, &block) | ||
| association(:#{name}).create!(*args, &block) | ||
| association = association(:#{name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| association.create!(*args, &block) | ||
| end | ||
@@ -48,4 +58,22 @@ CODE | ||
| def self.define_callbacks(model, reflection) | ||
| super | ||
| # If the record is saved or destroyed and `:touch` is set, the parent | ||
| # record gets a timestamp updated. We want to know about it, because | ||
| # deleting the association would change that side-effect and perhaps there | ||
| # is code relying on it. | ||
| if reflection.deprecated? && reflection.options[:touch] | ||
| model.before_save do | ||
| report_deprecated_association(reflection, context: ":touch has a side effect here") | ||
| end | ||
| model.before_destroy do | ||
| report_deprecated_association(reflection, context: ":touch has a side effect here") | ||
| end | ||
| end | ||
| end | ||
| private_class_method :valid_options, :define_accessors, :define_constructors | ||
| end | ||
| end |
@@ -113,3 +113,3 @@ # frozen_string_literal: true | ||
| # Finds an object in the collection responding to the +id+. Uses the same | ||
| # rules as ActiveRecord::FinderMethods.find. Returns ActiveRecord::RecordNotFound | ||
| # rules as ActiveRecord::FinderMethods.find. Raises ActiveRecord::RecordNotFound | ||
| # error if the object cannot be found. | ||
@@ -1129,2 +1129,22 @@ # | ||
| %w(insert insert_all insert! insert_all! upsert upsert_all).each do |method| | ||
| class_eval <<~RUBY, __FILE__, __LINE__ + 1 | ||
| def #{method}(...) | ||
| if @association&.target&.any? { |r| r.new_record? } | ||
| association_name = @association.reflection.name | ||
| ActiveRecord.deprecator.warn(<<~MSG) | ||
| Using #{method} on association \#{association_name} with unpersisted records | ||
| is deprecated and will be removed in Rails 8.2. | ||
| The unpersisted records will be lost after this operation. | ||
| Please either persist your records first or store them separately before | ||
| calling #{method}. | ||
| MSG | ||
| scope.#{method}(...) | ||
| else | ||
| scope.#{method}(...).tap { reset } | ||
| end | ||
| end | ||
| RUBY | ||
| end | ||
| delegate_methods = [ | ||
@@ -1135,5 +1155,3 @@ QueryMethods, | ||
| klass.public_instance_methods(false) | ||
| } - self.public_instance_methods(false) - [:select] + [ | ||
| :scoping, :values, :insert, :insert_all, :insert!, :insert_all!, :upsert, :upsert_all, :load_async | ||
| ] | ||
| } - self.public_instance_methods(false) - [ :select ] + [ :scoping, :values, :load_async ] | ||
@@ -1140,0 +1158,0 @@ delegate(*delegate_methods, to: :scope) |
@@ -265,2 +265,5 @@ # frozen_string_literal: true | ||
| end | ||
| class DeprecatedAssociationError < ActiveRecordError | ||
| end | ||
| end |
@@ -106,3 +106,3 @@ # frozen_string_literal: true | ||
| def instantiate(result_set, strict_loading_value, &block) | ||
| primary_key = Array(join_root.primary_key).map { |column| aliases.column_alias(join_root, column) } | ||
| primary_key = aliases.column_alias(join_root, join_root.primary_key) | ||
@@ -145,3 +145,3 @@ seen = Hash.new { |i, parent| | ||
| result_set.each { |row_hash| | ||
| parent_key = primary_key.empty? ? row_hash : row_hash.values_at(*primary_key) | ||
| parent_key = primary_key ? row_hash[primary_key] : row_hash | ||
| parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, column_types, &block) | ||
@@ -240,2 +240,4 @@ construct(parent, join_root, row_hash, seen, model_cache, strict_loading_value) | ||
| Deprecation.guard(reflection) { "referenced in query to join its table" } | ||
| JoinAssociation.new(reflection, build(right, reflection.klass)) | ||
@@ -242,0 +244,0 @@ end |
@@ -41,9 +41,3 @@ # frozen_string_literal: true | ||
| def group_and_load_similar(loaders) | ||
| non_through = loaders.grep_v(ThroughAssociation) | ||
| grouped = non_through.group_by do |loader| | ||
| [loader.loader_query, loader.klass] | ||
| end | ||
| grouped.each do |(query, _klass), similar_loaders| | ||
| loaders.grep_v(ThroughAssociation).group_by(&:loader_query).each_pair do |query, similar_loaders| | ||
| query.load_records_in_batch(similar_loaders) | ||
@@ -50,0 +44,0 @@ end |
@@ -121,2 +121,3 @@ # frozen_string_literal: true | ||
| grouped_records.flat_map do |reflection, reflection_records| | ||
| Deprecation.guard(reflection) { "referenced in query to preload records" } | ||
| preloaders_for_reflection(reflection, reflection_records) | ||
@@ -123,0 +124,0 @@ end |
@@ -116,3 +116,3 @@ # frozen_string_literal: true | ||
| super(attribute_names) | ||
| alias_attribute :id_value, :id if _has_attribute?("id") && !_has_attribute?("id_value") | ||
| alias_attribute :id_value, :id if _has_attribute?("id") | ||
| end | ||
@@ -119,0 +119,0 @@ |
@@ -54,2 +54,12 @@ # frozen_string_literal: true | ||
| # will set to +type.new+ | ||
| # * +comparable+ - Specify whether the deserialized object is safely comparable | ||
| # for the purpose of detecting changes. Defaults to +false+ | ||
| # When set to +false+ the old and new values will be compared by their serialized | ||
| # representation (e.g. JSON or YAML), which can sometimes cause two objects that are | ||
| # semantically equal to be considered different. | ||
| # For instance two hashes with the same keys and values but a different order have a | ||
| # different serialized representation, but are semantically equal once deserialized. | ||
| # If set to +true+ the comparison will be done on the deserialized object. | ||
| # This options should only be enabled if the +type+ is known to have | ||
| # a proper <tt>==</tt> method that deeply compare the objects. | ||
| # * +yaml+ - Optional. Yaml specific options. The allowed config is: | ||
@@ -184,3 +194,3 @@ # * +:permitted_classes+ - +Array+ with the permitted classes. | ||
| # | ||
| def serialize(attr_name, coder: nil, type: Object, yaml: {}, **options) | ||
| def serialize(attr_name, coder: nil, type: Object, comparable: false, yaml: {}, **options) | ||
| coder ||= default_column_serializer | ||
@@ -205,3 +215,3 @@ unless coder | ||
| cast_type = cast_type.subtype if Type::Serialized === cast_type | ||
| Type::Serialized.new(cast_type, column_serializer) | ||
| Type::Serialized.new(cast_type, column_serializer, comparable: comparable) | ||
| end | ||
@@ -215,4 +225,7 @@ end | ||
| # using the #as_json hook. | ||
| coder = Coders::JSON if coder == ::JSON | ||
| if coder == ::JSON || coder == Coders::JSON | ||
| coder = Coders::JSON.new | ||
| end | ||
| if coder == ::YAML || coder == Coders::YAMLColumn | ||
@@ -219,0 +232,0 @@ Coders::YAMLColumn.new(attr_name, type, **(yaml || {})) |
@@ -10,2 +10,3 @@ # frozen_string_literal: true | ||
| include ActiveModel::AttributeRegistration | ||
| include ActiveModel::Attributes::Normalization | ||
@@ -255,2 +256,3 @@ # = Active Record \Attributes | ||
| @default_attributes ||= begin | ||
| # TODO: Remove the need for a connection after we release 8.1. | ||
| attributes_hash = with_connection do |connection| | ||
@@ -315,2 +317,3 @@ columns_hash.transform_values do |column| | ||
| def type_for_column(connection, column) | ||
| # TODO: Remove the need for a connection after we release 8.1. | ||
| hook_attribute_type(column.name, super) | ||
@@ -317,0 +320,0 @@ end |
@@ -377,3 +377,3 @@ # frozen_string_literal: true | ||
| if context || record.changed_for_autosave? | ||
| if record.changed? || record.new_record? || context | ||
| associated_errors = record.errors.objects | ||
@@ -531,3 +531,3 @@ else | ||
| class_name = record._read_attribute(reflection.inverse_of.foreign_type) | ||
| reflection.active_record.polymorphic_name != class_name | ||
| reflection.active_record != record.class.polymorphic_class_for(class_name) | ||
| end | ||
@@ -534,0 +534,0 @@ |
@@ -259,3 +259,3 @@ # frozen_string_literal: true | ||
| # * AttributeAssignmentError - An error occurred while doing a mass assignment through the | ||
| # {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=] method. | ||
| # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. | ||
| # You can inspect the +attribute+ property of the exception object to determine which attribute | ||
@@ -266,3 +266,3 @@ # triggered the error. | ||
| # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the | ||
| # {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=] method. | ||
| # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. | ||
| # The +errors+ property of this exception contains an array of | ||
@@ -333,3 +333,2 @@ # AttributeAssignmentError | ||
| include Suppressor | ||
| include Normalization | ||
| include Marshalling::Methods | ||
@@ -336,0 +335,0 @@ |
| # frozen_string_literal: true | ||
| require "active_support/json" | ||
| module ActiveRecord | ||
| module Coders # :nodoc: | ||
| module JSON # :nodoc: | ||
| def self.dump(obj) | ||
| ActiveSupport::JSON.encode(obj) | ||
| class JSON # :nodoc: | ||
| DEFAULT_OPTIONS = { escape: false }.freeze | ||
| def initialize(options = nil) | ||
| @options = options ? DEFAULT_OPTIONS.merge(options) : DEFAULT_OPTIONS | ||
| @encoder = ActiveSupport::JSON::Encoding.json_encoder.new(options) | ||
| end | ||
| def self.load(json) | ||
| ActiveSupport::JSON.decode(json) unless json.blank? | ||
| def dump(obj) | ||
| @encoder.encode(obj) | ||
| end | ||
| def load(json) | ||
| ActiveSupport::JSON.decode(json, @options) unless json.blank? | ||
| end | ||
| end | ||
| end | ||
| end |
@@ -87,2 +87,3 @@ # frozen_string_literal: true | ||
| autoload :ColumnDefinition | ||
| autoload :ColumnMethods | ||
| autoload :ChangeColumnDefinition | ||
@@ -89,0 +90,0 @@ autoload :ChangeColumnDefaultDefinition |
@@ -45,3 +45,3 @@ # frozen_string_literal: true | ||
| attr_reader :visitor, :owner, :logger, :lock | ||
| attr_accessor :pinned # :nodoc: | ||
| attr_accessor :allow_preconnect | ||
| alias :in_use? :owner | ||
@@ -124,3 +124,3 @@ | ||
| def self.dbconsole(config, options = {}) | ||
| raise NotImplementedError | ||
| raise NotImplementedError.new("#{self.class} should define `dbconsole` that accepts a db config and options to implement connecting to the db console") | ||
| end | ||
@@ -133,2 +133,3 @@ | ||
| @unconfigured_connection = nil | ||
| @connected_since = nil | ||
@@ -146,2 +147,3 @@ if config_or_deprecated_connection.is_a?(Hash) | ||
| @unconfigured_connection = config_or_deprecated_connection | ||
| @connected_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @logger = deprecated_logger || ActiveRecord::Base.logger | ||
@@ -158,5 +160,5 @@ if deprecated_config | ||
| @owner = nil | ||
| @pinned = false | ||
| @pool = ActiveRecord::ConnectionAdapters::NullPool.new | ||
| @idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @allow_preconnect = true | ||
| @visitor = arel_visitor | ||
@@ -179,2 +181,4 @@ @statements = build_statement_pool | ||
| @verified = false | ||
| @pool_jitter = rand * max_jitter | ||
| end | ||
@@ -207,2 +211,7 @@ | ||
| MAX_JITTER = 0.0..1.0 # :nodoc: | ||
| def max_jitter | ||
| (@config[:pool_jitter] || 0.2).to_f.clamp(MAX_JITTER) | ||
| end | ||
| def replica? | ||
@@ -272,5 +281,9 @@ @config[:replica] || false | ||
| def valid_type?(type) # :nodoc: | ||
| !native_database_types[type].nil? | ||
| self.class.valid_type?(type) | ||
| end | ||
| def native_database_types # :nodoc: | ||
| self.class.native_database_types | ||
| end | ||
| # this method must only be called while holding connection pool's mutex | ||
@@ -312,4 +325,8 @@ def lease | ||
| def pool_jitter(duration) | ||
| duration * (1.0 - @pool_jitter) | ||
| end | ||
| # this method must only be called while holding connection pool's mutex | ||
| def expire | ||
| def expire(update_idle = true) # :nodoc: | ||
| if in_use? | ||
@@ -322,3 +339,3 @@ if @owner != ActiveSupport::IsolatedExecutionState.context | ||
| @idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) if update_idle | ||
| @owner = nil | ||
@@ -356,2 +373,17 @@ else | ||
| # Seconds since this connection was established. nil if not | ||
| # connected; infinity if the connection has been explicitly | ||
| # retired. | ||
| def connection_age # :nodoc: | ||
| if @raw_connection && @connected_since | ||
| Process.clock_gettime(Process::CLOCK_MONOTONIC) - @connected_since | ||
| end | ||
| end | ||
| # Mark the connection as needing to be retired, as if the age has | ||
| # exceeded the maximum allowed. | ||
| def force_retirement # :nodoc: | ||
| @connected_since &&= -Float::INFINITY | ||
| end | ||
| def unprepared_statement | ||
@@ -571,2 +603,6 @@ cache = prepared_statements_disabled_cache.add?(object_id) if @prepared_statements | ||
| def supports_disabling_indexes? | ||
| false | ||
| end | ||
| def return_value_after_insert?(column) # :nodoc: | ||
@@ -681,29 +717,33 @@ column.auto_populated? | ||
| @lock.synchronize do | ||
| attempt_configure_connection do | ||
| reconnect | ||
| @allow_preconnect = false | ||
| enable_lazy_transactions! | ||
| @raw_connection_dirty = false | ||
| @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @verified = true | ||
| reconnect | ||
| reset_transaction(restore: restore_transactions) do | ||
| clear_cache!(new_connection: true) | ||
| configure_connection | ||
| end | ||
| rescue => original_exception | ||
| translated_exception = translate_exception_class(original_exception, nil, nil) | ||
| retry_deadline_exceeded = deadline && deadline < Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| enable_lazy_transactions! | ||
| @raw_connection_dirty = false | ||
| @last_activity = @connected_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @verified = true | ||
| @allow_preconnect = true | ||
| if !retry_deadline_exceeded && retries_available > 0 | ||
| retries_available -= 1 | ||
| reset_transaction(restore: restore_transactions) do | ||
| clear_cache!(new_connection: true) | ||
| attempt_configure_connection | ||
| end | ||
| rescue => original_exception | ||
| translated_exception = translate_exception_class(original_exception, nil, nil) | ||
| retry_deadline_exceeded = deadline && deadline < Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| if retryable_connection_error?(translated_exception) | ||
| backoff(connection_retries - retries_available) | ||
| retry | ||
| end | ||
| if !retry_deadline_exceeded && retries_available > 0 | ||
| retries_available -= 1 | ||
| if retryable_connection_error?(translated_exception) | ||
| backoff(connection_retries - retries_available) | ||
| retry | ||
| end | ||
| end | ||
| raise translated_exception | ||
| end | ||
| @last_activity = nil | ||
| @verified = false | ||
| raise translated_exception | ||
| end | ||
@@ -716,7 +756,6 @@ end | ||
| @lock.synchronize do | ||
| @last_activity = nil | ||
| @verified = false | ||
| clear_cache!(new_connection: true) | ||
| reset_transaction | ||
| @raw_connection_dirty = false | ||
| @connected_since = nil | ||
| end | ||
@@ -744,7 +783,5 @@ end | ||
| def reset! | ||
| attempt_configure_connection do | ||
| clear_cache!(new_connection: true) | ||
| reset_transaction | ||
| configure_connection | ||
| end | ||
| clear_cache!(new_connection: true) | ||
| reset_transaction | ||
| attempt_configure_connection | ||
| end | ||
@@ -783,9 +820,8 @@ | ||
| if @unconfigured_connection | ||
| attempt_configure_connection do | ||
| @raw_connection = @unconfigured_connection | ||
| @unconfigured_connection = nil | ||
| configure_connection | ||
| @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @verified = true | ||
| end | ||
| @raw_connection = @unconfigured_connection | ||
| @unconfigured_connection = nil | ||
| attempt_configure_connection | ||
| @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @verified = true | ||
| @allow_preconnect = true | ||
| return | ||
@@ -798,2 +834,3 @@ end | ||
| @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @verified = true | ||
@@ -812,2 +849,6 @@ end | ||
| def verified? # :nodoc: | ||
| @verified | ||
| end | ||
| # Provides access to the underlying database driver for this adapter. For | ||
@@ -898,3 +939,3 @@ # example, this method returns a Mysql2::Client object in case of Mysql2Adapter, | ||
| precision = extract_precision(args.last) | ||
| klass.new(precision: precision, **kwargs) | ||
| klass.new(precision: precision, **kwargs).freeze | ||
| end | ||
@@ -911,2 +952,6 @@ end | ||
| def valid_type?(type) # :nodoc: | ||
| !native_database_types[type].nil? | ||
| end | ||
| private | ||
@@ -931,3 +976,3 @@ def initialize_type_map(m) | ||
| m.register_type %r(^json)i, Type::Json.new | ||
| m.register_type %r(^json)i, Type::Json.new.freeze | ||
@@ -940,5 +985,5 @@ m.register_type(%r(decimal)i) do |sql_type| | ||
| # FIXME: Remove this class as well | ||
| Type::DecimalWithoutScale.new(precision: precision) | ||
| Type::DecimalWithoutScale.new(precision: precision).freeze | ||
| else | ||
| Type::Decimal.new(precision: precision, scale: scale) | ||
| Type::Decimal.new(precision: precision, scale: scale).freeze | ||
| end | ||
@@ -951,3 +996,3 @@ end | ||
| limit = extract_limit(args.last) | ||
| klass.new(limit: limit) | ||
| klass.new(limit: limit).freeze | ||
| end | ||
@@ -1113,3 +1158,3 @@ end | ||
| def reconnect | ||
| raise NotImplementedError | ||
| raise NotImplementedError.new("#{self.class} should define `reconnect` to implement adapter-specific logic for reconnecting to the database") | ||
| end | ||
@@ -1165,3 +1210,3 @@ | ||
| def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, &block) # :doc: | ||
| def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, allow_retry: false, &block) # :doc: | ||
| instrumenter.instrument( | ||
@@ -1174,4 +1219,6 @@ "sql.active_record", | ||
| async: async, | ||
| allow_retry: allow_retry, | ||
| connection: self, | ||
| transaction: current_transaction.user_transaction.presence, | ||
| affected_rows: 0, | ||
| row_count: 0, | ||
@@ -1250,3 +1297,3 @@ &block | ||
| def attempt_configure_connection | ||
| yield | ||
| configure_connection | ||
| rescue Exception # Need to handle things such as Timeout::ExitException | ||
@@ -1253,0 +1300,0 @@ disconnect! |
@@ -45,3 +45,3 @@ # frozen_string_literal: true | ||
| blob: { name: "blob" }, | ||
| boolean: { name: "tinyint", limit: 1 }, | ||
| boolean: { name: "boolean" }, | ||
| json: { name: "json" }, | ||
@@ -85,2 +85,6 @@ } | ||
| end | ||
| def native_database_types # :nodoc: | ||
| NATIVE_DATABASE_TYPES | ||
| end | ||
| end | ||
@@ -183,2 +187,12 @@ | ||
| # See https://dev.mysql.com/doc/refman/8.0/en/invisible-indexes.html for more details on MySQL feature. | ||
| # See https://mariadb.com/kb/en/ignored-indexes/ for more details on the MariaDB feature. | ||
| def supports_disabling_indexes? | ||
| if mariadb? | ||
| database_version >= "10.6.0" | ||
| else | ||
| database_version >= "8.0.0" | ||
| end | ||
| end | ||
| def get_advisory_lock(lock_name, timeout = 0) # :nodoc: | ||
@@ -192,6 +206,2 @@ query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1 | ||
| def native_database_types | ||
| NATIVE_DATABASE_TYPES | ||
| end | ||
| def index_algorithms | ||
@@ -464,2 +474,20 @@ { | ||
| def enable_index(table_name, index_name) # :nodoc: | ||
| raise NotImplementedError unless supports_disabling_indexes? | ||
| query = <<~SQL | ||
| ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "NOT IGNORED" : "VISIBLE"} | ||
| SQL | ||
| execute(query) | ||
| end | ||
| def disable_index(table_name, index_name) # :nodoc: | ||
| raise NotImplementedError unless supports_disabling_indexes? | ||
| query = <<~SQL | ||
| ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "IGNORED" : "INVISIBLE"} | ||
| SQL | ||
| execute(query) | ||
| end | ||
| def add_sql_comment!(sql, comment) # :nodoc: | ||
@@ -740,8 +768,8 @@ sql << " COMMENT #{quote(comment)}" if comment.present? | ||
| def register_integer_type(mapping, key, **options) | ||
| def register_integer_type(mapping, key, limit:) | ||
| mapping.register_type(key) do |sql_type| | ||
| if /\bunsigned\b/.match?(sql_type) | ||
| Type::UnsignedInteger.new(**options) | ||
| Type::UnsignedInteger.new(limit: limit) | ||
| else | ||
| Type::Integer.new(**options) | ||
| Type::Integer.new(limit: limit) | ||
| end | ||
@@ -776,3 +804,3 @@ end | ||
| def handle_warnings(sql) | ||
| def handle_warnings(_initial_result, sql) | ||
| return if ActiveRecord.db_warnings_action.nil? || @raw_connection.warning_count == 0 | ||
@@ -783,3 +811,3 @@ | ||
| result = [ | ||
| ["Warning", nil, "Query had warning_count=#{warning_count} but ‘SHOW WARNINGS’ did not return the warnings. Check MySQL logs or database configuration."], | ||
| ["Warning", nil, "Query had warning_count=#{warning_count} but `SHOW WARNINGS` did not return the warnings. Check MySQL logs or database configuration."], | ||
| ] if result.count == 0 | ||
@@ -821,2 +849,4 @@ result.each do |level, code, message| | ||
| ER_FK_INCOMPATIBLE_COLUMNS = 3780 | ||
| ER_CHECK_CONSTRAINT_VIOLATED = 3819 | ||
| ER_CONSTRAINT_FAILED = 4025 | ||
| ER_CLIENT_INTERACTION_TIMEOUT = 4031 | ||
@@ -854,2 +884,4 @@ | ||
| NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool) | ||
| when ER_CHECK_CONSTRAINT_VIOLATED, ER_CONSTRAINT_FAILED | ||
| CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool) | ||
| when ER_LOCK_DEADLOCK | ||
@@ -973,3 +1005,3 @@ Deadlocked.new(message, sql: sql, binds: binds, connection_pool: @pool) | ||
| def column_definitions(table_name) # :nodoc: | ||
| internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") | ||
| internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA", allow_retry: true) | ||
| end | ||
@@ -976,0 +1008,0 @@ |
@@ -161,5 +161,3 @@ # frozen_string_literal: true | ||
| # Returns any connections in use by the current thread back to the pool, | ||
| # and also returns connections to the pool cached by threads that are no | ||
| # longer alive. | ||
| # Returns any connections in use by the current thread back to the pool. | ||
| def clear_active_connections!(role = nil) | ||
@@ -166,0 +164,0 @@ each_connection_pool(role).each do |pool| |
@@ -17,3 +17,3 @@ # frozen_string_literal: true | ||
| class NullConfig # :nodoc: | ||
| class NullConfig | ||
| def method_missing(...) | ||
@@ -23,3 +23,3 @@ nil | ||
| end | ||
| NULL_CONFIG = NullConfig.new # :nodoc: | ||
| NULL_CONFIG = NullConfig.new | ||
@@ -41,3 +41,2 @@ def initialize | ||
| def schema_cache; end | ||
| def query_cache; end | ||
| def connection_descriptor; end | ||
@@ -55,2 +54,7 @@ def checkin(_); end | ||
| end | ||
| def pool_transaction_isolation_level; end | ||
| def pool_transaction_isolation_level=(isolation_level) | ||
| raise NotImplementedError, "This method should never be called" | ||
| end | ||
| end | ||
@@ -108,9 +112,16 @@ | ||
| # | ||
| # * +pool+: maximum number of connections the pool may manage (default 5). | ||
| # * +checkout_timeout+: number of seconds to wait for a connection to | ||
| # become available before giving up and raising a timeout error (default | ||
| # 5 seconds). | ||
| # * +idle_timeout+: number of seconds that a connection will be kept | ||
| # unused in the pool before it is automatically disconnected (default | ||
| # 300 seconds). Set this to zero to keep connections forever. | ||
| # * +checkout_timeout+: number of seconds to wait for a connection to | ||
| # become available before giving up and raising a timeout error (default | ||
| # 5 seconds). | ||
| # * +keepalive+: number of seconds between keepalive checks if the | ||
| # connection has been idle (default 600 seconds). | ||
| # * +max_age+: number of seconds the pool will allow the connection to | ||
| # exist before retiring it at next checkin. (default Float::INFINITY). | ||
| # * +max_connections+: maximum number of connections the pool may manage (default 5). | ||
| # * +min_connections+: minimum number of connections the pool will open and maintain (default 0). | ||
| # * +pool_jitter+: maximum reduction factor to apply to +max_age+ and | ||
| # +keepalive+ intervals (default 0.2; range 0.0-1.0). | ||
| # | ||
@@ -123,2 +134,3 @@ #-- | ||
| # * @now_connecting | ||
| # * @maintaining | ||
| # * private methods that require being called in a +synchronize+ blocks | ||
@@ -129,3 +141,3 @@ # are now explicitly documented | ||
| # https://bugs.ruby-lang.org/issues/20688 | ||
| if ObjectSpace.const_defined?(:WeakKeyMap) && Gem::Version.new(RUBY_VERSION) >= "3.3.5" | ||
| if ObjectSpace.const_defined?(:WeakKeyMap) && RUBY_VERSION >= "3.3.5" | ||
| WeakThreadKeyMap = ObjectSpace::WeakKeyMap | ||
@@ -230,3 +242,4 @@ else | ||
| attr_accessor :automatic_reconnect, :checkout_timeout | ||
| attr_reader :db_config, :size, :reaper, :pool_config, :async_executor, :role, :shard | ||
| attr_reader :db_config, :max_connections, :min_connections, :max_age, :keepalive, :reaper, :pool_config, :async_executor, :role, :shard | ||
| alias :size :max_connections | ||
@@ -251,3 +264,6 @@ delegate :schema_reflection, :server_version, to: :pool_config | ||
| @idle_timeout = db_config.idle_timeout | ||
| @size = db_config.pool | ||
| @max_connections = db_config.max_connections | ||
| @min_connections = db_config.min_connections | ||
| @max_age = db_config.max_age | ||
| @keepalive = db_config.keepalive | ||
@@ -274,2 +290,8 @@ # This variable tracks the cache of threads mapped to reserved connections, with the | ||
| # Sometimes otherwise-idle connections are temporarily held by the Reaper for | ||
| # maintenance. This variable tracks the number of connections currently in that | ||
| # state -- if a thread requests a connection and there are none available, it | ||
| # will await any in-maintenance connections in preference to creating a new one. | ||
| @maintaining = 0 | ||
| @threads_blocking_new_connections = 0 | ||
@@ -285,2 +307,6 @@ | ||
| @activated = false | ||
| @original_context = ActiveSupport::IsolatedExecutionState.context | ||
| @reaper_lock = Monitor.new | ||
| @reaper = Reaper.new(self, db_config.reaping_frequency) | ||
@@ -291,4 +317,4 @@ @reaper.run | ||
| def inspect # :nodoc: | ||
| name_field = " name=#{db_config.name.inspect}" unless db_config.name == "primary" | ||
| shard_field = " shard=#{@shard.inspect}" unless @shard == :default | ||
| name_field = " name=#{name_inspect}" if name_inspect | ||
| shard_field = " shard=#{shard_inspect}" if shard_inspect | ||
@@ -323,2 +349,10 @@ "#<#{self.class.name} env_name=#{db_config.env_name.inspect}#{name_field} role=#{role.inspect}#{shard_field}>" | ||
| def activate | ||
| @activated = true | ||
| end | ||
| def activated? | ||
| @activated | ||
| end | ||
| # Retrieve the connection associated with the current thread, or call | ||
@@ -331,5 +365,4 @@ # #checkout to obtain one if necessary. | ||
| lease = connection_lease | ||
| lease.sticky = true | ||
| lease.connection ||= checkout | ||
| lease.sticky = true | ||
| lease.connection | ||
| end | ||
@@ -352,3 +385,2 @@ | ||
| @pinned_connection.lock_thread = ActiveSupport::IsolatedExecutionState.context if lock_thread | ||
| @pinned_connection.pinned = true | ||
| @pinned_connection.verify! # eagerly validate the connection | ||
@@ -376,3 +408,2 @@ @pinned_connection.begin_transaction joinable: false, _lazy: false | ||
| if @pinned_connection.nil? | ||
| connection.pinned = false | ||
| connection.steal! | ||
@@ -409,2 +440,4 @@ connection.lock_thread = nil | ||
| def release_connection(existing_lease = nil) | ||
| return if self.discarded? | ||
| if conn = connection_lease.release | ||
@@ -447,2 +480,20 @@ checkin conn | ||
| def with_pool_transaction_isolation_level(isolation_level, transaction_open, &block) # :nodoc: | ||
| if !ActiveRecord.default_transaction_isolation_level.nil? | ||
| begin | ||
| if transaction_open && self.pool_transaction_isolation_level != ActiveRecord.default_transaction_isolation_level | ||
| raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open" | ||
| end | ||
| old_level = self.pool_transaction_isolation_level | ||
| self.pool_transaction_isolation_level = isolation_level | ||
| yield | ||
| ensure | ||
| self.pool_transaction_isolation_level = old_level | ||
| end | ||
| else | ||
| yield | ||
| end | ||
| end | ||
| # Returns true if a connection has already been opened. | ||
@@ -475,14 +526,22 @@ def connected? | ||
| def disconnect(raise_on_acquisition_timeout = true) | ||
| with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do | ||
| synchronize do | ||
| @connections.each do |conn| | ||
| if conn.in_use? | ||
| conn.steal! | ||
| checkin conn | ||
| @reaper_lock.synchronize do | ||
| return if self.discarded? | ||
| with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do | ||
| synchronize do | ||
| return if self.discarded? | ||
| @connections.each do |conn| | ||
| if conn.in_use? | ||
| conn.steal! | ||
| checkin conn | ||
| end | ||
| conn.disconnect! | ||
| end | ||
| conn.disconnect! | ||
| @connections = [] | ||
| @leases.clear | ||
| @available.clear | ||
| # Stop maintaining the minimum size until reactivated | ||
| @activated = false | ||
| end | ||
| @connections = @pinned_connection ? [@pinned_connection] : [] | ||
| @leases.clear | ||
| @available.clear | ||
| end | ||
@@ -508,8 +567,10 @@ end | ||
| def discard! # :nodoc: | ||
| synchronize do | ||
| return if self.discarded? | ||
| @connections.each do |conn| | ||
| conn.discard! | ||
| @reaper_lock.synchronize do | ||
| synchronize do | ||
| return if self.discarded? | ||
| @connections.each do |conn| | ||
| conn.discard! | ||
| end | ||
| @connections = @available = @leases = nil | ||
| end | ||
| @connections = @available = @leases = nil | ||
| end | ||
@@ -522,2 +583,12 @@ end | ||
| def maintainable? # :nodoc: | ||
| synchronize do | ||
| @connections&.size&.> 0 || (activated? && @min_connections > 0) | ||
| end | ||
| end | ||
| def reaper_lock(&block) # :nodoc: | ||
| @reaper_lock.synchronize(&block) | ||
| end | ||
| # Clears reloadable connections from the pool and re-connects connections that | ||
@@ -628,3 +699,3 @@ # require reloading. | ||
| # @available.any_waiting? => true means that prior to removing this | ||
| # conn, the pool was at its max size (@connections.size == @size). | ||
| # conn, the pool was at its max size (@connections.size == @max_connections). | ||
| # This would mean that any threads stuck waiting in the queue wouldn't | ||
@@ -637,3 +708,3 @@ # know they could checkout_new_connection, so let's do it for them. | ||
| # connections into the Queue. | ||
| needs_new_connection = @available.any_waiting? | ||
| needs_new_connection = @available.num_waiting > @maintaining | ||
| end | ||
@@ -645,3 +716,3 @@ | ||
| # stale, we can live with that (bulk_make_new_connections will make | ||
| # sure not to exceed the pool's @size limit). | ||
| # sure not to exceed the pool's @max_connections limit). | ||
| bulk_make_new_connections(1) if needs_new_connection | ||
@@ -679,7 +750,23 @@ end | ||
| idle_connections = synchronize do | ||
| removed_connections = synchronize do | ||
| return if self.discarded? | ||
| @connections.select do |conn| | ||
| idle_connections = @connections.select do |conn| | ||
| !conn.in_use? && conn.seconds_idle >= minimum_idle | ||
| end.each do |conn| | ||
| end.sort_by { |conn| -conn.seconds_idle } # sort longest idle first | ||
| # Don't go below our configured pool minimum unless we're flushing | ||
| # everything | ||
| idles_to_retain = | ||
| if minimum_idle > 0 | ||
| @min_connections - (@connections.size - idle_connections.size) | ||
| else | ||
| 0 | ||
| end | ||
| if idles_to_retain > 0 | ||
| idle_connections.pop idles_to_retain | ||
| end | ||
| idle_connections.each do |conn| | ||
| conn.lease | ||
@@ -692,3 +779,3 @@ | ||
| idle_connections.each do |conn| | ||
| removed_connections.each do |conn| | ||
| conn.disconnect! | ||
@@ -699,8 +786,90 @@ end | ||
| # Disconnect all currently idle connections. Connections currently checked | ||
| # out are unaffected. | ||
| # out are unaffected. The pool will stop maintaining its minimum size until | ||
| # it is reactivated (such as by a subsequent checkout). | ||
| def flush! | ||
| reap | ||
| flush(-1) | ||
| # Stop maintaining the minimum size until reactivated | ||
| @activated = false | ||
| end | ||
| # Ensure that the pool contains at least the configured minimum number of | ||
| # connections. | ||
| def prepopulate | ||
| need_new_connections = nil | ||
| synchronize do | ||
| return if self.discarded? | ||
| # We don't want to start prepopulating until we know the pool is wanted, | ||
| # so we can avoid maintaining full pools in one-off scripts etc. | ||
| return unless @activated | ||
| need_new_connections = @connections.size < @min_connections | ||
| end | ||
| if need_new_connections | ||
| while new_conn = try_to_checkout_new_connection { @connections.size < @min_connections } | ||
| checkin(new_conn) | ||
| end | ||
| end | ||
| end | ||
| def retire_old_connections(max_age = @max_age) | ||
| max_age ||= Float::INFINITY | ||
| sequential_maintenance -> c { c.connection_age&.>= c.pool_jitter(max_age) } do |conn| | ||
| # Disconnect, then return the adapter to the pool. Preconnect will | ||
| # handle the rest. | ||
| conn.disconnect! | ||
| end | ||
| end | ||
| # Preconnect all connections in the pool. This saves pool users from | ||
| # having to wait for a connection to be established when first using it | ||
| # after checkout. | ||
| def preconnect | ||
| sequential_maintenance -> c { (!c.connected? || !c.verified?) && c.allow_preconnect } do |conn| | ||
| conn.connect! | ||
| rescue | ||
| # Wholesale rescue: there's nothing we can do but move on. The | ||
| # connection will go back to the pool, and the next consumer will | ||
| # presumably try to connect again -- which will either work, or | ||
| # fail and they'll be able to report the exception. | ||
| end | ||
| end | ||
| # Prod any connections that have been idle for longer than the configured | ||
| # keepalive time. This will incidentally verify the connection is still | ||
| # alive, but the main purpose is to show the server (and any intermediate | ||
| # network hops) that we're still here and using the connection. | ||
| def keep_alive(threshold = @keepalive) | ||
| return if threshold.nil? | ||
| sequential_maintenance -> c { (c.seconds_since_last_activity || 0) > c.pool_jitter(threshold) } do |conn| | ||
| # conn.active? will cause some amount of network activity, which is all | ||
| # we need to provide a keepalive signal. | ||
| # | ||
| # If it returns false, the connection is already broken; disconnect, | ||
| # so it can be found and repaired. | ||
| conn.disconnect! unless conn.active? | ||
| end | ||
| end | ||
| # Immediately mark all current connections as due for replacement, | ||
| # equivalent to them having reached +max_age+ -- even if there is | ||
| # no +max_age+ configured. | ||
| def recycle! | ||
| synchronize do | ||
| return if self.discarded? | ||
| @connections.each do |conn| | ||
| conn.force_retirement | ||
| end | ||
| end | ||
| retire_old_connections | ||
| end | ||
| def num_waiting_in_queue # :nodoc: | ||
@@ -710,2 +879,6 @@ @available.num_waiting | ||
| def num_available_in_queue # :nodoc: | ||
| @available.size | ||
| end | ||
| # Returns the connection pool's usage statistic. | ||
@@ -741,2 +914,12 @@ # | ||
| def pool_transaction_isolation_level | ||
| isolation_level_key = "activerecord_pool_transaction_isolation_level_#{db_config.name}" | ||
| ActiveSupport::IsolatedExecutionState[isolation_level_key] | ||
| end | ||
| def pool_transaction_isolation_level=(isolation_level) | ||
| isolation_level_key = "activerecord_pool_transaction_isolation_level_#{db_config.name}" | ||
| ActiveSupport::IsolatedExecutionState[isolation_level_key] = isolation_level | ||
| end | ||
| private | ||
@@ -751,3 +934,5 @@ def connection_lease | ||
| if @db_config.max_threads > 0 | ||
| name_with_shard = [name_inspect, shard_inspect].join("-").tr("_", "-") | ||
| Concurrent::ThreadPoolExecutor.new( | ||
| name: "ActiveRecord-#{name_with_shard}-async-query-executor", | ||
| min_threads: @db_config.min_threads, | ||
@@ -764,7 +949,105 @@ max_threads: @db_config.max_threads, | ||
| # Perform maintenance work on pool connections. This method will | ||
| # select a connection to work on by calling the +candidate_selector+ | ||
| # proc while holding the pool lock. If a connection is selected, it | ||
| # will be checked out for maintenance and passed to the | ||
| # +maintenance_work+ proc. The connection will always be returned to | ||
| # the pool after the proc completes. | ||
| # | ||
| # If the pool has async threads, all work will be scheduled there. | ||
| # Otherwise, this method will block until all work is complete. | ||
| # | ||
| # Each connection will only be processed once per call to this method, | ||
| # but (particularly in the async case) there is no protection against | ||
| # a second call to this method starting to work through the list | ||
| # before the first call has completed. (Though regular pool behaviour | ||
| # will prevent two instances from working on the same specific | ||
| # connection at the same time.) | ||
| def sequential_maintenance(candidate_selector, &maintenance_work) | ||
| # This hash doesn't need to be synchronized, because it's only | ||
| # used by one thread at a time: the +perform_work+ block gives | ||
| # up its right to +connections_visited+ when it schedules the | ||
| # next iteration. | ||
| connections_visited = Hash.new(false) | ||
| connections_visited.compare_by_identity | ||
| perform_work = lambda do | ||
| connection_to_maintain = nil | ||
| synchronize do | ||
| unless self.discarded? | ||
| if connection_to_maintain = @connections.select { |conn| !conn.in_use? }.select(&candidate_selector).sort_by(&:seconds_idle).find { |conn| !connections_visited[conn] } | ||
| checkout_for_maintenance connection_to_maintain | ||
| end | ||
| end | ||
| end | ||
| if connection_to_maintain | ||
| connections_visited[connection_to_maintain] = true | ||
| # If we're running async, we can schedule the next round of work | ||
| # as soon as we've grabbed a connection to work on. | ||
| @async_executor&.post(&perform_work) | ||
| begin | ||
| maintenance_work.call connection_to_maintain | ||
| ensure | ||
| return_from_maintenance connection_to_maintain | ||
| end | ||
| true | ||
| end | ||
| end | ||
| if @async_executor | ||
| @async_executor.post(&perform_work) | ||
| else | ||
| nil while perform_work.call | ||
| end | ||
| end | ||
| # Directly check a specific connection out of the pool. Skips callbacks. | ||
| # | ||
| # The connection must later either #return_from_maintenance or | ||
| # #remove_from_maintenance, or the pool will hang. | ||
| def checkout_for_maintenance(conn) | ||
| synchronize do | ||
| @maintaining += 1 | ||
| @available.delete(conn) | ||
| conn.lease | ||
| conn | ||
| end | ||
| end | ||
| # Return a connection to the pool after it has been checked out for | ||
| # maintenance. Does not update the connection's idle time, and skips | ||
| # callbacks. | ||
| #-- | ||
| # We assume that a connection that has required maintenance is less | ||
| # desirable (either it's been idle for a long time, or it was just | ||
| # created and hasn't been used yet). We'll put it at the back of the | ||
| # queue. | ||
| def return_from_maintenance(conn) | ||
| synchronize do | ||
| conn.expire(false) | ||
| @available.add_back(conn) | ||
| @maintaining -= 1 | ||
| end | ||
| end | ||
| # Remove a connection from the pool after it has been checked out for | ||
| # maintenance. It will be automatically replaced with a new connection if | ||
| # necessary. | ||
| def remove_from_maintenance(conn) | ||
| synchronize do | ||
| @maintaining -= 1 | ||
| remove conn | ||
| end | ||
| end | ||
| #-- | ||
| # this is unfortunately not concurrent | ||
| def bulk_make_new_connections(num_new_conns_needed) | ||
| num_new_conns_needed.times do | ||
| # try_to_checkout_new_connection will not exceed pool's @size limit | ||
| # try_to_checkout_new_connection will not exceed pool's @max_connections limit | ||
| if new_conn = try_to_checkout_new_connection | ||
@@ -782,5 +1065,7 @@ # make the new_conn available to the starving threads stuck @available Queue | ||
| def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true) | ||
| with_new_connections_blocked do | ||
| attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout) | ||
| yield | ||
| @reaper_lock.synchronize do | ||
| with_new_connections_blocked do | ||
| attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout) | ||
| yield | ||
| end | ||
| end | ||
@@ -905,3 +1190,3 @@ end | ||
| # of the said methods and avoid an additional +synchronize+ overhead. | ||
| if conn = @available.poll || try_to_checkout_new_connection | ||
| if conn = @available.poll || try_to_queue_for_background_connection(checkout_timeout) || try_to_checkout_new_connection | ||
| conn | ||
@@ -912,3 +1197,3 @@ else | ||
| # remove an inactive connection, or both | ||
| if conn = @available.poll || try_to_checkout_new_connection | ||
| if conn = @available.poll || try_to_queue_for_background_connection(checkout_timeout) || try_to_checkout_new_connection | ||
| conn | ||
@@ -924,2 +1209,27 @@ else | ||
| #-- | ||
| # If new connections are already being established in the background, | ||
| # and there are fewer threads already waiting than the number of | ||
| # upcoming connections, we can just get in queue and wait to be handed a | ||
| # connection. This avoids us overshooting the required connection count | ||
| # by starting a new connection ourselves, and is likely to be faster | ||
| # too (because at least some of the time it takes to establish a new | ||
| # connection must have already passed). | ||
| # | ||
| # If background connections are available, this method will block and | ||
| # return a connection. If no background connections are available, it | ||
| # will immediately return +nil+. | ||
| def try_to_queue_for_background_connection(checkout_timeout) | ||
| return unless @maintaining > 0 | ||
| synchronize do | ||
| return unless @maintaining > @available.num_waiting | ||
| # We are guaranteed the "maintaining" thread will return its promised | ||
| # connection within one maintenance-unit of time. Thus we can safely | ||
| # do a blocking wait with (functionally) no timeout. | ||
| @available.poll(100) | ||
| end | ||
| end | ||
| #-- | ||
| # if owner_thread param is omitted, this must be called in synchronize block | ||
@@ -933,4 +1243,7 @@ def remove_connection_from_thread_cache(conn, owner_thread = conn.owner) | ||
| # If the pool is not at a <tt>@size</tt> limit, establish new connection. Connecting | ||
| # If the pool is not at a <tt>@max_connections</tt> limit, establish new connection. Connecting | ||
| # to the DB is done outside main synchronized section. | ||
| # | ||
| # If a block is supplied, it is an additional constraint (checked while holding the | ||
| # pool lock) on whether a new connection should be established. | ||
| #-- | ||
@@ -941,6 +1254,12 @@ # Implementation constraint: a newly established connection returned by this | ||
| # first in synchronized section check if establishing new conns is allowed | ||
| # and increment @now_connecting, to prevent overstepping this pool's @size | ||
| # and increment @now_connecting, to prevent overstepping this pool's @max_connections | ||
| # constraint | ||
| do_checkout = synchronize do | ||
| if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size | ||
| return if self.discarded? | ||
| if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @max_connections && (!block_given? || yield) | ||
| if @connections.size > 0 || @original_context != ActiveSupport::IsolatedExecutionState.context | ||
| @activated = true | ||
| end | ||
| @now_connecting += 1 | ||
@@ -956,8 +1275,12 @@ end | ||
| synchronize do | ||
| @now_connecting -= 1 | ||
| if conn | ||
| adopt_connection(conn) | ||
| # returned conn needs to be already leased | ||
| conn.lease | ||
| if self.discarded? | ||
| conn.discard! | ||
| else | ||
| adopt_connection(conn) | ||
| # returned conn needs to be already leased | ||
| conn.lease | ||
| end | ||
| end | ||
| @now_connecting -= 1 | ||
| end | ||
@@ -994,4 +1317,12 @@ end | ||
| end | ||
| def name_inspect | ||
| db_config.name.inspect unless db_config.name == "primary" | ||
| end | ||
| def shard_inspect | ||
| shard.inspect unless shard == :default | ||
| end | ||
| end | ||
| end | ||
| end |
@@ -43,2 +43,10 @@ # frozen_string_literal: true | ||
| # Add +element+ to the back of the queue. Never blocks. | ||
| def add_back(element) | ||
| synchronize do | ||
| @queue.unshift element | ||
| @cond.signal | ||
| end | ||
| end | ||
| # If +element+ is in the queue, remove and return it, or +nil+. | ||
@@ -58,2 +66,9 @@ def delete(element) | ||
| # Number of elements in the queue. | ||
| def size | ||
| synchronize do | ||
| @queue.size | ||
| end | ||
| end | ||
| # Remove the head of the queue. | ||
@@ -60,0 +75,0 @@ # |
@@ -10,8 +10,25 @@ # frozen_string_literal: true | ||
| # | ||
| # Every +frequency+ seconds, the reaper will call +reap+ and +flush+ on | ||
| # +pool+. A reaper instantiated with a zero frequency will never reap | ||
| # the connection pool. | ||
| # The reaper is a singleton that exists in the background of the process | ||
| # and is responsible for general maintenance of all the connection pools. | ||
| # | ||
| # Configure the frequency by setting +reaping_frequency+ in your database | ||
| # YAML file (default 60 seconds). | ||
| # It will reclaim connections that are leased to now-dead threads, | ||
| # ensuring that a bad thread can't leak a pool slot forever. By definition, | ||
| # this involves touching currently-leased connections, but that is safe | ||
| # because the owning thread is known to be dead. | ||
| # | ||
| # Beyond that, it manages the health of available / unleased connections: | ||
| # * retiring connections that have been idle[1] for too long | ||
| # * creating occasional activity on inactive[1] connections | ||
| # * keeping the pool prepopulated up to its minimum size | ||
| # * proactively connecting to the target database from any pooled | ||
| # connections that had lazily deferred that step | ||
| # * resetting or replacing connections that are known to be broken | ||
| # | ||
| # | ||
| # [1]: "idle" and "inactive" here distinguish between connections that | ||
| # have not been requested by the application in a while (idle) and those | ||
| # that have not spoken to their remote server in a while (inactive). The | ||
| # former is a desirable opportunity to reduce our connection count | ||
| # (`idle_timeout`); the latter is a risk that the server or a firewall may | ||
| # drop a connection we still anticipate using (avoided by `keepalive`). | ||
| class Reaper | ||
@@ -40,2 +57,11 @@ attr_reader :pool, :frequency | ||
| def pools(refs = nil) # :nodoc: | ||
| refs ||= @mutex.synchronize { @pools.values.flatten(1) } | ||
| refs.filter_map do |ref| | ||
| ref.__getobj__ if ref.weakref_alive? | ||
| rescue WeakRef::RefError | ||
| end.select(&:maintainable?) | ||
| end | ||
| private | ||
@@ -51,14 +77,14 @@ def spawn_thread(frequency) | ||
| sleep t | ||
| refs = nil | ||
| @mutex.synchronize do | ||
| @pools[frequency].select! do |pool| | ||
| refs = @pools[frequency] | ||
| refs.select! do |pool| | ||
| pool.weakref_alive? && !pool.discarded? | ||
| end | ||
| @pools[frequency].each do |p| | ||
| p.reap | ||
| p.flush | ||
| rescue WeakRef::RefError | ||
| end | ||
| if @pools[frequency].empty? | ||
| if refs.empty? | ||
| @pools.delete(frequency) | ||
@@ -69,2 +95,15 @@ @threads.delete(frequency) | ||
| end | ||
| if running | ||
| pools(refs).each do |pool| | ||
| pool.reaper_lock do | ||
| pool.reap | ||
| pool.flush | ||
| pool.prepopulate | ||
| pool.retire_old_connections | ||
| pool.keep_alive | ||
| pool.preconnect | ||
| end | ||
| end | ||
| end | ||
| end | ||
@@ -71,0 +110,0 @@ end |
@@ -58,8 +58,11 @@ # frozen_string_literal: true | ||
| if prepared_statements | ||
| collector = collector() | ||
| collector.retryable = true | ||
| sql, binds = visitor.compile(arel.ast, collector) | ||
| query = klass.query(sql) | ||
| query = klass.query(sql, retryable: collector.retryable) | ||
| else | ||
| collector = klass.partial_query_collector | ||
| collector.retryable = true | ||
| parts, binds = visitor.compile(arel.ast, collector) | ||
| query = klass.partial_query(parts) | ||
| query = klass.partial_query(parts, retryable: collector.retryable) | ||
| end | ||
@@ -114,4 +117,4 @@ [query, binds] | ||
| def query(...) # :nodoc: | ||
| internal_exec_query(...).rows | ||
| def query(sql, name = nil, allow_retry: true, materialize_transactions: true) # :nodoc: | ||
| internal_exec_query(sql, name, allow_retry:, materialize_transactions:).rows | ||
| end | ||
@@ -356,3 +359,3 @@ | ||
| if !requires_new && current_transaction.joinable? | ||
| if isolation | ||
| if isolation && current_transaction.isolation != isolation | ||
| raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" | ||
@@ -505,16 +508,2 @@ end | ||
| # Sanitizes the given LIMIT parameter in order to prevent SQL injection. | ||
| # | ||
| # The +limit+ may be anything that can evaluate to a string via #to_s. It | ||
| # should look like an integer, or an Arel SQL literal. | ||
| # | ||
| # Returns Integer and Arel::Nodes::SqlLiteral limits as is. | ||
| def sanitize_limit(limit) | ||
| if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral) | ||
| limit | ||
| else | ||
| Integer(limit) | ||
| end | ||
| end | ||
| # Fixture value is quoted by Arel, however scalar values | ||
@@ -554,9 +543,20 @@ # are not quotable. In this case we want to convert | ||
| def default_insert_value(column) # :nodoc: | ||
| DEFAULT_INSERT_VALUE | ||
| end | ||
| private | ||
| DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze | ||
| private_constant :DEFAULT_INSERT_VALUE | ||
| # Lowest level way to execute a query. Doesn't check for illegal writes, doesn't annotate queries, yields a native result object. | ||
| def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, batch: false) | ||
| type_casted_binds = type_casted_binds(binds) | ||
| log(sql, name, binds, type_casted_binds, async: async) do |notification_payload| | ||
| log(sql, name, binds, type_casted_binds, async: async, allow_retry: allow_retry) do |notification_payload| | ||
| with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn| | ||
| perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch) | ||
| result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do | ||
| perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch) | ||
| end | ||
| handle_warnings(result, sql) | ||
| result | ||
| end | ||
@@ -570,2 +570,5 @@ end | ||
| def handle_warnings(raw_result, sql) | ||
| end | ||
| # Receive a native adapter result object and returns an ActiveRecord::Result object. | ||
@@ -606,9 +609,2 @@ def cast_result(raw_result) | ||
| DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze | ||
| private_constant :DEFAULT_INSERT_VALUE | ||
| def default_insert_value(column) | ||
| DEFAULT_INSERT_VALUE | ||
| end | ||
| def build_fixture_sql(fixtures, table_name) | ||
@@ -627,4 +623,4 @@ columns = schema_cache.columns_hash(table_name).reject { |_, column| supports_virtual_columns? && column.virtual? } | ||
| if fixture.key?(name) | ||
| type = lookup_cast_type_from_column(column) | ||
| with_yaml_fallback(type.serialize(fixture[name])) | ||
| # TODO: Remove fetch_cast_type and the need for connection after we release 8.1. | ||
| with_yaml_fallback(column.fetch_cast_type(self).serialize(fixture[name])) | ||
| else | ||
@@ -749,3 +745,3 @@ default_insert_value(column) | ||
| def extract_table_ref_from_insert_sql(sql) | ||
| if sql =~ /into\s("[-A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im | ||
| if sql =~ /into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im | ||
| $1.delete('"').strip | ||
@@ -752,0 +748,0 @@ end |
@@ -34,2 +34,14 @@ # frozen_string_literal: true | ||
| # This is the actual query cache store. | ||
| # | ||
| # It has an internal hash whose keys are either SQL strings, or arrays of | ||
| # two elements [SQL string, binds], if there are binds. The hash values | ||
| # are their corresponding ActiveRecord::Result objects. | ||
| # | ||
| # Keeping the hash size under max size is achieved with LRU eviction. | ||
| # | ||
| # The store gets passed a version object, which is shared among the query | ||
| # cache stores of a given connection pool (see ConnectionPoolConfiguration | ||
| # down below). The version value may be externally changed as a way to | ||
| # signal cache invalidation, that is why all methods have a guard for it. | ||
| class Store # :nodoc: | ||
@@ -98,2 +110,8 @@ attr_accessor :enabled, :dirties | ||
| # Each connection pool has one of these registries. They map execution | ||
| # contexts to query cache stores. | ||
| # | ||
| # The keys of the internal map are threads or fibers (whatever | ||
| # ActiveSupport::IsolatedExecutionState.context returns), and their | ||
| # associated values are their respective query cache stores. | ||
| class QueryCacheRegistry # :nodoc: | ||
@@ -196,2 +214,4 @@ def initialize | ||
| attr_accessor :query_cache | ||
| def initialize(*) | ||
@@ -202,17 +222,4 @@ super | ||
| attr_writer :query_cache | ||
| def query_cache | ||
| if @pinned && @owner != ActiveSupport::IsolatedExecutionState.context | ||
| # With transactional tests, if the connection is pinned, any thread | ||
| # other than the one that pinned the connection need to go through the | ||
| # query cache pool, so each thread get a different cache. | ||
| pool.query_cache | ||
| else | ||
| @query_cache | ||
| end | ||
| end | ||
| def query_cache_enabled | ||
| query_cache&.enabled? | ||
| @query_cache&.enabled? | ||
| end | ||
@@ -256,4 +263,4 @@ | ||
| # Such queries should not be cached. | ||
| if query_cache_enabled && !(arel.respond_to?(:locked) && arel.locked) | ||
| sql, binds, preparable, allow_retry = to_sql_and_binds(arel, binds, preparable) | ||
| if @query_cache&.enabled? && !(arel.respond_to?(:locked) && arel.locked) | ||
| sql, binds, preparable, allow_retry = to_sql_and_binds(arel, binds, preparable, allow_retry) | ||
@@ -281,3 +288,3 @@ if async | ||
| @lock.synchronize do | ||
| result = query_cache[key] | ||
| result = @query_cache[key] | ||
| end | ||
@@ -301,3 +308,3 @@ | ||
| @lock.synchronize do | ||
| result = query_cache.compute_if_absent(key) do | ||
| result = @query_cache.compute_if_absent(key) do | ||
| hit = false | ||
@@ -304,0 +311,0 @@ yield |
| # frozen_string_literal: true | ||
| require "active_support/core_ext/big_decimal/conversions" | ||
| require "active_support/multibyte/chars" | ||
@@ -87,3 +86,4 @@ module ActiveRecord | ||
| when Class then "'#{value}'" | ||
| else raise TypeError, "can't quote #{value.class.name}" | ||
| else | ||
| raise TypeError, "can't quote #{value.class.name}" | ||
| end | ||
@@ -97,3 +97,3 @@ end | ||
| case value | ||
| when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data | ||
| when Symbol, Type::Binary::Data, ActiveSupport::Multibyte::Chars | ||
| value.to_s | ||
@@ -107,3 +107,4 @@ when true then unquoted_true | ||
| when Date, Time then quoted_date(value) | ||
| else raise TypeError, "can't cast #{value.class.name}" | ||
| else | ||
| raise TypeError, "can't cast #{value.class.name}" | ||
| end | ||
@@ -119,15 +120,2 @@ end | ||
| # If you are having to call this function, you are likely doing something | ||
| # wrong. The column does not have sufficient type information if the user | ||
| # provided a custom type on the class level either explicitly (via | ||
| # Attributes::ClassMethods#attribute) or implicitly (via | ||
| # AttributeMethods::Serialization::ClassMethods#serialize, +time_zone_aware_attributes+). | ||
| # In almost all cases, the sql type should only be used to change quoting behavior, when the primitive to | ||
| # represent the type doesn't sufficiently reflect the differences | ||
| # (varchar vs binary) for example. The type used to get this primitive | ||
| # should have been provided before reaching the connection adapter. | ||
| def lookup_cast_type_from_column(column) # :nodoc: | ||
| lookup_cast_type(column.sql_type) | ||
| end | ||
| # Quotes a string, escaping any ' (single quote) and \ (backslash) | ||
@@ -165,3 +153,5 @@ # characters. | ||
| else | ||
| value = lookup_cast_type(column.sql_type).serialize(value) | ||
| # TODO: Remove fetch_cast_type and the need for connection after we release 8.1. | ||
| cast_type = column.fetch_cast_type(self) | ||
| value = cast_type.serialize(value) | ||
| quote(value) | ||
@@ -216,6 +206,6 @@ end | ||
| def sanitize_as_sql_comment(value) # :nodoc: | ||
| # Sanitize a string to appear within a SQL comment | ||
| # Sanitize a string to appear within an SQL comment | ||
| # For compatibility, this also surrounding "/*+", "/*", and "*/" | ||
| # charcacters, possibly with single surrounding space. | ||
| # Then follows that by replacing any internal "*/" or "/ *" with | ||
| # Then follows that by replacing any internal "*/" or "/*" with | ||
| # "* /" or "/ *" | ||
@@ -229,2 +219,7 @@ comment = value.to_s.dup | ||
| def lookup_cast_type(sql_type) # :nodoc: | ||
| # TODO: Make this method private after we release 8.1. | ||
| type_map.lookup(sql_type) | ||
| end | ||
| private | ||
@@ -240,8 +235,4 @@ def type_casted_binds(binds) | ||
| end | ||
| def lookup_cast_type(sql_type) | ||
| type_map.lookup(sql_type) | ||
| end | ||
| end | ||
| end | ||
| end |
@@ -20,3 +20,3 @@ # frozen_string_literal: true | ||
| :supports_index_include?, :supports_exclusion_constraints?, :supports_unique_constraints?, | ||
| :supports_nulls_not_distinct?, | ||
| :supports_nulls_not_distinct?, :lookup_cast_type, | ||
| to: :@conn, private: true | ||
@@ -152,3 +152,3 @@ | ||
| def add_column_options!(sql, options) | ||
| sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options) | ||
| sql << " DEFAULT #{quote_default_expression_for_column_definition(options[:default], options[:column])}" if options_include_default?(options) | ||
| # must explicitly check for :null to allow change_column to work on migrations | ||
@@ -167,2 +167,7 @@ if options[:null] == false | ||
| def quote_default_expression_for_column_definition(default, column_definition) | ||
| column_definition.cast_type = lookup_cast_type(column_definition.sql_type) | ||
| quote_default_expression(default, column_definition) | ||
| end | ||
| def to_sql(sql) | ||
@@ -169,0 +174,0 @@ sql = sql.to_sql if sql.respond_to?(:to_sql) |
@@ -78,3 +78,3 @@ # frozen_string_literal: true | ||
| # for generating a number of table creation or table changing SQL statements. | ||
| ColumnDefinition = Struct.new(:name, :type, :options, :sql_type) do # :nodoc: | ||
| ColumnDefinition = Struct.new(:name, :type, :options, :sql_type, :cast_type) do # :nodoc: | ||
| self::OPTION_NAMES = [ | ||
@@ -112,2 +112,6 @@ :limit, | ||
| end | ||
| def fetch_cast_type(connection) | ||
| cast_type | ||
| end | ||
| end | ||
@@ -308,31 +312,7 @@ | ||
| # Appends a primary key definition to the table definition. | ||
| # Can be called multiple times, but this is probably not a good idea. | ||
| def primary_key(name, type = :primary_key, **options) | ||
| column(name, type, **options.merge(primary_key: true)) | ||
| end | ||
| ## | ||
| # :method: column | ||
| # :call-seq: column(name, type, **options) | ||
| # | ||
| # Appends a column or columns of a specified type. | ||
| # | ||
| # t.string(:goat) | ||
| # t.string(:goat, :sheep) | ||
| # | ||
| # See TableDefinition#column | ||
| included do | ||
| define_column_methods :bigint, :binary, :boolean, :date, :datetime, :decimal, | ||
| :float, :integer, :json, :string, :text, :time, :timestamp, :virtual | ||
| alias :blob :binary | ||
| alias :numeric :decimal | ||
| end | ||
| class_methods do | ||
| def define_column_methods(*column_types) # :nodoc: | ||
| column_types.each do |column_type| | ||
| module_eval <<-RUBY, __FILE__, __LINE__ + 1 | ||
| private | ||
| def define_column_methods(*column_types) # :nodoc: | ||
| column_types.each do |column_type| | ||
| module_eval <<-RUBY, __FILE__, __LINE__ + 1 | ||
| def #{column_type}(*names, **options) | ||
@@ -342,7 +322,19 @@ raise ArgumentError, "Missing column name(s) for #{column_type}" if names.empty? | ||
| end | ||
| RUBY | ||
| RUBY | ||
| end | ||
| end | ||
| end | ||
| private :define_column_methods | ||
| end | ||
| extend ClassMethods | ||
| # Appends a primary key definition to the table definition. | ||
| # Can be called multiple times, but this is probably not a good idea. | ||
| def primary_key(name, type = :primary_key, **options) | ||
| column(name, type, **options, primary_key: true) | ||
| end | ||
| define_column_methods :bigint, :binary, :boolean, :date, :datetime, :decimal, | ||
| :float, :integer, :json, :string, :text, :time, :timestamp, :virtual | ||
| alias :blob :binary | ||
| alias :numeric :decimal | ||
| end | ||
@@ -358,3 +350,3 @@ | ||
| # | ||
| # class SomeMigration < ActiveRecord::Migration[8.0] | ||
| # class SomeMigration < ActiveRecord::Migration[8.1] | ||
| # def up | ||
@@ -773,3 +765,3 @@ # create_table :foo do |t| | ||
| # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?] | ||
| def index_exists?(column_name, **options) | ||
| def index_exists?(column_name = nil, **options) | ||
| @base.index_exists?(name, column_name, **options) | ||
@@ -776,0 +768,0 @@ end |
@@ -88,3 +88,4 @@ # frozen_string_literal: true | ||
| return unless column.has_default? | ||
| type = @connection.lookup_cast_type_from_column(column) | ||
| # TODO: Remove fetch_cast_type and the need for connection after we release 8.1. | ||
| type = column.fetch_cast_type(@connection) | ||
| default = type.deserialize(column.default) | ||
@@ -91,0 +92,0 @@ if default.nil? |
@@ -115,2 +115,3 @@ # frozen_string_literal: true | ||
| def joinable?; false; end | ||
| def isolation; nil; end | ||
| def add_record(record, _ = true); end | ||
@@ -154,2 +155,7 @@ def restartable?; false; end | ||
| # Returns the isolation level if it was explicitly set, nil otherwise | ||
| def isolation | ||
| @isolation_level | ||
| end | ||
| def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false) | ||
@@ -180,7 +186,7 @@ super() | ||
| def open? | ||
| true | ||
| !closed? | ||
| end | ||
| def closed? | ||
| false | ||
| @state.finalized? | ||
| end | ||
@@ -392,3 +398,3 @@ | ||
| delegate :materialize!, :materialized?, :restart, to: :@parent | ||
| delegate :materialize!, :materialized?, :restart, :isolation, to: :@parent | ||
@@ -412,2 +418,3 @@ def rollback | ||
| @parent_transaction = parent_transaction | ||
| parent_transaction.state.add_child(@state) | ||
@@ -422,2 +429,7 @@ | ||
| # Delegates to parent transaction's isolation level | ||
| def isolation | ||
| @parent_transaction.isolation | ||
| end | ||
| def materialize! | ||
@@ -629,2 +641,3 @@ connection.create_savepoint(savepoint_name) | ||
| def within_new_transaction(isolation: nil, joinable: true) | ||
| isolation ||= @connection.pool.pool_transaction_isolation_level | ||
| @connection.lock.synchronize do | ||
@@ -631,0 +644,0 @@ transaction = begin_transaction(isolation: isolation, joinable: joinable) |
@@ -20,7 +20,8 @@ # frozen_string_literal: true | ||
| # +null+ determines if this column allows +NULL+ values. | ||
| def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **) | ||
| def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **) | ||
| @name = name.freeze | ||
| @cast_type = cast_type | ||
| @sql_type_metadata = sql_type_metadata | ||
| @null = null | ||
| @default = default | ||
| @default = default.nil? || cast_type.mutable? ? default : cast_type.deserialize(default) | ||
| @default_function = default_function | ||
@@ -31,2 +32,7 @@ @collation = collation | ||
| def fetch_cast_type(connection) # :nodoc: | ||
| # TODO: Remove fetch_cast_type and the need for connection after we release 8.1. | ||
| @cast_type || connection.lookup_cast_type(sql_type) | ||
| end | ||
| def has_default? | ||
@@ -50,2 +56,3 @@ !default.nil? || default_function | ||
| @name = coder["name"] | ||
| @cast_type = coder["cast_type"] | ||
| @sql_type_metadata = coder["sql_type_metadata"] | ||
@@ -61,2 +68,3 @@ @null = coder["null"] | ||
| coder["name"] = @name | ||
| coder["cast_type"] = @cast_type | ||
| coder["sql_type_metadata"] = @sql_type_metadata | ||
@@ -82,2 +90,3 @@ coder["null"] = @null | ||
| name == other.name && | ||
| cast_type == other.cast_type && | ||
| default == other.default && | ||
@@ -96,2 +105,3 @@ sql_type_metadata == other.sql_type_metadata && | ||
| name.encoding.hash ^ | ||
| cast_type.hash ^ | ||
| default.hash ^ | ||
@@ -109,2 +119,5 @@ sql_type_metadata.hash ^ | ||
| protected | ||
| attr_reader :cast_type | ||
| private | ||
@@ -114,3 +127,3 @@ def deduplicated | ||
| @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata | ||
| @default = -default if default | ||
| @default = -default if String === default | ||
| @default_function = -default_function if default_function | ||
@@ -125,3 +138,3 @@ @collation = -collation if collation | ||
| def initialize(name, **) | ||
| super(name, nil) | ||
| super(name, nil, nil) | ||
| end | ||
@@ -128,0 +141,0 @@ end |
@@ -48,2 +48,6 @@ # frozen_string_literal: true | ||
| def default_insert_value(column) # :nodoc: | ||
| super unless column.auto_increment? | ||
| end | ||
| private | ||
@@ -55,6 +59,2 @@ # https://mariadb.com/kb/en/analyze-statement/ | ||
| def default_insert_value(column) | ||
| super unless column.auto_increment? | ||
| end | ||
| def returning_column_values(result) | ||
@@ -61,0 +61,0 @@ if supports_insert_returning? |
@@ -52,2 +52,4 @@ # frozen_string_literal: true | ||
| sql << "(#{quoted_columns(o)})" | ||
| sql << "INVISIBLE" if o.disabled? && !mariadb? | ||
| sql << "IGNORED" if o.disabled? && mariadb? | ||
@@ -54,0 +56,0 @@ add_sql_comment!(sql.join(" "), o.comment) |
@@ -8,2 +8,3 @@ # frozen_string_literal: true | ||
| extend ActiveSupport::Concern | ||
| extend ConnectionAdapters::ColumnMethods::ClassMethods | ||
@@ -46,9 +47,23 @@ ## | ||
| included do | ||
| define_column_methods :blob, :tinyblob, :mediumblob, :longblob, | ||
| :tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint, | ||
| :unsigned_float, :unsigned_decimal | ||
| define_column_methods :blob, :tinyblob, :mediumblob, :longblob, | ||
| :tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint | ||
| end | ||
| deprecate :unsigned_float, :unsigned_decimal, deprecator: ActiveRecord.deprecator | ||
| # = Active Record MySQL Adapter \Index Definition | ||
| class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition | ||
| attr_accessor :enabled | ||
| def initialize(*args, **kwargs) | ||
| @enabled = kwargs.key?(:enabled) ? kwargs.delete(:enabled) : true | ||
| super | ||
| end | ||
| def defined_for?(columns = nil, name: nil, unique: nil, valid: nil, include: nil, nulls_not_distinct: nil, enabled: nil, **options) | ||
| super(columns, name:, unique:, valid:, include:, nulls_not_distinct:, **options) && | ||
| (enabled.nil? || self.enabled == enabled) | ||
| end | ||
| def disabled? | ||
| !@enabled | ||
| end | ||
| end | ||
@@ -105,2 +120,24 @@ | ||
| include ColumnMethods | ||
| # Enables an index to be used by query optimizers. | ||
| # | ||
| # t.enable_index(:email) | ||
| # | ||
| # Note: only supported by MySQL version 8.0.0 and greater, and MariaDB version 10.6.0 and greater. | ||
| # | ||
| # See {connection.enable_index}[rdoc-ref:SchemaStatements#enable_index] | ||
| def enable_index(index_name) | ||
| @base.enable_index(name, index_name) | ||
| end | ||
| # Disables an index not to be used by query optimizers. | ||
| # | ||
| # t.disable_index(:email) | ||
| # | ||
| # Note: only supported by MySQL version 8.0.0 and greater, and MariaDB version 10.6.0 and greater. | ||
| # | ||
| # See {connection.disable_index}[rdoc-ref:SchemaStatements#disable_index] | ||
| def disable_index(index_name) | ||
| @base.disable_index(name, index_name) | ||
| end | ||
| end | ||
@@ -107,0 +144,0 @@ end |
@@ -24,3 +24,3 @@ # frozen_string_literal: true | ||
| indexes << [ | ||
| index = [ | ||
| row["Table"], | ||
@@ -34,4 +34,10 @@ row["Key_name"], | ||
| using: index_using, | ||
| comment: row["Index_comment"].presence | ||
| comment: row["Index_comment"].presence, | ||
| ] | ||
| if supports_disabling_indexes? | ||
| index[-1][:enabled] = mariadb? ? row["Ignored"] == "NO" : row["Visible"] == "YES" | ||
| end | ||
| indexes << index | ||
| end | ||
@@ -68,4 +74,3 @@ | ||
| end | ||
| IndexDefinition.new(*index, **options) | ||
| MySQL::IndexDefinition.new(*index, **options) | ||
| end | ||
@@ -80,2 +85,12 @@ rescue StatementInvalid => e | ||
| def create_index_definition(table_name, name, unique, columns, **options) | ||
| MySQL::IndexDefinition.new(table_name, name, unique, columns, **options) | ||
| end | ||
| def add_index_options(table_name, column_name, name: nil, if_not_exists: false, internal: false, **options) # :nodoc: | ||
| index, algorithm, if_not_exists = super | ||
| index.enabled = options[:enabled] unless options[:enabled].nil? | ||
| [index, algorithm, if_not_exists] | ||
| end | ||
| def remove_column(table_name, column_name, type = nil, **options) | ||
@@ -216,2 +231,3 @@ if foreign_key_exists?(table_name, column: column_name) | ||
| field["Field"], | ||
| lookup_cast_type(type_metadata.sql_type), | ||
| default, | ||
@@ -241,2 +257,8 @@ type_metadata, | ||
| def valid_index_options | ||
| index_options = super | ||
| index_options << :enabled if supports_disabling_indexes? | ||
| index_options | ||
| end | ||
| def add_options_for_index_columns(quoted_columns, **options) | ||
@@ -243,0 +265,0 @@ quoted_columns = add_index_length(quoted_columns, **options) |
@@ -16,2 +16,3 @@ # frozen_string_literal: true | ||
| ER_ACCESS_DENIED_ERROR = 1045 | ||
| ER_UNKNOWN_STMT_HANDLER = 1243 | ||
| ER_CONN_HOST_ERROR = 2003 | ||
@@ -18,0 +19,0 @@ ER_UNKNOWN_HOST_ERROR = 2005 |
@@ -53,20 +53,28 @@ # frozen_string_literal: true | ||
| if binds.nil? || binds.empty? | ||
| ActiveSupport::Dependencies.interlock.permit_concurrent_loads do | ||
| result = raw_connection.query(sql) | ||
| # Ref: https://github.com/brianmario/mysql2/pull/1383 | ||
| # As of mysql2 0.5.6 `#affected_rows` might raise Mysql2::Error if a prepared statement | ||
| # from that same connection was GCed while `#query` released the GVL. | ||
| # By avoiding to call `#affected_rows` when we have a result, we reduce the likeliness | ||
| # of hitting the bug. | ||
| @affected_rows_before_warnings = result&.size || raw_connection.affected_rows | ||
| end | ||
| result = raw_connection.query(sql) | ||
| # Ref: https://github.com/brianmario/mysql2/pull/1383 | ||
| # As of mysql2 0.5.6 `#affected_rows` might raise Mysql2::Error if a prepared statement | ||
| # from that same connection was GCed while `#query` released the GVL. | ||
| # By avoiding to call `#affected_rows` when we have a result, we reduce the likeliness | ||
| # of hitting the bug. | ||
| @affected_rows_before_warnings = result&.size || raw_connection.affected_rows | ||
| elsif prepare | ||
| stmt = @statements[sql] ||= raw_connection.prepare(sql) | ||
| retry_count = 1 | ||
| begin | ||
| ActiveSupport::Dependencies.interlock.permit_concurrent_loads do | ||
| result = stmt.execute(*type_casted_binds) | ||
| @affected_rows_before_warnings = stmt.affected_rows | ||
| stmt = @statements[sql] ||= raw_connection.prepare(sql) | ||
| result = stmt.execute(*type_casted_binds) | ||
| @affected_rows_before_warnings = stmt.affected_rows | ||
| rescue ::Mysql2::Error => error | ||
| @statements.delete(sql) | ||
| # Sometimes for an unknown reason, we get that error. | ||
| # It suggest somehow that the prepared statement was deallocated | ||
| # but the client doesn't know it. | ||
| # But we know that this error is safe to retry, so we do so after | ||
| # getting rid of the originally cached statement. | ||
| if error.error_number == Mysql2Adapter::ER_UNKNOWN_STMT_HANDLER | ||
| if retry_count.positive? | ||
| retry_count -= 1 | ||
| retry | ||
| end | ||
| end | ||
| rescue ::Mysql2::Error | ||
| @statements.delete(sql) | ||
| raise | ||
@@ -78,6 +86,4 @@ end | ||
| begin | ||
| ActiveSupport::Dependencies.interlock.permit_concurrent_loads do | ||
| result = stmt.execute(*type_casted_binds) | ||
| @affected_rows_before_warnings = stmt.affected_rows | ||
| end | ||
| result = stmt.execute(*type_casted_binds) | ||
| @affected_rows_before_warnings = stmt.affected_rows | ||
@@ -105,3 +111,2 @@ # Ref: https://github.com/brianmario/mysql2/pull/1383 | ||
| verified! | ||
| handle_warnings(sql) | ||
| result | ||
@@ -115,3 +120,3 @@ ensure | ||
| def cast_result(raw_result) | ||
| return ActiveRecord::Result.empty if raw_result.nil? | ||
| return ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings) if raw_result.nil? | ||
@@ -121,3 +126,3 @@ fields = raw_result.fields | ||
| result = if fields.empty? | ||
| ActiveRecord::Result.empty | ||
| ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings) | ||
| else | ||
@@ -124,0 +129,0 @@ ActiveRecord::Result.new(fields, raw_result.to_a) |
@@ -384,7 +384,2 @@ # frozen_string_literal: true | ||
| def clear_cache!(new_connection: false) | ||
| super | ||
| @schema_search_path = nil if new_connection | ||
| end | ||
| # Disconnects from the database if already connected. Otherwise, this | ||
@@ -406,6 +401,2 @@ # method does nothing. | ||
| def native_database_types # :nodoc: | ||
| self.class.native_database_types | ||
| end | ||
| def self.native_database_types # :nodoc: | ||
@@ -646,3 +637,3 @@ @native_database_types ||= begin | ||
| if version == 0 | ||
| raise ActiveRecord::ConnectionFailed, "Could not determine PostgreSQL version" | ||
| raise ActiveRecord::ConnectionNotEstablished, "Could not determine PostgreSQL version" | ||
| end | ||
@@ -681,2 +672,5 @@ version | ||
| end | ||
| if database_version >= 18_00_00 && Gem::Version.new(PG::VERSION) < Gem::Version.new("1.6.0") | ||
| warn "pg gem version #{PG::VERSION} is known to be incompatible with PostgreSQL 18+. Please upgrade to pg 1.6.0 or later." | ||
| end | ||
| end | ||
@@ -804,2 +798,4 @@ | ||
| UNIQUE_VIOLATION = "23505" | ||
| CHECK_VIOLATION = "23514" | ||
| EXCLUSION_VIOLATION = "23P01" | ||
| SERIALIZATION_FAILURE = "40001" | ||
@@ -836,2 +832,6 @@ DEADLOCK_DETECTED = "40P01" | ||
| InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool) | ||
| when CHECK_VIOLATION | ||
| CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool) | ||
| when EXCLUSION_VIOLATION | ||
| ExclusionViolation.new(message, sql: sql, binds: binds, connection_pool: @pool) | ||
| when VALUE_LIMIT_VIOLATION | ||
@@ -1007,4 +1007,2 @@ ValueTooLong.new(message, sql: sql, binds: binds, connection_pool: @pool) | ||
| schema_search_path # populate cache | ||
| reload_type_map | ||
@@ -1011,0 +1009,0 @@ end |
@@ -68,4 +68,3 @@ # frozen_string_literal: true | ||
| identity? == other.identity? && | ||
| serial? == other.serial? && | ||
| virtual? == other.virtual? | ||
| serial? == other.serial? | ||
| end | ||
@@ -78,4 +77,3 @@ alias :eql? :== | ||
| identity?.hash ^ | ||
| serial?.hash ^ | ||
| virtual?.hash | ||
| serial?.hash | ||
| end | ||
@@ -82,0 +80,0 @@ end |
@@ -14,4 +14,4 @@ # frozen_string_literal: true | ||
| # Queries the database and returns the results in an Array-like object | ||
| def query(sql, name = nil) # :nodoc: | ||
| result = internal_execute(sql, name) | ||
| def query(sql, name = nil, allow_retry: true, materialize_transactions: true) # :nodoc: | ||
| result = internal_execute(sql, name, allow_retry:, materialize_transactions:) | ||
| result.map_types!(@type_map_for_results).values | ||
@@ -131,10 +131,3 @@ end | ||
| # Skip @raw_connection.cancel (PG::Connection#cancel) when using libpq >= 18 with pg < 1.6.0, | ||
| # because the pg gem cannot obtain the backend_key in that case. | ||
| # This method is only called from exec_rollback_db_transaction and exec_restart_db_transaction. | ||
| # Even without cancel, rollback will still run. However, since any running | ||
| # query must finish first, the rollback may take longer. | ||
| if !(PG.library_version >= 18_00_00 && Gem::Version.new(PG::VERSION) < Gem::Version.new("1.6.0")) | ||
| @raw_connection.cancel | ||
| end | ||
| @raw_connection.cancel | ||
| @raw_connection.block | ||
@@ -175,3 +168,4 @@ rescue PG::Error | ||
| verified! | ||
| handle_warnings(result) | ||
| notification_payload[:affected_rows] = result.cmd_tuples | ||
| notification_payload[:row_count] = result.ntuples | ||
@@ -182,15 +176,16 @@ result | ||
| def cast_result(result) | ||
| if result.fields.empty? | ||
| result.clear | ||
| return ActiveRecord::Result.empty | ||
| ar_result = if result.fields.empty? | ||
| ActiveRecord::Result.empty(affected_rows: result.cmd_tuples) | ||
| else | ||
| fields = result.fields | ||
| types = Array.new(fields.size) | ||
| fields.size.times do |index| | ||
| ftype = result.ftype(index) | ||
| fmod = result.fmod(index) | ||
| types[index] = get_oid_type(ftype, fmod, fields[index]) | ||
| end | ||
| ActiveRecord::Result.new(fields, result.values, types.freeze, affected_rows: result.cmd_tuples) | ||
| end | ||
| types = {} | ||
| fields = result.fields | ||
| fields.each_with_index do |fname, i| | ||
| ftype = result.ftype i | ||
| fmod = result.fmod i | ||
| types[fname] = types[i] = get_oid_type(ftype, fmod, fname) | ||
| end | ||
| ar_result = ActiveRecord::Result.new(fields, result.values, types.freeze) | ||
| result.clear | ||
@@ -227,3 +222,3 @@ ar_result | ||
| def handle_warnings(sql) | ||
| def handle_warnings(result, sql) | ||
| @notice_receiver_sql_warnings.each do |warning| | ||
@@ -230,0 +225,0 @@ next if warning_ignored?(warning) |
@@ -19,4 +19,4 @@ # frozen_string_literal: true | ||
| @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter | ||
| @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter | ||
| @pg_encoder = PG::TextEncoder::Array.new(name: "#{type}[]".freeze, delimiter: delimiter).freeze | ||
| @pg_decoder = PG::TextDecoder::Array.new(name: "#{type}[]".freeze, delimiter: delimiter).freeze | ||
| end | ||
@@ -23,0 +23,0 @@ |
@@ -71,3 +71,3 @@ # frozen_string_literal: true | ||
| register_with_subtype(row["oid"], row["typelem"].to_i) do |subtype| | ||
| OID::Array.new(subtype, row["typdelim"]) | ||
| OID::Array.new(subtype, row["typdelim"].freeze) | ||
| end | ||
@@ -74,0 +74,0 @@ end |
@@ -156,2 +156,3 @@ # frozen_string_literal: true | ||
| # `column` may be either an instance of Column or ColumnDefinition. | ||
| def quote_default_expression(value, column) # :nodoc: | ||
@@ -163,4 +164,4 @@ if value.is_a?(Proc) | ||
| elsif column.respond_to?(:array?) | ||
| type = lookup_cast_type_from_column(column) | ||
| quote(type.serialize(value)) | ||
| # TODO: Remove fetch_cast_type and the need for connection after we release 8.1. | ||
| quote(column.fetch_cast_type(self).serialize(value)) | ||
| else | ||
@@ -191,12 +192,8 @@ super | ||
| def lookup_cast_type_from_column(column) # :nodoc: | ||
| verify! if type_map.nil? | ||
| type_map.lookup(column.oid, column.fmod, column.sql_type) | ||
| # TODO: Make this method private after we release 8.1. | ||
| def lookup_cast_type(sql_type) # :nodoc: | ||
| super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i) | ||
| end | ||
| private | ||
| def lookup_cast_type(sql_type) | ||
| super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i) | ||
| end | ||
| def encode_array(array_data) | ||
@@ -214,3 +211,13 @@ encoder = array_data.encoder | ||
| def encode_range(range) | ||
| "[#{type_cast_range_value(range.begin)},#{type_cast_range_value(range.end)}#{range.exclude_end? ? ')' : ']'}" | ||
| lower_bound = type_cast_range_value(range.begin) | ||
| upper_bound = if date_or_time_range?(range) | ||
| # Postgres will convert `[today,]` to `[today,)`, making it exclusive. | ||
| # We can use the special timestamp value `infinity` to force inclusion. | ||
| # https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-INFINITE | ||
| range.end.nil? ? "infinity" : type_cast(range.end) | ||
| else | ||
| type_cast_range_value(range.end) | ||
| end | ||
| "[#{lower_bound},#{upper_bound}#{range.exclude_end? ? ')' : ']'}" | ||
| end | ||
@@ -239,2 +246,6 @@ | ||
| end | ||
| def date_or_time_range?(range) | ||
| [range.begin.class, range.end.class].intersect?([Date, DateTime, Time]) | ||
| end | ||
| end | ||
@@ -241,0 +252,0 @@ end |
@@ -102,3 +102,3 @@ # frozen_string_literal: true | ||
| else | ||
| quoted_default = quote_default_expression(options[:default], column) | ||
| quoted_default = quote_default_expression_for_column_definition(options[:default], column) | ||
| change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}" | ||
@@ -105,0 +105,0 @@ end |
@@ -8,2 +8,3 @@ # frozen_string_literal: true | ||
| extend ActiveSupport::Concern | ||
| extend ConnectionAdapters::ColumnMethods::ClassMethods | ||
@@ -19,19 +20,7 @@ # Defines the primary key field. | ||
| # | ||
| # By default, this will use the <tt>gen_random_uuid()</tt> function from the | ||
| # +pgcrypto+ extension. As that extension is only available in | ||
| # PostgreSQL 9.4+, for earlier versions an explicit default can be set | ||
| # to use <tt>uuid_generate_v4()</tt> from the +uuid-ossp+ extension instead: | ||
| # By default, this will use the <tt>gen_random_uuid()</tt> function. | ||
| # | ||
| # create_table :stuffs, id: false do |t| | ||
| # t.primary_key :id, :uuid, default: "uuid_generate_v4()" | ||
| # t.uuid :foo_id | ||
| # t.timestamps | ||
| # end | ||
| # To use a UUID primary key without any defaults, set the +:default+ | ||
| # option to +nil+: | ||
| # | ||
| # To enable the appropriate extension, which is a requirement, use | ||
| # the +enable_extension+ method in your migrations. | ||
| # | ||
| # To use a UUID primary key without any of the extensions, set the | ||
| # +:default+ option to +nil+: | ||
| # | ||
| # create_table :stuffs, id: false do |t| | ||
@@ -186,8 +175,6 @@ # t.primary_key :id, :uuid, default: nil | ||
| included do | ||
| define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange, | ||
| :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr, | ||
| :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle, | ||
| :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml, :timestamptz, :enum | ||
| end | ||
| define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange, | ||
| :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr, | ||
| :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle, | ||
| :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml, :timestamptz, :enum | ||
| end | ||
@@ -194,0 +181,0 @@ |
@@ -8,2 +8,19 @@ # frozen_string_literal: true | ||
| private | ||
| attr_accessor :schema_name | ||
| def initialize(connection, options = {}) | ||
| super | ||
| @dump_schemas = | ||
| case ActiveRecord.dump_schemas | ||
| when :schema_search_path | ||
| connection.current_schemas | ||
| when String | ||
| schema_names = ActiveRecord.dump_schemas.split(",").map(&:strip) | ||
| schema_names & connection.schema_names | ||
| else | ||
| connection.schema_names | ||
| end | ||
| end | ||
| def extensions(stream) | ||
@@ -21,10 +38,11 @@ extensions = @connection.extensions | ||
| def types(stream) | ||
| types = @connection.enum_types | ||
| if types.any? | ||
| stream.puts " # Custom types defined in this database." | ||
| stream.puts " # Note that some types may not work with other database engines. Be careful if changing database." | ||
| types.sort.each do |name, values| | ||
| stream.puts " create_enum #{name.inspect}, #{values.inspect}" | ||
| within_each_schema do | ||
| types = @connection.enum_types | ||
| if types.any? | ||
| stream.puts " # Custom types defined in this database." | ||
| stream.puts " # Note that some types may not work with other database engines. Be careful if changing database." | ||
| types.sort.each do |name, values| | ||
| stream.puts " create_enum #{relation_name(name).inspect}, #{values.inspect}" | ||
| end | ||
| end | ||
| stream.puts | ||
| end | ||
@@ -34,3 +52,3 @@ end | ||
| def schemas(stream) | ||
| schema_names = @connection.schema_names - ["public"] | ||
| schema_names = @dump_schemas - ["public"] | ||
@@ -45,21 +63,24 @@ if schema_names.any? | ||
| def tables(stream) | ||
| previous_schema_had_tables = false | ||
| within_each_schema do | ||
| stream.puts if previous_schema_had_tables | ||
| super | ||
| previous_schema_had_tables = @connection.tables.any? | ||
| end | ||
| end | ||
| def exclusion_constraints_in_create(table, stream) | ||
| if (exclusion_constraints = @connection.exclusion_constraints(table)).any? | ||
| add_exclusion_constraint_statements = exclusion_constraints.map do |exclusion_constraint| | ||
| parts = [ | ||
| "t.exclusion_constraint #{exclusion_constraint.expression.inspect}" | ||
| ] | ||
| exclusion_constraint_statements = exclusion_constraints.map do |exclusion_constraint| | ||
| parts = [ exclusion_constraint.expression.inspect ] | ||
| parts << "where: #{exclusion_constraint.where.inspect}" if exclusion_constraint.where | ||
| parts << "using: #{exclusion_constraint.using.inspect}" if exclusion_constraint.using | ||
| parts << "deferrable: #{exclusion_constraint.deferrable.inspect}" if exclusion_constraint.deferrable | ||
| parts << "name: #{exclusion_constraint.name.inspect}" if exclusion_constraint.export_name_on_schema_dump? | ||
| if exclusion_constraint.export_name_on_schema_dump? | ||
| parts << "name: #{exclusion_constraint.name.inspect}" | ||
| end | ||
| " #{parts.join(', ')}" | ||
| " t.exclusion_constraint #{parts.join(', ')}" | ||
| end | ||
| stream.puts add_exclusion_constraint_statements.sort.join("\n") | ||
| stream.puts exclusion_constraint_statements.sort.join("\n") | ||
| end | ||
@@ -70,18 +91,12 @@ end | ||
| if (unique_constraints = @connection.unique_constraints(table)).any? | ||
| add_unique_constraint_statements = unique_constraints.map do |unique_constraint| | ||
| parts = [ | ||
| "t.unique_constraint #{unique_constraint.column.inspect}" | ||
| ] | ||
| unique_constraint_statements = unique_constraints.map do |unique_constraint| | ||
| parts = [ unique_constraint.column.inspect ] | ||
| parts << "nulls_not_distinct: #{unique_constraint.nulls_not_distinct.inspect}" if unique_constraint.nulls_not_distinct | ||
| parts << "deferrable: #{unique_constraint.deferrable.inspect}" if unique_constraint.deferrable | ||
| parts << "name: #{unique_constraint.name.inspect}" if unique_constraint.export_name_on_schema_dump? | ||
| if unique_constraint.export_name_on_schema_dump? | ||
| parts << "name: #{unique_constraint.name.inspect}" | ||
| end | ||
| " #{parts.join(', ')}" | ||
| " t.unique_constraint #{parts.join(', ')}" | ||
| end | ||
| stream.puts add_unique_constraint_statements.sort.join("\n") | ||
| stream.puts unique_constraint_statements.sort.join("\n") | ||
| end | ||
@@ -130,2 +145,22 @@ end | ||
| end | ||
| def within_each_schema | ||
| @dump_schemas.each do |schema_name| | ||
| old_search_path = @connection.schema_search_path | ||
| @connection.schema_search_path = schema_name | ||
| self.schema_name = schema_name | ||
| yield | ||
| ensure | ||
| self.schema_name = nil | ||
| @connection.schema_search_path = old_search_path | ||
| end | ||
| end | ||
| def relation_name(name) | ||
| if @dump_schemas.size == 1 | ||
| name | ||
| else | ||
| "#{schema_name}.#{name}" | ||
| end | ||
| end | ||
| end | ||
@@ -132,0 +167,0 @@ end |
@@ -15,5 +15,6 @@ # frozen_string_literal: true | ||
| # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, | ||
| # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>, | ||
| # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses | ||
| # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). | ||
| # <tt>:encoding</tt> (defaults to utf8), <tt>:locale_provider</tt>, <tt>:locale</tt>, | ||
| # <tt>:collation</tt>, <tt>:ctype</tt>, <tt>:tablespace</tt>, and | ||
| # <tt>:connection_limit</tt> (note that MySQL uses <tt>:charset</tt> while PostgreSQL | ||
| # uses <tt>:encoding</tt>). | ||
| # | ||
@@ -34,2 +35,6 @@ # Example: | ||
| " ENCODING = '#{value}'" | ||
| when :locale_provider | ||
| " LOCALE_PROVIDER = '#{value}'" | ||
| when :locale | ||
| " LOCALE = '#{value}'" | ||
| when :collation | ||
@@ -92,4 +97,9 @@ " LC_COLLATE = '#{value}'" | ||
| result = query(<<~SQL, "SCHEMA") | ||
| SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, | ||
| pg_catalog.obj_description(i.oid, 'pg_class') AS comment, d.indisvalid | ||
| SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), | ||
| pg_catalog.obj_description(i.oid, 'pg_class') AS comment, d.indisvalid, | ||
| ARRAY( | ||
| SELECT pg_get_indexdef(d.indexrelid, k + 1, true) | ||
| FROM generate_subscripts(d.indkey, 1) AS k | ||
| ORDER BY k | ||
| ) AS columns | ||
| FROM pg_class t | ||
@@ -111,5 +121,6 @@ INNER JOIN pg_index d ON t.oid = d.indrelid | ||
| inddef = row[3] | ||
| oid = row[4] | ||
| comment = row[5] | ||
| valid = row[6] | ||
| comment = row[4] | ||
| valid = row[5] | ||
| columns = decode_string_array(row[6]).map { |c| Utils.unquote_identifier(c.strip.gsub('""', '"')) } | ||
| using, expressions, include, nulls_not_distinct, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: INCLUDE \((.+?)\))?( NULLS NOT DISTINCT)?(?: WHERE (.+))?\z/m).flatten | ||
@@ -124,4 +135,2 @@ | ||
| else | ||
| columns = column_names_from_column_numbers(oid, indkey) | ||
| # prevent INCLUDE columns from being matched | ||
@@ -234,2 +243,10 @@ columns.reject! { |c| include_columns.include?(c) } | ||
| # Returns an array of the names of all schemas presently in the effective search path, | ||
| # in their priority order. | ||
| def current_schemas # :nodoc: | ||
| schemas = query_value("SELECT current_schemas(false)", "SCHEMA") | ||
| decoder = PG::TextDecoder::Array.new | ||
| decoder.decode(schemas) | ||
| end | ||
| # Returns the current database encoding format. | ||
@@ -279,2 +296,7 @@ def encoding | ||
| # Renames the schema for the given schema name. | ||
| def rename_schema(schema_name, new_name) | ||
| execute "ALTER SCHEMA #{quote_schema_name(schema_name)} RENAME TO #{quote_schema_name(new_name)}" | ||
| end | ||
| # Sets the schema search path to a string of comma-separated schema names. | ||
@@ -286,2 +308,3 @@ # Names beginning with $ have to be quoted (e.g. $user => '$user'). | ||
| def schema_search_path=(schema_csv) | ||
| return if schema_csv == @schema_search_path | ||
| if schema_csv | ||
@@ -331,3 +354,3 @@ internal_execute("SET search_path TO #{schema_csv}") | ||
| query_value("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA") | ||
| internal_execute("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA") | ||
| else | ||
@@ -363,3 +386,3 @@ @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger | ||
| query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA") | ||
| internal_execute("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA") | ||
| end | ||
@@ -597,12 +620,28 @@ end | ||
| fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false) | ||
| SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conkey, c.confkey, c.conrelid, c.confrelid | ||
| SELECT t2.oid::regclass::text AS to_table, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conrelid, c.confrelid, | ||
| ( | ||
| SELECT array_agg(a.attname ORDER BY idx) | ||
| FROM ( | ||
| SELECT idx, c.conkey[idx] AS conkey_elem | ||
| FROM generate_subscripts(c.conkey, 1) AS idx | ||
| ) indexed_conkeys | ||
| JOIN pg_attribute a ON a.attrelid = t1.oid | ||
| AND a.attnum = indexed_conkeys.conkey_elem | ||
| ) AS conkey_names, | ||
| ( | ||
| SELECT array_agg(a.attname ORDER BY idx) | ||
| FROM ( | ||
| SELECT idx, c.confkey[idx] AS confkey_elem | ||
| FROM generate_subscripts(c.confkey, 1) AS idx | ||
| ) indexed_confkeys | ||
| JOIN pg_attribute a ON a.attrelid = t2.oid | ||
| AND a.attnum = indexed_confkeys.confkey_elem | ||
| ) AS confkey_names | ||
| FROM pg_constraint c | ||
| JOIN pg_class t1 ON c.conrelid = t1.oid | ||
| JOIN pg_class t2 ON c.confrelid = t2.oid | ||
| JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid | ||
| JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid | ||
| JOIN pg_namespace t3 ON c.connamespace = t3.oid | ||
| JOIN pg_namespace n ON c.connamespace = n.oid | ||
| WHERE c.contype = 'f' | ||
| AND t1.relname = #{scope[:name]} | ||
| AND t3.nspname = #{scope[:schema]} | ||
| AND n.nspname = #{scope[:schema]} | ||
| ORDER BY c.conname | ||
@@ -613,17 +652,10 @@ SQL | ||
| to_table = Utils.unquote_identifier(row["to_table"]) | ||
| conkey = row["conkey"].scan(/\d+/).map(&:to_i) | ||
| confkey = row["confkey"].scan(/\d+/).map(&:to_i) | ||
| if conkey.size > 1 | ||
| column = column_names_from_column_numbers(row["conrelid"], conkey) | ||
| primary_key = column_names_from_column_numbers(row["confrelid"], confkey) | ||
| else | ||
| column = Utils.unquote_identifier(row["column"]) | ||
| primary_key = row["primary_key"] | ||
| end | ||
| column = decode_string_array(row["conkey_names"]) | ||
| primary_key = decode_string_array(row["confkey_names"]) | ||
| options = { | ||
| column: column, | ||
| column: column.size == 1 ? column.first : column, | ||
| name: row["name"], | ||
| primary_key: primary_key | ||
| primary_key: primary_key.size == 1 ? primary_key.first : primary_key | ||
| } | ||
@@ -713,3 +745,12 @@ | ||
| unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false) | ||
| SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef | ||
| SELECT c.conname, c.conrelid, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef, | ||
| ( | ||
| SELECT array_agg(a.attname ORDER BY idx) | ||
| FROM ( | ||
| SELECT idx, c.conkey[idx] AS conkey_elem | ||
| FROM generate_subscripts(c.conkey, 1) AS idx | ||
| ) indexed_conkeys | ||
| JOIN pg_attribute a ON a.attrelid = t.oid | ||
| AND a.attnum = indexed_conkeys.conkey_elem | ||
| ) AS conkey_names | ||
| FROM pg_constraint c | ||
@@ -724,4 +765,3 @@ JOIN pg_class t ON c.conrelid = t.oid | ||
| unique_info.map do |row| | ||
| conkey = row["conkey"].delete("{}").split(",").map(&:to_i) | ||
| columns = column_names_from_column_numbers(row["conrelid"], conkey) | ||
| columns = decode_string_array(row["conkey_names"]) | ||
@@ -997,2 +1037,3 @@ nulls_not_distinct = row["constraintdef"].start_with?("UNIQUE NULLS NOT DISTINCT") | ||
| column_name, | ||
| get_oid_type(oid.to_i, fmod.to_i, column_name, type), | ||
| default_value, | ||
@@ -1167,9 +1208,4 @@ type_metadata, | ||
| def column_names_from_column_numbers(table_oid, column_numbers) | ||
| Hash[query(<<~SQL, "SCHEMA")].values_at(*column_numbers).compact | ||
| SELECT a.attnum, a.attname | ||
| FROM pg_attribute a | ||
| WHERE a.attrelid = #{table_oid} | ||
| AND a.attnum IN (#{column_numbers.join(", ")}) | ||
| SQL | ||
| def decode_string_array(value) | ||
| PG::TextDecoder::Array.new.decode(value) | ||
| end | ||
@@ -1176,0 +1212,0 @@ end |
@@ -274,6 +274,6 @@ # frozen_string_literal: true | ||
| def encode_with(coder) # :nodoc: | ||
| coder["columns"] = @columns.sort.to_h | ||
| coder["columns"] = @columns.sort.to_h.transform_values { _1.sort_by(&:name) } | ||
| coder["primary_keys"] = @primary_keys.sort.to_h | ||
| coder["data_sources"] = @data_sources.sort.to_h | ||
| coder["indexes"] = @indexes.sort.to_h | ||
| coder["indexes"] = @indexes.sort.to_h.transform_values { _1.sort_by(&:name) } | ||
| coder["version"] = @version | ||
@@ -280,0 +280,0 @@ end |
@@ -22,10 +22,26 @@ # frozen_string_literal: true | ||
| module ConnectionAdapters # :nodoc: | ||
| # = Active Record SQLite3 Adapter | ||
| # = Active Record \SQLite3 Adapter | ||
| # | ||
| # The SQLite3 adapter works with the sqlite3-ruby drivers | ||
| # (available as gem from https://rubygems.org/gems/sqlite3). | ||
| # The \SQLite3 adapter works with the sqlite3[https://sparklemotion.github.io/sqlite3-ruby/] | ||
| # driver. | ||
| # | ||
| # ==== Options | ||
| # | ||
| # * <tt>:database</tt> - Path to the database file. | ||
| # * +:database+ (String): Filesystem path to the database file. | ||
| # * +:statement_limit+ (Integer): Maximum number of prepared statements to cache per database connection. (default: 1000) | ||
| # * +:timeout+ (Integer): Timeout in milliseconds to use when waiting for a lock. (default: no wait) | ||
| # * +:strict+ (Boolean): Enable or disable strict mode. When enabled, this will | ||
| # {disallow double-quoted string literals in SQL | ||
| # statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted]. | ||
| # (default: see strict_strings_by_default) | ||
| # * +:extensions+ (Array): (<b>requires sqlite3 v2.4.0</b>) Each entry specifies a sqlite extension | ||
| # to load for this database. The entry may be a filesystem path, or the name of a class that | ||
| # responds to +.to_path+ to provide the filesystem path for the extension. See {sqlite3-ruby | ||
| # documentation}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#class-SQLite3::Database-label-SQLite+Extensions] | ||
| # for more information. | ||
| # | ||
| # There may be other options available specific to the SQLite3 driver. Please read the | ||
| # documentation for | ||
| # {SQLite3::Database.new}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#method-c-new] | ||
| # | ||
| class SQLite3Adapter < AbstractAdapter | ||
@@ -50,6 +66,10 @@ ADAPTER_NAME = "SQLite" | ||
| args << "-header" if options[:header] | ||
| args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil) | ||
| args << File.expand_path(config.database, defined?(Rails.root) ? Rails.root : nil) | ||
| find_cmd_and_exec(ActiveRecord.database_cli[:sqlite], *args) | ||
| end | ||
| def native_database_types # :nodoc: | ||
| NATIVE_DATABASE_TYPES | ||
| end | ||
| end | ||
@@ -63,8 +83,15 @@ | ||
| # :singleton-method: | ||
| # Configure the SQLite3Adapter to be used in a strict strings mode. | ||
| # This will disable double-quoted string literals, because otherwise typos can silently go unnoticed. | ||
| # For example, it is possible to create an index for a non existing column. | ||
| # | ||
| # Configure the SQLite3Adapter to be used in a "strict strings" mode. When enabled, this will | ||
| # {disallow double-quoted string literals in SQL | ||
| # statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted], | ||
| # which may prevent some typographical errors like creating an index for a non-existent | ||
| # column. The default is +false+. | ||
| # | ||
| # If you wish to enable this mode you can add the following line to your application.rb file: | ||
| # | ||
| # config.active_record.sqlite3_adapter_strict_strings_by_default = true | ||
| # | ||
| # This can also be configured on individual databases by setting the +strict:+ option. | ||
| # | ||
| class_attribute :strict_strings_by_default, default: false | ||
@@ -128,5 +155,9 @@ | ||
| @last_affected_rows = nil | ||
| @previous_read_uncommitted = nil | ||
| @config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict) | ||
| extensions = @config.fetch(:extensions, []).map do |extension| | ||
| extension.safe_constantize || extension | ||
| end | ||
| @connection_parameters = @config.merge( | ||
@@ -136,2 +167,3 @@ database: @config[:database].to_s, | ||
| default_transaction_mode: :immediate, | ||
| extensions: extensions | ||
| ) | ||
@@ -161,3 +193,3 @@ end | ||
| def supports_expression_index? | ||
| database_version >= "3.9.0" | ||
| true | ||
| end | ||
@@ -190,3 +222,3 @@ | ||
| def supports_common_table_expressions? | ||
| database_version >= "3.8.3" | ||
| true | ||
| end | ||
@@ -239,6 +271,2 @@ | ||
| def native_database_types # :nodoc: | ||
| NATIVE_DATABASE_TYPES | ||
| end | ||
| # Returns the current database encoding format as a string, e.g. 'UTF-8' | ||
@@ -302,3 +330,3 @@ def encoding | ||
| VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.*)\)/i | ||
| VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.+)\)/i | ||
@@ -490,4 +518,4 @@ # Returns a list of defined virtual tables | ||
| def check_version # :nodoc: | ||
| if database_version < "3.8.0" | ||
| raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.8." | ||
| if database_version < "3.23.0" | ||
| raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.23.0." | ||
| end | ||
@@ -548,2 +576,4 @@ end | ||
| [ $1 ].pack("H*") | ||
| when "TRUE", "FALSE" | ||
| default | ||
| else | ||
@@ -597,4 +627,4 @@ # Anything else is blank or some function | ||
| disable_referential_integrity do | ||
| transaction do | ||
| transaction do | ||
| disable_referential_integrity do | ||
| move_table(table_name, altered_table_name, options.merge(temporary: true)) | ||
@@ -640,4 +670,4 @@ move_table(altered_table_name, table_name, &caller) | ||
| elsif column.has_default? | ||
| type = lookup_cast_type_from_column(column) | ||
| default = type.deserialize(column.default) | ||
| # TODO: Remove fetch_cast_type and the need for connection after we release 8.1. | ||
| default = column.fetch_cast_type(self).deserialize(column.default) | ||
| default = -> { column.default_function } if default.nil? | ||
@@ -716,2 +746,4 @@ | ||
| InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool) | ||
| elsif exception.message.match?(/CHECK constraint failed: .*/i) | ||
| CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool) | ||
| elsif exception.message.match?(/called on a closed database/i) | ||
@@ -806,5 +838,5 @@ ConnectionNotEstablished.new(exception, connection_pool: @pool) | ||
| if supports_virtual_columns? | ||
| internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA") | ||
| internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA", allow_retry: true) | ||
| else | ||
| internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") | ||
| internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA", allow_retry: true) | ||
| end | ||
@@ -836,14 +868,6 @@ end | ||
| def configure_connection | ||
| if @config[:timeout] && @config[:retries] | ||
| raise ArgumentError, "Cannot specify both timeout and retries arguments" | ||
| elsif @config[:timeout] | ||
| if @config[:timeout] | ||
| timeout = self.class.type_cast_config_to_integer(@config[:timeout]) | ||
| raise TypeError, "timeout must be integer, not #{timeout}" unless timeout.is_a?(Integer) | ||
| @raw_connection.busy_handler_timeout = timeout | ||
| elsif @config[:retries] | ||
| ActiveRecord.deprecator.warn(<<~MSG) | ||
| The retries option is deprecated and will be removed in Rails 8.1. Use timeout instead. | ||
| MSG | ||
| retries = self.class.type_cast_config_to_integer(@config[:retries]) | ||
| raw_connection.busy_handler { |count| count <= retries } | ||
| end | ||
@@ -850,0 +874,0 @@ |
@@ -49,5 +49,3 @@ # frozen_string_literal: true | ||
| super && | ||
| auto_increment? == other.auto_increment? && | ||
| rowid == other.rowid && | ||
| generated_type == other.generated_type | ||
| auto_increment? == other.auto_increment? | ||
| end | ||
@@ -60,8 +58,4 @@ alias :eql? :== | ||
| auto_increment?.hash ^ | ||
| rowid.hash ^ | ||
| virtual?.hash | ||
| rowid.hash | ||
| end | ||
| protected | ||
| attr_reader :generated_type | ||
| end | ||
@@ -68,0 +62,0 @@ end |
@@ -64,2 +64,10 @@ # frozen_string_literal: true | ||
| def default_insert_value(column) # :nodoc: | ||
| if column.default_function | ||
| Arel.sql(column.default_function) | ||
| else | ||
| column.default | ||
| end | ||
| end | ||
| private | ||
@@ -80,18 +88,15 @@ def internal_begin_transaction(mode, isolation) | ||
| def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false) | ||
| total_changes_before_query = raw_connection.total_changes | ||
| affected_rows = nil | ||
| if batch | ||
| raw_connection.execute_batch2(sql) | ||
| elsif prepare | ||
| stmt = @statements[sql] ||= raw_connection.prepare(sql) | ||
| stmt.reset! | ||
| stmt.bind_params(type_casted_binds) | ||
| result = if stmt.column_count.zero? # No return | ||
| stmt.step | ||
| ActiveRecord::Result.empty | ||
| else | ||
| stmt = if prepare | ||
| @statements[sql] ||= raw_connection.prepare(sql) | ||
| @statements[sql].reset! | ||
| else | ||
| ActiveRecord::Result.new(stmt.columns, stmt.to_a) | ||
| # Don't cache statements if they are not prepared. | ||
| raw_connection.prepare(sql) | ||
| end | ||
| else | ||
| # Don't cache statements if they are not prepared. | ||
| stmt = raw_connection.prepare(sql) | ||
| begin | ||
@@ -103,13 +108,28 @@ unless binds.nil? || binds.empty? | ||
| stmt.step | ||
| ActiveRecord::Result.empty | ||
| affected_rows = if raw_connection.total_changes > total_changes_before_query | ||
| raw_connection.changes | ||
| else | ||
| 0 | ||
| end | ||
| ActiveRecord::Result.empty(affected_rows: affected_rows) | ||
| else | ||
| ActiveRecord::Result.new(stmt.columns, stmt.to_a) | ||
| rows = stmt.to_a | ||
| affected_rows = if raw_connection.total_changes > total_changes_before_query | ||
| raw_connection.changes | ||
| else | ||
| 0 | ||
| end | ||
| ActiveRecord::Result.new(stmt.columns, rows, stmt.types.map { |t| type_map.lookup(t) }, affected_rows: affected_rows) | ||
| end | ||
| ensure | ||
| stmt.close | ||
| stmt.close unless prepare | ||
| end | ||
| end | ||
| @last_affected_rows = raw_connection.changes | ||
| verified! | ||
| notification_payload[:affected_rows] = affected_rows | ||
| notification_payload[:row_count] = result&.length || 0 | ||
@@ -126,3 +146,3 @@ result | ||
| def affected_rows(result) | ||
| @last_affected_rows | ||
| result.affected_rows | ||
| end | ||
@@ -142,10 +162,2 @@ | ||
| end | ||
| def default_insert_value(column) | ||
| if column.default_function | ||
| Arel.sql(column.default_function) | ||
| else | ||
| column.default | ||
| end | ||
| end | ||
| end | ||
@@ -152,0 +164,0 @@ end |
@@ -83,6 +83,2 @@ # frozen_string_literal: true | ||
| def quoted_true | ||
| "1" | ||
| end | ||
| def unquoted_true | ||
@@ -92,6 +88,2 @@ 1 | ||
| def quoted_false | ||
| "0" | ||
| end | ||
| def unquoted_false | ||
@@ -98,0 +90,0 @@ 0 |
@@ -9,3 +9,3 @@ # frozen_string_literal: true | ||
| def virtual_tables(stream) | ||
| virtual_tables = @connection.virtual_tables.reject { |name, _| ignored?(name) } | ||
| virtual_tables = @connection.virtual_tables | ||
| if virtual_tables.any? | ||
@@ -23,21 +23,9 @@ stream.puts | ||
| def default_primary_key?(column) | ||
| schema_type(column) == :integer && primary_key_has_autoincrement? | ||
| schema_type(column) == :integer | ||
| end | ||
| def explicit_primary_key_default?(column) | ||
| column.bigint? || (column.type == :integer && !primary_key_has_autoincrement?) | ||
| column.bigint? | ||
| end | ||
| def primary_key_has_autoincrement? | ||
| return false unless table_name | ||
| table_sql = @connection.query_value(<<~SQL, "SCHEMA") | ||
| SELECT sql FROM sqlite_master WHERE name = #{@connection.quote(table_name)} AND type = 'table' | ||
| UNION ALL | ||
| SELECT sql FROM sqlite_temp_master WHERE name = #{@connection.quote(table_name)} AND type = 'table' | ||
| SQL | ||
| table_sql.to_s.match?(/\bAUTOINCREMENT\b/i) | ||
| end | ||
| def prepare_column_options(column) | ||
@@ -44,0 +32,0 @@ spec = super |
@@ -66,19 +66,9 @@ # frozen_string_literal: true | ||
| def remove_foreign_key(from_table, to_table = nil, **options) | ||
| return if options.delete(:if_exists) == true && !foreign_key_exists?(from_table, to_table) | ||
| return if options.delete(:if_exists) && !foreign_key_exists?(from_table, to_table, **options.slice(:column)) | ||
| to_table ||= options[:to_table] | ||
| options = options.except(:name, :to_table, :validate) | ||
| fkey = foreign_key_for!(from_table, to_table: to_table, **options) | ||
| foreign_keys = foreign_keys(from_table) | ||
| fkey = foreign_keys.detect do |fk| | ||
| table = to_table || begin | ||
| table = options[:column].to_s.delete_suffix("_id") | ||
| Base.pluralize_table_names ? table.pluralize : table | ||
| end | ||
| table = strip_table_name_prefix_and_suffix(table) | ||
| options = options.slice(*fk.options.keys) | ||
| fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table) | ||
| fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s } | ||
| end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table || options}") | ||
| foreign_keys.delete(fkey) | ||
@@ -161,2 +151,3 @@ alter_table(from_table, foreign_keys) | ||
| field["name"], | ||
| lookup_cast_type(field["type"]), | ||
| default_value, | ||
@@ -163,0 +154,0 @@ type_metadata, |
@@ -184,3 +184,3 @@ # frozen_string_literal: true | ||
| case exception | ||
| when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError | ||
| when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError, ::Trilogy::SSLError | ||
| return ConnectionFailed.new(message, connection_pool: @pool) | ||
@@ -187,0 +187,0 @@ when ::Trilogy::Error |
@@ -32,3 +32,4 @@ # frozen_string_literal: true | ||
| verified! | ||
| handle_warnings(sql) | ||
| notification_payload[:affected_rows] = result.affected_rows | ||
| notification_payload[:row_count] = result.count | ||
@@ -44,5 +45,5 @@ result | ||
| if result.fields.empty? | ||
| ActiveRecord::Result.empty | ||
| ActiveRecord::Result.empty(affected_rows: result.affected_rows) | ||
| else | ||
| ActiveRecord::Result.new(result.fields, result.rows) | ||
| ActiveRecord::Result.new(result.fields, result.rows, affected_rows: result.affected_rows) | ||
| end | ||
@@ -49,0 +50,0 @@ end |
@@ -175,7 +175,5 @@ # frozen_string_literal: true | ||
| append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: classes) | ||
| begin | ||
| yield | ||
| ensure | ||
| connected_to_stack.pop | ||
| end | ||
| yield | ||
| ensure | ||
| connected_to_stack.pop | ||
| end | ||
@@ -401,9 +399,7 @@ | ||
| append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: [self]) | ||
| begin | ||
| return_value = yield | ||
| return_value.load if return_value.is_a? ActiveRecord::Relation | ||
| return_value | ||
| ensure | ||
| self.connected_to_stack.pop | ||
| end | ||
| return_value = yield | ||
| return_value.load if return_value.is_a? ActiveRecord::Relation | ||
| return_value | ||
| ensure | ||
| self.connected_to_stack.pop | ||
| end | ||
@@ -410,0 +406,0 @@ |
| # frozen_string_literal: true | ||
| require "active_support/core_ext/enumerable" | ||
| require "active_support/core_ext/module/delegation" | ||
| require "active_support/parameter_filter" | ||
@@ -115,3 +114,3 @@ require "concurrent/map" | ||
| # | ||
| # When set to `:all` inspect will list all the record's attributes: | ||
| # When set to +:all+ inspect will list all the record's attributes: | ||
| # | ||
@@ -362,2 +361,4 @@ # Post.attributes_for_inspect = :all | ||
| @filter_attributes = filter_attributes | ||
| FilterAttributeHandler.sensitive_attribute_was_declared(self, filter_attributes) | ||
| end | ||
@@ -456,3 +457,3 @@ | ||
| statement.execute(values.flatten, connection, allow_retry: true).then do |r| | ||
| statement.execute(values.flatten, connection).then do |r| | ||
| r.first | ||
@@ -647,3 +648,3 @@ rescue TypeError | ||
| if primary_key_values_present? | ||
| if self.class.composite_primary_key? ? primary_key_values_present? : id | ||
| self.class.hash ^ id.hash | ||
@@ -650,0 +651,0 @@ else |
@@ -20,3 +20,3 @@ # frozen_string_literal: true | ||
| # | ||
| # * +id+ - The id of the object you wish to reset a counter on. | ||
| # * +id+ - The id of the object you wish to reset a counter on or an array of ids. | ||
| # * +counters+ - One or more association counters to reset. Association name or counter name can be given. | ||
@@ -32,2 +32,5 @@ # * <tt>:touch</tt> - Touch timestamp columns when updating. | ||
| # | ||
| # # For posts with ids #1 and #2, reset the comments_count | ||
| # Post.reset_counters([1, 2], :comments) | ||
| # | ||
| # # Like above, but also touch the updated_at and/or updated_on | ||
@@ -37,5 +40,14 @@ # # attributes. | ||
| def reset_counters(id, *counters, touch: nil) | ||
| object = find(id) | ||
| ids = if composite_primary_key? | ||
| if id.first.is_a?(Array) | ||
| id | ||
| else | ||
| [id] | ||
| end | ||
| else | ||
| Array(id) | ||
| end | ||
| updates = {} | ||
| updates = Hash.new { |h, k| h[k] = {} } | ||
| counters.each do |counter_association| | ||
@@ -54,2 +66,3 @@ has_many_association = _reflect_on_association(counter_association) | ||
| counter_association = counter_association.to_sym | ||
| foreign_key = has_many_association.foreign_key.to_s | ||
@@ -60,5 +73,12 @@ child_class = has_many_association.klass | ||
| count_was = object.send(counter_name) | ||
| count = object.send(counter_association).count(:all) | ||
| updates[counter_name] = count if count != count_was | ||
| counts = | ||
| unscoped | ||
| .joins(counter_association) | ||
| .where(primary_key => ids) | ||
| .group(primary_key) | ||
| .count(:all) | ||
| ids.each do |id| | ||
| updates[id].merge!(counter_name => counts[id] || 0) | ||
| end | ||
| end | ||
@@ -71,6 +91,11 @@ | ||
| touch_updates = touch_attributes_with_time(*names, **options) | ||
| updates.merge!(touch_updates) | ||
| updates.each_value do |record_updates| | ||
| record_updates.merge!(touch_updates) | ||
| end | ||
| end | ||
| unscoped.where(primary_key => [object.id]).update_all(updates) if updates.any? | ||
| updates.each do |id, record_updates| | ||
| unscoped.where(primary_key => [id]).update_all(record_updates) | ||
| end | ||
@@ -77,0 +102,0 @@ true |
@@ -39,5 +39,7 @@ # frozen_string_literal: true | ||
| # | ||
| # ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config| | ||
| # next unless config.key?(:vitess) | ||
| # VitessConfig.new(env_name, name, config) | ||
| # ActiveSupport.on_load(:active_record_database_configurations) do | ||
| # ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config| | ||
| # next unless config.key?(:vitess) | ||
| # VitessConfig.new(env_name, name, config) | ||
| # end | ||
| # end | ||
@@ -310,2 +312,4 @@ # | ||
| end | ||
| ActiveSupport.run_load_hooks(:active_record_database_configurations, DatabaseConfigurations) | ||
| end |
@@ -51,6 +51,10 @@ # frozen_string_literal: true | ||
| def pool | ||
| def min_connections | ||
| raise NotImplementedError | ||
| end | ||
| def max_connections | ||
| raise NotImplementedError | ||
| end | ||
| def min_threads | ||
@@ -57,0 +61,0 @@ raise NotImplementedError |
@@ -41,2 +41,3 @@ # frozen_string_literal: true | ||
| @configuration_hash = configuration_hash.symbolize_keys.freeze | ||
| validate_configuration! | ||
| end | ||
@@ -73,6 +74,13 @@ | ||
| def pool | ||
| (configuration_hash[:pool] || 5).to_i | ||
| def max_connections | ||
| (configuration_hash[:max_connections] || configuration_hash[:pool] || 5).to_i | ||
| end | ||
| def min_connections | ||
| (configuration_hash[:min_connections] || 0).to_i | ||
| end | ||
| alias :pool :max_connections | ||
| deprecate pool: :max_connections, deprecator: ActiveRecord.deprecator | ||
| def min_threads | ||
@@ -83,5 +91,14 @@ (configuration_hash[:min_threads] || 0).to_i | ||
| def max_threads | ||
| (configuration_hash[:max_threads] || pool).to_i | ||
| (configuration_hash[:max_threads] || max_connections).to_i | ||
| end | ||
| def max_age | ||
| v = configuration_hash[:max_age]&.to_i | ||
| if v && v > 0 | ||
| v | ||
| else | ||
| Float::INFINITY | ||
| end | ||
| end | ||
| def query_cache | ||
@@ -99,6 +116,4 @@ configuration_hash[:query_cache] | ||
| # `reaping_frequency` is configurable mostly for historical reasons, but it | ||
| # could also be useful if someone wants a very low `idle_timeout`. | ||
| def reaping_frequency | ||
| configuration_hash.fetch(:reaping_frequency, 60)&.to_f | ||
| def reaping_frequency # :nodoc: | ||
| configuration_hash.fetch(:reaping_frequency, default_reaping_frequency)&.to_f | ||
| end | ||
@@ -111,2 +126,7 @@ | ||
| def keepalive | ||
| keepalive = (configuration_hash[:keepalive] || 600).to_f | ||
| keepalive if keepalive > 0 | ||
| end | ||
| def adapter | ||
@@ -167,4 +187,4 @@ configuration_hash[:adapter]&.to_s | ||
| def schema_format # :nodoc: | ||
| format = configuration_hash[:schema_format]&.to_sym || ActiveRecord.schema_format | ||
| raise "Invalid schema format" unless [ :ruby, :sql ].include? format | ||
| format = configuration_hash.fetch(:schema_format, ActiveRecord.schema_format).to_sym | ||
| raise "Invalid schema format" unless [:ruby, :sql].include?(format) | ||
| format | ||
@@ -190,4 +210,25 @@ end | ||
| end | ||
| def default_reaping_frequency | ||
| # Reap every 20 seconds by default, but run more often as necessary to | ||
| # meet other configured timeouts. | ||
| [20, idle_timeout, max_age, keepalive].compact.min | ||
| end | ||
| def validate_configuration! | ||
| if configuration_hash[:pool] && configuration_hash[:max_connections] | ||
| pool_val = configuration_hash[:pool].to_i | ||
| max_conn_val = configuration_hash[:max_connections].to_i | ||
| if pool_val != max_conn_val | ||
| raise "Ambiguous configuration: 'pool' (#{pool_val}) and 'max_connections' (#{max_conn_val}) are set to different values. Prefer just 'max_connections'." | ||
| end | ||
| end | ||
| if configuration_hash[:pool] && configuration_hash[:min_connections] | ||
| raise "Ambiguous configuration: when setting 'min_connections', use 'max_connections' instead of 'pool'." | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
@@ -50,5 +50,4 @@ # frozen_string_literal: true | ||
| if @configuration_hash[:query_cache] == "false" | ||
| @configuration_hash[:query_cache] = false | ||
| end | ||
| query_cache = parse_query_cache | ||
| @configuration_hash[:query_cache] = query_cache unless query_cache.nil? | ||
@@ -62,2 +61,13 @@ to_boolean!(@configuration_hash, :replica) | ||
| private | ||
| def parse_query_cache | ||
| case value = @configuration_hash[:query_cache] | ||
| when /\A\d+\z/ | ||
| value.to_i | ||
| when "false" | ||
| false | ||
| else | ||
| value | ||
| end | ||
| end | ||
| def to_boolean!(configuration_hash, key) | ||
@@ -64,0 +74,0 @@ if configuration_hash[key].is_a?(String) |
@@ -232,3 +232,3 @@ # frozen_string_literal: true | ||
| def delegated_type(role, types:, **options) | ||
| belongs_to role, options.delete(:scope), **options.merge(polymorphic: true) | ||
| belongs_to role, options.delete(:scope), **options, polymorphic: true | ||
| define_delegated_type_methods role, types: types, options: options | ||
@@ -235,0 +235,0 @@ end |
@@ -10,4 +10,6 @@ # frozen_string_literal: true | ||
| else | ||
| match = Method.match(self, name) | ||
| match && match.valid? || super | ||
| super || begin | ||
| match = Method.match(name) | ||
| match && match.valid?(self, name) | ||
| end | ||
| end | ||
@@ -17,6 +19,6 @@ end | ||
| def method_missing(name, ...) | ||
| match = Method.match(self, name) | ||
| match = Method.match(name) | ||
| if match && match.valid? | ||
| match.define | ||
| if match && match.valid?(self, name) | ||
| match.define(self, name) | ||
| send(name, ...) | ||
@@ -29,93 +31,76 @@ else | ||
| class Method | ||
| @matchers = [] | ||
| class << self | ||
| attr_reader :matchers | ||
| def match(model, name) | ||
| klass = matchers.find { |k| k.pattern.match?(name) } | ||
| klass.new(model, name) if klass | ||
| def match(name) | ||
| FindBy.match?(name) || FindByBang.match?(name) | ||
| end | ||
| def pattern | ||
| @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/ | ||
| def valid?(model, name) | ||
| attribute_names(model, name.to_s).all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } | ||
| end | ||
| def prefix | ||
| raise NotImplementedError | ||
| def define(model, name) | ||
| model.class_eval <<-CODE, __FILE__, __LINE__ + 1 | ||
| def self.#{name}(#{signature(model, name)}) | ||
| #{body(model, name)} | ||
| end | ||
| CODE | ||
| end | ||
| def suffix | ||
| "" | ||
| end | ||
| end | ||
| private | ||
| def make_pattern(prefix, suffix) | ||
| /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/ | ||
| end | ||
| attr_reader :model, :name, :attribute_names | ||
| def attribute_names(model, name) | ||
| attribute_names = name.match(pattern)[1].split("_and_") | ||
| attribute_names.map! { |name| model.attribute_aliases[name] || name } | ||
| end | ||
| def initialize(model, method_name) | ||
| @model = model | ||
| @name = method_name.to_s | ||
| @attribute_names = @name.match(self.class.pattern)[1].split("_and_") | ||
| @attribute_names.map! { |name| @model.attribute_aliases[name] || name } | ||
| end | ||
| def body(model, method_name) | ||
| "#{finder}(#{attributes_hash(model, method_name)})" | ||
| end | ||
| def valid? | ||
| attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } | ||
| end | ||
| # The parameters in the signature may have reserved Ruby words, in order | ||
| # to prevent errors, we start each param name with `_`. | ||
| def signature(model, method_name) | ||
| attribute_names(model, method_name.to_s).map { |name| "_#{name}" }.join(", ") | ||
| end | ||
| def define | ||
| model.class_eval <<-CODE, __FILE__, __LINE__ + 1 | ||
| def self.#{name}(#{signature}) | ||
| #{body} | ||
| # Given that the parameters starts with `_`, the finder needs to use the | ||
| # same parameter name. | ||
| def attributes_hash(model, method_name) | ||
| "{" + attribute_names(model, method_name).map { |name| ":#{name} => _#{name}" }.join(",") + "}" | ||
| end | ||
| CODE | ||
| end | ||
| end | ||
| private | ||
| def body | ||
| "#{finder}(#{attributes_hash})" | ||
| end | ||
| class FindBy < Method | ||
| @pattern = make_pattern("find_by", "") | ||
| # The parameters in the signature may have reserved Ruby words, in order | ||
| # to prevent errors, we start each param name with `_`. | ||
| def signature | ||
| attribute_names.map { |name| "_#{name}" }.join(", ") | ||
| end | ||
| class << self | ||
| attr_reader :pattern | ||
| # Given that the parameters starts with `_`, the finder needs to use the | ||
| # same parameter name. | ||
| def attributes_hash | ||
| "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}" | ||
| def match?(name) | ||
| pattern.match?(name) && self | ||
| end | ||
| def finder | ||
| raise NotImplementedError | ||
| "find_by" | ||
| end | ||
| end | ||
| class FindBy < Method | ||
| Method.matchers << self | ||
| def self.prefix | ||
| "find_by" | ||
| end | ||
| def finder | ||
| "find_by" | ||
| end | ||
| end | ||
| class FindByBang < Method | ||
| Method.matchers << self | ||
| @pattern = make_pattern("find_by", "!") | ||
| def self.prefix | ||
| "find_by" | ||
| end | ||
| class << self | ||
| attr_reader :pattern | ||
| def self.suffix | ||
| "!" | ||
| end | ||
| def match?(name) | ||
| pattern.match?(name) && self | ||
| end | ||
| def finder | ||
| "find_by!" | ||
| def finder | ||
| "find_by!" | ||
| end | ||
| end | ||
@@ -122,0 +107,0 @@ end |
@@ -33,6 +33,6 @@ # frozen_string_literal: true | ||
| # data. | ||
| # * <tt>:support_unencrypted_data</tt> - If `config.active_record.encryption.support_unencrypted_data` is +true+, | ||
| # you can set this to +false+ to opt out of unencrypted data support for this attribute. This is useful for | ||
| # scenarios where you encrypt one column, and want to disable support for unencrypted data without having to tweak | ||
| # the global setting. | ||
| # * <tt>:support_unencrypted_data</tt> - When true, unencrypted data can be read normally. When false, it will raise errors. | ||
| # Falls back to +config.active_record.encryption.support_unencrypted_data+ if no value is provided. | ||
| # This is useful for scenarios where you encrypt one column, and want to disable support for unencrypted data | ||
| # without having to tweak the global setting. | ||
| # * <tt>:downcase</tt> - When true, it converts the encrypted content to downcase automatically. This allows to | ||
@@ -39,0 +39,0 @@ # effectively ignore case when querying data. Notice that the case is lost. Use +:ignore_case+ if you are interested |
@@ -62,3 +62,3 @@ # frozen_string_literal: true | ||
| def support_unencrypted_data? | ||
| ActiveRecord::Encryption.config.support_unencrypted_data && scheme.support_unencrypted_data? && !previous_type? | ||
| scheme.support_unencrypted_data? && !previous_type? | ||
| end | ||
@@ -65,0 +65,0 @@ |
@@ -62,3 +62,3 @@ # frozen_string_literal: true | ||
| def merge(other_scheme) | ||
| self.class.new(**to_h.merge(other_scheme.to_h)) | ||
| self.class.new(**to_h, **other_scheme.to_h) | ||
| end | ||
@@ -65,0 +65,0 @@ |
@@ -224,3 +224,3 @@ # frozen_string_literal: true | ||
| def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, validate: false, **options) | ||
| assert_valid_enum_definition_values(values) | ||
| values = assert_valid_enum_definition_values(values) | ||
| assert_valid_enum_options(options) | ||
@@ -346,2 +346,16 @@ | ||
| end | ||
| values = values.transform_values do |value| | ||
| value.is_a?(Symbol) ? value.name : value | ||
| end | ||
| values.each_value do |value| | ||
| case value | ||
| when String, Integer, true, false, nil | ||
| # noop | ||
| else | ||
| raise ArgumentError, "Enum values #{values} must be only booleans, integers, symbols or strings, got: #{value.class}" | ||
| end | ||
| end | ||
| when Array | ||
@@ -362,2 +376,4 @@ if values.empty? | ||
| end | ||
| values | ||
| end | ||
@@ -374,3 +390,3 @@ | ||
| "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \ | ||
| "this will generate a %{type} method \"%{method}\", which is already defined " \ | ||
| "this will generate %{type} method \"%{method}\", which is already defined " \ | ||
| "by %{source}." | ||
@@ -381,15 +397,15 @@ private_constant :ENUM_CONFLICT_MESSAGE | ||
| if klass_method && dangerous_class_method?(method_name) | ||
| raise_conflict_error(enum_name, method_name, type: "class") | ||
| raise_conflict_error(enum_name, method_name, "a class") | ||
| elsif klass_method && method_defined_within?(method_name, Relation) | ||
| raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name) | ||
| raise_conflict_error(enum_name, method_name, "a class", source: Relation.name) | ||
| elsif klass_method && method_name.to_sym == :id | ||
| raise_conflict_error(enum_name, method_name) | ||
| raise_conflict_error(enum_name, method_name, "an instance") | ||
| elsif !klass_method && dangerous_attribute_method?(method_name) | ||
| raise_conflict_error(enum_name, method_name) | ||
| raise_conflict_error(enum_name, method_name, "an instance") | ||
| elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module) | ||
| raise_conflict_error(enum_name, method_name, source: "another enum") | ||
| raise_conflict_error(enum_name, method_name, "an instance", source: "another enum") | ||
| end | ||
| end | ||
| def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Record") | ||
| def raise_conflict_error(enum_name, method_name, type, source: "Active Record") | ||
| raise ArgumentError, ENUM_CONFLICT_MESSAGE % { | ||
@@ -396,0 +412,0 @@ enum: enum_name, |
| # frozen_string_literal: true | ||
| require "active_support/deprecation" | ||
@@ -16,3 +15,3 @@ module ActiveRecord | ||
| # (for example due to improper usage of column that | ||
| # {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema.inheritance_column] | ||
| # {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema::ClassMethods#inheritance_column] | ||
| # points to). | ||
@@ -297,2 +296,10 @@ class SubclassNotFound < ActiveRecordError | ||
| # Raised when a record cannot be inserted or updated because it would violate a check constraint. | ||
| class CheckViolation < StatementInvalid | ||
| end | ||
| # Raised when a record cannot be inserted or updated because it would violate an exclusion constraint. | ||
| class ExclusionViolation < StatementInvalid | ||
| end | ||
| # Raised when a record cannot be inserted or updated because a value too long for a column type. | ||
@@ -344,11 +351,11 @@ class ValueTooLong < StatementInvalid | ||
| NoDatabaseError.new(<<~MSG) | ||
| We could not find your database: #{db_name}. Available database configurations can be found in config/database.yml. | ||
| Database not found: #{db_name}. Available database configurations can be found in config/database.yml. | ||
| To resolve this error: | ||
| - Did you not create the database, or did you delete it? To create the database, run: | ||
| - Create the database by running: | ||
| bin/rails db:create | ||
| - Has the database name changed? Verify that config/database.yml contains the correct database name. | ||
| - Verify that config/database.yml contains the correct database name. | ||
| MSG | ||
@@ -450,3 +457,3 @@ end | ||
| # Raised when an error occurred while doing a mass assignment to an attribute through the | ||
| # {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=] method. | ||
| # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. | ||
| # The exception has an +attribute+ property that is the name of the offending attribute. | ||
@@ -464,3 +471,3 @@ class AttributeAssignmentError < ActiveRecordError | ||
| # Raised when there are multiple errors while doing a mass assignment through the | ||
| # {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=] | ||
| # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] | ||
| # method. The exception has an +errors+ property that contains an array of AttributeAssignmentError | ||
@@ -498,2 +505,3 @@ # objects, each corresponding to the error while assigning to an attribute. | ||
| # relation = Task.all | ||
| # relation.load | ||
| # relation.loaded? # => true | ||
@@ -561,2 +569,7 @@ # | ||
| # MissingRequiredOrderError is raised when a relation requires ordering but | ||
| # lacks any +order+ values in scope or any model order columns to use. | ||
| class MissingRequiredOrderError < ActiveRecordError | ||
| end | ||
| # IrreversibleOrderError is raised when a relation's order is too complex for | ||
@@ -619,4 +632,7 @@ # +reverse_order+ to automatically reverse. | ||
| end | ||
| class DeprecatedAssociationError < ActiveRecordError | ||
| end | ||
| end | ||
| require "active_record/associations/errors" |
| # frozen_string_literal: true | ||
| require "active_support/core_ext/module/delegation" | ||
@@ -5,0 +4,0 @@ module ActiveRecord |
@@ -245,6 +245,6 @@ # frozen_string_literal: true | ||
| # | ||
| # george: # generated id: 503576764 | ||
| # george: # generated id: 380982691 | ||
| # name: George the Monkey | ||
| # | ||
| # reginald: # generated id: 324201669 | ||
| # reginald: # generated id: 41001176 | ||
| # name: Reginald the Pirate | ||
@@ -251,0 +251,0 @@ # |
@@ -108,3 +108,2 @@ # frozen_string_literal: true | ||
| return unless @mutex.try_lock | ||
| previous_instrumenter = ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] | ||
| begin | ||
@@ -118,3 +117,2 @@ if pending? | ||
| ensure | ||
| ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = previous_instrumenter | ||
| @mutex.unlock | ||
@@ -121,0 +119,0 @@ end |
@@ -11,5 +11,5 @@ # frozen_string_literal: true | ||
| MAJOR = 8 | ||
| MINOR = 0 | ||
| TINY = 5 | ||
| PRE = nil | ||
| MINOR = 1 | ||
| TINY = 0 | ||
| PRE = "beta1" | ||
@@ -16,0 +16,0 @@ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") |
@@ -100,3 +100,3 @@ # frozen_string_literal: true | ||
| # | ||
| # Consider the following behaviour: | ||
| # Consider the following behavior: | ||
| # | ||
@@ -103,0 +103,0 @@ # class ApplicationRecord < ActiveRecord::Base |
@@ -14,3 +14,3 @@ # frozen_string_literal: true | ||
| new(relation, c, ...).execute | ||
| end | ||
| end.tap { relation.reset } | ||
| end | ||
@@ -43,3 +43,3 @@ end | ||
| @unique_by = find_unique_index_for(@unique_by) if @on_duplicate != :raise | ||
| @unique_by = find_unique_index_for(@unique_by) | ||
@@ -53,3 +53,3 @@ configure_on_duplicate_update_logic | ||
| message = +"#{model.name} " | ||
| message = +"#{model} " | ||
| message << "Bulk " if inserts.many? | ||
@@ -231,3 +231,3 @@ message << (on_duplicate == :update ? "Upsert" : "Insert") | ||
| delegate :skip_duplicates?, :update_duplicates?, :keys, :keys_including_timestamps, :record_timestamps?, to: :insert_all | ||
| delegate :skip_duplicates?, :update_duplicates?, :keys, :keys_including_timestamps, :record_timestamps?, :primary_keys, to: :insert_all | ||
@@ -243,7 +243,12 @@ def initialize(insert_all) | ||
| def values_list | ||
| types = extract_types_from_columns_on(model.table_name, keys: keys_including_timestamps) | ||
| types = extract_types_for(keys_including_timestamps) | ||
| values_list = insert_all.map_key_with_value do |key, value| | ||
| next value if Arel::Nodes::SqlLiteral === value | ||
| ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value)) | ||
| if Arel::Nodes::SqlLiteral === value | ||
| value | ||
| elsif primary_keys.include?(key) && value.nil? | ||
| connection.default_insert_value(model.columns_hash[key]) | ||
| else | ||
| ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value)) | ||
| end | ||
| end | ||
@@ -311,4 +316,4 @@ | ||
| def extract_types_from_columns_on(table_name, keys:) | ||
| columns = @model.schema_cache.columns_hash(table_name) | ||
| def extract_types_for(keys) | ||
| columns = @model.columns_hash | ||
@@ -315,0 +320,0 @@ unknown_column = (keys - columns.keys).first |
@@ -104,2 +104,9 @@ # frozen_string_literal: true | ||
| if self[locking_column].nil? | ||
| raise(<<-MSG.squish) | ||
| For optimistic locking, locking_column ('#{locking_column}') can't be nil. | ||
| Are you missing a default value or validation on '#{locking_column}'? | ||
| MSG | ||
| end | ||
| self[locking_column] += 1 | ||
@@ -106,0 +113,0 @@ |
@@ -70,2 +70,6 @@ # frozen_string_literal: true | ||
| def lock!(lock = true) | ||
| if self.class.current_preventing_writes | ||
| raise ActiveRecord::ReadOnlyError, "Lock query attempted while in readonly mode" | ||
| end | ||
| if persisted? | ||
@@ -83,2 +87,3 @@ if has_changes_to_save? | ||
| end | ||
| self | ||
@@ -85,0 +90,0 @@ end |
@@ -130,7 +130,3 @@ # frozen_string_literal: true | ||
| def query_source_location | ||
| Thread.each_caller_location do |location| | ||
| frame = backtrace_cleaner.clean_frame(location) | ||
| return frame if frame | ||
| end | ||
| nil | ||
| backtrace_cleaner.first_clean_frame | ||
| end | ||
@@ -137,0 +133,0 @@ |
@@ -12,21 +12,36 @@ # frozen_string_literal: true | ||
| # | ||
| # The ShardSelector takes a set of options (currently only +lock+ is supported) | ||
| # that can be used by the middleware to alter behavior. +lock+ is | ||
| # true by default and will prohibit the request from switching shards once | ||
| # inside the block. If +lock+ is false, then shard swapping will be allowed. | ||
| # For tenant based sharding, +lock+ should always be true to prevent application | ||
| # code from mistakenly switching between tenants. | ||
| # == Setup | ||
| # | ||
| # Options can be set in the config: | ||
| # Applications must provide a resolver that will provide application-specific logic for | ||
| # selecting the appropriate shard. Setting +config.active_record.shard_resolver+ will cause | ||
| # Rails to add ShardSelector to the default middleware stack. | ||
| # | ||
| # config.active_record.shard_selector = { lock: true } | ||
| # The resolver, along with any configuration options, can be set in the application | ||
| # configuration using an initializer like so: | ||
| # | ||
| # Applications must also provide the code for the resolver as it depends on application | ||
| # specific models. An example resolver would look like this: | ||
| # Rails.application.configure do | ||
| # config.active_record.shard_selector = { lock: false, class_name: "AnimalsRecord" } | ||
| # config.active_record.shard_resolver = ->(request) { | ||
| # subdomain = request.subdomain | ||
| # tenant = Tenant.find_by_subdomain!(subdomain) | ||
| # tenant.shard | ||
| # } | ||
| # end | ||
| # | ||
| # config.active_record.shard_resolver = ->(request) { | ||
| # subdomain = request.subdomain | ||
| # tenant = Tenant.find_by_subdomain!(subdomain) | ||
| # tenant.shard | ||
| # } | ||
| # == Configuration | ||
| # | ||
| # The behavior of ShardSelector can be altered through some configuration options. | ||
| # | ||
| # [+lock:+] | ||
| # +lock+ is true by default and will prohibit the request from switching shards once inside | ||
| # the block. If +lock+ is false, then shard switching will be allowed. For tenant based | ||
| # sharding, +lock+ should always be true to prevent application code from mistakenly switching | ||
| # between tenants. | ||
| # | ||
| # [+class_name:+] | ||
| # +class_name+ is the name of the abstract connection class to switch. By | ||
| # default, the ShardSelector will use ActiveRecord::Base, but if the | ||
| # application has multiple databases, then this option should be set to | ||
| # the name of the sharded database's abstract connection class. | ||
| # | ||
| class ShardSelector | ||
@@ -57,4 +72,6 @@ def initialize(app, resolver, options = {}) | ||
| def set_shard(shard, &block) | ||
| ActiveRecord::Base.connected_to(shard: shard.to_sym) do | ||
| ActiveRecord::Base.prohibit_shard_swapping(options.fetch(:lock, true), &block) | ||
| klass = options[:class_name]&.constantize || ActiveRecord::Base | ||
| klass.connected_to(shard: shard.to_sym) do | ||
| klass.prohibit_shard_swapping(options.fetch(:lock, true), &block) | ||
| end | ||
@@ -61,0 +78,0 @@ end |
@@ -21,3 +21,3 @@ # frozen_string_literal: true | ||
| # | ||
| # class IrreversibleMigrationExample < ActiveRecord::Migration[8.0] | ||
| # class IrreversibleMigrationExample < ActiveRecord::Migration[8.1] | ||
| # def change | ||
@@ -40,3 +40,3 @@ # create_table :distributors do |t| | ||
| # | ||
| # class ReversibleMigrationExample < ActiveRecord::Migration[8.0] | ||
| # class ReversibleMigrationExample < ActiveRecord::Migration[8.1] | ||
| # def up | ||
@@ -66,3 +66,3 @@ # create_table :distributors do |t| | ||
| # | ||
| # class ReversibleMigrationExample < ActiveRecord::Migration[8.0] | ||
| # class ReversibleMigrationExample < ActiveRecord::Migration[8.1] | ||
| # def change | ||
@@ -252,3 +252,3 @@ # create_table :distributors do |t| | ||
| # | ||
| # class AddSsl < ActiveRecord::Migration[8.0] | ||
| # class AddSsl < ActiveRecord::Migration[8.1] | ||
| # def up | ||
@@ -273,3 +273,3 @@ # add_column :accounts, :ssl_enabled, :boolean, default: true | ||
| # | ||
| # class AddSystemSettings < ActiveRecord::Migration[8.0] | ||
| # class AddSystemSettings < ActiveRecord::Migration[8.1] | ||
| # def up | ||
@@ -403,3 +403,3 @@ # create_table :system_settings do |t| | ||
| # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this: | ||
| # class AddFieldnameToTablename < ActiveRecord::Migration[8.0] | ||
| # class AddFieldnameToTablename < ActiveRecord::Migration[8.1] | ||
| # def change | ||
@@ -430,3 +430,3 @@ # add_column :tablenames, :fieldname, :string | ||
| # | ||
| # class RemoveEmptyTags < ActiveRecord::Migration[8.0] | ||
| # class RemoveEmptyTags < ActiveRecord::Migration[8.1] | ||
| # def up | ||
@@ -444,3 +444,3 @@ # Tag.all.each { |tag| tag.destroy if tag.pages.empty? } | ||
| # | ||
| # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[8.0] | ||
| # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[8.1] | ||
| # def up | ||
@@ -459,3 +459,3 @@ # remove_column :items, :incomplete_items_count | ||
| # | ||
| # class MakeJoinUnique < ActiveRecord::Migration[8.0] | ||
| # class MakeJoinUnique < ActiveRecord::Migration[8.1] | ||
| # def up | ||
@@ -477,3 +477,3 @@ # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" | ||
| # | ||
| # class AddPeopleSalary < ActiveRecord::Migration[8.0] | ||
| # class AddPeopleSalary < ActiveRecord::Migration[8.1] | ||
| # def up | ||
@@ -540,3 +540,3 @@ # add_column :people, :salary, :integer | ||
| # | ||
| # class TenderloveMigration < ActiveRecord::Migration[8.0] | ||
| # class TenderloveMigration < ActiveRecord::Migration[8.1] | ||
| # def change | ||
@@ -571,3 +571,3 @@ # create_table(:horses) do |t| | ||
| # | ||
| # class ChangeEnum < ActiveRecord::Migration[8.0] | ||
| # class ChangeEnum < ActiveRecord::Migration[8.1] | ||
| # disable_ddl_transaction! | ||
@@ -585,2 +585,3 @@ # | ||
| autoload :Compatibility, "active_record/migration/compatibility" | ||
| autoload :DefaultSchemaVersionsFormatter, "active_record/migration/default_schema_versions_formatter" | ||
| autoload :JoinTable, "active_record/migration/join_table" | ||
@@ -797,2 +798,7 @@ autoload :ExecutionStrategy, "active_record/migration/execution_strategy" | ||
| end | ||
| def respond_to_missing?(method, include_private = false) | ||
| return false if nearest_delegate == delegate | ||
| nearest_delegate.respond_to?(method, include_private) | ||
| end | ||
| end | ||
@@ -835,3 +841,3 @@ | ||
| # | ||
| # class FixTLMigration < ActiveRecord::Migration[8.0] | ||
| # class FixTLMigration < ActiveRecord::Migration[8.1] | ||
| # def change | ||
@@ -855,3 +861,3 @@ # revert do | ||
| # | ||
| # class FixupTLMigration < ActiveRecord::Migration[8.0] | ||
| # class FixupTLMigration < ActiveRecord::Migration[8.1] | ||
| # def change | ||
@@ -907,3 +913,3 @@ # revert TenderloveMigration | ||
| # | ||
| # class SplitNameMigration < ActiveRecord::Migration[8.0] | ||
| # class SplitNameMigration < ActiveRecord::Migration[8.1] | ||
| # def change | ||
@@ -936,3 +942,3 @@ # add_column :users, :first_name, :string | ||
| # | ||
| # class AddPublishedToPosts < ActiveRecord::Migration[8.0] | ||
| # class AddPublishedToPosts < ActiveRecord::Migration[8.1] | ||
| # def change | ||
@@ -1190,2 +1196,6 @@ # add_column :posts, :published, :boolean, default: false | ||
| end | ||
| def respond_to_missing?(method, include_private = false) | ||
| execution_strategy.respond_to?(method, include_private) || super | ||
| end | ||
| end | ||
@@ -1192,0 +1202,0 @@ |
@@ -47,2 +47,4 @@ # frozen_string_literal: true | ||
| # * rename_table | ||
| # * enable_index | ||
| # * disable_index | ||
| class CommandRecorder | ||
@@ -62,3 +64,4 @@ ReversibleAndIrreversibleMethods = [ | ||
| :create_schema, :drop_schema, | ||
| :create_virtual_table, :drop_virtual_table | ||
| :create_virtual_table, :drop_virtual_table, | ||
| :enable_index, :disable_index | ||
| ] | ||
@@ -144,3 +147,3 @@ include JoinTable | ||
| commands = recorder.commands | ||
| @commands << [:change_table, [table_name], -> t { bulk_change_table(t.name, commands.reverse) }] | ||
| @commands << [:change_table, [table_name], -> t { bulk_change_table(table_name, commands) }] | ||
| else | ||
@@ -189,2 +192,12 @@ yield delegate.update_table_definition(table_name, self) | ||
| def invert_enable_index(args) | ||
| table_name, index_name = args | ||
| [:disable_index, [table_name, index_name]] | ||
| end | ||
| def invert_disable_index(args) | ||
| table_name, index_name = args | ||
| [:enable_index, [table_name, index_name]] | ||
| end | ||
| def invert_transaction(args, &block) | ||
@@ -191,0 +204,0 @@ sub_recorder = CommandRecorder.new(delegate) |
@@ -24,3 +24,3 @@ # frozen_string_literal: true | ||
| # There are classes for each prior Rails version. Each class descends from the *next* Rails version, so: | ||
| # 5.2 < 6.0 < 6.1 < 7.0 < 7.1 < 7.2 < 8.0 | ||
| # 5.2 < 6.0 < 6.1 < 7.0 < 7.1 < 7.2 < 8.0 < 8.1 | ||
| # | ||
@@ -33,4 +33,28 @@ # If you are introducing new migration functionality that should only apply from Rails 7 onward, then you should | ||
| # for migrations written for 5.2, but will for migrations written for 6.0. | ||
| V8_0 = Current | ||
| V8_1 = Current | ||
| class V8_0 < V8_1 | ||
| module RemoveForeignKeyColumnMatch | ||
| def remove_foreign_key(from_table, to_table = nil, **options) | ||
| options[:_skip_column_match] = true | ||
| super | ||
| end | ||
| end | ||
| module TableDefinition | ||
| def remove_foreign_key(to_table = nil, **options) | ||
| options[:_skip_column_match] = true | ||
| super | ||
| end | ||
| end | ||
| include RemoveForeignKeyColumnMatch | ||
| private | ||
| def compatible_table_definition(t) | ||
| t.singleton_class.prepend(TableDefinition) | ||
| super | ||
| end | ||
| end | ||
| class V7_2 < V8_0 | ||
@@ -159,5 +183,3 @@ end | ||
| def compatible_table_definition(t) | ||
| class << t | ||
| prepend TableDefinition | ||
| end | ||
| t.singleton_class.prepend(TableDefinition) | ||
| super | ||
@@ -223,5 +245,3 @@ end | ||
| def compatible_table_definition(t) | ||
| class << t | ||
| prepend TableDefinition | ||
| end | ||
| t.singleton_class.prepend(TableDefinition) | ||
| super | ||
@@ -267,5 +287,3 @@ end | ||
| def compatible_table_definition(t) | ||
| class << t | ||
| prepend TableDefinition | ||
| end | ||
| t.singleton_class.prepend(TableDefinition) | ||
| super | ||
@@ -316,5 +334,3 @@ end | ||
| def compatible_table_definition(t) | ||
| class << t | ||
| prepend TableDefinition | ||
| end | ||
| t.singleton_class.prepend(TableDefinition) | ||
| super | ||
@@ -325,5 +341,3 @@ end | ||
| recorder = super | ||
| class << recorder | ||
| prepend CommandRecorder | ||
| end | ||
| recorder.singleton_class.prepend(CommandRecorder) | ||
| recorder | ||
@@ -416,5 +430,3 @@ end | ||
| def compatible_table_definition(t) | ||
| class << t | ||
| prepend TableDefinition | ||
| end | ||
| t.singleton_class.prepend(TableDefinition) | ||
| super | ||
@@ -453,3 +465,3 @@ end | ||
| def index_exists?(table_name, column_name, **options) | ||
| def index_exists?(table_name, column_name = nil, **options) | ||
| column_names = Array(column_name).map(&:to_s) | ||
@@ -472,5 +484,3 @@ options[:name] = | ||
| def compatible_table_definition(t) | ||
| class << t | ||
| prepend TableDefinition | ||
| end | ||
| t.singleton_class.prepend(TableDefinition) | ||
| super | ||
@@ -477,0 +487,0 @@ end |
@@ -116,3 +116,3 @@ # frozen_string_literal: true | ||
| # | ||
| # The name of the column records are ordered by if no explicit order clause | ||
| # The name of the column(s) records are ordered by if no explicit order clause | ||
| # is used during an ordered finder call. If not set the primary key is used. | ||
@@ -124,6 +124,8 @@ | ||
| # | ||
| # Sets the column to sort records by when no explicit order clause is used | ||
| # during an ordered finder call. Useful when the primary key is not an | ||
| # auto-incrementing integer, for example when it's a UUID. Records are subsorted | ||
| # by the primary key if it exists to ensure deterministic results. | ||
| # Sets the column(s) to sort records by when no explicit order clause is used | ||
| # during an ordered finder call. Useful for models where the primary key isn't an | ||
| # auto-incrementing integer (such as UUID). | ||
| # | ||
| # By default, records are subsorted by primary key to ensure deterministic results. | ||
| # To disable this subsort behavior, set `implicit_order_column` to `["column_name", nil]`. | ||
@@ -506,3 +508,3 @@ ## | ||
| # | ||
| # class CreateJobLevels < ActiveRecord::Migration[8.0] | ||
| # class CreateJobLevels < ActiveRecord::Migration[8.1] | ||
| # def up | ||
@@ -626,3 +628,4 @@ # create_table :job_levels do |t| | ||
| def type_for_column(connection, column) | ||
| type = connection.lookup_cast_type_from_column(column) | ||
| # TODO: Remove the need for a connection after we release 8.1. | ||
| type = column.fetch_cast_type(connection) | ||
@@ -629,0 +632,0 @@ if immutable_strings_by_default && type.respond_to?(:to_immutable_string) |
@@ -390,2 +390,4 @@ # frozen_string_literal: true | ||
| def #{association_name}_attributes=(attributes) | ||
| association = association(:#{association_name}) | ||
| deprecated_associations_api_guard(association, __method__) | ||
| assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) | ||
@@ -392,0 +394,0 @@ end |
@@ -495,2 +495,3 @@ # frozen_string_literal: true | ||
| becoming.instance_variable_set(:@new_record, new_record?) | ||
| becoming.instance_variable_set(:@previously_new_record, previously_new_record?) | ||
| becoming.instance_variable_set(:@destroyed, destroyed?) | ||
@@ -585,4 +586,4 @@ becoming.errors.copy!(errors) | ||
| # Equivalent to <code>update_columns(name => value)</code>. | ||
| def update_column(name, value) | ||
| update_columns(name => value) | ||
| def update_column(name, value, touch: nil) | ||
| update_columns(name => value, touch: touch) | ||
| end | ||
@@ -601,3 +602,3 @@ | ||
| # * \Callbacks are skipped. | ||
| # * +updated_at+/+updated_on+ are not updated. | ||
| # * +updated_at+/+updated_on+ are updated if the +touch+ option is set to +true+. | ||
| # * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all | ||
@@ -607,2 +608,16 @@ # | ||
| # objects, or when at least one of the attributes is marked as readonly. | ||
| # | ||
| # ==== Parameters | ||
| # | ||
| # * <tt>:touch</tt> option - Touch the timestamp columns when updating. | ||
| # * If attribute names are passed, they are updated along with +updated_at+/+updated_on+ attributes. | ||
| # | ||
| # ==== Examples | ||
| # | ||
| # # Update a single attribute. | ||
| # user.update_columns(last_request_at: Time.current) | ||
| # | ||
| # # Update with touch option. | ||
| # user.update_columns(last_request_at: Time.current, touch: true) | ||
| def update_columns(attributes) | ||
@@ -619,2 +634,11 @@ raise ActiveRecordError, "cannot update a new record" if new_record? | ||
| touch = attributes.delete("touch") | ||
| if touch | ||
| names = touch if touch != true | ||
| names = Array.wrap(names) | ||
| options = names.extract_options! | ||
| touch_updates = self.class.touch_attributes_with_time(*names, **options) | ||
| attributes.with_defaults!(touch_updates) unless touch_updates.empty? | ||
| end | ||
| update_constraints = _query_constraints_hash | ||
@@ -648,4 +672,11 @@ attributes = attributes.each_with_object({}) do |(k, v), h| | ||
| # +update_counters+, see that for more. | ||
| # | ||
| # This method raises an ActiveRecord::ActiveRecordError when called on new | ||
| # objects, or when at least one of the attributes is marked as readonly. | ||
| # | ||
| # Returns +self+. | ||
| def increment!(attribute, by = 1, touch: nil) | ||
| raise ActiveRecordError, "cannot update a new record" if new_record? | ||
| raise ActiveRecordError, "cannot update a destroyed record" if destroyed? | ||
| increment(attribute, by) | ||
@@ -652,0 +683,0 @@ change = public_send(attribute) - (public_send(:"#{attribute}_in_database") || 0) |
@@ -6,2 +6,3 @@ # frozen_string_literal: true | ||
| class QueryCache | ||
| # ActiveRecord::Base extends this module, so these methods are available in models. | ||
| module ClassMethods | ||
@@ -24,7 +25,11 @@ # Enable the query cache within the block if Active Record is configured. | ||
| # Disable the query cache within the block if Active Record is configured. | ||
| # If it's not, it will execute the given block. | ||
| # Runs the block with the query cache disabled. | ||
| # | ||
| # Set <tt>dirties: false</tt> to prevent query caches on all connections from being cleared by write operations. | ||
| # (By default, write operations dirty all connections' query caches in case they are replicas whose cache would now be outdated.) | ||
| # If the query cache was enabled before the block was executed, it is | ||
| # enabled again after it. | ||
| # | ||
| # Set <tt>dirties: false</tt> to prevent query caches on all connections | ||
| # from being cleared by write operations. (By default, write operations | ||
| # dirty all connections' query caches in case they are replicas whose | ||
| # cache would now be outdated.) | ||
| def uncached(dirties: true, &block) | ||
@@ -39,20 +44,22 @@ if connected? || !configurations.empty? | ||
| def self.run | ||
| ActiveRecord::Base.connection_handler.each_connection_pool.reject(&:query_cache_enabled).each do |pool| | ||
| next if pool.db_config&.query_cache == false | ||
| pool.enable_query_cache! | ||
| module ExecutorHooks # :nodoc: | ||
| def self.run | ||
| ActiveRecord::Base.connection_handler.each_connection_pool.reject(&:query_cache_enabled).each do |pool| | ||
| next if pool.db_config&.query_cache == false | ||
| pool.enable_query_cache! | ||
| end | ||
| end | ||
| end | ||
| def self.complete(pools) | ||
| pools.each do |pool| | ||
| pool.disable_query_cache! | ||
| pool.clear_query_cache | ||
| def self.complete(pools) | ||
| pools.each do |pool| | ||
| pool.disable_query_cache! | ||
| pool.clear_query_cache | ||
| end | ||
| end | ||
| end | ||
| def self.install_executor_hooks(executor = ActiveSupport::Executor) | ||
| executor.register_hook(self) | ||
| def self.install_executor_hooks(executor = ActiveSupport::Executor) # :nodoc: | ||
| executor.register_hook(ExecutorHooks) | ||
| end | ||
| end | ||
| end |
@@ -72,3 +72,3 @@ # frozen_string_literal: true | ||
| # | ||
| # ActiveRecord::QueryLogs.prepend_comment = true | ||
| # config.active_record.query_log_tags_prepend_comment = true | ||
| # | ||
@@ -161,7 +161,3 @@ # For applications where the content will not change during the lifetime of | ||
| def query_source_location # :nodoc: | ||
| Thread.each_caller_location do |location| | ||
| frame = LogSubscriber.backtrace_cleaner.clean_frame(location) | ||
| return frame if frame | ||
| end | ||
| nil | ||
| LogSubscriber.backtrace_cleaner.first_clean_frame | ||
| end | ||
@@ -220,3 +216,3 @@ | ||
| def escape_sql_comment(content) | ||
| # Sanitize a string to appear within a SQL comment | ||
| # Sanitize a string to appear within an SQL comment | ||
| # For compatibility, this also surrounding "/*+", "/*", and "*/" | ||
@@ -223,0 +219,0 @@ # characters, possibly with single surrounding space. |
@@ -38,5 +38,8 @@ # frozen_string_literal: true | ||
| config.active_record.cache_query_log_tags = false | ||
| config.active_record.query_log_tags_prepend_comment = false | ||
| config.active_record.raise_on_assign_to_attr_readonly = false | ||
| config.active_record.belongs_to_required_validates_foreign_key = true | ||
| config.active_record.generate_secure_token_on = :create | ||
| config.active_record.use_legacy_signed_id_verifier = :generate_and_verify | ||
| config.active_record.deprecated_associations_options = { mode: :warn, backtrace: false } | ||
@@ -233,2 +236,3 @@ config.active_record.queues = ActiveSupport::InheritableOptions.new | ||
| :cache_query_log_tags, | ||
| :query_log_tags_prepend_comment, | ||
| :sqlite3_adapter_strict_strings_by_default, | ||
@@ -238,2 +242,3 @@ :check_schema_cache_dump_version, | ||
| :postgresql_adapter_decode_dates, | ||
| :use_legacy_signed_id_verifier, | ||
| ) | ||
@@ -280,2 +285,9 @@ | ||
| initializer "active_record.job_checkpoints" do | ||
| require "active_record/railties/job_checkpoints" | ||
| ActiveSupport.on_load(:active_job_continuable) do | ||
| prepend ActiveRecord::Railties::JobCheckpoints | ||
| end | ||
| end | ||
| initializer "active_record.set_reloader_hooks" do | ||
@@ -326,5 +338,18 @@ ActiveSupport.on_load(:active_record) do | ||
| initializer "active_record.set_signed_id_verifier_secret" do | ||
| ActiveSupport.on_load(:active_record) do | ||
| self.signed_id_verifier_secret ||= -> { Rails.application.key_generator.generate_key("active_record/signed_id") } | ||
| initializer "active_record.filter_attributes_as_log_parameters" do |app| | ||
| ActiveRecord::FilterAttributeHandler.new(app).enable | ||
| end | ||
| initializer "active_record.configure_message_verifiers" do |app| | ||
| ActiveRecord.message_verifiers = app.message_verifiers | ||
| use_legacy_signed_id_verifier = app.config.active_record.use_legacy_signed_id_verifier | ||
| legacy_options = { digest: "SHA256", serializer: JSON, url_safe: true } | ||
| if use_legacy_signed_id_verifier == :generate_and_verify | ||
| app.message_verifiers.prepend { |salt| legacy_options if salt == "active_record/signed_id" } | ||
| elsif use_legacy_signed_id_verifier == :verify | ||
| app.message_verifiers.rotate { |salt| legacy_options if salt == "active_record/signed_id" } | ||
| elsif use_legacy_signed_id_verifier | ||
| raise ArgumentError, "Unrecognized value for config.active_record.use_legacy_signed_id_verifier: #{use_legacy_signed_id_verifier.inspect}" | ||
| end | ||
@@ -395,2 +420,6 @@ end | ||
| end | ||
| if app.config.active_record.query_log_tags_prepend_comment | ||
| ActiveRecord::QueryLogs.prepend_comment = true | ||
| end | ||
| end | ||
@@ -397,0 +426,0 @@ end |
@@ -166,2 +166,14 @@ # frozen_string_literal: true | ||
| namespace :reset do | ||
| ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |name| | ||
| desc "Drop and recreate the #{name} database using migrations" | ||
| task name => :load_config do | ||
| db_namespace["drop:#{name}"].invoke | ||
| db_namespace["create:#{name}"].invoke | ||
| db_namespace["schema:dump:#{name}"].invoke | ||
| db_namespace["migrate:#{name}"].invoke | ||
| end | ||
| end | ||
| end | ||
| desc 'Run the "up" for a given migration VERSION.' | ||
@@ -337,3 +349,3 @@ task up: :load_config do | ||
| pending_migrations = pending_migrations.flatten! | ||
| pending_migrations.flatten! | ||
@@ -459,3 +471,3 @@ if pending_migrations.any? | ||
| task load: [:load_config, :check_protected_environments] do | ||
| ActiveRecord::Tasks::DatabaseTasks.load_schema_current(ENV["SCHEMA_FORMAT"], ENV["SCHEMA"]) | ||
| ActiveRecord::Tasks::DatabaseTasks.load_schema_current(nil, ENV["SCHEMA"]) | ||
| end | ||
@@ -465,3 +477,3 @@ | ||
| ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |name| | ||
| desc "Create a database schema file (either db/schema.rb or db/structure.sql, depending on `ENV['SCHEMA_FORMAT']` or `config.active_record.schema_format`) for #{name} database" | ||
| desc "Create a database schema file (either db/schema.rb or db/structure.sql, depending on configuration) for #{name} database" | ||
| task name => :load_config do | ||
@@ -481,3 +493,3 @@ ActiveRecord::Tasks::DatabaseTasks.with_temporary_pool_for_each(name: name) do |pool| | ||
| desc "Load a database schema file (either db/schema.rb or db/structure.sql, depending on configuration) into the #{name} database" | ||
| task name => "db:test:purge:#{name}" do | ||
| task name => [:load_config, :check_protected_environments] do | ||
| ActiveRecord::Tasks::DatabaseTasks.with_temporary_pool_for_each(name: name) do |pool| | ||
@@ -484,0 +496,0 @@ db_config = pool.db_config |
@@ -8,17 +8,16 @@ # frozen_string_literal: true | ||
| module JobRuntime # :nodoc: | ||
| private | ||
| def instrument(operation, payload = {}, &block) | ||
| if operation == :perform && block | ||
| super(operation, payload) do | ||
| db_runtime_before_perform = ActiveRecord::RuntimeRegistry.sql_runtime | ||
| result = block.call | ||
| payload[:db_runtime] = ActiveRecord::RuntimeRegistry.sql_runtime - db_runtime_before_perform | ||
| result | ||
| end | ||
| else | ||
| super | ||
| def instrument(operation, payload = {}, &block) # :nodoc: | ||
| if operation == :perform && block | ||
| super(operation, payload) do | ||
| db_runtime_before_perform = ActiveRecord::RuntimeRegistry.sql_runtime | ||
| result = block.call | ||
| payload[:db_runtime] = ActiveRecord::RuntimeRegistry.sql_runtime - db_runtime_before_perform | ||
| result | ||
| end | ||
| else | ||
| super | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
@@ -428,6 +428,10 @@ # frozen_string_literal: true | ||
| if active_record.name.demodulize == class_name | ||
| return compute_class("::#{class_name}") rescue NameError | ||
| begin | ||
| compute_class("::#{class_name}") | ||
| rescue NameError | ||
| compute_class(class_name) | ||
| end | ||
| else | ||
| compute_class(class_name) | ||
| end | ||
| compute_class(class_name) | ||
| end | ||
@@ -520,2 +524,4 @@ | ||
| super | ||
| @validated = false | ||
| @type = -(options[:foreign_type]&.to_s || "#{options[:as]}_type") if options[:as] | ||
@@ -539,2 +545,4 @@ @foreign_type = -(options[:foreign_type]&.to_s || "#{name}_type") if options[:polymorphic] | ||
| @deprecated = !!options[:deprecated] | ||
| ensure_option_not_given_as_class!(:class_name) | ||
@@ -622,2 +630,4 @@ end | ||
| def check_validity! | ||
| return if @validated | ||
| check_validity_of_inverse! | ||
@@ -632,2 +642,4 @@ | ||
| end | ||
| @validated = true | ||
| end | ||
@@ -750,2 +762,6 @@ | ||
| def deprecated? | ||
| @deprecated | ||
| end | ||
| private | ||
@@ -984,2 +1000,4 @@ # Attempts to find the inverse association name automatically. | ||
| super() | ||
| @validated = false | ||
| @delegate_reflection = delegate_reflection | ||
@@ -1148,2 +1166,4 @@ @klass = delegate_reflection.options[:anonymous_class] | ||
| def check_validity! | ||
| return if @validated | ||
| if through_reflection.nil? | ||
@@ -1186,2 +1206,4 @@ raise HasManyThroughAssociationNotFoundError.new(active_record, self) | ||
| check_validity_of_inverse! | ||
| @validated = true | ||
| end | ||
@@ -1207,2 +1229,6 @@ | ||
| def deprecated_nested_reflections | ||
| @deprecated_nested_reflections ||= collect_deprecated_nested_reflections | ||
| end | ||
| protected | ||
@@ -1232,2 +1258,15 @@ def actual_source_reflection # FIXME: this is a horrible name | ||
| def collect_deprecated_nested_reflections | ||
| result = [] | ||
| [through_reflection, source_reflection].each do |reflection| | ||
| result << reflection if reflection.deprecated? | ||
| # Both the through and the source reflections could be through | ||
| # themselves. Nesting can go an arbitrary number of levels down. | ||
| if reflection.through_reflection? | ||
| result.concat(reflection.deprecated_nested_reflections) | ||
| end | ||
| end | ||
| result | ||
| end | ||
| delegate_methods = AssociationReflection.public_instance_methods - | ||
@@ -1234,0 +1273,0 @@ public_instance_methods |
@@ -63,3 +63,3 @@ # frozen_string_literal: true | ||
| CLAUSE_METHODS = [:where, :having, :from] | ||
| INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :with, :with_recursive] | ||
| INVALID_METHODS_FOR_UPDATE_AND_DELETE_ALL = [:distinct, :with, :with_recursive] | ||
@@ -311,3 +311,3 @@ VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS | ||
| # Like #find_or_create_by, but calls {new}[rdoc-ref:Core.new] | ||
| # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new] | ||
| # instead of {create}[rdoc-ref:Persistence::ClassMethods#create]. | ||
@@ -605,2 +605,14 @@ def find_or_initialize_by(attributes, &block) | ||
| invalid_methods = INVALID_METHODS_FOR_UPDATE_AND_DELETE_ALL.select do |method| | ||
| value = @values[method] | ||
| method == :distinct ? value : value&.any? | ||
| end | ||
| if invalid_methods.any? | ||
| ActiveRecord.deprecator.warn <<~MESSAGE | ||
| `#{invalid_methods.join(', ')}` is not supported by `update_all` and was never included in the generated query. | ||
| Calling `#{invalid_methods.join(', ')}` with `update_all` will raise an error in Rails 8.2. | ||
| MESSAGE | ||
| end | ||
| if updates.is_a?(Hash) | ||
@@ -619,7 +631,5 @@ if model.locking_enabled? && | ||
| model.with_connection do |c| | ||
| arel = eager_loading? ? apply_join_dependency.arel : build_arel(c) | ||
| arel = eager_loading? ? apply_join_dependency.arel : arel() | ||
| arel.source.left = table | ||
| group_values_arel_columns = arel_columns(group_values.uniq) | ||
| having_clause_ast = having_clause.ast unless having_clause.empty? | ||
| key = if model.composite_primary_key? | ||
@@ -630,3 +640,3 @@ primary_key.map { |pk| table[pk] } | ||
| end | ||
| stmt = arel.compile_update(values, key, having_clause_ast, group_values_arel_columns) | ||
| stmt = arel.compile_update(values, key) | ||
| c.update(stmt, "#{model} Update All").tap { reset } | ||
@@ -1018,3 +1028,3 @@ end | ||
| # | ||
| # This call deletes the affected posts all at once with a single DELETE statement. | ||
| # Both calls delete the affected posts all at once with a single DELETE statement. | ||
| # If you need to destroy dependent associations or call your <tt>before_*</tt> or | ||
@@ -1030,3 +1040,3 @@ # +after_destroy+ callbacks, use the #destroy_all method instead. | ||
| invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| | ||
| invalid_methods = INVALID_METHODS_FOR_UPDATE_AND_DELETE_ALL.select do |method| | ||
| value = @values[method] | ||
@@ -1040,7 +1050,5 @@ method == :distinct ? value : value&.any? | ||
| model.with_connection do |c| | ||
| arel = eager_loading? ? apply_join_dependency.arel : build_arel(c) | ||
| arel = eager_loading? ? apply_join_dependency.arel : arel() | ||
| arel.source.left = table | ||
| group_values_arel_columns = arel_columns(group_values.uniq) | ||
| having_clause_ast = having_clause.ast unless having_clause.empty? | ||
| key = if model.composite_primary_key? | ||
@@ -1051,3 +1059,3 @@ primary_key.map { |pk| table[pk] } | ||
| end | ||
| stmt = arel.compile_delete(key, having_clause_ast, group_values_arel_columns) | ||
| stmt = arel.compile_delete(key) | ||
@@ -1417,3 +1425,3 @@ c.delete(stmt, "#{model} Delete All").tap { reset } | ||
| bind = predicate_builder.build_bind_attribute(attribute.name, value.abs) | ||
| expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attribute), 0) | ||
| expr = table.coalesce(attribute, 0) | ||
| expr = value < 0 ? expr - bind : expr + bind | ||
@@ -1424,2 +1432,6 @@ expr.expr | ||
| def exec_queries(&block) | ||
| if lock_value && model.current_preventing_writes | ||
| raise ActiveRecord::ReadOnlyError, "Lock query attempted while in readonly mode" | ||
| end | ||
| skip_query_cache_if_necessary do | ||
@@ -1464,3 +1476,3 @@ rows = if scheduled? | ||
| @_join_dependency = join_dependency | ||
| c.select_all(relation.arel, "SQL", async: async) | ||
| c.select_all(relation.arel, "#{model.name} Eager Load", async: async) | ||
| end | ||
@@ -1467,0 +1479,0 @@ end |
@@ -438,11 +438,23 @@ # frozen_string_literal: true | ||
| values = records.pluck(*cursor) | ||
| yielded_relation = where(cursor => values) | ||
| values_size = values.size | ||
| values_last = values.last | ||
| yielded_relation = where(cursor => values).order(batch_orders.to_h) | ||
| yielded_relation.load_records(records) | ||
| elsif (empty_scope && use_ranges != false) || use_ranges | ||
| values = batch_relation.pluck(*cursor) | ||
| # Efficiently peak at the last value for the next batch using offset and limit. | ||
| values_size = batch_limit | ||
| values_last = batch_relation.offset(batch_limit - 1).pick(*cursor) | ||
| finish = values.last | ||
| if finish | ||
| yielded_relation = apply_finish_limit(batch_relation, cursor, finish, batch_orders) | ||
| yielded_relation = yielded_relation.except(:limit, :order) | ||
| # If the last value is not found using offset, there is at most one more batch of size < batch_limit. | ||
| # Retry by getting the whole list of remaining values so that we have the exact size and last value. | ||
| unless values_last | ||
| values = batch_relation.pluck(*cursor) | ||
| values_size = values.size | ||
| values_last = values.last | ||
| end | ||
| # Finally, build the yielded relation if at least one value found. | ||
| if values_last | ||
| yielded_relation = apply_finish_limit(batch_relation, cursor, values_last, batch_orders) | ||
| yielded_relation = yielded_relation.except(:limit).reorder(batch_orders.to_h) | ||
| yielded_relation.skip_query_cache!(false) | ||
@@ -452,8 +464,10 @@ end | ||
| values = batch_relation.pluck(*cursor) | ||
| yielded_relation = where(cursor => values) | ||
| values_size = values.size | ||
| values_last = values.last | ||
| yielded_relation = where(cursor => values).order(batch_orders.to_h) | ||
| end | ||
| break if values.empty? | ||
| break if values_size == 0 | ||
| if values.flatten.any?(nil) | ||
| if [values_last].flatten.any?(nil) | ||
| raise ArgumentError, "Not all of the batch cursor columns were included in the custom select clause "\ | ||
@@ -465,6 +479,6 @@ "or some columns contain nil." | ||
| break if values.length < batch_limit | ||
| break if values_size < batch_limit | ||
| if limit_value | ||
| remaining -= values.length | ||
| remaining -= values_size | ||
@@ -487,3 +501,3 @@ if remaining == 0 | ||
| cursor_value = values.last | ||
| cursor_value = values_last | ||
| batch_relation = batch_condition(relation, cursor, cursor_value, operators) | ||
@@ -490,0 +504,0 @@ end |
@@ -289,2 +289,7 @@ # frozen_string_literal: true | ||
| # | ||
| # Be aware that #pluck ignores any previous select clauses | ||
| # | ||
| # Person.select(:name).pluck(:id) | ||
| # # SELECT people.id FROM people | ||
| # | ||
| # See also #ids. | ||
@@ -318,3 +323,3 @@ def pluck(*column_names) | ||
| result = skip_query_cache_if_necessary do | ||
| if where_clause.contradiction? | ||
| if where_clause.contradiction? && !possible_aggregation?(column_names) | ||
| ActiveRecord::Result.empty(async: @async) | ||
@@ -422,3 +427,3 @@ else | ||
| else | ||
| arel_column(column_name) | ||
| arel_column(column_name.to_s) | ||
| end | ||
@@ -467,2 +472,12 @@ end | ||
| def possible_aggregation?(column_names) | ||
| column_names.all? do |column_name| | ||
| if column_name.is_a?(String) | ||
| column_name.include?("(") | ||
| else | ||
| Arel.arel_node?(column_name) | ||
| end | ||
| end | ||
| end | ||
| def operation_over_aggregate_column(column, operation, distinct) | ||
@@ -519,3 +534,2 @@ operation == "count" ? column.count(distinct) : column.public_send(operation) | ||
| group_fields = group_values | ||
| group_fields = group_fields.uniq if group_fields.size > 1 | ||
@@ -543,3 +557,3 @@ if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym) | ||
| select_value = operation_over_aggregate_column(column, operation, distinct) | ||
| select_value.as(model.adapter_class.quote_column_name(column_alias)) | ||
| select_value = select_value.as(model.adapter_class.quote_column_name(column_alias)) | ||
@@ -671,2 +685,3 @@ select_values = [select_value] | ||
| relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct | ||
| relation.unscope!(:order) | ||
| else | ||
@@ -680,9 +695,5 @@ column_alias = Arel.sql("count_column") | ||
| if column_name == :all | ||
| relation.unscope(:order).build_subquery(subquery_alias, select_value) | ||
| else | ||
| relation.build_subquery(subquery_alias, select_value) | ||
| end | ||
| relation.build_subquery(subquery_alias, select_value) | ||
| end | ||
| end | ||
| end |
| # frozen_string_literal: true | ||
| require "active_support/core_ext/module/delegation" | ||
@@ -5,0 +4,0 @@ module ActiveRecord |
@@ -144,3 +144,3 @@ # frozen_string_literal: true | ||
| def sole | ||
| found, undesired = first(2) | ||
| found, undesired = take(2) | ||
@@ -152,3 +152,3 @@ if found.nil? | ||
| else | ||
| raise ActiveRecord::SoleRecordExceeded.new(self) | ||
| raise ActiveRecord::SoleRecordExceeded.new(model) | ||
| end | ||
@@ -447,3 +447,3 @@ end | ||
| else | ||
| relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) | ||
| relation = except(:select, :distinct, :order)._select!(Arel.sql(ONE_AS_ONE, retryable: true)).limit!(1) | ||
| end | ||
@@ -645,4 +645,23 @@ | ||
| def ordered_relation | ||
| if order_values.empty? && (model.implicit_order_column || !model.query_constraints_list.nil? || primary_key) | ||
| order(_order_columns.map { |column| table[column].asc }) | ||
| if order_values.empty? | ||
| if !_order_columns.empty? | ||
| return order(_order_columns.map { |column| table[column].asc }) | ||
| end | ||
| if ActiveRecord.raise_on_missing_required_finder_order_columns | ||
| raise MissingRequiredOrderError, <<~MSG.squish | ||
| Relation has no order values, and #{model} has no order columns to use as a default. | ||
| Set at least one of `implicit_order_column`, `query_constraints` or `primary_key` on | ||
| the model when no `order `is specified on the relation. | ||
| MSG | ||
| else | ||
| ActiveRecord.deprecator.warn(<<~MSG) | ||
| Calling order dependent finder methods (e.g. `#first`, `#second`) without `order` values on the relation, | ||
| and on a model (#{model}) that does not have any order columns (`implicit_order_column`, `query_constraints`, | ||
| or `primary_key`) to fall back on is deprecated and will raise `ActiveRecord::MissingRequiredOrderError` | ||
| in Rails 8.2. | ||
| MSG | ||
| self | ||
| end | ||
| else | ||
@@ -654,14 +673,11 @@ self | ||
| def _order_columns | ||
| oc = [] | ||
| columns = Array(model.implicit_order_column) | ||
| oc << model.implicit_order_column if model.implicit_order_column | ||
| oc << model.query_constraints_list if model.query_constraints_list | ||
| return columns.compact if columns.length.positive? && columns.last.nil? | ||
| if model.primary_key && model.query_constraints_list.nil? | ||
| oc << model.primary_key | ||
| end | ||
| columns += Array(model.query_constraints_list || model.primary_key) | ||
| oc.flatten.uniq.compact | ||
| columns.uniq.compact | ||
| end | ||
| end | ||
| end |
@@ -88,5 +88,5 @@ # frozen_string_literal: true | ||
| if other.model == relation.model | ||
| relation.select_values |= other.select_values | ||
| relation.select_values += other.select_values if relation.select_values != other.select_values | ||
| else | ||
| relation.select_values |= other.instance_eval do | ||
| relation.select_values += other.instance_eval do | ||
| arel_columns(select_values) | ||
@@ -93,0 +93,0 @@ end |
@@ -40,3 +40,3 @@ # frozen_string_literal: true | ||
| # The handler can be any object that responds to +call+, and will be used | ||
| # for any value that +===+ the class given. For example: | ||
| # for any value that <tt>===</tt> the class given. For example: | ||
| # | ||
@@ -86,3 +86,3 @@ # MyCustomDateRange = Struct.new(:start, :end) | ||
| def expand_from_hash(attributes, &block) | ||
| return ["1=0"] if attributes.empty? | ||
| return [Arel.sql("1=0", retryable: true)] if attributes.empty? | ||
@@ -89,0 +89,0 @@ attributes.flat_map do |key, value| |
@@ -34,5 +34,3 @@ # frozen_string_literal: true | ||
| array_predicates = ranges.map! { |range| predicate_builder.build(attribute, range) } | ||
| values_predicate.or( | ||
| Arel::Nodes::Grouping.new Arel::Nodes::Or.new(array_predicates) | ||
| ) | ||
| array_predicates.inject(values_predicate, &:or) | ||
| end | ||
@@ -39,0 +37,0 @@ end |
@@ -18,3 +18,5 @@ # frozen_string_literal: true | ||
| elsif @type.mutable? # If the type is simply mutable, we deep_dup it. | ||
| @value_before_type_cast = @value_before_type_cast.deep_dup | ||
| unless @value_before_type_cast.frozen? | ||
| @value_before_type_cast = @value_before_type_cast.deep_dup | ||
| end | ||
| end | ||
@@ -21,0 +23,0 @@ end |
@@ -179,4 +179,2 @@ # frozen_string_literal: true | ||
| def except_predicates(columns) | ||
| return predicates if columns.empty? | ||
| attrs = columns.extract! { |node| node.is_a?(Arel::Attribute) } | ||
@@ -186,3 +184,3 @@ non_attrs = columns.extract! { |node| node.is_a?(Arel::Predications) } | ||
| predicates.reject do |node| | ||
| if !non_attrs.empty? && equality_node?(node) && node.left.is_a?(Arel::Predications) | ||
| if !non_attrs.empty? && node.equality? && node.left.is_a?(Arel::Predications) | ||
| non_attrs.include?(node.left) | ||
@@ -199,3 +197,3 @@ end || Arel.fetch_attribute(node) do |attr| | ||
| when Arel::Nodes::SqlLiteral, ::String | ||
| wrap_sql_literal(node) | ||
| Arel::Nodes::Grouping.new(node) | ||
| else node | ||
@@ -211,9 +209,2 @@ end | ||
| def wrap_sql_literal(node) | ||
| if ::String === node | ||
| node = Arel.sql(node) | ||
| end | ||
| Arel::Nodes::Grouping.new(node) | ||
| end | ||
| def extract_node_value(node) | ||
@@ -220,0 +211,0 @@ if node.respond_to?(:value_before_type_cast) |
@@ -32,2 +32,7 @@ # frozen_string_literal: true | ||
| # | ||
| # # Get the number of rows affected by the query: | ||
| # result = ActiveRecord::Base.lease_connection.exec_query('INSERT INTO posts (title, body) VALUES ("title_3", "body_3"), ("title_4", "body_4")') | ||
| # result.affected_rows | ||
| # # => 2 | ||
| # | ||
| # # ActiveRecord::Result also includes Enumerable. | ||
@@ -93,13 +98,13 @@ # result.each do |row| | ||
| attr_reader :columns, :rows, :column_types | ||
| attr_reader :columns, :rows, :affected_rows | ||
| def self.empty(async: false) # :nodoc: | ||
| def self.empty(async: false, affected_rows: nil) # :nodoc: | ||
| if async | ||
| EMPTY_ASYNC | ||
| FutureResult.wrap(new(EMPTY_ARRAY, EMPTY_ARRAY, EMPTY_HASH, affected_rows: affected_rows)).freeze | ||
| else | ||
| EMPTY | ||
| new(EMPTY_ARRAY, EMPTY_ARRAY, EMPTY_HASH, affected_rows: affected_rows).freeze | ||
| end | ||
| end | ||
| def initialize(columns, rows, column_types = nil) | ||
| def initialize(columns, rows, column_types = nil, affected_rows: nil) | ||
| # We freeze the strings to prevent them getting duped when | ||
@@ -110,4 +115,6 @@ # used as keys in ActiveRecord::Base's @attributes hash | ||
| @hash_rows = nil | ||
| @column_types = column_types || EMPTY_HASH | ||
| @column_types = column_types.freeze | ||
| @types_hash = nil | ||
| @column_indexes = nil | ||
| @affected_rows = affected_rows | ||
| end | ||
@@ -160,2 +167,20 @@ | ||
| # Returns the +ActiveRecord::Type+ type of all columns. | ||
| # Note that not all database adapters return the result types, | ||
| # so the hash may be empty. | ||
| def column_types | ||
| if @column_types | ||
| @types_hash ||= begin | ||
| types = {} | ||
| @columns.each_with_index do |name, index| | ||
| type = @column_types[index] || Type.default_value | ||
| types[name] = types[index] = type | ||
| end | ||
| types.freeze | ||
| end | ||
| else | ||
| EMPTY_HASH | ||
| end | ||
| end | ||
| def result # :nodoc: | ||
@@ -169,3 +194,3 @@ self | ||
| def cast_values(type_overrides = {}) # :nodoc: | ||
| def cast_values(type_overrides = nil) # :nodoc: | ||
| if columns.one? | ||
@@ -198,3 +223,2 @@ # Separated to avoid allocating an array per row | ||
| @rows = rows.dup | ||
| @column_types = column_types.dup | ||
| @hash_rows = nil | ||
@@ -205,3 +229,4 @@ end | ||
| hash_rows.freeze | ||
| indexed_rows.freeze | ||
| indexed_rows | ||
| column_types | ||
| super | ||
@@ -214,3 +239,3 @@ end | ||
| hash = {} | ||
| length = columns.length | ||
| length = columns.length | ||
| while index < length | ||
@@ -233,6 +258,10 @@ hash[columns[index]] = index | ||
| def column_type(name, index, type_overrides) | ||
| type_overrides.fetch(name) do | ||
| column_types.fetch(index) do | ||
| column_types.fetch(name, Type.default_value) | ||
| if type_overrides | ||
| type_overrides.fetch(name) do | ||
| column_type(name, index, nil) | ||
| end | ||
| elsif @column_types | ||
| @column_types[index] || Type.default_value | ||
| else | ||
| Type.default_value | ||
| end | ||
@@ -249,12 +278,6 @@ end | ||
| empty_array = [].freeze | ||
| EMPTY_ARRAY = [].freeze | ||
| EMPTY_HASH = {}.freeze | ||
| private_constant :EMPTY_HASH | ||
| EMPTY = new(empty_array, empty_array, EMPTY_HASH).freeze | ||
| private_constant :EMPTY | ||
| EMPTY_ASYNC = FutureResult.wrap(EMPTY).freeze | ||
| private_constant :EMPTY_ASYNC | ||
| private_constant :EMPTY_ARRAY, :EMPTY_HASH | ||
| end | ||
| end |
@@ -164,2 +164,4 @@ # frozen_string_literal: true | ||
| # # => "role = '0'" | ||
| # | ||
| # Before using this method, please consider if Arel.sql would be better for your use-case | ||
| def sanitize_sql_array(ary) | ||
@@ -166,0 +168,0 @@ statement, *values = ary |
@@ -168,3 +168,3 @@ # frozen_string_literal: true | ||
| tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" | ||
| tbl.print " create_table #{relation_name(remove_prefix_and_suffix(table)).inspect}" | ||
@@ -196,3 +196,3 @@ case pk | ||
| # then dump all non-primary key columns | ||
| columns.each do |column| | ||
| columns.sort_by(&:name).each do |column| | ||
| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) | ||
@@ -238,3 +238,3 @@ next if column.name == pk | ||
| table_name = remove_prefix_and_suffix(index.table).inspect | ||
| " add_index #{([table_name] + index_parts(index)).join(', ')}" | ||
| " add_index #{([relation_name(table_name)] + index_parts(index)).join(', ')}" | ||
| end | ||
@@ -283,2 +283,3 @@ | ||
| index_parts << "comment: #{index.comment.inspect}" if index.comment | ||
| index_parts << "enabled: #{index.enabled.inspect}" if @connection.supports_disabling_indexes? && index.disabled? | ||
| index_parts | ||
@@ -324,4 +325,4 @@ end | ||
| parts = [ | ||
| "add_foreign_key #{remove_prefix_and_suffix(foreign_key.from_table).inspect}", | ||
| remove_prefix_and_suffix(foreign_key.to_table).inspect, | ||
| relation_name(remove_prefix_and_suffix(foreign_key.from_table)).inspect, | ||
| relation_name(remove_prefix_and_suffix(foreign_key.to_table)).inspect, | ||
| ] | ||
@@ -337,6 +338,3 @@ | ||
| if foreign_key.export_name_on_schema_dump? | ||
| parts << "name: #{foreign_key.name.inspect}" | ||
| end | ||
| parts << "name: #{foreign_key.name.inspect}" if foreign_key.export_name_on_schema_dump? | ||
| parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update | ||
@@ -347,3 +345,3 @@ parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete | ||
| " #{parts.join(', ')}" | ||
| " add_foreign_key #{parts.join(', ')}" | ||
| end | ||
@@ -373,2 +371,6 @@ | ||
| def relation_name(name) | ||
| name | ||
| end | ||
| def remove_prefix_and_suffix(table) | ||
@@ -375,0 +377,0 @@ # This method appears at the top when profiling active_record test cases run. |
| # frozen_string_literal: true | ||
| require "active_support/core_ext/module/delegation" | ||
@@ -5,0 +4,0 @@ module ActiveRecord |
@@ -9,2 +9,4 @@ # frozen_string_literal: true | ||
| included do | ||
| class_attribute :_signed_id_verifier, instance_accessor: false, instance_predicate: false | ||
| ## | ||
@@ -15,2 +17,16 @@ # :singleton-method: | ||
| class_attribute :signed_id_verifier_secret, instance_writer: false | ||
| module DeprecateSignedIdVerifierSecret | ||
| def signed_id_verifier_secret=(secret) | ||
| ActiveRecord.deprecator.warn(<<~MSG) | ||
| ActiveRecord::Base.signed_id_verifier_secret is deprecated and will be removed in Rails 8.2. | ||
| If the secret is model-specific, set Model.signed_id_verifier instead. | ||
| Otherwise, configure Rails.application.message_verifiers (or ActiveRecord.message_verifiers) with the secret. | ||
| MSG | ||
| super | ||
| end | ||
| end | ||
| singleton_class.prepend DeprecateSignedIdVerifierSecret | ||
| end | ||
@@ -54,6 +70,7 @@ | ||
| # User.find_signed signed_id, purpose: :password_reset # => User.first | ||
| def find_signed(signed_id, purpose: nil) | ||
| def find_signed(signed_id, purpose: nil, on_rotation: nil) | ||
| raise UnknownPrimaryKey.new(self) if primary_key.nil? | ||
| if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose)) | ||
| options = { on_rotation: on_rotation }.compact | ||
| if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose), **options) | ||
| find_by primary_key => id | ||
@@ -75,4 +92,5 @@ end | ||
| # User.find_signed! signed_id # => ActiveRecord::RecordNotFound | ||
| def find_signed!(signed_id, purpose: nil) | ||
| if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose)) | ||
| def find_signed!(signed_id, purpose: nil, on_rotation: nil) | ||
| options = { on_rotation: on_rotation }.compact | ||
| if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose), **options) | ||
| find(id) | ||
@@ -82,16 +100,22 @@ end | ||
| # The verifier instance that all signed ids are generated and verified from. By default, it'll be initialized | ||
| # with the class-level +signed_id_verifier_secret+, which within Rails comes from | ||
| # {Rails.application.key_generator}[rdoc-ref:Rails::Application#key_generator]. | ||
| # By default, it's SHA256 for the digest and JSON for the serialization. | ||
| def signed_id_verifier | ||
| @signed_id_verifier ||= begin | ||
| secret = signed_id_verifier_secret | ||
| secret = secret.call if secret.respond_to?(:call) | ||
| if signed_id_verifier_secret | ||
| @signed_id_verifier ||= begin | ||
| secret = signed_id_verifier_secret | ||
| secret = secret.call if secret.respond_to?(:call) | ||
| if secret.nil? | ||
| raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed ids" | ||
| else | ||
| if secret.nil? | ||
| raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed IDs" | ||
| end | ||
| ActiveSupport::MessageVerifier.new secret, digest: "SHA256", serializer: JSON, url_safe: true | ||
| end | ||
| else | ||
| return _signed_id_verifier if _signed_id_verifier | ||
| if ActiveRecord.message_verifiers.nil? | ||
| raise "You must set ActiveRecord.message_verifiers to use signed IDs" | ||
| end | ||
| ActiveRecord.message_verifiers["active_record/signed_id"] | ||
| end | ||
@@ -104,3 +128,7 @@ end | ||
| def signed_id_verifier=(verifier) | ||
| @signed_id_verifier = verifier | ||
| if signed_id_verifier_secret | ||
| @signed_id_verifier = verifier | ||
| else | ||
| self._signed_id_verifier = verifier | ||
| end | ||
| end | ||
@@ -107,0 +135,0 @@ |
@@ -34,4 +34,7 @@ # frozen_string_literal: true | ||
| class Query # :nodoc: | ||
| def initialize(sql) | ||
| attr_reader :retryable | ||
| def initialize(sql, retryable:) | ||
| @sql = sql | ||
| @retryable = retryable | ||
| end | ||
@@ -45,3 +48,3 @@ | ||
| class PartialQuery < Query # :nodoc: | ||
| def initialize(values) | ||
| def initialize(values, retryable:) | ||
| @values = values | ||
@@ -51,2 +54,3 @@ @indexes = values.each_with_index.find_all { |thing, i| | ||
| }.map(&:last) | ||
| @retryable = retryable | ||
| end | ||
@@ -100,8 +104,8 @@ | ||
| def self.query(sql) | ||
| Query.new(sql) | ||
| def self.query(...) | ||
| Query.new(...) | ||
| end | ||
| def self.partial_query(values) | ||
| PartialQuery.new(values) | ||
| def self.partial_query(...) | ||
| PartialQuery.new(...) | ||
| end | ||
@@ -149,3 +153,3 @@ | ||
| def execute(params, connection, allow_retry: false, async: false, &block) | ||
| def execute(params, connection, async: false, &block) | ||
| bind_values = @bind_map.bind params | ||
@@ -155,5 +159,5 @@ sql = @query_builder.sql_for bind_values, connection | ||
| if async | ||
| @model.async_find_by_sql(sql, bind_values, preparable: true, allow_retry: allow_retry, &block) | ||
| @model.async_find_by_sql(sql, bind_values, preparable: true, allow_retry: @query_builder.retryable, &block) | ||
| else | ||
| @model.find_by_sql(sql, bind_values, preparable: true, allow_retry: allow_retry, &block) | ||
| @model.find_by_sql(sql, bind_values, preparable: true, allow_retry: @query_builder.retryable, &block) | ||
| end | ||
@@ -160,0 +164,0 @@ rescue ::RangeError |
@@ -149,3 +149,4 @@ # frozen_string_literal: true | ||
| prev_store, new_store = changes[store_attribute] | ||
| prev_store&.dig(key) != new_store&.dig(key) | ||
| accessor = store_accessor_for(store_attribute) | ||
| accessor.get(prev_store, key) != accessor.get(new_store, key) | ||
| end | ||
@@ -156,3 +157,4 @@ | ||
| prev_store, new_store = changes[store_attribute] | ||
| [prev_store&.dig(key), new_store&.dig(key)] | ||
| accessor = store_accessor_for(store_attribute) | ||
| [accessor.get(prev_store, key), accessor.get(new_store, key)] | ||
| end | ||
@@ -163,3 +165,4 @@ | ||
| prev_store, _new_store = changes[store_attribute] | ||
| prev_store&.dig(key) | ||
| accessor = store_accessor_for(store_attribute) | ||
| accessor.get(prev_store, key) | ||
| end | ||
@@ -170,3 +173,4 @@ | ||
| prev_store, new_store = saved_changes[store_attribute] | ||
| prev_store&.dig(key) != new_store&.dig(key) | ||
| accessor = store_accessor_for(store_attribute) | ||
| accessor.get(prev_store, key) != accessor.get(new_store, key) | ||
| end | ||
@@ -177,3 +181,4 @@ | ||
| prev_store, new_store = saved_changes[store_attribute] | ||
| [prev_store&.dig(key), new_store&.dig(key)] | ||
| accessor = store_accessor_for(store_attribute) | ||
| [accessor.get(prev_store, key), accessor.get(new_store, key)] | ||
| end | ||
@@ -184,3 +189,4 @@ | ||
| prev_store, _new_store = saved_changes[store_attribute] | ||
| prev_store&.dig(key) | ||
| accessor = store_accessor_for(store_attribute) | ||
| accessor.get(prev_store, key) | ||
| end | ||
@@ -234,14 +240,27 @@ end | ||
| class HashAccessor # :nodoc: | ||
| def self.get(store_object, key) | ||
| if store_object | ||
| store_object[key] | ||
| end | ||
| end | ||
| def self.read(object, attribute, key) | ||
| prepare(object, attribute) | ||
| object.public_send(attribute)[key] | ||
| store_object = prepare(object, attribute) | ||
| store_object[key] | ||
| end | ||
| def self.write(object, attribute, key, value) | ||
| prepare(object, attribute) | ||
| object.public_send(attribute)[key] = value if value != read(object, attribute, key) | ||
| store_object = prepare(object, attribute) | ||
| store_object[key] = value if value != store_object[key] | ||
| end | ||
| def self.prepare(object, attribute) | ||
| object.public_send :"#{attribute}=", {} unless object.send(attribute) | ||
| store_object = object.public_send(attribute) | ||
| if store_object.nil? | ||
| store_object = {} | ||
| object.public_send(:"#{attribute}=", store_object) | ||
| end | ||
| store_object | ||
| end | ||
@@ -251,8 +270,12 @@ end | ||
| class StringKeyedHashAccessor < HashAccessor # :nodoc: | ||
| def self.get(store_object, key) | ||
| super store_object, Symbol === key ? key.name : key.to_s | ||
| end | ||
| def self.read(object, attribute, key) | ||
| super object, attribute, key.to_s | ||
| super object, attribute, Symbol === key ? key.name : key.to_s | ||
| end | ||
| def self.write(object, attribute, key, value) | ||
| super object, attribute, key.to_s, value | ||
| super object, attribute, Symbol === key ? key.name : key.to_s, value | ||
| end | ||
@@ -262,9 +285,11 @@ end | ||
| class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc: | ||
| def self.prepare(object, store_attribute) | ||
| attribute = object.send(store_attribute) | ||
| unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) | ||
| attribute = IndifferentCoder.as_indifferent_hash(attribute) | ||
| object.public_send :"#{store_attribute}=", attribute | ||
| def self.prepare(object, attribute) | ||
| store_object = object.public_send(attribute) | ||
| unless store_object.is_a?(ActiveSupport::HashWithIndifferentAccess) | ||
| store_object = IndifferentCoder.as_indifferent_hash(store_object) | ||
| object.public_send :"#{attribute}=", store_object | ||
| end | ||
| attribute | ||
| store_object | ||
| end | ||
@@ -271,0 +296,0 @@ end |
@@ -69,3 +69,3 @@ # frozen_string_literal: true | ||
| configs_for(env_name: environment).each do |db_config| | ||
| check_current_protected_environment!(db_config) | ||
| database_adapter_for(db_config).check_current_protected_environment!(db_config, migration_class) | ||
| end | ||
@@ -151,4 +151,2 @@ end | ||
| database_configs.each do |db_config| | ||
| next unless db_config.database_tasks? | ||
| yield db_config.name | ||
@@ -458,3 +456,3 @@ end | ||
| File.open(filename, "a") do |f| | ||
| f.puts migration_connection.dump_schema_information | ||
| f.puts migration_connection.dump_schema_versions | ||
| f.print "\n" | ||
@@ -646,19 +644,2 @@ end | ||
| def check_current_protected_environment!(db_config) | ||
| with_temporary_pool(db_config) do |pool| | ||
| migration_context = pool.migration_context | ||
| current = migration_context.current_environment | ||
| stored = migration_context.last_stored_environment | ||
| if migration_context.protected_environment? | ||
| raise ActiveRecord::ProtectedEnvironmentError.new(stored) | ||
| end | ||
| if stored && stored != current | ||
| raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored) | ||
| end | ||
| rescue ActiveRecord::NoDatabaseError | ||
| end | ||
| end | ||
| def initialize_database(db_config) | ||
@@ -665,0 +646,0 @@ with_temporary_pool(db_config) do |
@@ -5,12 +5,3 @@ # frozen_string_literal: true | ||
| module Tasks # :nodoc: | ||
| class MySQLDatabaseTasks # :nodoc: | ||
| def self.using_database_configurations? | ||
| true | ||
| end | ||
| def initialize(db_config) | ||
| @db_config = db_config | ||
| @configuration_hash = db_config.configuration_hash | ||
| end | ||
| class MySQLDatabaseTasks < AbstractTasks # :nodoc: | ||
| def create | ||
@@ -37,6 +28,2 @@ establish_connection(configuration_hash_without_database) | ||
| def collation | ||
| connection.collation | ||
| end | ||
| def structure_dump(filename, extra_flags) | ||
@@ -58,3 +45,3 @@ args = prepare_command_options | ||
| run_cmd("mysqldump", args, "dumping") | ||
| run_cmd("mysqldump", *args) | ||
| end | ||
@@ -68,20 +55,6 @@ | ||
| run_cmd("mysql", args, "loading") | ||
| run_cmd("mysql", *args) | ||
| end | ||
| private | ||
| attr_reader :db_config, :configuration_hash | ||
| def connection | ||
| ActiveRecord::Base.lease_connection | ||
| end | ||
| def establish_connection(config = db_config) | ||
| ActiveRecord::Base.establish_connection(config) | ||
| end | ||
| def configuration_hash_without_database | ||
| configuration_hash.merge(database: nil) | ||
| end | ||
| def creation_options | ||
@@ -112,14 +85,4 @@ Hash.new.tap do |options| | ||
| end | ||
| def run_cmd(cmd, args, action) | ||
| fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) | ||
| end | ||
| def run_cmd_error(cmd, args, action) | ||
| msg = +"failed to execute: `#{cmd}`\n" | ||
| msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" | ||
| msg | ||
| end | ||
| end | ||
| end | ||
| end |
@@ -7,3 +7,3 @@ # frozen_string_literal: true | ||
| module Tasks # :nodoc: | ||
| class PostgreSQLDatabaseTasks # :nodoc: | ||
| class PostgreSQLDatabaseTasks < AbstractTasks # :nodoc: | ||
| DEFAULT_ENCODING = ENV["CHARSET"] || "utf8" | ||
@@ -13,11 +13,2 @@ ON_ERROR_STOP_1 = "ON_ERROR_STOP=1" | ||
| def self.using_database_configurations? | ||
| true | ||
| end | ||
| def initialize(db_config) | ||
| @db_config = db_config | ||
| @configuration_hash = db_config.configuration_hash | ||
| end | ||
| def create(connection_already_established = false) | ||
@@ -34,10 +25,2 @@ establish_connection(public_schema_config) unless connection_already_established | ||
| def charset | ||
| connection.encoding | ||
| end | ||
| def collation | ||
| connection.collation | ||
| end | ||
| def purge | ||
@@ -78,3 +61,3 @@ ActiveRecord::Base.connection_handler.clear_active_connections!(:all) | ||
| args << db_config.database | ||
| run_cmd("pg_dump", args, "dumping") | ||
| run_cmd("pg_dump", *args) | ||
| remove_sql_header_comments(filename) | ||
@@ -89,16 +72,6 @@ File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } | ||
| args << db_config.database | ||
| run_cmd("psql", args, "loading") | ||
| run_cmd("psql", *args) | ||
| end | ||
| private | ||
| attr_reader :db_config, :configuration_hash | ||
| def connection | ||
| ActiveRecord::Base.lease_connection | ||
| end | ||
| def establish_connection(config = db_config) | ||
| ActiveRecord::Base.establish_connection(config) | ||
| end | ||
| def encoding | ||
@@ -125,13 +98,6 @@ configuration_hash[:encoding] || DEFAULT_ENCODING | ||
| def run_cmd(cmd, args, action) | ||
| fail run_cmd_error(cmd, args, action) unless Kernel.system(psql_env, cmd, *args) | ||
| def run_cmd(cmd, *args, **opts) | ||
| fail run_cmd_error(cmd, args) unless Kernel.system(psql_env, cmd, *args, **opts) | ||
| end | ||
| def run_cmd_error(cmd, args, action) | ||
| msg = +"failed to execute:\n" | ||
| msg << "#{cmd} #{args.join(' ')}\n\n" | ||
| msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" | ||
| msg | ||
| end | ||
| def remove_sql_header_comments(filename) | ||
@@ -138,0 +104,0 @@ removing_comments = true |
@@ -5,7 +5,3 @@ # frozen_string_literal: true | ||
| module Tasks # :nodoc: | ||
| class SQLiteDatabaseTasks # :nodoc: | ||
| def self.using_database_configurations? | ||
| true | ||
| end | ||
| class SQLiteDatabaseTasks < AbstractTasks # :nodoc: | ||
| def initialize(db_config, root = ActiveRecord::Tasks::DatabaseTasks.root) | ||
@@ -41,6 +37,2 @@ @db_config = db_config | ||
| def charset | ||
| connection.encoding | ||
| end | ||
| def structure_dump(filename, extra_flags) | ||
@@ -59,3 +51,4 @@ args = [] | ||
| end | ||
| run_cmd("sqlite3", args, filename) | ||
| run_cmd("sqlite3", *args, out: filename) | ||
| end | ||
@@ -68,9 +61,15 @@ | ||
| def check_current_protected_environment!(db_config, migration_class) | ||
| super | ||
| rescue ActiveRecord::StatementInvalid => e | ||
| case e.cause | ||
| when SQLite3::ReadOnlyException | ||
| else | ||
| raise e | ||
| end | ||
| end | ||
| private | ||
| attr_reader :db_config, :root | ||
| attr_reader :root | ||
| def connection | ||
| ActiveRecord::Base.lease_connection | ||
| end | ||
| def establish_connection(config = db_config) | ||
@@ -80,15 +79,4 @@ ActiveRecord::Base.establish_connection(config) | ||
| end | ||
| def run_cmd(cmd, args, out) | ||
| fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out) | ||
| end | ||
| def run_cmd_error(cmd, args) | ||
| msg = +"failed to execute:\n" | ||
| msg << "#{cmd} #{args.join(' ')}\n\n" | ||
| msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" | ||
| msg | ||
| end | ||
| end | ||
| end | ||
| end |
@@ -7,4 +7,12 @@ # frozen_string_literal: true | ||
| module TestDatabases # :nodoc: | ||
| ActiveSupport::Testing::Parallelization.before_fork_hook do | ||
| if ActiveSupport.parallelize_test_databases | ||
| ActiveRecord::Base.connection_handler.clear_all_connections! | ||
| end | ||
| end | ||
| ActiveSupport::Testing::Parallelization.after_fork_hook do |i| | ||
| create_and_load_schema(i, env_name: ActiveRecord::ConnectionHandling::DEFAULT_ENV.call) | ||
| if ActiveSupport.parallelize_test_databases | ||
| create_and_load_schema(i, env_name: ActiveRecord::ConnectionHandling::DEFAULT_ENV.call) | ||
| end | ||
| end | ||
@@ -16,3 +24,3 @@ | ||
| ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config| | ||
| db_config._database = "#{db_config.database}-#{i}" | ||
| db_config._database = "#{db_config.database}_#{i}" | ||
@@ -19,0 +27,0 @@ ActiveRecord::Tasks::DatabaseTasks.reconstruct_from_schema(db_config, nil) |
@@ -39,2 +39,3 @@ # frozen_string_literal: true | ||
| class_attribute :fixture_sets, default: {} | ||
| class_attribute :database_transactions_config, default: {} | ||
@@ -45,2 +46,16 @@ ActiveSupport.run_load_hooks(:active_record_fixtures, self) | ||
| module ClassMethods | ||
| # Do not use transactional tests for the given database. This overrides | ||
| # the default setting as defined by `use_transactional_tests`, which | ||
| # applies to all database connection pools not explicitly configured here. | ||
| def skip_transactional_tests_for_database(database_name) | ||
| use_transactional_tests_for_database(database_name, false) | ||
| end | ||
| # Enable or disable transactions per database. This overrides the default | ||
| # setting as defined by `use_transactional_tests`, which applies to all | ||
| # database connection pools not explicitly configured here. | ||
| def use_transactional_tests_for_database(database_name, enabled = true) | ||
| self.database_transactions_config = database_transactions_config.merge(database_name => enabled) | ||
| end | ||
| # Sets the model class for a fixture when the class name cannot be inferred from the fixture name. | ||
@@ -111,3 +126,4 @@ # | ||
| def run_in_transaction? | ||
| use_transactional_tests && | ||
| has_explicit_config = database_transactions_config.any? { |_, enabled| enabled } | ||
| (use_transactional_tests || has_explicit_config) && | ||
| !self.class.uses_transaction?(name) | ||
@@ -175,2 +191,6 @@ end | ||
| def transactional_tests_for_pool?(pool) | ||
| database_transactions_config.fetch(pool.db_config.name.to_sym, use_transactional_tests) | ||
| end | ||
| def setup_transactional_fixtures | ||
@@ -181,2 +201,6 @@ setup_shared_connection_pool | ||
| @fixture_connection_pools = ActiveRecord::Base.connection_handler.connection_pool_list(:writing) | ||
| # Filter to pools that want to use transactions | ||
| @fixture_connection_pools.select! { |pool| transactional_tests_for_pool?(pool) } | ||
| @fixture_connection_pools.each do |pool| | ||
@@ -197,3 +221,4 @@ pool.pin_connection!(lock_threads) | ||
| unless @fixture_connection_pools.include?(pool) | ||
| # Don't begin a transaction if we've already done so, or are not using them for this pool | ||
| if !@fixture_connection_pools.include?(pool) && transactional_tests_for_pool?(pool) | ||
| pool.pin_connection!(lock_threads) | ||
@@ -200,0 +225,0 @@ pool.lease_connection |
@@ -14,8 +14,14 @@ # frozen_string_literal: true | ||
| # | ||
| # If the +:include_schema+ option is provided, any queries (including schema related) are counted. | ||
| # Any unmaterialized transactions will be materialized to ensure only | ||
| # queries attempted intside the block are counted. | ||
| # | ||
| # If the +:include_schema+ option is provided, any queries (including | ||
| # schema related) are counted. Setting this option also skips leasing a | ||
| # connection to materialize pending transactions since we want to count | ||
| # queries executed at connection open (e.g., type map). | ||
| # | ||
| # assert_queries_count(1, include_schema: true) { Post.columns } | ||
| # | ||
| def assert_queries_count(count = nil, include_schema: false, &block) | ||
| ActiveRecord::Base.lease_connection.materialize_transactions | ||
| ActiveRecord::Base.lease_connection.materialize_transactions unless include_schema | ||
@@ -22,0 +28,0 @@ counter = SQLCounter.new |
@@ -172,4 +172,6 @@ # frozen_string_literal: true | ||
| all_timestamp_attributes_in_model.each do |attribute_name| | ||
| self[attribute_name] = nil | ||
| clear_attribute_change(attribute_name) | ||
| if self[attribute_name] | ||
| self[attribute_name] = nil | ||
| clear_attribute_change(attribute_name) | ||
| end | ||
| end | ||
@@ -176,0 +178,0 @@ end |
@@ -29,3 +29,3 @@ # frozen_string_literal: true | ||
| # | ||
| # Closed transactions are `blank?` too. | ||
| # Closed transactions are +blank?+ too. | ||
| # | ||
@@ -105,5 +105,2 @@ # == Callbacks | ||
| # the block is never called. | ||
| # | ||
| # If the transaction is already finalized, attempting to register a callback | ||
| # will raise ActiveRecord::ActiveRecordError. | ||
| def after_rollback(&block) | ||
@@ -120,3 +117,3 @@ @internal_transaction&.after_rollback(&block) | ||
| def closed? | ||
| @internal_transaction.nil? || @internal_transaction.state.finalized? | ||
| @internal_transaction.nil? || @internal_transaction.closed? | ||
| end | ||
@@ -123,0 +120,0 @@ |
@@ -233,6 +233,26 @@ # frozen_string_literal: true | ||
| with_connection do |connection| | ||
| connection.transaction(**options, &block) | ||
| connection.pool.with_pool_transaction_isolation_level(ActiveRecord.default_transaction_isolation_level, connection.transaction_open?) do | ||
| connection.transaction(**options, &block) | ||
| end | ||
| end | ||
| end | ||
| # Makes all transactions the current pool use the isolation level initiated within the block. | ||
| def with_pool_transaction_isolation_level(isolation_level, &block) | ||
| if current_transaction.open? | ||
| raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open" | ||
| end | ||
| old_level = connection_pool.pool_transaction_isolation_level | ||
| connection_pool.pool_transaction_isolation_level = isolation_level | ||
| yield | ||
| ensure | ||
| connection_pool.pool_transaction_isolation_level = old_level | ||
| end | ||
| # Returns the default isolation level for the connection pool, set earlier by #with_pool_transaction_isolation_level. | ||
| def pool_transaction_isolation_level | ||
| connection_pool.pool_transaction_isolation_level | ||
| end | ||
| # Returns a representation of the current transaction state, | ||
@@ -411,14 +431,16 @@ # which can be a top level transaction, a savepoint, or the absence of a transaction. | ||
| self.class.with_connection do |connection| | ||
| status = nil | ||
| ensure_finalize = !connection.transaction_open? | ||
| connection.pool.with_pool_transaction_isolation_level(ActiveRecord.default_transaction_isolation_level, connection.transaction_open?) do | ||
| status = nil | ||
| ensure_finalize = !connection.transaction_open? | ||
| connection.transaction do | ||
| add_to_transaction(ensure_finalize || has_transactional_callbacks?) | ||
| remember_transaction_record_state | ||
| connection.transaction do | ||
| add_to_transaction(ensure_finalize || has_transactional_callbacks?) | ||
| remember_transaction_record_state | ||
| status = yield | ||
| raise ActiveRecord::Rollback unless status | ||
| status = yield | ||
| raise ActiveRecord::Rollback unless status | ||
| end | ||
| @_last_transaction_return_status = status | ||
| status | ||
| end | ||
| @_last_transaction_return_status = status | ||
| status | ||
| end | ||
@@ -425,0 +447,0 @@ end |
@@ -22,3 +22,4 @@ # frozen_string_literal: true | ||
| if column | ||
| type = @klass.with_connection { |connection| connection.lookup_cast_type_from_column(column) } | ||
| # TODO: Remove fetch_cast_type and the need for connection after we release 8.1. | ||
| type = column.fetch_cast_type(@klass.lease_connection) | ||
| end | ||
@@ -25,0 +26,0 @@ end |
@@ -29,2 +29,3 @@ # frozen_string_literal: true | ||
| else | ||
| value.freeze | ||
| @mapping[key] = proc { value } | ||
@@ -54,3 +55,3 @@ end | ||
| def perform_fetch(type, *args, &block) | ||
| @mapping.fetch(type, block).call(type, *args) | ||
| @mapping.fetch(type, block).call(type, *args).freeze | ||
| end | ||
@@ -57,0 +58,0 @@ end |
@@ -19,2 +19,9 @@ # frozen_string_literal: true | ||
| end | ||
| def ==(other) | ||
| super(other) && timezone == other.timezone | ||
| end | ||
| protected | ||
| attr_reader :timezone | ||
| end | ||
@@ -21,0 +28,0 @@ end |
| # frozen_string_literal: true | ||
| require "active_support/json" | ||
| module ActiveRecord | ||
@@ -14,7 +16,18 @@ module Type | ||
| return value unless value.is_a?(::String) | ||
| ActiveSupport::JSON.decode(value) rescue nil | ||
| begin | ||
| ActiveSupport::JSON.decode(value) | ||
| rescue JSON::ParserError => e | ||
| # NOTE: This may hide json with duplicate keys. We don't really want to just ignore it | ||
| # but it's the best we can do in order to still allow updating columns that somehow already | ||
| # contain invalid json from some other source. | ||
| # See https://github.com/rails/rails/pull/55536 | ||
| ActiveSupport.error_reporter.report(e, source: "application.active_record") | ||
| nil | ||
| end | ||
| end | ||
| JSON_ENCODER = ActiveSupport::JSON::Encoding.json_encoder.new(escape: false) | ||
| def serialize(value) | ||
| ActiveSupport::JSON.encode(value) unless value.nil? | ||
| JSON_ENCODER.encode(value) unless value.nil? | ||
| end | ||
@@ -21,0 +34,0 @@ |
@@ -12,5 +12,6 @@ # frozen_string_literal: true | ||
| def initialize(subtype, coder) | ||
| def initialize(subtype, coder, comparable: false) | ||
| @subtype = subtype | ||
| @coder = coder | ||
| @comparable = comparable | ||
| super(subtype) | ||
@@ -38,5 +39,11 @@ end | ||
| return false if value.nil? | ||
| raw_new_value = encoded(value) | ||
| raw_old_value.nil? != raw_new_value.nil? || | ||
| subtype.changed_in_place?(raw_old_value, raw_new_value) | ||
| if @comparable | ||
| old_value = deserialize(raw_old_value) | ||
| old_value != value | ||
| else | ||
| raw_new_value = encoded(value) | ||
| raw_old_value.nil? != raw_new_value.nil? || | ||
| subtype.changed_in_place?(raw_old_value, raw_new_value) | ||
| end | ||
| end | ||
@@ -63,7 +70,2 @@ | ||
| private | ||
| # Prevent Ruby 4.0 "delegator does not forward private method" warning. | ||
| # Kernel#inspect calls instance_variables_to_inspect which, without this, | ||
| # triggers Delegator#respond_to_missing? for a private method. | ||
| define_method(:instance_variables_to_inspect, Kernel.instance_method(:instance_variables)) | ||
| def default_value?(value) | ||
@@ -70,0 +72,0 @@ value == coder.load(nil) |
@@ -49,3 +49,3 @@ # frozen_string_literal: true | ||
| if matching_pair | ||
| matching_pair.last.call(lookup_key) | ||
| matching_pair.last.call(lookup_key).freeze | ||
| elsif @parent | ||
@@ -52,0 +52,0 @@ @parent.perform_fetch(lookup_key, &block) |
@@ -10,3 +10,3 @@ # frozen_string_literal: true | ||
| if Array(value).reject { |association| valid_object?(association, context) }.any? | ||
| record.errors.add(attribute, :invalid, **options.merge(value: value)) | ||
| record.errors.add(attribute, :invalid, **options, value: value) | ||
| end | ||
@@ -13,0 +13,0 @@ end |
+3
-1
@@ -53,3 +53,5 @@ # frozen_string_literal: true | ||
| def self.sql(sql_string, *positional_binds, retryable: false, **named_binds) | ||
| if positional_binds.empty? && named_binds.empty? | ||
| if Arel::Nodes::SqlLiteral === sql_string | ||
| sql_string | ||
| elsif positional_binds.empty? && named_binds.empty? | ||
| Arel::Nodes::SqlLiteral.new(sql_string, retryable: retryable) | ||
@@ -56,0 +58,0 @@ else |
@@ -6,2 +6,4 @@ # frozen_string_literal: true | ||
| def as(other) | ||
| other = other.name if other.is_a?(Symbol) | ||
| Nodes::As.new self, Nodes::SqlLiteral.new(other, retryable: true) | ||
@@ -8,0 +10,0 @@ end |
+6
-11
@@ -17,8 +17,3 @@ # frozen_string_literal: true | ||
| def compile_update( | ||
| values, | ||
| key = nil, | ||
| having_clause = nil, | ||
| group_values_columns = [] | ||
| ) | ||
| def compile_update(values, key = nil) | ||
| um = UpdateManager.new(source) | ||
@@ -33,8 +28,8 @@ um.set(values) | ||
| um.group(group_values_columns) unless group_values_columns.empty? | ||
| um.having(having_clause) unless having_clause.nil? | ||
| um.ast.groups = @ctx.groups | ||
| @ctx.havings.each { |h| um.having(h) } | ||
| um | ||
| end | ||
| def compile_delete(key = nil, having_clause = nil, group_values_columns = []) | ||
| def compile_delete(key = nil) | ||
| dm = DeleteManager.new(source) | ||
@@ -47,4 +42,4 @@ dm.take(limit) | ||
| dm.key = key | ||
| dm.group(group_values_columns) unless group_values_columns.empty? | ||
| dm.having(having_clause) unless having_clause.nil? | ||
| dm.ast.groups = @ctx.groups | ||
| @ctx.havings.each { |h| dm.having(h) } | ||
| dm | ||
@@ -51,0 +46,0 @@ end |
@@ -48,4 +48,2 @@ # frozen_string_literal: true | ||
| # function | ||
| # FIXME: Function + Alias can be rewritten as a Function and Alias node. | ||
| # We should make Function a Unary node and deprecate the use of "aliaz" | ||
| require "arel/nodes/function" | ||
@@ -52,0 +50,0 @@ require "arel/nodes/count" |
@@ -6,4 +6,4 @@ # frozen_string_literal: true | ||
| class Count < Arel::Nodes::Function | ||
| def initialize(expr, distinct = false, aliaz = nil) | ||
| super(expr, aliaz) | ||
| def initialize(expr, distinct = false) | ||
| super(expr) | ||
| @distinct = distinct | ||
@@ -10,0 +10,0 @@ end |
@@ -8,18 +8,13 @@ # frozen_string_literal: true | ||
| include Arel::FilterPredications | ||
| attr_accessor :expressions, :alias, :distinct | ||
| def initialize(expr, aliaz = nil) | ||
| attr_accessor :expressions, :distinct | ||
| def initialize(expr) | ||
| super() | ||
| @expressions = expr | ||
| @alias = aliaz && SqlLiteral.new(aliaz) | ||
| @distinct = false | ||
| end | ||
| def as(aliaz) | ||
| self.alias = SqlLiteral.new(aliaz) | ||
| self | ||
| end | ||
| def hash | ||
| [@expressions, @alias, @distinct].hash | ||
| [@expressions, @distinct].hash | ||
| end | ||
@@ -30,3 +25,2 @@ | ||
| self.expressions == other.expressions && | ||
| self.alias == other.alias && | ||
| self.distinct == other.distinct | ||
@@ -33,0 +27,0 @@ end |
@@ -8,4 +8,4 @@ # frozen_string_literal: true | ||
| def initialize(name, expr, aliaz = nil) | ||
| super(expr, aliaz) | ||
| def initialize(name, expr) | ||
| super(expr) | ||
| @name = name | ||
@@ -12,0 +12,0 @@ end |
@@ -9,3 +9,3 @@ # frozen_string_literal: true | ||
| # abstract syntax tree (AST) of the statement using various types of Arel::Nodes::Node. Each node represents a | ||
| # fragment of a SQL statement. | ||
| # fragment of an SQL statement. | ||
| # | ||
@@ -12,0 +12,0 @@ # The intermediate representation allows Arel to compile the statement into the database's specific SQL dialect |
@@ -234,3 +234,5 @@ # frozen_string_literal: true | ||
| nodes = others.map { |expr| send(method_id, expr, *extras) } | ||
| Nodes::Grouping.new Nodes::Or.new(nodes) | ||
| Nodes::Grouping.new nodes.inject { |memo, node| | ||
| Nodes::Or.new([memo, node]) | ||
| } | ||
| end | ||
@@ -237,0 +239,0 @@ |
@@ -77,4 +77,9 @@ # frozen_string_literal: true | ||
| # FIXME: backwards compat | ||
| column = Nodes::SqlLiteral.new(column) if String === column | ||
| column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column | ||
| case column | ||
| when Nodes::SqlLiteral | ||
| when String | ||
| column = Nodes::SqlLiteral.new(column) | ||
| when Symbol | ||
| column = Nodes::SqlLiteral.new(column.name) | ||
| end | ||
@@ -81,0 +86,0 @@ @ctx.groups.push Nodes::Group.new column |
@@ -37,3 +37,2 @@ # frozen_string_literal: true | ||
| visit_edge o, "distinct" | ||
| visit_edge o, "alias" | ||
| end | ||
@@ -112,3 +111,2 @@ | ||
| visit_edge o, "expressions" | ||
| visit_edge o, "alias" | ||
| end | ||
@@ -120,3 +118,2 @@ | ||
| visit_edge o, "distinct" | ||
| visit_edge o, "alias" | ||
| end | ||
@@ -123,0 +120,0 @@ |
@@ -7,2 +7,51 @@ # frozen_string_literal: true | ||
| private | ||
| def visit_Arel_Nodes_UpdateStatement(o, collector) | ||
| collector.retryable = false | ||
| o = prepare_update_statement(o) | ||
| collector << "UPDATE " | ||
| # UPDATE with JOIN is in the form of: | ||
| # | ||
| # UPDATE t1 AS __active_record_update_alias | ||
| # SET .. | ||
| # FROM t1 JOIN t2 ON t2.join_id = t1.join_id .. | ||
| # WHERE t1.id = __active_record_update_alias.id AND .. | ||
| if has_join_sources?(o) | ||
| collector = visit o.relation.left, collector | ||
| collect_nodes_for o.values, collector, " SET " | ||
| collector << " FROM " | ||
| collector = inject_join o.relation.right, collector, " " | ||
| else | ||
| collector = visit o.relation, collector | ||
| collect_nodes_for o.values, collector, " SET " | ||
| end | ||
| collect_nodes_for o.wheres, collector, " WHERE ", " AND " | ||
| collect_nodes_for o.orders, collector, " ORDER BY " | ||
| maybe_visit o.limit, collector | ||
| maybe_visit o.comment, collector | ||
| end | ||
| # In the simple case, PostgreSQL allows us to place FROM or JOINs directly into the UPDATE | ||
| # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support | ||
| # these, we must use a subquery. | ||
| def prepare_update_statement(o) | ||
| if o.key && has_join_sources?(o) && !has_group_by_and_having?(o) && !has_limit_or_offset_or_orders?(o) | ||
| # Join clauses cannot reference the target table, so alias the | ||
| # updated table, place the entire relation in the FROM clause, and | ||
| # add a self-join (which requires the primary key) | ||
| stmt = o.clone | ||
| stmt.relation, stmt.wheres = o.relation.clone, o.wheres.clone | ||
| stmt.relation.right = [stmt.relation.left, *stmt.relation.right] | ||
| stmt.relation.left = stmt.relation.left.alias("__active_record_update_alias") | ||
| Array.wrap(o.key).each do |key| | ||
| stmt.wheres << key.eq(stmt.relation.left[key.name]) | ||
| end | ||
| stmt | ||
| else | ||
| super | ||
| end | ||
| end | ||
| def visit_Arel_Nodes_Matches(o, collector) | ||
@@ -70,2 +119,8 @@ op = o.case_sensitive ? " LIKE " : " ILIKE " | ||
| def visit_Arel_Nodes_InnerJoin(o, collector) | ||
| return super if o.right | ||
| collector << "CROSS JOIN " | ||
| visit o.left, collector | ||
| end | ||
| def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) | ||
@@ -72,0 +127,0 @@ collector = visit o.left, collector |
@@ -7,2 +7,57 @@ # frozen_string_literal: true | ||
| private | ||
| def visit_Arel_Nodes_UpdateStatement(o, collector) | ||
| collector.retryable = false | ||
| o = prepare_update_statement(o) | ||
| collector << "UPDATE " | ||
| # UPDATE with JOIN is in the form of: | ||
| # | ||
| # UPDATE t1 AS __active_record_update_alias | ||
| # SET .. | ||
| # FROM t1 JOIN t2 ON t2.join_id = t1.join_id .. | ||
| # WHERE t1.id = __active_record_update_alias.id AND .. | ||
| if has_join_sources?(o) | ||
| collector = visit o.relation.left, collector | ||
| collect_nodes_for o.values, collector, " SET " | ||
| collector << " FROM " | ||
| collector = inject_join o.relation.right, collector, " " | ||
| else | ||
| collector = visit o.relation, collector | ||
| collect_nodes_for o.values, collector, " SET " | ||
| end | ||
| collect_nodes_for o.wheres, collector, " WHERE ", " AND " | ||
| collect_nodes_for o.orders, collector, " ORDER BY " | ||
| maybe_visit o.limit, collector | ||
| maybe_visit o.comment, collector | ||
| end | ||
| def prepare_update_statement(o) | ||
| # Sqlite need to be built with the SQLITE_ENABLE_UPDATE_DELETE_LIMIT compile-time option | ||
| # to support LIMIT/OFFSET/ORDER in UPDATE and DELETE statements. | ||
| if o.key && has_join_sources?(o) && !has_group_by_and_having?(o) && !has_limit_or_offset_or_orders?(o) | ||
| # Join clauses cannot reference the target table, so alias the | ||
| # updated table, place the entire relation in the FROM clause, and | ||
| # add a self-join (which requires the primary key) | ||
| stmt = o.clone | ||
| stmt.relation, stmt.wheres = o.relation.clone, o.wheres.clone | ||
| stmt.relation.right = [stmt.relation.left, *stmt.relation.right] | ||
| stmt.relation.left = stmt.relation.left.alias("__active_record_update_alias") | ||
| Array.wrap(o.key).each do |key| | ||
| stmt.wheres << key.eq(stmt.relation.left[key.name]) | ||
| end | ||
| stmt | ||
| else | ||
| super | ||
| end | ||
| end | ||
| def visit_Arel_Nodes_TableAlias(o, collector) | ||
| # "AS" is not optional in "{UPDATE | DELETE} table AS alias ..." | ||
| collector = visit o.relation, collector | ||
| collector << " AS " | ||
| collector << quote_table_name(o.name) | ||
| end | ||
| # Locks are not supported in SQLite | ||
@@ -18,10 +73,2 @@ def visit_Arel_Nodes_Lock(o, collector) | ||
| def visit_Arel_Nodes_True(o, collector) | ||
| collector << "1" | ||
| end | ||
| def visit_Arel_Nodes_False(o, collector) | ||
| collector << "0" | ||
| end | ||
| def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) | ||
@@ -28,0 +75,0 @@ collector = visit o.left, collector |
@@ -80,9 +80,3 @@ # frozen_string_literal: true | ||
| collector << "EXISTS (" | ||
| collector = visit(o.expressions, collector) << ")" | ||
| if o.alias | ||
| collector << " AS " | ||
| visit o.alias, collector | ||
| else | ||
| collector | ||
| end | ||
| visit(o.expressions, collector) << ")" | ||
| end | ||
@@ -394,9 +388,3 @@ | ||
| collector << "DISTINCT " if o.distinct | ||
| collector = inject_join(o.expressions, collector, ", ") << ")" | ||
| if o.alias | ||
| collector << " AS " | ||
| visit o.alias, collector | ||
| else | ||
| collector | ||
| end | ||
| inject_join(o.expressions, collector, ", ") << ")" | ||
| end | ||
@@ -1005,9 +993,3 @@ | ||
| end | ||
| collector = inject_join(o.expressions, collector, ", ") << ")" | ||
| if o.alias | ||
| collector << " AS " | ||
| visit o.alias, collector | ||
| else | ||
| collector | ||
| end | ||
| inject_join(o.expressions, collector, ", ") << ")" | ||
| end | ||
@@ -1014,0 +996,0 @@ |
+1
-1
@@ -142,3 +142,3 @@ = Active Record -- Object-relational mapping in \Rails | ||
| class AddSystemSettings < ActiveRecord::Migration[8.0] | ||
| class AddSystemSettings < ActiveRecord::Migration[8.1] | ||
| def up | ||
@@ -145,0 +145,0 @@ create_table :system_settings do |t| |
| # frozen_string_literal: true | ||
| module ActiveRecord # :nodoc: | ||
| module Normalization | ||
| extend ActiveSupport::Concern | ||
| included do | ||
| class_attribute :normalized_attributes, default: Set.new | ||
| before_validation :normalize_changed_in_place_attributes | ||
| end | ||
| # Normalizes a specified attribute using its declared normalizations. | ||
| # | ||
| # ==== Examples | ||
| # | ||
| # class User < ActiveRecord::Base | ||
| # normalizes :email, with: -> email { email.strip.downcase } | ||
| # end | ||
| # | ||
| # legacy_user = User.find(1) | ||
| # legacy_user.email # => " CRUISE-CONTROL@EXAMPLE.COM\n" | ||
| # legacy_user.normalize_attribute(:email) | ||
| # legacy_user.email # => "cruise-control@example.com" | ||
| # legacy_user.save | ||
| def normalize_attribute(name) | ||
| # Treat the value as a new, unnormalized value. | ||
| self[name] = self[name] | ||
| end | ||
| module ClassMethods | ||
| # Declares a normalization for one or more attributes. The normalization | ||
| # is applied when the attribute is assigned or updated, and the normalized | ||
| # value will be persisted to the database. The normalization is also | ||
| # applied to the corresponding keyword argument of query methods. This | ||
| # allows a record to be created and later queried using unnormalized | ||
| # values. | ||
| # | ||
| # However, to prevent confusion, the normalization will not be applied | ||
| # when the attribute is fetched from the database. This means that if a | ||
| # record was persisted before the normalization was declared, the record's | ||
| # attribute will not be normalized until either it is assigned a new | ||
| # value, or it is explicitly migrated via Normalization#normalize_attribute. | ||
| # | ||
| # Because the normalization may be applied multiple times, it should be | ||
| # _idempotent_. In other words, applying the normalization more than once | ||
| # should have the same result as applying it only once. | ||
| # | ||
| # By default, the normalization will not be applied to +nil+ values. This | ||
| # behavior can be changed with the +:apply_to_nil+ option. | ||
| # | ||
| # Be aware that if your app was created before Rails 7.1, and your app | ||
| # marshals instances of the targeted model (for example, when caching), | ||
| # then you should set ActiveRecord.marshalling_format_version to +7.1+ or | ||
| # higher via either <tt>config.load_defaults 7.1</tt> or | ||
| # <tt>config.active_record.marshalling_format_version = 7.1</tt>. | ||
| # Otherwise, +Marshal+ may attempt to serialize the normalization +Proc+ | ||
| # and raise +TypeError+. | ||
| # | ||
| # ==== Options | ||
| # | ||
| # * +:with+ - Any callable object that accepts the attribute's value as | ||
| # its sole argument, and returns it normalized. | ||
| # * +:apply_to_nil+ - Whether to apply the normalization to +nil+ values. | ||
| # Defaults to +false+. | ||
| # | ||
| # ==== Examples | ||
| # | ||
| # class User < ActiveRecord::Base | ||
| # normalizes :email, with: -> email { email.strip.downcase } | ||
| # normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") } | ||
| # end | ||
| # | ||
| # user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n") | ||
| # user.email # => "cruise-control@example.com" | ||
| # | ||
| # user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") | ||
| # user.email # => "cruise-control@example.com" | ||
| # user.email_before_type_cast # => "cruise-control@example.com" | ||
| # | ||
| # User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count # => 1 | ||
| # User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0 | ||
| # | ||
| # User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true | ||
| # User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false | ||
| # | ||
| # User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309" | ||
| def normalizes(*names, with:, apply_to_nil: false) | ||
| decorate_attributes(names) do |name, cast_type| | ||
| NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil) | ||
| end | ||
| self.normalized_attributes += names.map(&:to_sym) | ||
| end | ||
| # Normalizes a given +value+ using normalizations declared for +name+. | ||
| # | ||
| # ==== Examples | ||
| # | ||
| # class User < ActiveRecord::Base | ||
| # normalizes :email, with: -> email { email.strip.downcase } | ||
| # end | ||
| # | ||
| # User.normalize_value_for(:email, " CRUISE-CONTROL@EXAMPLE.COM\n") | ||
| # # => "cruise-control@example.com" | ||
| def normalize_value_for(name, value) | ||
| type_for_attribute(name).cast(value) | ||
| end | ||
| end | ||
| private | ||
| def normalize_changed_in_place_attributes | ||
| self.class.normalized_attributes.each do |name| | ||
| normalize_attribute(name) if attribute_changed_in_place?(name) | ||
| end | ||
| end | ||
| class NormalizedValueType < DelegateClass(ActiveModel::Type::Value) # :nodoc: | ||
| include ActiveModel::Type::SerializeCastValue | ||
| attr_reader :cast_type, :normalizer, :normalize_nil | ||
| alias :normalize_nil? :normalize_nil | ||
| def initialize(cast_type:, normalizer:, normalize_nil:) | ||
| @cast_type = cast_type | ||
| @normalizer = normalizer | ||
| @normalize_nil = normalize_nil | ||
| super(cast_type) | ||
| end | ||
| def cast(value) | ||
| normalize(super(value)) | ||
| end | ||
| def serialize(value) | ||
| serialize_cast_value(cast(value)) | ||
| end | ||
| def serialize_cast_value(value) | ||
| ActiveModel::Type::SerializeCastValue.serialize(cast_type, value) | ||
| end | ||
| def ==(other) | ||
| self.class == other.class && | ||
| normalize_nil? == other.normalize_nil? && | ||
| normalizer == other.normalizer && | ||
| cast_type == other.cast_type | ||
| end | ||
| alias eql? == | ||
| def hash | ||
| [self.class, cast_type, normalizer, normalize_nil?].hash | ||
| end | ||
| define_method(:inspect, Kernel.instance_method(:inspect)) | ||
| private | ||
| def normalize(value) | ||
| normalizer.call(value) unless value.nil? && !normalize_nil? | ||
| end | ||
| end | ||
| end | ||
| end |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display