activerecord 7.2.3 → 8.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +612 -1055
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +2 -1
  5. data/lib/active_record/associations/association.rb +35 -11
  6. data/lib/active_record/associations/builder/association.rb +23 -11
  7. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  8. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  9. data/lib/active_record/associations/builder/has_one.rb +1 -1
  10. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  11. data/lib/active_record/associations/collection_association.rb +1 -1
  12. data/lib/active_record/associations/collection_proxy.rb +22 -4
  13. data/lib/active_record/associations/deprecation.rb +88 -0
  14. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  15. data/lib/active_record/associations/errors.rb +3 -0
  16. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  17. data/lib/active_record/associations/join_dependency.rb +4 -2
  18. data/lib/active_record/associations/preloader/association.rb +2 -2
  19. data/lib/active_record/associations/preloader/batch.rb +7 -1
  20. data/lib/active_record/associations/preloader/branch.rb +1 -0
  21. data/lib/active_record/associations/singular_association.rb +8 -3
  22. data/lib/active_record/associations.rb +192 -24
  23. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  24. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  25. data/lib/active_record/attribute_methods/query.rb +34 -0
  26. data/lib/active_record/attribute_methods/serialization.rb +16 -3
  27. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  28. data/lib/active_record/attributes.rb +3 -0
  29. data/lib/active_record/autosave_association.rb +69 -27
  30. data/lib/active_record/base.rb +1 -2
  31. data/lib/active_record/coders/json.rb +14 -5
  32. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +35 -28
  33. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  34. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -13
  35. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +412 -88
  36. data/lib/active_record/connection_adapters/abstract/database_statements.rb +137 -75
  37. data/lib/active_record/connection_adapters/abstract/query_cache.rb +27 -5
  38. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -25
  39. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  40. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +32 -35
  41. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  42. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +122 -32
  43. data/lib/active_record/connection_adapters/abstract/transaction.rb +40 -8
  44. data/lib/active_record/connection_adapters/abstract_adapter.rb +150 -91
  45. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +63 -52
  46. data/lib/active_record/connection_adapters/column.rb +17 -4
  47. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  48. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  49. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  50. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  51. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +73 -46
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +89 -94
  53. data/lib/active_record/connection_adapters/mysql2_adapter.rb +2 -10
  54. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  55. data/lib/active_record/connection_adapters/postgresql/column.rb +8 -2
  56. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +76 -45
  57. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  58. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  59. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  60. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  61. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  62. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  63. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +14 -33
  64. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +71 -32
  65. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +139 -63
  66. data/lib/active_record/connection_adapters/postgresql_adapter.rb +78 -105
  67. data/lib/active_record/connection_adapters/schema_cache.rb +3 -5
  68. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  69. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +90 -98
  70. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  71. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  72. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  73. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +13 -14
  74. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +102 -37
  75. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  76. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +38 -67
  77. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
  78. data/lib/active_record/connection_adapters.rb +1 -56
  79. data/lib/active_record/connection_handling.rb +25 -2
  80. data/lib/active_record/core.rb +33 -17
  81. data/lib/active_record/counter_cache.rb +33 -8
  82. data/lib/active_record/database_configurations/database_config.rb +9 -1
  83. data/lib/active_record/database_configurations/hash_config.rb +67 -9
  84. data/lib/active_record/database_configurations/url_config.rb +13 -3
  85. data/lib/active_record/database_configurations.rb +7 -3
  86. data/lib/active_record/delegated_type.rb +1 -1
  87. data/lib/active_record/dynamic_matchers.rb +54 -69
  88. data/lib/active_record/encryption/config.rb +3 -1
  89. data/lib/active_record/encryption/encryptable_record.rb +8 -8
  90. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  91. data/lib/active_record/encryption/encryptor.rb +28 -8
  92. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  93. data/lib/active_record/encryption/scheme.rb +9 -2
  94. data/lib/active_record/enum.rb +33 -30
  95. data/lib/active_record/errors.rb +33 -9
  96. data/lib/active_record/explain.rb +1 -1
  97. data/lib/active_record/explain_registry.rb +51 -2
  98. data/lib/active_record/filter_attribute_handler.rb +73 -0
  99. data/lib/active_record/fixtures.rb +2 -4
  100. data/lib/active_record/future_result.rb +15 -9
  101. data/lib/active_record/gem_version.rb +2 -2
  102. data/lib/active_record/inheritance.rb +1 -1
  103. data/lib/active_record/insert_all.rb +14 -9
  104. data/lib/active_record/locking/optimistic.rb +8 -1
  105. data/lib/active_record/locking/pessimistic.rb +5 -0
  106. data/lib/active_record/log_subscriber.rb +3 -13
  107. data/lib/active_record/middleware/shard_selector.rb +34 -17
  108. data/lib/active_record/migration/command_recorder.rb +45 -12
  109. data/lib/active_record/migration/compatibility.rb +37 -24
  110. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  111. data/lib/active_record/migration.rb +48 -42
  112. data/lib/active_record/model_schema.rb +38 -13
  113. data/lib/active_record/nested_attributes.rb +6 -6
  114. data/lib/active_record/persistence.rb +162 -133
  115. data/lib/active_record/query_cache.rb +22 -15
  116. data/lib/active_record/query_logs.rb +100 -52
  117. data/lib/active_record/query_logs_formatter.rb +17 -28
  118. data/lib/active_record/querying.rb +8 -8
  119. data/lib/active_record/railtie.rb +35 -30
  120. data/lib/active_record/railties/controller_runtime.rb +11 -6
  121. data/lib/active_record/railties/databases.rake +26 -38
  122. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  123. data/lib/active_record/railties/job_runtime.rb +10 -11
  124. data/lib/active_record/reflection.rb +53 -21
  125. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  126. data/lib/active_record/relation/batches.rb +147 -73
  127. data/lib/active_record/relation/calculations.rb +52 -40
  128. data/lib/active_record/relation/delegation.rb +25 -15
  129. data/lib/active_record/relation/finder_methods.rb +40 -24
  130. data/lib/active_record/relation/merger.rb +8 -8
  131. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  132. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  133. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  134. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  135. data/lib/active_record/relation/predicate_builder.rb +22 -7
  136. data/lib/active_record/relation/query_attribute.rb +3 -1
  137. data/lib/active_record/relation/query_methods.rb +140 -86
  138. data/lib/active_record/relation/spawn_methods.rb +7 -7
  139. data/lib/active_record/relation/where_clause.rb +2 -9
  140. data/lib/active_record/relation.rb +107 -75
  141. data/lib/active_record/result.rb +109 -24
  142. data/lib/active_record/runtime_registry.rb +42 -58
  143. data/lib/active_record/sanitization.rb +9 -6
  144. data/lib/active_record/schema_dumper.rb +18 -11
  145. data/lib/active_record/schema_migration.rb +2 -1
  146. data/lib/active_record/scoping/named.rb +5 -2
  147. data/lib/active_record/scoping.rb +0 -1
  148. data/lib/active_record/signed_id.rb +43 -15
  149. data/lib/active_record/statement_cache.rb +24 -20
  150. data/lib/active_record/store.rb +51 -22
  151. data/lib/active_record/structured_event_subscriber.rb +85 -0
  152. data/lib/active_record/table_metadata.rb +6 -23
  153. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  154. data/lib/active_record/tasks/database_tasks.rb +85 -85
  155. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -42
  156. data/lib/active_record/tasks/postgresql_database_tasks.rb +7 -40
  157. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -28
  158. data/lib/active_record/test_databases.rb +14 -4
  159. data/lib/active_record/test_fixtures.rb +39 -2
  160. data/lib/active_record/testing/query_assertions.rb +8 -2
  161. data/lib/active_record/timestamp.rb +4 -2
  162. data/lib/active_record/token_for.rb +1 -1
  163. data/lib/active_record/transaction.rb +2 -5
  164. data/lib/active_record/transactions.rb +37 -16
  165. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  166. data/lib/active_record/type/internal/timezone.rb +7 -0
  167. data/lib/active_record/type/json.rb +13 -2
  168. data/lib/active_record/type/serialized.rb +16 -4
  169. data/lib/active_record/type/type_map.rb +1 -1
  170. data/lib/active_record/type_caster/connection.rb +2 -1
  171. data/lib/active_record/validations/associated.rb +1 -1
  172. data/lib/active_record/validations/uniqueness.rb +8 -8
  173. data/lib/active_record.rb +84 -49
  174. data/lib/arel/alias_predication.rb +2 -0
  175. data/lib/arel/collectors/bind.rb +2 -2
  176. data/lib/arel/collectors/sql_string.rb +1 -1
  177. data/lib/arel/collectors/substitute_binds.rb +2 -2
  178. data/lib/arel/crud.rb +6 -11
  179. data/lib/arel/nodes/binary.rb +1 -1
  180. data/lib/arel/nodes/count.rb +2 -2
  181. data/lib/arel/nodes/function.rb +4 -10
  182. data/lib/arel/nodes/named_function.rb +2 -2
  183. data/lib/arel/nodes/node.rb +2 -2
  184. data/lib/arel/nodes/sql_literal.rb +1 -1
  185. data/lib/arel/nodes.rb +0 -2
  186. data/lib/arel/predications.rb +1 -3
  187. data/lib/arel/select_manager.rb +7 -2
  188. data/lib/arel/table.rb +3 -7
  189. data/lib/arel/visitors/dot.rb +0 -3
  190. data/lib/arel/visitors/postgresql.rb +55 -0
  191. data/lib/arel/visitors/sqlite.rb +55 -8
  192. data/lib/arel/visitors/to_sql.rb +3 -21
  193. data/lib/arel.rb +3 -1
  194. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  195. metadata +16 -13
  196. data/lib/active_record/explain_subscriber.rb +0 -34
  197. data/lib/active_record/normalization.rb +0 -163
  198. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
