activerecord 7.1.5.2 → 8.1.2.1

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 (244) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +567 -2443
  3. data/README.rdoc +16 -16
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +3 -2
  6. data/lib/active_record/associations/alias_tracker.rb +31 -23
  7. data/lib/active_record/associations/association.rb +43 -12
  8. data/lib/active_record/associations/belongs_to_association.rb +31 -8
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/association.rb +23 -11
  11. data/lib/active_record/associations/builder/belongs_to.rb +18 -4
  12. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  13. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  14. data/lib/active_record/associations/builder/has_many.rb +3 -4
  15. data/lib/active_record/associations/builder/has_one.rb +3 -4
  16. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  17. data/lib/active_record/associations/collection_association.rb +17 -9
  18. data/lib/active_record/associations/collection_proxy.rb +36 -5
  19. data/lib/active_record/associations/deprecation.rb +88 -0
  20. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  21. data/lib/active_record/associations/errors.rb +268 -0
  22. data/lib/active_record/associations/has_many_association.rb +1 -1
  23. data/lib/active_record/associations/has_many_through_association.rb +10 -3
  24. data/lib/active_record/associations/join_dependency/join_association.rb +1 -1
  25. data/lib/active_record/associations/join_dependency.rb +4 -2
  26. data/lib/active_record/associations/nested_error.rb +47 -0
  27. data/lib/active_record/associations/preloader/association.rb +4 -3
  28. data/lib/active_record/associations/preloader/batch.rb +7 -1
  29. data/lib/active_record/associations/preloader/branch.rb +8 -1
  30. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  31. data/lib/active_record/associations/singular_association.rb +14 -3
  32. data/lib/active_record/associations/through_association.rb +1 -1
  33. data/lib/active_record/associations.rb +250 -315
  34. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  35. data/lib/active_record/attribute_assignment.rb +0 -2
  36. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  37. data/lib/active_record/attribute_methods/primary_key.rb +25 -61
  38. data/lib/active_record/attribute_methods/query.rb +34 -0
  39. data/lib/active_record/attribute_methods/read.rb +1 -13
  40. data/lib/active_record/attribute_methods/serialization.rb +20 -27
  41. data/lib/active_record/attribute_methods/time_zone_conversion.rb +19 -20
  42. data/lib/active_record/attribute_methods.rb +71 -75
  43. data/lib/active_record/attributes.rb +96 -68
  44. data/lib/active_record/autosave_association.rb +93 -58
  45. data/lib/active_record/base.rb +5 -7
  46. data/lib/active_record/callbacks.rb +1 -1
  47. data/lib/active_record/coders/json.rb +14 -5
  48. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +50 -126
  49. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  50. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +52 -13
  51. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +681 -135
  52. data/lib/active_record/connection_adapters/abstract/database_statements.rb +165 -86
  53. data/lib/active_record/connection_adapters/abstract/query_cache.rb +226 -78
  54. data/lib/active_record/connection_adapters/abstract/quoting.rb +79 -114
  55. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  56. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +37 -36
  57. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  58. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +136 -34
  59. data/lib/active_record/connection_adapters/abstract/transaction.rb +163 -68
  60. data/lib/active_record/connection_adapters/abstract_adapter.rb +197 -129
  61. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +112 -63
  62. data/lib/active_record/connection_adapters/column.rb +17 -4
  63. data/lib/active_record/connection_adapters/mysql/database_statements.rb +11 -3
  64. data/lib/active_record/connection_adapters/mysql/quoting.rb +50 -57
  65. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  66. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  67. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +79 -46
  68. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +97 -101
  69. data/lib/active_record/connection_adapters/mysql2_adapter.rb +14 -33
  70. data/lib/active_record/connection_adapters/pool_config.rb +14 -13
  71. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  72. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +98 -44
  73. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  74. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  75. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  76. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  77. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  78. data/lib/active_record/connection_adapters/postgresql/quoting.rb +79 -68
  79. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  80. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  81. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +44 -41
  82. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +69 -32
  83. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +155 -75
  84. data/lib/active_record/connection_adapters/postgresql_adapter.rb +104 -121
  85. data/lib/active_record/connection_adapters/schema_cache.rb +126 -133
  86. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  87. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +91 -95
  88. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +57 -54
  89. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +16 -0
  90. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  91. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +43 -2
  92. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +38 -15
  93. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +231 -111
  94. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  95. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +40 -69
  96. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -65
  97. data/lib/active_record/connection_adapters.rb +66 -0
  98. data/lib/active_record/connection_handling.rb +88 -46
  99. data/lib/active_record/core.rb +142 -58
  100. data/lib/active_record/counter_cache.rb +52 -18
  101. data/lib/active_record/database_configurations/connection_url_resolver.rb +9 -2
  102. data/lib/active_record/database_configurations/database_config.rb +28 -5
  103. data/lib/active_record/database_configurations/hash_config.rb +103 -41
  104. data/lib/active_record/database_configurations/url_config.rb +30 -1
  105. data/lib/active_record/database_configurations.rb +8 -4
  106. data/lib/active_record/delegated_type.rb +43 -19
  107. data/lib/active_record/dynamic_matchers.rb +56 -71
  108. data/lib/active_record/encryption/config.rb +3 -1
  109. data/lib/active_record/encryption/encryptable_record.rb +12 -12
  110. data/lib/active_record/encryption/encrypted_attribute_type.rb +35 -6
  111. data/lib/active_record/encryption/encryptor.rb +58 -22
  112. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  113. data/lib/active_record/encryption/key_provider.rb +1 -1
  114. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  115. data/lib/active_record/encryption/message_serializer.rb +4 -0
  116. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  117. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  118. data/lib/active_record/encryption/scheme.rb +9 -2
  119. data/lib/active_record/enum.rb +57 -36
  120. data/lib/active_record/errors.rb +76 -26
  121. data/lib/active_record/explain.rb +14 -25
  122. data/lib/active_record/explain_registry.rb +51 -2
  123. data/lib/active_record/filter_attribute_handler.rb +73 -0
  124. data/lib/active_record/fixture_set/table_row.rb +19 -2
  125. data/lib/active_record/fixtures.rb +39 -35
  126. data/lib/active_record/future_result.rb +21 -13
  127. data/lib/active_record/gem_version.rb +3 -3
  128. data/lib/active_record/inheritance.rb +5 -3
  129. data/lib/active_record/insert_all.rb +28 -20
  130. data/lib/active_record/integration.rb +4 -1
  131. data/lib/active_record/internal_metadata.rb +48 -34
  132. data/lib/active_record/locking/optimistic.rb +15 -7
  133. data/lib/active_record/locking/pessimistic.rb +5 -0
  134. data/lib/active_record/log_subscriber.rb +3 -34
  135. data/lib/active_record/message_pack.rb +1 -1
  136. data/lib/active_record/middleware/shard_selector.rb +34 -17
  137. data/lib/active_record/migration/command_recorder.rb +46 -14
  138. data/lib/active_record/migration/compatibility.rb +40 -25
  139. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  140. data/lib/active_record/migration/default_strategy.rb +4 -5
  141. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  142. data/lib/active_record/migration.rb +119 -103
  143. data/lib/active_record/model_schema.rb +66 -78
  144. data/lib/active_record/nested_attributes.rb +17 -9
  145. data/lib/active_record/persistence.rb +161 -454
  146. data/lib/active_record/query_cache.rb +33 -15
  147. data/lib/active_record/query_logs.rb +106 -39
  148. data/lib/active_record/query_logs_formatter.rb +17 -28
  149. data/lib/active_record/querying.rb +28 -16
  150. data/lib/active_record/railtie.rb +61 -74
  151. data/lib/active_record/railties/controller_runtime.rb +19 -5
  152. data/lib/active_record/railties/databases.rake +64 -78
  153. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  154. data/lib/active_record/railties/job_runtime.rb +10 -11
  155. data/lib/active_record/reflection.rb +147 -53
  156. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  157. data/lib/active_record/relation/batches.rb +153 -73
  158. data/lib/active_record/relation/calculations.rb +146 -96
  159. data/lib/active_record/relation/delegation.rb +30 -23
  160. data/lib/active_record/relation/finder_methods.rb +64 -34
  161. data/lib/active_record/relation/merger.rb +12 -14
  162. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  163. data/lib/active_record/relation/predicate_builder/association_query_value.rb +19 -11
  164. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  165. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  166. data/lib/active_record/relation/predicate_builder.rb +25 -10
  167. data/lib/active_record/relation/query_attribute.rb +4 -2
  168. data/lib/active_record/relation/query_methods.rb +351 -124
  169. data/lib/active_record/relation/spawn_methods.rb +9 -25
  170. data/lib/active_record/relation/where_clause.rb +17 -30
  171. data/lib/active_record/relation.rb +590 -124
  172. data/lib/active_record/result.rb +131 -59
  173. data/lib/active_record/runtime_registry.rb +44 -21
  174. data/lib/active_record/sanitization.rb +33 -25
  175. data/lib/active_record/schema.rb +8 -6
  176. data/lib/active_record/schema_dumper.rb +66 -31
  177. data/lib/active_record/schema_migration.rb +31 -14
  178. data/lib/active_record/scoping/named.rb +6 -2
  179. data/lib/active_record/scoping.rb +0 -1
  180. data/lib/active_record/secure_token.rb +3 -3
  181. data/lib/active_record/signed_id.rb +68 -20
  182. data/lib/active_record/statement_cache.rb +29 -25
  183. data/lib/active_record/store.rb +51 -22
  184. data/lib/active_record/structured_event_subscriber.rb +85 -0
  185. data/lib/active_record/table_metadata.rb +7 -33
  186. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  187. data/lib/active_record/tasks/database_tasks.rb +118 -90
  188. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -42
  189. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  190. data/lib/active_record/tasks/sqlite_database_tasks.rb +17 -28
  191. data/lib/active_record/test_databases.rb +14 -4
  192. data/lib/active_record/test_fixtures.rb +123 -89
  193. data/lib/active_record/testing/query_assertions.rb +127 -0
  194. data/lib/active_record/timestamp.rb +6 -4
  195. data/lib/active_record/token_for.rb +22 -12
  196. data/lib/active_record/touch_later.rb +1 -1
  197. data/lib/active_record/transaction.rb +129 -0
  198. data/lib/active_record/transactions.rb +97 -18
  199. data/lib/active_record/translation.rb +0 -2
  200. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  201. data/lib/active_record/type/internal/timezone.rb +7 -0
  202. data/lib/active_record/type/json.rb +15 -2
  203. data/lib/active_record/type/serialized.rb +12 -7
  204. data/lib/active_record/type/type_map.rb +1 -1
  205. data/lib/active_record/type_caster/connection.rb +5 -4
  206. data/lib/active_record/validations/associated.rb +10 -4
  207. data/lib/active_record/validations/uniqueness.rb +23 -18
  208. data/lib/active_record/validations.rb +4 -1
  209. data/lib/active_record.rb +203 -59
  210. data/lib/arel/alias_predication.rb +3 -1
  211. data/lib/arel/collectors/bind.rb +4 -2
  212. data/lib/arel/collectors/composite.rb +7 -0
  213. data/lib/arel/collectors/sql_string.rb +2 -2
  214. data/lib/arel/collectors/substitute_binds.rb +3 -3
  215. data/lib/arel/crud.rb +8 -11
  216. data/lib/arel/delete_manager.rb +5 -0
  217. data/lib/arel/nodes/binary.rb +1 -7
  218. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  219. data/lib/arel/nodes/count.rb +2 -2
  220. data/lib/arel/nodes/delete_statement.rb +4 -2
  221. data/lib/arel/nodes/function.rb +4 -10
  222. data/lib/arel/nodes/named_function.rb +2 -2
  223. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  224. data/lib/arel/nodes/node.rb +6 -5
  225. data/lib/arel/nodes/sql_literal.rb +8 -1
  226. data/lib/arel/nodes/update_statement.rb +4 -2
  227. data/lib/arel/nodes.rb +2 -4
  228. data/lib/arel/predications.rb +1 -1
  229. data/lib/arel/select_manager.rb +14 -5
  230. data/lib/arel/table.rb +3 -7
  231. data/lib/arel/tree_manager.rb +3 -2
  232. data/lib/arel/update_manager.rb +7 -1
  233. data/lib/arel/visitors/dot.rb +3 -3
  234. data/lib/arel/visitors/mysql.rb +9 -4
  235. data/lib/arel/visitors/postgresql.rb +56 -12
  236. data/lib/arel/visitors/sqlite.rb +80 -8
  237. data/lib/arel/visitors/to_sql.rb +34 -37
  238. data/lib/arel.rb +10 -4
  239. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  240. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  241. metadata +23 -14
  242. data/lib/active_record/explain_subscriber.rb +0 -34
  243. data/lib/active_record/normalization.rb +0 -167
  244. data/lib/active_record/relation/record_fetch_warning.rb +0 -49
