activerecord 7.1.5.1 → 8.0.2

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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +369 -2484
  3. data/README.rdoc +15 -15
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +2 -1
  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 +21 -8
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/association.rb +7 -6
  11. data/lib/active_record/associations/builder/belongs_to.rb +1 -0
  12. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  13. data/lib/active_record/associations/builder/has_many.rb +3 -4
  14. data/lib/active_record/associations/builder/has_one.rb +3 -4
  15. data/lib/active_record/associations/collection_association.rb +17 -9
  16. data/lib/active_record/associations/collection_proxy.rb +14 -1
  17. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  18. data/lib/active_record/associations/errors.rb +265 -0
  19. data/lib/active_record/associations/has_many_association.rb +1 -1
  20. data/lib/active_record/associations/has_many_through_association.rb +10 -3
  21. data/lib/active_record/associations/join_dependency/join_association.rb +1 -1
  22. data/lib/active_record/associations/nested_error.rb +47 -0
  23. data/lib/active_record/associations/preloader/association.rb +4 -3
  24. data/lib/active_record/associations/preloader/branch.rb +7 -1
  25. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  26. data/lib/active_record/associations/singular_association.rb +14 -3
  27. data/lib/active_record/associations/through_association.rb +1 -1
  28. data/lib/active_record/associations.rb +92 -295
  29. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  30. data/lib/active_record/attribute_assignment.rb +0 -2
  31. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  32. data/lib/active_record/attribute_methods/primary_key.rb +25 -61
  33. data/lib/active_record/attribute_methods/read.rb +1 -13
  34. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  35. data/lib/active_record/attribute_methods/time_zone_conversion.rb +9 -18
  36. data/lib/active_record/attribute_methods.rb +71 -75
  37. data/lib/active_record/attributes.rb +63 -49
  38. data/lib/active_record/autosave_association.rb +92 -57
  39. data/lib/active_record/base.rb +2 -3
  40. data/lib/active_record/callbacks.rb +1 -1
  41. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +48 -122
  42. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  43. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -1
  44. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +286 -77
  45. data/lib/active_record/connection_adapters/abstract/database_statements.rb +119 -55
  46. data/lib/active_record/connection_adapters/abstract/query_cache.rb +197 -76
  47. data/lib/active_record/connection_adapters/abstract/quoting.rb +66 -92
  48. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  49. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +12 -3
  50. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -12
  51. data/lib/active_record/connection_adapters/abstract/transaction.rb +140 -67
  52. data/lib/active_record/connection_adapters/abstract_adapter.rb +85 -90
  53. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +71 -52
  54. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  55. data/lib/active_record/connection_adapters/mysql/quoting.rb +50 -57
  56. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  57. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +56 -45
  58. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +92 -101
  59. data/lib/active_record/connection_adapters/mysql2_adapter.rb +13 -31
  60. data/lib/active_record/connection_adapters/pool_config.rb +14 -13
  61. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +86 -41
  62. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  63. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  64. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  65. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  66. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  67. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  68. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
  69. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +36 -20
  70. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +3 -2
  71. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +75 -28
  72. data/lib/active_record/connection_adapters/postgresql_adapter.rb +73 -113
  73. data/lib/active_record/connection_adapters/schema_cache.rb +124 -131
  74. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  75. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +81 -97
  76. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +57 -46
  77. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +16 -0
  78. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  79. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +29 -0
  80. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +35 -3
  81. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +183 -87
  82. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  83. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +39 -69
  84. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -65
  85. data/lib/active_record/connection_adapters.rb +65 -0
  86. data/lib/active_record/connection_handling.rb +74 -37
  87. data/lib/active_record/core.rb +132 -51
  88. data/lib/active_record/counter_cache.rb +19 -10
  89. data/lib/active_record/database_configurations/connection_url_resolver.rb +9 -2
  90. data/lib/active_record/database_configurations/database_config.rb +23 -4
  91. data/lib/active_record/database_configurations/hash_config.rb +46 -34
  92. data/lib/active_record/database_configurations/url_config.rb +20 -1
  93. data/lib/active_record/database_configurations.rb +1 -1
  94. data/lib/active_record/delegated_type.rb +41 -17
  95. data/lib/active_record/dynamic_matchers.rb +2 -2
  96. data/lib/active_record/encryption/config.rb +3 -1
  97. data/lib/active_record/encryption/encryptable_record.rb +7 -7
  98. data/lib/active_record/encryption/encrypted_attribute_type.rb +33 -4
  99. data/lib/active_record/encryption/encryptor.rb +28 -6
  100. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  101. data/lib/active_record/encryption/key_provider.rb +1 -1
  102. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  103. data/lib/active_record/encryption/message_serializer.rb +4 -0
  104. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  105. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  106. data/lib/active_record/encryption/scheme.rb +8 -1
  107. data/lib/active_record/enum.rb +20 -16
  108. data/lib/active_record/errors.rb +54 -20
  109. data/lib/active_record/explain.rb +13 -24
  110. data/lib/active_record/fixtures.rb +37 -33
  111. data/lib/active_record/future_result.rb +21 -13
  112. data/lib/active_record/gem_version.rb +4 -4
  113. data/lib/active_record/inheritance.rb +4 -2
  114. data/lib/active_record/insert_all.rb +19 -16
  115. data/lib/active_record/integration.rb +4 -1
  116. data/lib/active_record/internal_metadata.rb +48 -34
  117. data/lib/active_record/locking/optimistic.rb +8 -7
  118. data/lib/active_record/log_subscriber.rb +5 -32
  119. data/lib/active_record/message_pack.rb +1 -1
  120. data/lib/active_record/migration/command_recorder.rb +33 -14
  121. data/lib/active_record/migration/compatibility.rb +8 -3
  122. data/lib/active_record/migration/default_strategy.rb +4 -5
  123. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  124. data/lib/active_record/migration.rb +104 -98
  125. data/lib/active_record/model_schema.rb +32 -70
  126. data/lib/active_record/nested_attributes.rb +15 -9
  127. data/lib/active_record/normalization.rb +3 -7
  128. data/lib/active_record/persistence.rb +127 -451
  129. data/lib/active_record/query_cache.rb +19 -8
  130. data/lib/active_record/query_logs.rb +104 -37
  131. data/lib/active_record/query_logs_formatter.rb +17 -28
  132. data/lib/active_record/querying.rb +24 -12
  133. data/lib/active_record/railtie.rb +26 -68
  134. data/lib/active_record/railties/controller_runtime.rb +13 -4
  135. data/lib/active_record/railties/databases.rake +43 -61
  136. data/lib/active_record/reflection.rb +112 -53
  137. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  138. data/lib/active_record/relation/batches.rb +138 -72
  139. data/lib/active_record/relation/calculations.rb +122 -82
  140. data/lib/active_record/relation/delegation.rb +30 -22
  141. data/lib/active_record/relation/finder_methods.rb +32 -18
  142. data/lib/active_record/relation/merger.rb +12 -14
  143. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  144. data/lib/active_record/relation/predicate_builder/association_query_value.rb +10 -2
  145. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  146. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  147. data/lib/active_record/relation/predicate_builder.rb +16 -3
  148. data/lib/active_record/relation/query_attribute.rb +1 -1
  149. data/lib/active_record/relation/query_methods.rb +317 -101
  150. data/lib/active_record/relation/spawn_methods.rb +3 -19
  151. data/lib/active_record/relation/where_clause.rb +7 -19
  152. data/lib/active_record/relation.rb +561 -119
  153. data/lib/active_record/result.rb +95 -46
  154. data/lib/active_record/runtime_registry.rb +39 -0
  155. data/lib/active_record/sanitization.rb +31 -25
  156. data/lib/active_record/schema.rb +8 -6
  157. data/lib/active_record/schema_dumper.rb +53 -20
  158. data/lib/active_record/schema_migration.rb +31 -14
  159. data/lib/active_record/scoping/named.rb +6 -2
  160. data/lib/active_record/signed_id.rb +24 -4
  161. data/lib/active_record/statement_cache.rb +19 -19
  162. data/lib/active_record/store.rb +7 -3
  163. data/lib/active_record/table_metadata.rb +2 -13
  164. data/lib/active_record/tasks/database_tasks.rb +87 -58
  165. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -3
  166. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  167. data/lib/active_record/tasks/sqlite_database_tasks.rb +4 -3
  168. data/lib/active_record/test_fixtures.rb +98 -89
  169. data/lib/active_record/testing/query_assertions.rb +121 -0
  170. data/lib/active_record/timestamp.rb +2 -2
  171. data/lib/active_record/token_for.rb +22 -12
  172. data/lib/active_record/touch_later.rb +1 -1
  173. data/lib/active_record/transaction.rb +132 -0
  174. data/lib/active_record/transactions.rb +72 -17
  175. data/lib/active_record/translation.rb +0 -2
  176. data/lib/active_record/type/serialized.rb +1 -3
  177. data/lib/active_record/type_caster/connection.rb +4 -4
  178. data/lib/active_record/validations/associated.rb +9 -3
  179. data/lib/active_record/validations/uniqueness.rb +23 -18
  180. data/lib/active_record/validations.rb +4 -1
  181. data/lib/active_record.rb +138 -57
  182. data/lib/arel/alias_predication.rb +1 -1
  183. data/lib/arel/collectors/bind.rb +4 -2
  184. data/lib/arel/collectors/composite.rb +7 -0
  185. data/lib/arel/collectors/sql_string.rb +2 -2
  186. data/lib/arel/collectors/substitute_binds.rb +3 -3
  187. data/lib/arel/nodes/binary.rb +1 -7
  188. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  189. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  190. data/lib/arel/nodes/node.rb +5 -4
  191. data/lib/arel/nodes/sql_literal.rb +8 -1
  192. data/lib/arel/nodes.rb +2 -2
  193. data/lib/arel/predications.rb +1 -1
  194. data/lib/arel/select_manager.rb +1 -1
  195. data/lib/arel/table.rb +3 -7
  196. data/lib/arel/tree_manager.rb +3 -2
  197. data/lib/arel/update_manager.rb +2 -1
  198. data/lib/arel/visitors/dot.rb +1 -0
  199. data/lib/arel/visitors/mysql.rb +9 -4
  200. data/lib/arel/visitors/postgresql.rb +1 -12
  201. data/lib/arel/visitors/sqlite.rb +25 -0
  202. data/lib/arel/visitors/to_sql.rb +29 -16
  203. data/lib/arel.rb +7 -3
  204. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  205. metadata +18 -16
  206. data/lib/active_record/relation/record_fetch_warning.rb +0 -49