@@ -248,18 +248,16 @@ module ActiveRecord
248
248
 
249
249
  im = Arel::InsertManager.new(arel_table)
250
250
 
251
- with_connection do |c|
252
- if values.empty?
253
- im.insert(connection.empty_insert_statement_value(primary_key))
254
- else
255
- im.insert(values.transform_keys { |name| arel_table[name] })
256
- end
257
-
258
- connection.insert(
259
- im, "#{self} Create", primary_key || false, primary_key_value,
260
- returning: returning
261
- )
251
+ if values.empty?
252
+ im.insert(connection.empty_insert_statement_value(primary_key))
253
+ else
254
+ im.insert(values.transform_keys { |name| arel_table[name] })
262
255
  end
256
+
257
+ connection.insert(
258
+ im, "#{self} Create", primary_key || false, primary_key_value,
259
+ returning: returning
260
+ )
263
261
  end
264
262
 
265
263
  def _update_record(values, constraints) # :nodoc:
@@ -494,6 +492,7 @@ module ActiveRecord
494
492
  becoming.instance_variable_set(:@attributes, @attributes)
495
493
  becoming.instance_variable_set(:@mutations_from_database, @mutations_from_database ||= nil)
496
494
  becoming.instance_variable_set(:@new_record, new_record?)
495
+ becoming.instance_variable_set(:@previously_new_record, previously_new_record?)
497
496
  becoming.instance_variable_set(:@destroyed, destroyed?)