@@ -36,11 +36,26 @@ module ActiveRecord
36
36
  class_attribute :pre_loaded_fixtures, default: false
37
37
  class_attribute :lock_threads, default: true
38
38
  class_attribute :fixture_sets, default: {}
39
+ class_attribute :database_transactions_config, default: {}
39
40
 
40
41
  ActiveSupport.run_load_hooks(:active_record_fixtures, self)
41
42
  end
42
43
 
43
44
  module ClassMethods
45
+ # Do not use transactional tests for the given database. This overrides
46
+ # the default setting as defined by `use_transactional_tests`, which
47
+ # applies to all database connection pools not explicitly configured here.
48
+ def skip_transactional_tests_for_database(database_name)
49
+ use_transactional_tests_for_database(database_name, false)
50
+ end
51
+
52
+ # Enable or disable transactions per database. This overrides the default
53
+ # setting as defined by `use_transactional_tests`, which applies to all
54
+ # database connection pools not explicitly configured here.
55
+ def use_transactional_tests_for_database(database_name, enabled = true)
56
+ self.database_transactions_config = database_transactions_config.merge(database_name => enabled)
57
+ end
58
+
44
59
  # Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
45
60
  #
46
61
  # Examples:
@@ -53,20 +68,6 @@ module ActiveRecord
53
68
  self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys)