@@ -53,20 +53,6 @@ module ActiveRecord
53
53
  self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys)
54
54
  end
55
55
 
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
56
  def fixtures(*fixture_set_names)
71
57
  if fixture_set_names.first == :all
72
58
  raise StandardError, "No fixture path found. Please set `#{self}.fixture_paths`." if fixture_paths.blank?
@@ -79,7 +65,7 @@ module ActiveRecord
79
65
  fixture_set_names = fixture_set_names.flatten.map(&:to_s)
80
66
  end
81
67
 
82
- self.fixture_table_names |= fixture_set_names
68
+ self.fixture_table_names = (fixture_table_names | fixture_set_names).sort
83
69
  setup_fixture_accessors(fixture_set_names)
84
70
  end
85
71
 
@@ -110,45 +96,87 @@ module ActiveRecord
110
96
  end
111
97
  end
112
98
 
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)
99
+ # Generic fixture accessor for fixture names that may conflict with other methods.
100
+ #
101
+ # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
102
+ # assert_equal "Ruby on Rails", fixture(:web_sites, :rubyonrails).name
103
+ def fixture(fixture_set_name, *fixture_names)
104
+ active_record_fixture(fixture_set_name, *fixture_names)
125
105
  end
126
106
 
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"
107
+ private
108
+ def run_in_transaction?
109
+ use_transactional_tests &&
110
+ !self.class.uses_transaction?(name)
130
111
  end