498
497
  becoming.errors.copy!(errors)
499
498
  end
@@ -583,8 +582,8 @@ module ActiveRecord
583
582
  end
584
583
 
585
584
  # Equivalent to <code>update_columns(name => value)</code>.
586
- def update_column(name, value)
587
- update_columns(name => value)
585
+ def update_column(name, value, touch: nil)
586
+ update_columns(name => value, touch: touch)
588
587
  end
589
588
 
590
589
  # Updates the attributes directly in the database issuing an UPDATE SQL
@@ -598,11 +597,25 @@ module ActiveRecord
598
597
  #
599
598
  # * \Validations are skipped.
600
599
  # * \Callbacks are skipped.
601
- # * +updated_at+/+updated_on+ are not updated.
600
+ # * +updated_at+/+updated_on+ are updated if the +touch+ option is set to +true+.
602
601
  # * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all
603
602
  #
604
603
  # This method raises an ActiveRecord::ActiveRecordError when called on new
605
604
  # objects, or when at least one of the attributes is marked as readonly.
605
+ #
606
+ # ==== Parameters
607
+ #
608
+ # * <tt>:touch</tt> option - Touch the timestamp columns when updating.
609
+ # * If attribute names are passed, they are updated along with +updated_at+/+updated_on+ attributes.
610
+ #
611
+ # ==== Examples
612
+ #
613
+ # # Update a single attribute.
614
+ # user.update_columns(last_request_at: Time.current)
615
+ #
616
+ # # Update with touch option.
617
+ # user.update_columns(last_request_at: Time.current, touch: true)
618
+
606
619
  def update_columns(attributes)