54
69
  end
55
70
 
56
- def fixture_path # :nodoc:
57
- ActiveRecord.deprecator.warn(<<~WARNING)
58
- TestFixtures.fixture_path is deprecated and will be removed in Rails 7.2. Use .fixture_paths instead.
59
- If multiple fixture paths have been configured with .fixture_paths, then .fixture_path will just return
60
- the first path.
61
- WARNING
62
- fixture_paths.first
63
- end
64
-
65
- def fixture_path=(path) # :nodoc:
66
- ActiveRecord.deprecator.warn("TestFixtures.fixture_path= is deprecated and will be removed in Rails 7.2. Use .fixture_paths= instead.")
67
- self.fixture_paths = Array(path)
68
- end
69
-
70
71
  def fixtures(*fixture_set_names)
71
72
  if fixture_set_names.first == :all
72
73
  raise StandardError, "No fixture path found. Please set `#{self}.fixture_paths`." if fixture_paths.blank?
@@ -79,7 +80,7 @@ module ActiveRecord
79
80
  fixture_set_names = fixture_set_names.flatten.map(&:to_s)
80
81
  end
81
82
 
82
- self.fixture_table_names |= fixture_set_names
83
+ self.fixture_table_names = (fixture_table_names | fixture_set_names).sort
83
84
  setup_fixture_accessors(fixture_set_names)