131
112
 
132
- @fixture_cache = {}
133
- @fixture_connections = []
134
- @@already_loaded_fixtures ||= {}
135
- @connection_subscriber = nil
136
- @saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
113
+ def setup_fixtures(config = ActiveRecord::Base)
114
+ if pre_loaded_fixtures && !use_transactional_tests
115
+ raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
116
+ end
137
117
 
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]
118
+ @fixture_cache = {}
119
+ @fixture_cache_key = [self.class.fixture_table_names.dup, self.class.fixture_paths.dup, self.class.fixture_class_names.dup]
120
+ @fixture_connection_pools = []
121
+ @@already_loaded_fixtures ||= {}
122
+ @connection_subscriber = nil
123
+ @saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
124
+
125
+ if run_in_transaction?
126
+ # Load fixtures once and begin transaction.
127
+ @loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key]
128
+ unless @loaded_fixtures
129
+ @@already_loaded_fixtures.clear
130
+ @loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key] = load_fixtures(config)
131
+ end
132
+
133
+ setup_transactional_fixtures
142
134
  else
135
+ # Load fixtures for every test.
136
+ ActiveRecord::FixtureSet.reset_cache
137
+ invalidate_already_loaded_fixtures
143
138
  @loaded_fixtures = load_fixtures(config)