607
620
  raise ActiveRecordError, "cannot update a new record" if new_record?
608
621
  raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
@@ -614,6 +627,15 @@ module ActiveRecord
614
627
  verify_readonly_attribute(name) || name
615
628
  end
616
629
 
630
+ touch = attributes.delete("touch")
631
+ if touch
632
+ names = touch if touch != true
633
+ names = Array.wrap(names)
634
+ options = names.extract_options!
635
+ touch_updates = self.class.touch_attributes_with_time(*names, **options)
636
+ attributes.with_defaults!(touch_updates) unless touch_updates.empty?
637
+ end
638
+
617
639
  update_constraints = _query_constraints_hash
618
640
  attributes = attributes.each_with_object({}) do |(k, v), h|
619
641
  h[k] = @attributes.write_cast_value(k, v)
@@ -642,8 +664,15 @@ module ActiveRecord
642
664
  # This means that any other modified attributes will still be dirty.
643
665
  # Validations and callbacks are skipped. Supports the +touch+ option from
644
666
  # +update_counters+, see that for more.
667
+ #
668
+ # This method raises an ActiveRecord::ActiveRecordError when called on new
669
+ # objects, or when at least one of the attributes is marked as readonly.
670
+ #
645
671
  # Returns +self+.
646
672
  def increment!(attribute, by = 1, touch: nil)
673
+ raise ActiveRecordError, "cannot update a new record" if new_record?
674
+ raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
675
+
647
676
  increment(attribute, by)
648
677
  change = public_send(attribute) - (public_send(:"#{attribute}_in_database") || 0)
649
678
  self.class.update_counters(id, attribute => change, touch: touch)
@@ -812,159 +841,159 @@ module ActiveRecord
812
841
  end
813
842
  end
814
843
 
815
- private
816
- def init_internals
817
- super
818
- @_trigger_destroy_callback = @_trigger_update_callback = nil
819
- @previously_new_record = false
820
- end
844
+ private
845
+ def init_internals
846
+ super
847
+ @_trigger_destroy_callback = @_trigger_update_callback = nil
848
+ @previously_new_record = false
849
+ end
821
850
 
822
- def strict_loaded_associations
823
- @association_cache.find_all do |_, assoc|
824
- assoc.owner.strict_loading? && !assoc.owner.strict_loading_n_plus_one_only?
825
- end.map(&:first)
826
- end
851
+ def strict_loaded_associations
852
+ @association_cache.find_all do |_, assoc|
853
+ assoc.owner.strict_loading? && !assoc.owner.strict_loading_n_plus_one_only?
854
+ end.map(&:first)
855
+ end
827
856
 
828
- def _find_record(options)
829
- all_queries = options ? options[:all_queries] : nil
830
- base = self.class.all(all_queries: all_queries).preload(strict_loaded_associations)
857
+ def _find_record(options)
858
+ all_queries = options ? options[:all_queries] : nil
859
+ base = self.class.all(all_queries: all_queries).preload(strict_loaded_associations)
831
860
 
832
- if options && options[:lock]
833
- base.lock(options[:lock]).find_by!(_in_memory_query_constraints_hash)
834
- else
835
- base.find_by!(_in_memory_query_constraints_hash)
861
+ if options && options[:lock]
862
+ base.lock(options[:lock]).find_by!(_in_memory_query_constraints_hash)
863
+ else
864
+ base.find_by!(_in_memory_query_constraints_hash)
865
+ end
836
866
  end
837
- end
838
867
 
839
- def _in_memory_query_constraints_hash
840
- if self.class.query_constraints_list.nil?
841
- { @primary_key => id }
842
- else
843
- self.class.query_constraints_list.index_with do |column_name|
844
- attribute(column_name)
868
+ def _in_memory_query_constraints_hash
869
+ if self.class.query_constraints_list.nil?
870
+ { @primary_key => id }
871
+ else
872
+ self.class.query_constraints_list.index_with do |column_name|
873
+ attribute(column_name)
874
+ end
845
875
  end
846
876
  end
847
- end
848
877
 
849
- def apply_scoping?(options)
850
- !(options && options[:unscoped]) &&
851
- (self.class.default_scopes?(all_queries: true) || self.class.global_current_scope)
852
- end
878
+ def apply_scoping?(options)
879
+ !(options && options[:unscoped]) &&
880
+ (self.class.default_scopes?(all_queries: true) || self.class.global_current_scope)
881
+ end
853
882
 