84
85
  end
85
86
 
@@ -110,45 +111,96 @@ module ActiveRecord
110
111
  end
111
112
  end
112
113
 
113
- def fixture_path # :nodoc:
114
- ActiveRecord.deprecator.warn(<<~WARNING)
115
- TestFixtures#fixture_path is deprecated and will be removed in Rails 7.2. Use #fixture_paths instead.
116
- If multiple fixture paths have been configured with #fixture_paths, then #fixture_path will just return
117
- the first path.
118
- WARNING
119
- fixture_paths.first
120
- end
121
-
122
- def run_in_transaction?
123
- use_transactional_tests &&
124
- !self.class.uses_transaction?(name)
114
+ # Generic fixture accessor for fixture names that may conflict with other methods.
115
+ #
116
+ # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
117
+ # assert_equal "Ruby on Rails", fixture(:web_sites, :rubyonrails).name
118
+ def fixture(fixture_set_name, *fixture_names)
119
+ active_record_fixture(fixture_set_name, *fixture_names)
125
120
  end
126
121
 
127
- def setup_fixtures(config = ActiveRecord::Base)
128
- if pre_loaded_fixtures && !use_transactional_tests
129
- raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
122
+ private
123
+ def run_in_transaction?
124
+ has_explicit_config = database_transactions_config.any? { |_, enabled| enabled }
125
+ (use_transactional_tests || has_explicit_config) &&
126
+ !self.class.uses_transaction?(name)
130
127
  end
131
128
 
132
- @fixture_cache = {}
133
- @fixture_connections = []
134
- @@already_loaded_fixtures ||= {}
135
- @connection_subscriber = nil
136
- @saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
129
+ def setup_fixtures(config = ActiveRecord::Base)
130
+ if pre_loaded_fixtures && !use_transactional_tests
131
+ raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
132
+ end
137
133
 
138
- # Load fixtures once and begin transaction.
139
- if run_in_transaction?
140
- if @@already_loaded_fixtures[self.class]
141
- @loaded_fixtures = @@already_loaded_fixtures[self.class]
134
+ @fixture_cache = {}
135
+ @fixture_cache_key = [self.class.fixture_table_names.dup, self.class.fixture_paths.dup, self.class.fixture_class_names.dup]
136
+ @fixture_connection_pools = []
137
+ @@already_loaded_fixtures ||= {}
138
+ @connection_subscriber = nil
139
+ @saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
140
+
141
+ if run_in_transaction?
142
+ # Load fixtures once and begin transaction.
143
+ @loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key]
144
+ unless @loaded_fixtures
145
+ @@already_loaded_fixtures.clear
146
+ @loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key] = load_fixtures(config)
147
+ end
148
+
149
+ setup_transactional_fixtures
142
150
  else
151
+ # Load fixtures for every test.
152
+ ActiveRecord::FixtureSet.reset_cache
153
+ invalidate_already_loaded_fixtures
143
154
  @loaded_fixtures = load_fixtures(config)
144
- @@already_loaded_fixtures[self.class] = @loaded_fixtures
145
155
  end