144
- @@already_loaded_fixtures[self.class] = @loaded_fixtures
145
139
  end
140
+ setup_asynchronous_queries_session
141
+
142
+ # Instantiate fixtures for every test if requested.
143
+ instantiate_fixtures if use_instantiated_fixtures
144
+ end
145
+
146
+ def teardown_fixtures
147
+ teardown_asynchronous_queries_session
148
+
149
+ # Rollback changes if a transaction is active.
150
+ if run_in_transaction?
151
+ teardown_transactional_fixtures
152
+ else
153
+ ActiveRecord::FixtureSet.reset_cache
154
+ invalidate_already_loaded_fixtures
155
+ end
156
+
157
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
158
+ end
159
+
160
+ def setup_asynchronous_queries_session
161
+ @_async_queries_session = ActiveRecord::Base.asynchronous_queries_tracker.start_session
162
+ end
163
+
164
+ def teardown_asynchronous_queries_session
165
+ ActiveRecord::Base.asynchronous_queries_tracker.finalize_session(true) if @_async_queries_session
166
+ end
167
+
168
+ def invalidate_already_loaded_fixtures
169
+ @@already_loaded_fixtures.clear
170
+ end
171
+
172
+ def setup_transactional_fixtures
173
+ setup_shared_connection_pool
146
174
 
147
175
  # 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
176
+ @fixture_connection_pools = ActiveRecord::Base.connection_handler.connection_pool_list(:writing)
177
+ @fixture_connection_pools.each do |pool|
178
+ pool.pin_connection!(lock_threads)
179
+ pool.lease_connection
152
180
  end
153
181
 
154
182
  # When connections are established in the future, begin a transaction too
@@ -157,59 +185,32 @@ module ActiveRecord
157
185
  shard = payload[:shard] if payload.key?(:shard)
158
186
 
159
187
  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
188
+ pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(connection_name, shard: shard)
189
+ if pool
167
190
  setup_shared_connection_pool
168
191
 
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
192
+ unless @fixture_connection_pools.include?(pool)
193
+ pool.pin_connection!(lock_threads)
194
+ pool.lease_connection
195
+ @fixture_connection_pools << pool
173
196
  end
174
197
  end
175
198
  end
176
199
  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
200
  end
184
201
 
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?
202
+ def teardown_transactional_fixtures
192
203
  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
204
+
205
+ unless @fixture_connection_pools.map(&:unpin_connection!).all?
206
+ # Something caused the transaction to be committed or rolled back
207
+ # We can no longer trust the database is in a clean state.
208
+ @@already_loaded_fixtures.clear
196
209
  end
197
- @fixture_connections.clear
210
+ @fixture_connection_pools.clear
198
211
  teardown_shared_connection_pool
199
- else
200
- ActiveRecord::FixtureSet.reset_cache
201
212
  end
202
213
 
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
214
  # Shares the writing connection pool with connections on
214
215
  # other handlers.
215
216
  #
@@ -272,22 +273,30 @@ module ActiveRecord
272
273
  use_instantiated_fixtures != :no_instances
273
274
  end
274
275
 
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)
276
+ def method_missing(method, ...)
277
+ if fixture_sets.key?(method.name)
278
+ active_record_fixture(method, ...)
278
279
  else
279
280
  super
280
281
  end
281
282
  end
282
283
 
283
- def respond_to_missing?(name, include_private = false)
284
- if include_private && fixture_sets.key?(name.to_s)
284
+ def respond_to_missing?(method, include_private = false)
285
+ if include_private && fixture_sets.key?(method.name)
285
286
  true
286
287
  else
287
288
  super
288
289
  end
289
290
  end
290
291
 
292
+ def active_record_fixture(fixture_set_name, *fixture_names)
293
+ if fs_name = fixture_sets[fixture_set_name.name]
294
+ access_fixture(fs_name, *fixture_names)
295
+ else
296
+ raise StandardError, "No fixture set named '#{fixture_set_name.inspect}'"
297
+ end
298
+ end
299
+
291
300
  def access_fixture(fs_name, *fixture_names)
292
301
  force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
293
302
  return_single_record = fixture_names.size == 1