854
- def _query_constraints_hash
855
- if self.class.query_constraints_list.nil?
856
- { @primary_key => id_in_database }
857
- else
858
- self.class.query_constraints_list.index_with do |column_name|
859
- attribute_in_database(column_name)
883
+ def _query_constraints_hash
884
+ if self.class.query_constraints_list.nil?
885
+ { @primary_key => id_in_database }
886
+ else
887
+ self.class.query_constraints_list.index_with do |column_name|
888
+ attribute_in_database(column_name)
889
+ end
860
890
  end
861
891
  end
862
- end
863
892
 
864
- # A hook to be overridden by association modules.
865
- def destroy_associations
866
- end
867
-
868
- def destroy_row
869
- _delete_row
870
- end
871
-
872
- def _delete_row
873
- self.class._delete_record(_query_constraints_hash)
874
- end
893
+ # A hook to be overridden by association modules.
894
+ def destroy_associations
895
+ end
875
896
 
876
- def _touch_row(attribute_names, time)
877
- time ||= current_time_from_proper_timezone
897
+ def destroy_row
898
+ _delete_row
899
+ end
878
900
 
879
- attribute_names.each do |attr_name|
880
- _write_attribute(attr_name, time)
901
+ def _delete_row
902
+ self.class._delete_record(_query_constraints_hash)
881
903
  end
882
904
 
883
- _update_row(attribute_names, "touch")
884
- end
905
+ def _touch_row(attribute_names, time)
906
+ time ||= current_time_from_proper_timezone
885
907
 
886
- def _update_row(attribute_names, attempted_action = "update")
887
- self.class._update_record(
888
- attributes_with_values(attribute_names),
889
- _query_constraints_hash
890
- )
891
- end
908
+ attribute_names.each do |attr_name|
909
+ _write_attribute(attr_name, time)
910
+ end
892
911
 
893
- def create_or_update(**, &block)
894
- _raise_readonly_record_error if readonly?
895
- return false if destroyed?
896
- result = new_record? ? _create_record(&block) : _update_record(&block)
897
- result != false
898
- end
912
+ _update_row(attribute_names, "touch")
913
+ end
899
914
 
900
- # Updates the associated record with values matching those of the instance attributes.
901
- # Returns the number of affected rows.
902
- def _update_record(attribute_names = self.attribute_names)
903
- attribute_names = attributes_for_update(attribute_names)
915
+ def _update_row(attribute_names, attempted_action = "update")
916
+ self.class._update_record(
917
+ attributes_with_values(attribute_names),
918
+ _query_constraints_hash
919
+ )
920
+ end
904
921
 
905
- if attribute_names.empty?
906
- affected_rows = 0
907
- @_trigger_update_callback = true
908
- else
909
- affected_rows = _update_row(attribute_names)
910
- @_trigger_update_callback = affected_rows == 1
922
+ def create_or_update(**, &block)
923
+ _raise_readonly_record_error if readonly?
924
+ return false if destroyed?
925
+ result = new_record? ? _create_record(&block) : _update_record(&block)
926
+ result != false
911
927
  end
912
928
 
913
- @previously_new_record = false
929
+ # Updates the associated record with values matching those of the instance attributes.
930
+ # Returns the number of affected rows.
931
+ def _update_record(attribute_names = self.attribute_names)
932
+ attribute_names = attributes_for_update(attribute_names)
933
+
934
+ if attribute_names.empty?
935
+ affected_rows = 0
936
+ @_trigger_update_callback = true
937
+ else
938
+ affected_rows = _update_row(attribute_names)
939
+ @_trigger_update_callback = affected_rows == 1
940
+ end
914
941
 
915
- yield(self) if block_given?
942
+ @previously_new_record = false
916
943
 
917
- affected_rows
918
- end
944
+ yield(self) if block_given?
919
945
 
920
- # Creates a record with values matching those of the instance attributes
921
- # and returns its id.
922
- def _create_record(attribute_names = self.attribute_names)
923
- attribute_names = attributes_for_create(attribute_names)
946
+ affected_rows
947
+ end
924
948
 