156
+ setup_asynchronous_queries_session
157
+
158
+ # Instantiate fixtures for every test if requested.
159
+ instantiate_fixtures if use_instantiated_fixtures
160
+ end
161
+
162
+ def teardown_fixtures
163
+ teardown_asynchronous_queries_session
164
+
165
+ # Rollback changes if a transaction is active.
166
+ if run_in_transaction?
167
+ teardown_transactional_fixtures
168
+ else
169
+ ActiveRecord::FixtureSet.reset_cache
170
+ invalidate_already_loaded_fixtures
171
+ end
172
+
173
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
174
+ end
175
+
176
+ def setup_asynchronous_queries_session
177
+ @_async_queries_session = ActiveRecord::Base.asynchronous_queries_tracker.start_session
178
+ end
179
+
180
+ def teardown_asynchronous_queries_session
181
+ ActiveRecord::Base.asynchronous_queries_tracker.finalize_session(true) if @_async_queries_session
182
+ end
183
+
184
+ def invalidate_already_loaded_fixtures
185
+ @@already_loaded_fixtures.clear
186
+ end
187
+
188
+ def transactional_tests_for_pool?(pool)
189
+ database_transactions_config.fetch(pool.db_config.name.to_sym, use_transactional_tests)
190
+ end
191
+
192
+ def setup_transactional_fixtures
193
+ setup_shared_connection_pool
146
194
 
147
195
  # Begin transactions for connections already established
148
- @fixture_connections = enlist_fixture_connections
149
- @fixture_connections.each do |connection|
150
- connection.begin_transaction joinable: false, _lazy: false
151
- connection.pool.lock_thread = true if lock_threads
196
+ @fixture_connection_pools = ActiveRecord::Base.connection_handler.connection_pool_list(:writing)
197
+
198
+ # Filter to pools that want to use transactions
199
+ @fixture_connection_pools.select! { |pool| transactional_tests_for_pool?(pool) }
200
+
201
+ @fixture_connection_pools.each do |pool|
202
+ pool.pin_connection!(lock_threads)
203
+ pool.lease_connection
152
204
  end
153
205
 
154
206
  # When connections are established in the future, begin a transaction too
@@ -157,59 +209,33 @@ module ActiveRecord
157
209
  shard = payload[:shard] if payload.key?(:shard)
158
210
 
159
211
  if connection_name
160
- begin
161
- connection = ActiveRecord::Base.connection_handler.retrieve_connection(connection_name, shard: shard)
162
- rescue ConnectionNotEstablished
163
- connection = nil
164
- end
165
-
166
- if connection
212
+ pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(connection_name, shard: shard)
213
+ if pool
167
214
  setup_shared_connection_pool
168
215
 
169
- if !@fixture_connections.include?(connection)
170
- connection.begin_transaction joinable: false, _lazy: false
171
- connection.pool.lock_thread = true if lock_threads
172
- @fixture_connections << connection
216
+ # Don't begin a transaction if we've already done so, or are not using them for this pool
217
+ if !@fixture_connection_pools.include?(pool) && transactional_tests_for_pool?(pool)
218
+ pool.pin_connection!(lock_threads)
219
+ pool.lease_connection
220
+ @fixture_connection_pools << pool
173
221
  end
174
222
  end
175
223
  end
176
224
  end
177
-
178
- # Load fixtures for every test.
179
- else
180
- ActiveRecord::FixtureSet.reset_cache
181
- @@already_loaded_fixtures[self.class] = nil
182
- @loaded_fixtures = load_fixtures(config)
183
225
  end
184
226
 
185
- # Instantiate fixtures for every test if requested.
186
- instantiate_fixtures if use_instantiated_fixtures
187
- end
188
-
189
- def teardown_fixtures
190
- # Rollback changes if a transaction is active.
191
- if run_in_transaction?
227
+ def teardown_transactional_fixtures
192
228
  ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
193
- @fixture_connections.each do |connection|
194
- connection.rollback_transaction if connection.transaction_open?
195
- connection.pool.lock_thread = false
229
+
230
+ unless @fixture_connection_pools.map(&:unpin_connection!).all?
231
+ # Something caused the transaction to be committed or rolled back
232
+ # We can no longer trust the database is in a clean state.
233
+ @@already_loaded_fixtures.clear
196
234
  end
197
- @fixture_connections.clear
235
+ @fixture_connection_pools.clear
198
236
  teardown_shared_connection_pool
199
- else
200
- ActiveRecord::FixtureSet.reset_cache
201
237
  end
202
238
 
203
- ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
204
- end
205
-
206
- def enlist_fixture_connections
207
- setup_shared_connection_pool
208
-
209
- ActiveRecord::Base.connection_handler.connection_pool_list(:writing).map(&:connection)
210
- end
211
-
212
- private
213
239
  # Shares the writing connection pool with connections on
214
240
  # other handlers.
215
241
  #
@@ -272,22 +298,30 @@ module ActiveRecord
272
298
  use_instantiated_fixtures != :no_instances
273
299
  end
274
300
 
275
- def method_missing(name, *args, **kwargs, &block)
276
- if fs_name = fixture_sets[name.to_s]
277
- access_fixture(fs_name, *args, **kwargs, &block)
301
+ def method_missing(method, ...)
302
+ if fixture_sets.key?(method.name)
303
+ active_record_fixture(method, ...)
278
304
  else