@@ -0,0 +1,121 @@
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
+ # If the +:include_schema+ option is provided, any queries (including schema related) are counted.
15
+ #
16
+ # assert_queries_count(1, include_schema: true) { Post.columns }
17
+ #
18
+ def assert_queries_count(count = nil, include_schema: false, &block)
19
+ ActiveRecord::Base.lease_connection.materialize_transactions
20
+
21
+ counter = SQLCounter.new
22
+ ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
23
+ result = _assert_nothing_raised_or_warn("assert_queries_count", &block)
24
+ queries = include_schema ? counter.log_all : counter.log
25
+ if count
26
+ assert_equal count, queries.size, "#{queries.size} instead of #{count} queries were executed. Queries: #{queries.join("\n\n")}"
27
+ else
28
+ assert_operator queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
29
+ end
30
+ result
31
+ end
32
+ end
33
+
34
+ # Asserts that no SQL queries are executed in the given block.
35
+ #
36
+ # assert_no_queries { post.comments }
37
+ #
38
+ # If the +:include_schema+ option is provided, any queries (including schema related) are counted.
39
+ #
40
+ # assert_no_queries(include_schema: true) { Post.columns }
41
+ #
42
+ def assert_no_queries(include_schema: false, &block)
43
+ assert_queries_count(0, include_schema: include_schema, &block)
44
+ end
45
+
46
+ # Asserts that the SQL queries executed in the given block match expected pattern.
47
+ #
48
+ # # Check for exact number of queries
49
+ # assert_queries_match(/LIMIT \?/, count: 1) { Post.first }
50
+ #
51
+ # # Check for any number of queries
52
+ # assert_queries_match(/LIMIT \?/) { Post.first }
53
+ #
54
+ # If the +:include_schema+ option is provided, any queries (including schema related)
55
+ # that match the matcher are considered.
56
+ #
57
+ # assert_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
58
+ #
59
+ def assert_queries_match(match, count: nil, include_schema: false, &block)
60
+ ActiveRecord::Base.lease_connection.materialize_transactions
61
+
62
+ counter = SQLCounter.new
63
+ ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
64
+ result = _assert_nothing_raised_or_warn("assert_queries_match", &block)
65
+ queries = include_schema ? counter.log_all : counter.log
66
+ matched_queries = queries.select { |query| match === query }
67
+
68
+ if count
69
+ assert_equal count, matched_queries.size, "#{matched_queries.size} instead of #{count} queries were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
70
+ else
71
+ assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
72
+ end
73
+
74
+ result
75
+ end
76
+ end
77
+
78
+ # Asserts that no SQL queries matching the pattern are executed in the given block.
79
+ #
80
+ # assert_no_queries_match(/SELECT/i) { post.comments }
81
+ #
82
+ # If the +:include_schema+ option is provided, any queries (including schema related)
83
+ # that match the matcher are counted.
84
+ #
85
+ # assert_no_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
86
+ #
87
+ def assert_no_queries_match(match, include_schema: false, &block)
88
+ assert_queries_match(match, count: 0, include_schema: include_schema, &block)
89
+ end
90
+
91
+ class SQLCounter # :nodoc:
92
+ attr_reader :log_full, :log_all
93
+
94
+ def initialize
95
+ @log_full = []
96
+ @log_all = []
97
+ end
98
+
99
+ def log
100
+ @log_full.map(&:first)
101
+ end
102
+
103
+ def call(*, payload)
104
+ return if payload[:cached]
105
+
106
+ sql = payload[:sql]
107
+ @log_all << sql
108
+
109
+ unless payload[:name] == "SCHEMA"
110
+ bound_values = (payload[:binds] || []).map do |value|
111
+ value = value.value_for_database if value.respond_to?(:value_for_database)
112
+ value
113
+ end
114
+
115
+ @log_full << [sql, bound_values]
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ 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,7 +162,7 @@ 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
 
@@ -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,132 @@
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
+ #
105
+ # If the transaction is already finalized, attempting to register a callback
106
+ # will raise ActiveRecord::ActiveRecordError.
107
+ def after_rollback(&block)
108
+ @internal_transaction&.after_rollback(&block)
109
+ end
110
+
111
+ # Returns true if the transaction exists and isn't finalized yet.
112
+ def open?
113
+ !closed?
114
+ end
115
+
116
+ # Returns true if the transaction doesn't exist or is finalized.
117
+ def closed?
118
+ @internal_transaction.nil? || @internal_transaction.state.finalized?
119
+ end
120
+
121
+ alias_method :blank?, :closed?
122
+
123
+ # Returns a UUID for this transaction or +nil+ if no transaction is open.
124
+ def uuid
125
+ if @internal_transaction
126
+ @uuid ||= Digest::UUID.uuid_v4
127
+ end
128
+ end
129
+
130
+ NULL_TRANSACTION = new(nil).freeze
131
+ end
132
+ end