925
- self.class.with_connection do |connection|
926
- returning_columns = self.class._returning_columns_for_insert(connection)
949
+ # Creates a record with values matching those of the instance attributes
950
+ # and returns its id.
951
+ def _create_record(attribute_names = self.attribute_names)
952
+ attribute_names = attributes_for_create(attribute_names)
927
953
 
928
- returning_values = self.class._insert_record(
929
- connection,
930
- attributes_with_values(attribute_names),
931
- returning_columns
932
- )
954
+ self.class.with_connection do |connection|
955
+ returning_columns = self.class._returning_columns_for_insert(connection)
933
956
 
934
- returning_columns.zip(returning_values).each do |column, value|
935
- _write_attribute(column, value) if !_read_attribute(column)
936
- end if returning_values
937
- end
957
+ returning_values = self.class._insert_record(
958
+ connection,
959
+ attributes_with_values(attribute_names),
960
+ returning_columns
961
+ )
938
962
 
939
- @new_record = false
940
- @previously_new_record = true
963
+ returning_columns.zip(returning_values).each do |column, value|
964
+ _write_attribute(column, type_for_attribute(column).deserialize(value)) if !_read_attribute(column)
965
+ end if returning_values
966
+ end
941
967
 
942
- yield(self) if block_given?
968
+ @new_record = false
969
+ @previously_new_record = true
943
970
 
944
- id
945
- end
971
+ yield(self) if block_given?
946
972
 
947
- def verify_readonly_attribute(name)
948
- raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attribute?(name)
949
- end
973
+ id
974
+ end
950
975
 
951
- def _raise_record_not_destroyed
952
- @_association_destroy_exception ||= nil
953
- key = self.class.primary_key
954
- raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy #{self.class} with #{key}=#{id}", self)
955
- ensure
956
- @_association_destroy_exception = nil
957
- end
976
+ def verify_readonly_attribute(name)
977
+ raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attribute?(name)
978
+ end
958
979
 
959
- def _raise_readonly_record_error
960
- raise ReadOnlyRecord, "#{self.class} is marked as readonly"
961
- end
980
+ def _raise_record_not_destroyed
981
+ @_association_destroy_exception ||= nil
982
+ key = self.class.primary_key
983
+ raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy #{self.class} with #{key}=#{id}", self)
984
+ ensure
985
+ @_association_destroy_exception = nil
986
+ end
962
987
 
963
- def _raise_record_not_touched_error
964
- raise ActiveRecordError, <<~MSG.squish
965
- Cannot touch on a new or destroyed record object. Consider using
966
- persisted?, new_record?, or destroyed? before touching.
967
- MSG
968
- end
988
+ def _raise_readonly_record_error
989
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly"
990
+ end
991
+
992
+ def _raise_record_not_touched_error
993
+ raise ActiveRecordError, <<~MSG.squish
994
+ Cannot touch on a new or destroyed record object. Consider using
995
+ persisted?, new_record?, or destroyed? before touching.
996
+ MSG
997
+ end
969
998
  end
970
999
  end
@@ -3,6 +3,7 @@
3
3
  module ActiveRecord
4
4
  # = Active Record Query Cache
5
5
  class QueryCache
6
+ # ActiveRecord::Base extends this module, so these methods are available in models.
6
7
  module ClassMethods
7
8
  # Enable the query cache within the block if Active Record is configured.
8
9
  # If it's not, it will execute the given block.
@@ -20,11 +21,15 @@ module ActiveRecord
20
21
  end
21
22
  end
22
23
 
23
- # Disable the query cache within the block if Active Record is configured.
24
- # If it's not, it will execute the given block.
24
+ # Runs the block with the query cache disabled.
25
+ #
26
+ # If the query cache was enabled before the block was executed, it is
27
+ # enabled again after it.
25
28
  #
26
- # Set <tt>dirties: false</tt> to prevent query caches on all connections from being cleared by write operations.
27
- # (By default, write operations dirty all connections' query caches in case they are replicas whose cache would now be outdated.)
29
+ # Set <tt>dirties: false</tt> to prevent query caches on all connections
30
+ # from being cleared by write operations. (By default, write operations
31
+ # dirty all connections' query caches in case they are replicas whose
32
+ # cache would now be outdated.)
28
33
  def uncached(dirties: true, &block)
29
34
  if connected? || !configurations.empty?