279
305
  super
280
306
  end
281
307
  end
282
308
 
283
- def respond_to_missing?(name, include_private = false)
284
- if include_private && fixture_sets.key?(name.to_s)
309
+ def respond_to_missing?(method, include_private = false)
310
+ if include_private && fixture_sets.key?(method.name)
285
311
  true
286
312
  else
287
313
  super
288
314
  end
289
315
  end
290
316
 
317
+ def active_record_fixture(fixture_set_name, *fixture_names)
318
+ if fs_name = fixture_sets[fixture_set_name.name]
319
+ access_fixture(fs_name, *fixture_names)
320
+ else
321
+ raise StandardError, "No fixture set named '#{fixture_set_name.inspect}'"
322
+ end
323
+ end
324
+
291
325
  def access_fixture(fs_name, *fixture_names)
292
326
  force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
293
327
  return_single_record = fixture_names.size == 1
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Assertions
5
+ module QueryAssertions
6
+ # Asserts that the number of SQL queries executed in the given block matches the expected count.
7
+ #
8
+ # # Check for exact number of queries
9
+ # assert_queries_count(1) { Post.first }
10
+ #
11
+ # # Check for any number of queries
12
+ # assert_queries_count { Post.first }
13
+ #
14
+ # Any unmaterialized transactions will be materialized to ensure only
15
+ # queries attempted intside the block are counted.
16
+ #
17
+ # If the +:include_schema+ option is provided, any queries (including
18
+ # schema related) are counted. Setting this option also skips leasing a
19
+ # connection to materialize pending transactions since we want to count
20
+ # queries executed at connection open (e.g., type map).
21
+ #
22
+ # assert_queries_count(1, include_schema: true) { Post.columns }
23
+ #
24
+ def assert_queries_count(count = nil, include_schema: false, &block)
25
+ ActiveRecord::Base.lease_connection.materialize_transactions unless include_schema
26
+
27
+ counter = SQLCounter.new
28
+ ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
29
+ result = _assert_nothing_raised_or_warn("assert_queries_count", &block)
30
+ queries = include_schema ? counter.log_all : counter.log
31
+ if count
32
+ assert_equal count, queries.size, "#{queries.size} instead of #{count} queries were executed. Queries: #{queries.join("\n\n")}"
33
+ else
34
+ assert_operator queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
35
+ end
36
+ result
37
+ end
38
+ end
39
+
40
+ # Asserts that no SQL queries are executed in the given block.
41
+ #
42
+ # assert_no_queries { post.comments }
43
+ #
44
+ # If the +:include_schema+ option is provided, any queries (including schema related) are counted.
45
+ #
46
+ # assert_no_queries(include_schema: true) { Post.columns }
47
+ #
48
+ def assert_no_queries(include_schema: false, &block)
49
+ assert_queries_count(0, include_schema: include_schema, &block)
50
+ end
51
+
52
+ # Asserts that the SQL queries executed in the given block match expected pattern.
53
+ #
54
+ # # Check for exact number of queries
55
+ # assert_queries_match(/LIMIT \?/, count: 1) { Post.first }
56
+ #
57
+ # # Check for any number of queries
58
+ # assert_queries_match(/LIMIT \?/) { Post.first }
59
+ #
60
+ # If the +:include_schema+ option is provided, any queries (including schema related)
61
+ # that match the matcher are considered.
62
+ #
63
+ # assert_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
64
+ #
65
+ def assert_queries_match(match, count: nil, include_schema: false, &block)
66
+ ActiveRecord::Base.lease_connection.materialize_transactions
67
+
68
+ counter = SQLCounter.new
69
+ ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
70
+ result = _assert_nothing_raised_or_warn("assert_queries_match", &block)
71
+ queries = include_schema ? counter.log_all : counter.log
72
+ matched_queries = queries.select { |query| match === query }
73
+
74
+ if count
75
+ assert_equal count, matched_queries.size, "#{matched_queries.size} instead of #{count} queries were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
76
+ else
77
+ assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
78
+ end
79
+
80
+ result
81
+ end
82
+ end
83
+
84
+ # Asserts that no SQL queries matching the pattern are executed in the given block.
85
+ #
86
+ # assert_no_queries_match(/SELECT/i) { post.comments }
87
+ #
88
+ # If the +:include_schema+ option is provided, any queries (including schema related)
89
+ # that match the matcher are counted.
90
+ #
91
+ # assert_no_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
92
+ #
93
+ def assert_no_queries_match(match, include_schema: false, &block)
94
+ assert_queries_match(match, count: 0, include_schema: include_schema, &block)
95
+ end
96
+
97
+ class SQLCounter # :nodoc:
98
+ attr_reader :log_full, :log_all
99
+
100
+ def initialize
101
+ @log_full = []
102
+ @log_all = []
103
+ end
104
+
105
+ def log
106
+ @log_full.map(&:first)
107
+ end
108
+
109
+ def call(*, payload)
110
+ return if payload[:cached]
111
+
112
+ sql = payload[:sql]
113
+ @log_all << sql
114
+
115
+ unless payload[:name] == "SCHEMA"
116
+ bound_values = (payload[:binds] || []).map do |value|
117
+ value = value.value_for_database if value.respond_to?(:value_for_database)
118
+ value
119
+ end
120
+
121
+ @log_full << [sql, bound_values]
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -77,7 +77,7 @@ module ActiveRecord
77
77
  end
