activerecord
Advanced tools
+91
-0
@@ -0,1 +1,92 @@ | ||
| ## Rails 8.0.5 (March 24, 2026) ## | ||
| * Fix `insert_all` and `upsert_all` log message when called on anonymous classes. | ||
| *Gabriel Sobrinho* | ||
| * Respect `ActiveRecord::SchemaDumper.ignore_tables` when dumping SQLite virtual tables. | ||
| *Hans Schnedlitz* | ||
| * Restore previous instrumenter after `execute_or_skip` | ||
| `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. | ||
| *Rosa Gutierrez* | ||
| * Fix Ruby 4.0 delegator warning when calling inspect on ActiveRecord::Type::Serialized. | ||
| *Hammad Khan* | ||
| * Fix support for table names containing hyphens. | ||
| *Evgeniy Demin* | ||
| * Fix column deduplication for SQLite3 and PostgreSQL virtual (generated) columns. | ||
| `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. | ||
| *Jay Huber* | ||
| * Fix merging relations with arel equality predicates with null relations. | ||
| *fatkodima* | ||
| * Fix SQLite3 schema dump for non-autoincrement integer primary keys. | ||
| Previously, `schema.rb` should incorrectly restore that table with an auto incrementing | ||
| primary key. | ||
| *Chris Hasiński* | ||
| * Fix PostgreSQL `schema_search_path` not being reapplied after `reset!` or `reconnect!`. | ||
| The `schema_search_path` configured in `database.yml` is now correctly | ||
| reapplied instead of falling back to PostgreSQL defaults. | ||
| *Tobias Egli* | ||
| * 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: | ||
| ``` | ||
| undefined method `key?' for nil:NilClass (NoMethodError) | ||
| if !type_map.key?(oid) | ||
| ``` | ||
| *Jean Boussier* | ||
| * Fix `eager_load` when loading `has_many` assocations with composite primary keys. | ||
| This would result in some records being loaded multiple times. | ||
| *Martin-Alexander* | ||
| ## Rails 8.0.4.1 (March 23, 2026) ## | ||
@@ -2,0 +93,0 @@ |
@@ -106,3 +106,3 @@ # frozen_string_literal: true | ||
| def instantiate(result_set, strict_loading_value, &block) | ||
| primary_key = aliases.column_alias(join_root, join_root.primary_key) | ||
| primary_key = Array(join_root.primary_key).map { |column| aliases.column_alias(join_root, column) } | ||
@@ -145,3 +145,3 @@ seen = Hash.new { |i, parent| | ||
| result_set.each { |row_hash| | ||
| parent_key = primary_key ? row_hash[primary_key] : row_hash | ||
| parent_key = primary_key.empty? ? row_hash : row_hash.values_at(*primary_key) | ||
| parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, column_types, &block) | ||
@@ -148,0 +148,0 @@ construct(parent, join_root, row_hash, seen, model_cache, strict_loading_value) |
@@ -41,3 +41,9 @@ # frozen_string_literal: true | ||
| def group_and_load_similar(loaders) | ||
| loaders.grep_v(ThroughAssociation).group_by(&:loader_query).each_pair do |query, 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| | ||
| query.load_records_in_batch(similar_loaders) | ||
@@ -44,0 +50,0 @@ end |
@@ -669,30 +669,29 @@ # frozen_string_literal: true | ||
| @lock.synchronize do | ||
| reconnect | ||
| attempt_configure_connection do | ||
| reconnect | ||
| enable_lazy_transactions! | ||
| @raw_connection_dirty = false | ||
| @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @verified = true | ||
| enable_lazy_transactions! | ||
| @raw_connection_dirty = false | ||
| @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @verified = true | ||
| 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) | ||
| 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) | ||
| if !retry_deadline_exceeded && retries_available > 0 | ||
| retries_available -= 1 | ||
| if !retry_deadline_exceeded && retries_available > 0 | ||
| retries_available -= 1 | ||
| if retryable_connection_error?(translated_exception) | ||
| backoff(connection_retries - retries_available) | ||
| retry | ||
| 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 | ||
@@ -705,2 +704,4 @@ end | ||
| @lock.synchronize do | ||
| @last_activity = nil | ||
| @verified = false | ||
| clear_cache!(new_connection: true) | ||
@@ -731,5 +732,7 @@ reset_transaction | ||
| def reset! | ||
| clear_cache!(new_connection: true) | ||
| reset_transaction | ||
| attempt_configure_connection | ||
| attempt_configure_connection do | ||
| clear_cache!(new_connection: true) | ||
| reset_transaction | ||
| configure_connection | ||
| end | ||
| end | ||
@@ -768,7 +771,9 @@ | ||
| if @unconfigured_connection | ||
| @raw_connection = @unconfigured_connection | ||
| @unconfigured_connection = nil | ||
| attempt_configure_connection | ||
| @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @verified = true | ||
| attempt_configure_connection do | ||
| @raw_connection = @unconfigured_connection | ||
| @unconfigured_connection = nil | ||
| configure_connection | ||
| @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| @verified = true | ||
| end | ||
| return | ||
@@ -1223,3 +1228,3 @@ end | ||
| def attempt_configure_connection | ||
| configure_connection | ||
| yield | ||
| rescue Exception # Need to handle things such as Timeout::ExitException | ||
@@ -1226,0 +1231,0 @@ disconnect! |
@@ -123,3 +123,3 @@ # frozen_string_literal: true | ||
| # https://bugs.ruby-lang.org/issues/20688 | ||
| if ObjectSpace.const_defined?(:WeakKeyMap) && RUBY_VERSION >= "3.3.5" | ||
| if ObjectSpace.const_defined?(:WeakKeyMap) && Gem::Version.new(RUBY_VERSION) >= "3.3.5" | ||
| WeakThreadKeyMap = ObjectSpace::WeakKeyMap | ||
@@ -467,3 +467,3 @@ else | ||
| end | ||
| @connections = [] | ||
| @connections = @pinned_connection ? [@pinned_connection] : [] | ||
| @leases.clear | ||
@@ -470,0 +470,0 @@ @available.clear |
@@ -741,3 +741,3 @@ # frozen_string_literal: true | ||
| 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 | ||
@@ -744,0 +744,0 @@ end |
@@ -384,2 +384,7 @@ # 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 | ||
@@ -997,2 +1002,4 @@ # method does nothing. | ||
| schema_search_path # populate cache | ||
| reload_type_map | ||
@@ -999,0 +1006,0 @@ end |
@@ -68,3 +68,4 @@ # frozen_string_literal: true | ||
| identity? == other.identity? && | ||
| serial? == other.serial? | ||
| serial? == other.serial? && | ||
| virtual? == other.virtual? | ||
| end | ||
@@ -77,3 +78,4 @@ alias :eql? :== | ||
| identity?.hash ^ | ||
| serial?.hash | ||
| serial?.hash ^ | ||
| virtual?.hash | ||
| end | ||
@@ -80,0 +82,0 @@ end |
@@ -294,3 +294,3 @@ # frozen_string_literal: true | ||
| VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.+)\)/i | ||
| VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.*)\)/i | ||
@@ -297,0 +297,0 @@ # Returns a list of defined virtual tables |
@@ -49,3 +49,5 @@ # frozen_string_literal: true | ||
| super && | ||
| auto_increment? == other.auto_increment? | ||
| auto_increment? == other.auto_increment? && | ||
| rowid == other.rowid && | ||
| generated_type == other.generated_type | ||
| end | ||
@@ -58,4 +60,8 @@ alias :eql? :== | ||
| auto_increment?.hash ^ | ||
| rowid.hash | ||
| rowid.hash ^ | ||
| virtual?.hash | ||
| end | ||
| protected | ||
| attr_reader :generated_type | ||
| end | ||
@@ -62,0 +68,0 @@ end |
@@ -9,3 +9,3 @@ # frozen_string_literal: true | ||
| def virtual_tables(stream) | ||
| virtual_tables = @connection.virtual_tables | ||
| virtual_tables = @connection.virtual_tables.reject { |name, _| ignored?(name) } | ||
| if virtual_tables.any? | ||
@@ -23,9 +23,21 @@ stream.puts | ||
| def default_primary_key?(column) | ||
| schema_type(column) == :integer | ||
| schema_type(column) == :integer && primary_key_has_autoincrement? | ||
| end | ||
| def explicit_primary_key_default?(column) | ||
| column.bigint? | ||
| column.bigint? || (column.type == :integer && !primary_key_has_autoincrement?) | ||
| 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) | ||
@@ -32,0 +44,0 @@ spec = super |
@@ -108,2 +108,3 @@ # frozen_string_literal: true | ||
| return unless @mutex.try_lock | ||
| previous_instrumenter = ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] | ||
| begin | ||
@@ -117,2 +118,3 @@ if pending? | ||
| ensure | ||
| ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = previous_instrumenter | ||
| @mutex.unlock | ||
@@ -119,0 +121,0 @@ end |
@@ -12,4 +12,4 @@ # frozen_string_literal: true | ||
| MINOR = 0 | ||
| TINY = 4 | ||
| PRE = "1" | ||
| TINY = 5 | ||
| PRE = nil | ||
@@ -16,0 +16,0 @@ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") |
@@ -42,3 +42,3 @@ # frozen_string_literal: true | ||
| @unique_by = find_unique_index_for(@unique_by) | ||
| @unique_by = find_unique_index_for(@unique_by) if @on_duplicate != :raise | ||
@@ -52,3 +52,3 @@ configure_on_duplicate_update_logic | ||
| message = +"#{model} " | ||
| message = +"#{model.name} " | ||
| message << "Bulk " if inserts.many? | ||
@@ -55,0 +55,0 @@ message << (on_duplicate == :update ? "Upsert" : "Insert") |
@@ -142,3 +142,3 @@ # frozen_string_literal: true | ||
| commands = recorder.commands | ||
| @commands << [:change_table, [table_name], -> t { bulk_change_table(table_name, commands) }] | ||
| @commands << [:change_table, [table_name], -> t { bulk_change_table(t.name, commands.reverse) }] | ||
| else | ||
@@ -145,0 +145,0 @@ yield delegate.update_table_definition(table_name, self) |
@@ -1013,3 +1013,3 @@ # frozen_string_literal: true | ||
| # | ||
| # Both calls delete the affected posts all at once with a single DELETE statement. | ||
| # This call deletes 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 | ||
@@ -1016,0 +1016,0 @@ # +after_destroy+ callbacks, use the #destroy_all method instead. |
@@ -151,3 +151,3 @@ # frozen_string_literal: true | ||
| else | ||
| raise ActiveRecord::SoleRecordExceeded.new(model) | ||
| raise ActiveRecord::SoleRecordExceeded.new(self) | ||
| end | ||
@@ -154,0 +154,0 @@ end |
@@ -34,3 +34,5 @@ # frozen_string_literal: true | ||
| array_predicates = ranges.map! { |range| predicate_builder.build(attribute, range) } | ||
| array_predicates.inject(values_predicate, &:or) | ||
| values_predicate.or( | ||
| Arel::Nodes::Grouping.new Arel::Nodes::Or.new(array_predicates) | ||
| ) | ||
| end | ||
@@ -37,0 +39,0 @@ end |
@@ -185,3 +185,3 @@ # frozen_string_literal: true | ||
| predicates.reject do |node| | ||
| if !non_attrs.empty? && node.equality? && node.left.is_a?(Arel::Predications) | ||
| if !non_attrs.empty? && equality_node?(node) && node.left.is_a?(Arel::Predications) | ||
| non_attrs.include?(node.left) | ||
@@ -188,0 +188,0 @@ end || Arel.fetch_attribute(node) do |attr| |
@@ -61,2 +61,7 @@ # frozen_string_literal: true | ||
| 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) | ||
@@ -63,0 +68,0 @@ value == coder.load(nil) |
@@ -234,5 +234,3 @@ # frozen_string_literal: true | ||
| nodes = others.map { |expr| send(method_id, expr, *extras) } | ||
| Nodes::Grouping.new nodes.inject { |memo, node| | ||
| Nodes::Or.new([memo, node]) | ||
| } | ||
| Nodes::Grouping.new Nodes::Or.new(nodes) | ||
| end | ||
@@ -239,0 +237,0 @@ |
Sorry, the diff of this file is too big to display