30
35
  connection_pool.disable_query_cache(dirties: dirties, &block)
@@ -34,22 +39,24 @@ module ActiveRecord
34
39
  end
35
40
  end
36
41
 
37
- def self.run
38
- ActiveRecord::Base.connection_handler.each_connection_pool.reject(&:query_cache_enabled).each do |pool|
39
- next if pool.db_config&.query_cache == false
40
- pool.enable_query_cache!
42
+ module ExecutorHooks # :nodoc:
43
+ def self.run
44
+ ActiveRecord::Base.connection_handler.each_connection_pool.reject(&:query_cache_enabled).each do |pool|
45
+ next if pool.db_config&.query_cache == false
46
+ pool.enable_query_cache!
47
+ end
41
48
  end
42
- end
43
49
 
44
- def self.complete(pools)
45
- pools.each do |pool|
46
- pool.disable_query_cache!
47
- pool.clear_query_cache
50
+ def self.complete(pools)
51
+ pools.each do |pool|
52
+ pool.disable_query_cache!
53
+ pool.clear_query_cache
54
+ end
48
55
  end
49
56
  end
50
57
 
51
- def self.install_executor_hooks(executor = ActiveSupport::Executor)
52
- executor.register_hook(self)
58
+ def self.install_executor_hooks(executor = ActiveSupport::Executor) # :nodoc:
59
+ executor.register_hook(ExecutorHooks)
53
60
  end
54
61
  end
55
62
  end
@@ -69,21 +69,77 @@ module ActiveRecord
69
69
  #
70
70
  # Tag comments can be prepended to the query:
71
71
  #
72
- # ActiveRecord::QueryLogs.prepend_comment = true
72
+ # config.active_record.query_log_tags_prepend_comment = true
73
73
  #
74
74
  # For applications where the content will not change during the lifetime of
75
75
  # the request or job execution, the tags can be cached for reuse in every query:
76
76
  #
77
77
  # config.active_record.cache_query_log_tags = true
78
78
  module QueryLogs
79
- mattr_accessor :taggings, instance_accessor: false, default: {}
80
- mattr_accessor :tags, instance_accessor: false, default: [ :application ]
81
- mattr_accessor :prepend_comment, instance_accessor: false, default: false
82
- mattr_accessor :cache_query_log_tags, instance_accessor: false, default: false
83
- mattr_accessor :tags_formatter, instance_accessor: false
79
+ class GetKeyHandler # :nodoc:
80
+ def initialize(name)
81
+ @name = name
82
+ end
83
+
84
+ def call(context)
85
+ context[@name]
86
+ end
87
+ end
88
+
89
+ class IdentityHandler # :nodoc:
90
+ def initialize(value)
91
+ @value = value
92
+ end
93
+
94
+ def call(_context)
95
+ @value
96
+ end
97
+ end
98
+
99
+ class ZeroArityHandler # :nodoc:
100
+ def initialize(proc)
101
+ @proc = proc
102
+ end
103
+
104
+ def call(_context)
105
+ @proc.call
106
+ end
107
+ end
108
+
109
+ @taggings = {}.freeze
110
+ @tags = [ :application ].freeze
111
+ @prepend_comment = false
112
+ @cache_query_log_tags = false
113
+ @tags_formatter = false
114
+
84
115
  thread_mattr_accessor :cached_comment, instance_accessor: false
85
116
 
86
117
  class << self
118
+ attr_reader :tags, :taggings, :tags_formatter # :nodoc:
119
+ attr_accessor :prepend_comment, :cache_query_log_tags # :nodoc:
120
+
121
+ def taggings=(taggings) # :nodoc:
122
+ @taggings = taggings.freeze
123
+ @handlers = rebuild_handlers
124
+ end
125
+
126
+ def tags=(tags) # :nodoc:
127
+ @tags = tags.freeze
128
+ @handlers = rebuild_handlers
129
+ end
130
+
131
+ def tags_formatter=(format) # :nodoc:
132
+ @formatter = case format
133
+ when :legacy
134
+ LegacyFormatter
135
+ when :sqlcommenter
136
+ SQLCommenter
137
+ else
138
+ raise ArgumentError, "Formatter is unsupported: #{format}"
139
+ end
140
+ @tags_formatter = format
141
+ end
142
+
87
143
  def call(sql, connection) # :nodoc:
88
144
  comment = self.comment(connection)