78
78
 
79
79
  def current_time_from_proper_timezone
80
- connection.default_timezone == :utc ? Time.now.utc : Time.now
80
+ with_connection { |c| c.default_timezone == :utc ? Time.now.utc : Time.now }
81
81
  end
82
82
 
83
83
  protected
@@ -162,15 +162,17 @@ module ActiveRecord
162
162
 
163
163
  def max_updated_column_timestamp
164
164
  timestamp_attributes_for_update_in_model
165
- .filter_map { |attr| self[attr]&.to_time }
165
+ .filter_map { |attr| (v = self[attr]) && (v.is_a?(::Time) ? v : v.to_time) }
166
166
  .max
167
167
  end
168
168
 
169
169
  # Clear attributes and changed_attributes
170
170
  def clear_timestamp_attributes
171
171
  all_timestamp_attributes_in_model.each do |attribute_name|
172
- self[attribute_name] = nil
173
- clear_attribute_change(attribute_name)
172
+ if self[attribute_name]
173
+ self[attribute_name] = nil
174
+ clear_attribute_change(attribute_name)
175
+ end
174
176
  end
175
177
  end
176
178
  end
@@ -35,6 +35,24 @@ module ActiveRecord
35
35
  end
36
36
  end
37
37
 
38
+ module RelationMethods
39
+ # Finds a record using a given +token+ for a predefined +purpose+. Returns
40
+ # +nil+ if the token is invalid or the record was not found.
41
+ def find_by_token_for(purpose, token)
42
+ raise UnknownPrimaryKey.new(self) unless model.primary_key
43
+ model.token_definitions.fetch(purpose).resolve_token(token) { |id| find_by(model.primary_key => [id]) }
44
+ end
45
+
46
+ # Finds a record using a given +token+ for a predefined +purpose+. Raises
47
+ # ActiveSupport::MessageVerifier::InvalidSignature if the token is invalid
48
+ # (e.g. expired, bad format, etc). Raises ActiveRecord::RecordNotFound if
49
+ # the token is valid but the record was not found.
50
+ def find_by_token_for!(purpose, token)
51
+ model.token_definitions.fetch(purpose).resolve_token(token) { |id| find(id) } ||
52
+ (raise ActiveSupport::MessageVerifier::InvalidSignature)
53
+ end
54
+ end
55
+
38
56
  module ClassMethods
39
57
  # Defines the behavior of tokens generated for a specific +purpose+.
40
58
  # A token can be generated by calling TokenFor#generate_token_for on a
@@ -85,20 +103,12 @@ module ActiveRecord
85
103
  self.token_definitions = token_definitions.merge(purpose => TokenDefinition.new(self, purpose, expires_in, block))
86
104
  end
87
105
 
88
- # Finds a record using a given +token+ for a predefined +purpose+. Returns
89
- # +nil+ if the token is invalid or the record was not found.
90
- def find_by_token_for(purpose, token)
91
- raise UnknownPrimaryKey.new(self) unless primary_key
92
- token_definitions.fetch(purpose).resolve_token(token) { |id| find_by(primary_key => id) }
106
+ def find_by_token_for(purpose, token) # :nodoc:
107
+ all.find_by_token_for(purpose, token)
93
108
  end
94
109
 
95
- # Finds a record using a given +token+ for a predefined +purpose+. Raises
96
- # ActiveSupport::MessageVerifier::InvalidSignature if the token is invalid
97
- # (e.g. expired, bad format, etc). Raises ActiveRecord::RecordNotFound if
98
- # the token is valid but the record was not found.
99
- def find_by_token_for!(purpose, token)
100
- token_definitions.fetch(purpose).resolve_token(token) { |id| find(id) } ||
101
- (raise ActiveSupport::MessageVerifier::InvalidSignature)
110
+ def find_by_token_for!(purpose, token) # :nodoc:
111
+ all.find_by_token_for!(purpose, token)
102
112
  end
103
113
  end
104
114
 
@@ -64,7 +64,7 @@ module ActiveRecord
64
64
  end
65
65
 
66
66
  def has_defer_touch_attrs?
67
- defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present?
67
+ @_defer_touch_attrs.present?
68
68
  end
69
69
  end
70
70
  end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/digest"
4
+
5
+ module ActiveRecord
6
+ # Class specifies the interface to interact with the current transaction state.
7
+ #
8
+ # It can either map to an actual transaction/savepoint, or represent the
9
+ # absence of a transaction.
10
+ #
11
+ # == State
12
+ #
13
+ # We say that a transaction is _finalized_ when it wraps a real transaction
14
+ # that has been either committed or rolled back.
15
+ #
16
+ # A transaction is _open_ if it wraps a real transaction that is not finalized.
17
+ #
18
+ # On the other hand, a transaction is _closed_ when it is not open. That is,
19
+ # when it represents absence of transaction, or it wraps a real but finalized
20
+ # one.
21
+ #
22
+ # You can check whether a transaction is open or closed with the +open?+ and
23
+ # +closed?+ predicates:
24
+ #
25
+ # if Article.current_transaction.open?
26
+ # # We are inside a real and not finalized transaction.
27
+ # end
28
+ #
29
+ # Closed transactions are +blank?+ too.
30
+ #
31
+ # == Callbacks
32
+ #
33
+ # After updating the database state, you may sometimes need to perform some extra work, or reflect these
34
+ # changes in a remote system like clearing or updating a cache:
35
+ #
36
+ # def publish_article(article)
37
+ # article.update!(published: true)
38
+ # NotificationService.article_published(article)
39
+ # end
40
+ #
41
+ # The above code works but has one important flaw, which is that it no longer works properly if called inside
42
+ # a transaction, as it will interact with the remote system before the changes are persisted:
43
+ #
44
+ # Article.transaction do
45
+ # article = create_article(article)
46
+ # publish_article(article)
47
+ # end
48
+ #
49
+ # The callbacks offered by ActiveRecord::Transaction allow to rewriting this method in a way that is compatible
50
+ # with transactions:
51
+ #
52
+ # def publish_article(article)
53
+ # article.update!(published: true)
54
+ # Article.current_transaction.after_commit do
55
+ # NotificationService.article_published(article)
56
+ # end
57
+ # end
58
+ #
59
+ # In the above example, if +publish_article+ is called inside a transaction, the callback will be invoked
60
+ # after the transaction is successfully committed, and if called outside a transaction, the callback will be invoked
61
+ # immediately.
62
+ #
63
+ # == Caveats
64
+ #
65
+ # When using after_commit callbacks, it is important to note that if the callback raises an error, the transaction
66
+ # won't be rolled back as it was already committed. Relying solely on these to synchronize state between multiple
67
+ # systems may lead to consistency issues.
68
+ class Transaction
69
+ def initialize(internal_transaction) # :nodoc:
70
+ @internal_transaction = internal_transaction
71
+ @uuid = nil
72
+ end
73
+
74
+ # Registers a block to be called after the transaction is fully committed.
75
+ #
76
+ # If there is no currently open transactions, the block is called
77
+ # immediately, unless the transaction is finalized, in which case attempting
78
+ # to register the callback raises ActiveRecord::ActiveRecordError.
79
+ #
80
+ # If the transaction has a parent transaction, the callback is transferred to
81
+ # the parent when the current transaction commits, or dropped when the current transaction
82
+ # is rolled back. This operation is repeated until the outermost transaction is reached.
83
+ #
84
+ # If the callback raises an error, the transaction remains committed.
85
+ def after_commit(&block)
86
+ if @internal_transaction.nil?
87
+ yield
88
+ else
89
+ @internal_transaction.after_commit(&block)
90
+ end
91
+ end
92
+
93
+ # Registers a block to be called after the transaction is rolled back.
94
+ #
95
+ # If there is no currently open transactions, the block is not called. But
96
+ # if the transaction is finalized, attempting to register the callback
97
+ # raises ActiveRecord::ActiveRecordError.
98
+ #
99
+ # If the transaction is successfully committed but has a parent
100
+ # transaction, the callback is automatically added to the parent transaction.
101
+ #
102
+ # If the entire chain of nested transactions are all successfully committed,
103
+ # the block is never called.
104
+ def after_rollback(&block)
105
+ @internal_transaction&.after_rollback(&block)
106
+ end
107
+
108
+ # Returns true if the transaction exists and isn't finalized yet.
109
+ def open?
110
+ !closed?
111
+ end
112
+
113
+ # Returns true if the transaction doesn't exist or is finalized.
114
+ def closed?
115
+ @internal_transaction.nil? || @internal_transaction.closed?
116
+ end
117
+
118
+ alias_method :blank?, :closed?
119
+
120
+ # Returns a UUID for this transaction or +nil+ if no transaction is open.
121
+ def uuid
122
+ if @internal_transaction
123
+ @uuid ||= Digest::UUID.uuid_v4
124
+ end
125
+ end
126
+
127
+ NULL_TRANSACTION = new(nil).freeze
128
+ end
129
+ end