89
145
 
@@ -100,36 +156,42 @@ module ActiveRecord
100
156
  self.cached_comment = nil
101
157
  end
102
158
 
103
- # Updates the formatter to be what the passed in format is.
104
- def update_formatter(format)
105
- self.tags_formatter =
106
- case format
107
- when :legacy
108
- LegacyFormatter.new
109
- when :sqlcommenter
110
- SQLCommenter.new
111
- else
112
- raise ArgumentError, "Formatter is unsupported: #{formatter}"
113
- end
159
+ def query_source_location # :nodoc:
160
+ LogSubscriber.backtrace_cleaner.first_clean_frame
114
161
  end
115
162
 
116
- if Thread.respond_to?(:each_caller_location)
117
- def query_source_location # :nodoc:
118
- Thread.each_caller_location do |location|
119
- frame = LogSubscriber.backtrace_cleaner.clean_frame(location)
120
- return frame if frame
163
+ ActiveSupport::ExecutionContext.after_change { ActiveRecord::QueryLogs.clear_cache }
164
+
165
+ private
166
+ def rebuild_handlers
167
+ handlers = []
168
+ @tags.each do |i|
169
+ if i.is_a?(Hash)
170
+ i.each do |k, v|
171
+ handlers << [k, build_handler(k, v)]
172
+ end
173
+ else
174
+ handlers << [i, build_handler(i)]
175
+ end
121
176
  end
122
- nil
123
- end
124
- else
125
- def query_source_location # :nodoc:
126
- LogSubscriber.backtrace_cleaner.clean(caller_locations(1).each).first
177
+ handlers.sort_by! { |(key, _)| key.to_s }
127
178
  end
128
- end
129
179
 
130
- ActiveSupport::ExecutionContext.after_change { ActiveRecord::QueryLogs.clear_cache }
180
+ def build_handler(name, handler = nil)
181
+ handler ||= @taggings[name]
182
+ if handler.nil?
183
+ GetKeyHandler.new(name)
184
+ elsif handler.respond_to?(:call)
185
+ if handler.arity == 0
186
+ ZeroArityHandler.new(handler)
187
+ else
188
+ handler
189
+ end
190
+ else
191
+ IdentityHandler.new(handler)
192
+ end
193
+ end
131
194
 
132
- private
133
195
  # Returns an SQL comment +String+ containing the query log tags.
134
196
  # Sets and returns a cached comment if <tt>cache_query_log_tags</tt> is +true+.
135
197
  def comment(connection)
@@ -140,10 +202,6 @@ module ActiveRecord
140
202
  end
141
203
  end
142
204
 
143
- def formatter
144
- self.tags_formatter || self.update_formatter(:legacy)
145
- end
146
-
147
205
  def uncached_comment(connection)
148
206
  content = tag_content(connection)
149
207
 
@@ -153,7 +211,7 @@ module ActiveRecord
153
211
  end
154
212
 
155
213
  def escape_sql_comment(content)
156
- # Sanitize a string to appear within a SQL comment
214
+ # Sanitize a string to appear within an SQL comment
157
215
  # For compatibility, this also surrounding "/*+", "/*", and "*/"
158
216
  # characters, possibly with single surrounding space.
159
217
  # Then follows that by replacing any internal "*/" or "/ *" with
@@ -169,25 +227,15 @@ module ActiveRecord
169
227
  context = ActiveSupport::ExecutionContext.to_h
170
228
  context[:connection] ||= connection
171
229
 
172
- pairs = tags.flat_map { |i| [*i] }.filter_map do |tag|
173
- key, handler = tag
174
- handler ||= taggings[key]
175
-
176
- val = if handler.nil?
177
- context[key]
178
- elsif handler.respond_to?(:call)
179
- if handler.arity == 0
180
- handler.call
181
- else
182
- handler.call(context)
183
- end
184
- else
185
- handler
186
- end
187
- [key, val] unless val.nil?
230
+ pairs = @handlers.filter_map do |(key, handler)|
231
+ val = handler.call(context)
232
+ @formatter.format(key, val) unless val.nil?
188
233
  end
189
- self.formatter.format(pairs)
234
+ @formatter.join(pairs)
190
235
  end
191
236
  end
237
+
238
+ @handlers = rebuild_handlers
239
+ self.tags_formatter = :legacy
192
240
  end
193
241
  end