activerecord 7.1.4.1 → 7.2.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 (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +643 -2274
  3. data/README.rdoc +15 -15
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +25 -19
  7. data/lib/active_record/associations/association.rb +15 -8
  8. data/lib/active_record/associations/belongs_to_association.rb +14 -7
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/belongs_to.rb +1 -0
  11. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +3 -4
  13. data/lib/active_record/associations/builder/has_one.rb +3 -4
  14. data/lib/active_record/associations/collection_association.rb +7 -1
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/errors.rb +265 -0
  17. data/lib/active_record/associations/has_many_association.rb +1 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +7 -1
  19. data/lib/active_record/associations/join_dependency/join_association.rb +30 -27
  20. data/lib/active_record/associations/join_dependency.rb +4 -4
  21. data/lib/active_record/associations/nested_error.rb +47 -0
  22. data/lib/active_record/associations/preloader/association.rb +2 -1
  23. data/lib/active_record/associations/preloader/branch.rb +7 -1
  24. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  25. data/lib/active_record/associations/singular_association.rb +6 -0
  26. data/lib/active_record/associations/through_association.rb +1 -1
  27. data/lib/active_record/associations.rb +59 -292
  28. data/lib/active_record/attribute_assignment.rb +0 -2
  29. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  30. data/lib/active_record/attribute_methods/primary_key.rb +23 -55
  31. data/lib/active_record/attribute_methods/read.rb +1 -13
  32. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  33. data/lib/active_record/attribute_methods/time_zone_conversion.rb +11 -6
  34. data/lib/active_record/attribute_methods.rb +54 -63
  35. data/lib/active_record/attributes.rb +61 -47
  36. data/lib/active_record/autosave_association.rb +12 -29
  37. data/lib/active_record/base.rb +2 -3
  38. data/lib/active_record/callbacks.rb +1 -1
  39. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +24 -107
  40. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -0
  41. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +270 -65
  42. data/lib/active_record/connection_adapters/abstract/database_statements.rb +34 -17
  43. data/lib/active_record/connection_adapters/abstract/query_cache.rb +189 -74
  44. data/lib/active_record/connection_adapters/abstract/quoting.rb +65 -91
  45. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  46. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +15 -6
  47. data/lib/active_record/connection_adapters/abstract/transaction.rb +125 -62
  48. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -44
  49. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +40 -10
  50. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  51. data/lib/active_record/connection_adapters/mysql/quoting.rb +43 -48
  52. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +6 -0
  53. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +16 -15
  54. data/lib/active_record/connection_adapters/mysql2_adapter.rb +5 -23
  55. data/lib/active_record/connection_adapters/pool_config.rb +7 -6
  56. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +27 -4
  57. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
  58. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  59. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  60. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  61. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -0
  62. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +17 -11
  63. data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -24
  64. data/lib/active_record/connection_adapters/schema_cache.rb +123 -128
  65. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  66. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +10 -6
  67. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +44 -46
  68. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  69. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  70. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  71. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +25 -2
  72. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +125 -75
  73. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +15 -15
  74. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -48
  75. data/lib/active_record/connection_adapters.rb +121 -0
  76. data/lib/active_record/connection_handling.rb +56 -41
  77. data/lib/active_record/core.rb +86 -38
  78. data/lib/active_record/counter_cache.rb +18 -9
  79. data/lib/active_record/database_configurations/connection_url_resolver.rb +8 -3
  80. data/lib/active_record/database_configurations/database_config.rb +19 -4
  81. data/lib/active_record/database_configurations/hash_config.rb +38 -34
  82. data/lib/active_record/database_configurations/url_config.rb +20 -1
  83. data/lib/active_record/database_configurations.rb +1 -1
  84. data/lib/active_record/delegated_type.rb +24 -0
  85. data/lib/active_record/dynamic_matchers.rb +2 -2
  86. data/lib/active_record/encryption/encryptable_record.rb +3 -3
  87. data/lib/active_record/encryption/encrypted_attribute_type.rb +24 -4
  88. data/lib/active_record/encryption/encryptor.rb +18 -3
  89. data/lib/active_record/encryption/key_provider.rb +1 -1
  90. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  91. data/lib/active_record/encryption/message_serializer.rb +4 -0
  92. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  93. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  94. data/lib/active_record/encryption.rb +2 -0
  95. data/lib/active_record/enum.rb +19 -2
  96. data/lib/active_record/errors.rb +46 -20
  97. data/lib/active_record/explain.rb +13 -24
  98. data/lib/active_record/fixtures.rb +37 -31
  99. data/lib/active_record/future_result.rb +8 -4
  100. data/lib/active_record/gem_version.rb +2 -2
  101. data/lib/active_record/inheritance.rb +4 -2
  102. data/lib/active_record/insert_all.rb +18 -15
  103. data/lib/active_record/integration.rb +4 -1
  104. data/lib/active_record/internal_metadata.rb +48 -34
  105. data/lib/active_record/locking/optimistic.rb +7 -6
  106. data/lib/active_record/log_subscriber.rb +0 -21
  107. data/lib/active_record/marshalling.rb +4 -1
  108. data/lib/active_record/message_pack.rb +1 -1
  109. data/lib/active_record/migration/command_recorder.rb +2 -3
  110. data/lib/active_record/migration/compatibility.rb +5 -3
  111. data/lib/active_record/migration/default_strategy.rb +4 -5
  112. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  113. data/lib/active_record/migration.rb +85 -76
  114. data/lib/active_record/model_schema.rb +32 -68
  115. data/lib/active_record/nested_attributes.rb +24 -5
  116. data/lib/active_record/normalization.rb +3 -7
  117. data/lib/active_record/persistence.rb +30 -352
  118. data/lib/active_record/query_cache.rb +19 -8
  119. data/lib/active_record/query_logs.rb +15 -0
  120. data/lib/active_record/querying.rb +21 -9
  121. data/lib/active_record/railtie.rb +42 -57
  122. data/lib/active_record/railties/controller_runtime.rb +13 -4
  123. data/lib/active_record/railties/databases.rake +40 -43
  124. data/lib/active_record/reflection.rb +98 -36
  125. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  126. data/lib/active_record/relation/batches.rb +14 -8
  127. data/lib/active_record/relation/calculations.rb +96 -63
  128. data/lib/active_record/relation/delegation.rb +8 -11
  129. data/lib/active_record/relation/finder_methods.rb +16 -2
  130. data/lib/active_record/relation/merger.rb +4 -6
  131. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  132. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -3
  133. data/lib/active_record/relation/predicate_builder.rb +3 -3
  134. data/lib/active_record/relation/query_methods.rb +224 -58
  135. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  136. data/lib/active_record/relation/spawn_methods.rb +2 -18
  137. data/lib/active_record/relation/where_clause.rb +7 -19
  138. data/lib/active_record/relation.rb +496 -72
  139. data/lib/active_record/result.rb +31 -44
  140. data/lib/active_record/runtime_registry.rb +39 -0
  141. data/lib/active_record/sanitization.rb +24 -19
  142. data/lib/active_record/schema.rb +8 -6
  143. data/lib/active_record/schema_dumper.rb +19 -9
  144. data/lib/active_record/schema_migration.rb +30 -14
  145. data/lib/active_record/scoping/named.rb +1 -0
  146. data/lib/active_record/signed_id.rb +20 -1
  147. data/lib/active_record/statement_cache.rb +7 -7
  148. data/lib/active_record/table_metadata.rb +1 -10
  149. data/lib/active_record/tasks/database_tasks.rb +81 -42
  150. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  151. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  152. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
  153. data/lib/active_record/test_fixtures.rb +86 -89
  154. data/lib/active_record/testing/query_assertions.rb +121 -0
  155. data/lib/active_record/timestamp.rb +2 -2
  156. data/lib/active_record/token_for.rb +22 -12
  157. data/lib/active_record/touch_later.rb +1 -1
  158. data/lib/active_record/transaction.rb +132 -0
  159. data/lib/active_record/transactions.rb +70 -14
  160. data/lib/active_record/translation.rb +0 -2
  161. data/lib/active_record/type/serialized.rb +1 -3
  162. data/lib/active_record/type_caster/connection.rb +4 -4
  163. data/lib/active_record/validations/associated.rb +9 -3
  164. data/lib/active_record/validations/uniqueness.rb +15 -10
  165. data/lib/active_record/validations.rb +4 -1
  166. data/lib/active_record.rb +148 -39
  167. data/lib/arel/alias_predication.rb +1 -1
  168. data/lib/arel/collectors/bind.rb +2 -0
  169. data/lib/arel/collectors/composite.rb +7 -0
  170. data/lib/arel/collectors/sql_string.rb +1 -1
  171. data/lib/arel/collectors/substitute_binds.rb +1 -1
  172. data/lib/arel/nodes/binary.rb +0 -6
  173. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  174. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  175. data/lib/arel/nodes/node.rb +4 -3
  176. data/lib/arel/nodes/sql_literal.rb +7 -0
  177. data/lib/arel/nodes.rb +2 -2
  178. data/lib/arel/predications.rb +1 -1
  179. data/lib/arel/select_manager.rb +1 -1
  180. data/lib/arel/tree_manager.rb +3 -2
  181. data/lib/arel/update_manager.rb +2 -1
  182. data/lib/arel/visitors/dot.rb +1 -0
  183. data/lib/arel/visitors/mysql.rb +9 -4
  184. data/lib/arel/visitors/postgresql.rb +1 -12
  185. data/lib/arel/visitors/sqlite.rb +25 -0
  186. data/lib/arel/visitors/to_sql.rb +29 -16
  187. data/lib/arel.rb +7 -3
  188. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  189. metadata +18 -12
@@ -125,11 +125,11 @@ module ActiveRecord
125
125
  end
126
126
 
127
127
  def create_all
128
- each_local_configuration do |db_config|
129
- with_temporary_connection(db_config) do
130
- create(db_config)
131
- end
132
- end
128
+ db_config = migration_connection.pool.db_config
129
+
130
+ each_local_configuration { |db_config| create(db_config) }
131
+
132
+ migration_class.establish_connection(db_config)
133
133
  end
134
134
 
135
135
  def setup_initial_database_yaml # :nodoc:
@@ -175,11 +175,12 @@ module ActiveRecord
175
175
 
176
176
  def prepare_all
177
177
  seed = false
178
+ dump_db_configs = []
178
179
 
179
180
  each_current_configuration(env) do |db_config|
180
181
  with_temporary_pool(db_config) do
181
182
  begin
182
- database_initialized = migration_connection.schema_migration.table_exists?
183
+ database_initialized = migration_connection_pool.schema_migration.table_exists?
183
184
  rescue ActiveRecord::NoDatabaseError
184
185
  create(db_config)
185
186
  retry
@@ -197,15 +198,25 @@ module ActiveRecord
197
198
 
198
199
  each_current_environment(env) do |environment|
199
200
  db_configs_with_versions(environment).sort.each do |version, db_configs|
201
+ dump_db_configs |= db_configs
202
+
200
203
  db_configs.each do |db_config|
201
204
  with_temporary_pool(db_config) do
202
205
  migrate(version)
203
- dump_schema(db_config) if ActiveRecord.dump_schema_after_migration
204
206
  end
205
207
  end
206
208
  end
207
209
  end
208
210
 
211
+ # Dump schema for databases that were migrated.
212
+ if ActiveRecord.dump_schema_after_migration
213
+ dump_db_configs.each do |db_config|
214
+ with_temporary_pool(db_config) do
215
+ dump_schema(db_config)
216
+ end
217
+ end
218
+ end
219
+
209
220
  load_seed if seed
210
221
  end
211
222
 
@@ -248,7 +259,7 @@ module ActiveRecord
248
259
 
249
260
  check_target_version
250
261
 
251
- migration_connection.migration_context.migrate(target_version) do |migration|
262
+ migration_connection_pool.migration_context.migrate(target_version) do |migration|
252
263
  if version.blank?
253
264
  scope.blank? || scope == migration.scope
254
265
  else
@@ -258,7 +269,7 @@ module ActiveRecord
258
269
  Migration.write("No migrations ran. (using #{scope} scope)") if scope.present? && migrations_ran.empty?
259
270
  end
260
271
 
261
- migration_connection.schema_cache.clear!
272
+ migration_connection_pool.schema_cache.clear!
262
273
  ensure
263
274
  Migration.verbose = verbose_was
264
275
  end
@@ -266,9 +277,9 @@ module ActiveRecord
266
277
  def db_configs_with_versions(environment = env) # :nodoc:
267
278
  db_configs_with_versions = Hash.new { |h, k| h[k] = [] }
268
279
 
269
- with_temporary_connection_for_each(env: environment) do |conn|
270
- db_config = conn.pool.db_config
271
- versions_to_run = conn.migration_context.pending_migration_versions
280
+ with_temporary_pool_for_each(env: environment) do |pool|
281
+ db_config = pool.db_config
282
+ versions_to_run = pool.migration_context.pending_migration_versions
272
283
  target_version = ActiveRecord::Tasks::DatabaseTasks.target_version
273
284
 
274
285
  versions_to_run.each do |version|
@@ -281,15 +292,15 @@ module ActiveRecord
281
292
  end
282
293
 
283
294
  def migrate_status
284
- unless migration_connection.schema_migration.table_exists?
295
+ unless migration_connection_pool.schema_migration.table_exists?
285
296
  Kernel.abort "Schema migrations table does not exist yet."
286
297
  end
287
298
 
288
299
  # output
289
- puts "\ndatabase: #{migration_connection.pool.db_config.database}\n\n"
300
+ puts "\ndatabase: #{migration_connection_pool.db_config.database}\n\n"
290
301
  puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
291
302
  puts "-" * 50
292
- migration_connection.migration_context.migrations_status.each do |status, version, name|
303
+ migration_connection_pool.migration_context.migrations_status.each do |status, version, name|
293
304
  puts "#{status.center(8)} #{version.ljust(14)} #{name}"
294
305
  end
295
306
  puts
@@ -370,7 +381,7 @@ module ActiveRecord
370
381
  raise ArgumentError, "unknown format #{format.inspect}"
371
382
  end
372
383
 
373
- migration_connection.internal_metadata.create_table_and_set_flags(db_config.env_name, schema_sha1(file))
384
+ migration_connection_pool.internal_metadata.create_table_and_set_flags(db_config.env_name, schema_sha1(file))
374
385
  ensure
375
386
  Migration.verbose = verbose_was
376
387
  end
@@ -382,11 +393,12 @@ module ActiveRecord
382
393
 
383
394
  return true unless file && File.exist?(file)
384
395
 
385
- with_temporary_connection(db_config) do |connection|
386
- return false unless connection.internal_metadata.enabled?
387
- return false unless connection.internal_metadata.table_exists?
396
+ with_temporary_pool(db_config) do |pool|
397
+ internal_metadata = pool.internal_metadata
398
+ return false unless internal_metadata.enabled?
399
+ return false unless internal_metadata.table_exists?
388
400
 
389
- connection.internal_metadata[:schema_sha1] == schema_sha1(file)
401
+ internal_metadata[:schema_sha1] == schema_sha1(file)
390
402
  end
391
403
  end
392
404
 
@@ -397,7 +409,7 @@ module ActiveRecord
397
409
 
398
410
  with_temporary_pool(db_config, clobber: true) do
399
411
  if schema_up_to_date?(db_config, format, file)
400
- truncate_tables(db_config)
412
+ truncate_tables(db_config) unless ENV["SKIP_TEST_DATABASE_TRUNCATE"]
401
413
  else
402
414
  purge(db_config)
403
415
  load_schema(db_config, format, file)
@@ -419,11 +431,11 @@ module ActiveRecord
419
431
  case format
420
432
  when :ruby
421
433
  File.open(filename, "w:utf-8") do |file|
422
- ActiveRecord::SchemaDumper.dump(migration_connection, file)
434
+ ActiveRecord::SchemaDumper.dump(migration_connection_pool, file)
423
435
  end
424
436
  when :sql
425
437
  structure_dump(db_config, filename)
426
- if migration_connection.schema_migration.table_exists?
438
+ if migration_connection_pool.schema_migration.table_exists?
427
439
  File.open(filename, "a") do |f|
428
440
  f.puts migration_connection.dump_schema_information
429
441
  f.print "\n"
@@ -445,14 +457,26 @@ module ActiveRecord
445
457
  end
446
458
  end
447
459
 
448
- def cache_dump_filename(db_config_name, schema_cache_path: nil)
449
- filename = if ActiveRecord::Base.configurations.primary?(db_config_name)
450
- "schema_cache.yml"
460
+ def cache_dump_filename(db_config_or_name, schema_cache_path: nil)
461
+ if db_config_or_name.is_a?(DatabaseConfigurations::DatabaseConfig)
462
+ schema_cache_path ||
463
+ db_config_or_name.schema_cache_path ||
464
+ schema_cache_env ||
465
+ db_config_or_name.default_schema_cache_path(ActiveRecord::Tasks::DatabaseTasks.db_dir)
451
466
  else
452
- "#{db_config_name}_schema_cache.yml"
453
- end
467
+ ActiveRecord.deprecator.warn(<<~MSG.squish)
468
+ Passing a database name to `cache_dump_filename` is deprecated and will be removed in Rails 8.0. Pass a
469
+ `ActiveRecord::DatabaseConfigurations::DatabaseConfig` object instead.
470
+ MSG
454
471
 
455
- schema_cache_path || ENV["SCHEMA_CACHE"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
472
+ filename = if ActiveRecord::Base.configurations.primary?(db_config_or_name)
473
+ "schema_cache.yml"
474
+ else
475
+ "#{db_config_or_name}_schema_cache.yml"
476
+ end
477
+
478
+ schema_cache_path || schema_cache_env || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
479
+ end
456
480
  end
457
481
 
458
482
  def load_schema_current(format = ActiveRecord.schema_format, file = nil, environment = env)
@@ -484,29 +508,29 @@ module ActiveRecord
484
508
  # Dumps the schema cache in YAML format for the connection into the file
485
509
  #
486
510
  # ==== Examples
487
- # ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, "tmp/schema_dump.yaml")
488
- def dump_schema_cache(conn, filename)
489
- conn.schema_cache.dump_to(filename)
511
+ # ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.lease_connection, "tmp/schema_dump.yaml")
512
+ def dump_schema_cache(conn_or_pool, filename)
513
+ conn_or_pool.schema_cache.dump_to(filename)
490
514
  end
491
515
 
492
516
  def clear_schema_cache(filename)
493
517
  FileUtils.rm_f filename, verbose: false
494
518
  end
495
519
 
496
- def with_temporary_connection_for_each(env: ActiveRecord::Tasks::DatabaseTasks.env, name: nil, clobber: false, &block) # :nodoc:
520
+ def with_temporary_pool_for_each(env: ActiveRecord::Tasks::DatabaseTasks.env, name: nil, clobber: false, &block) # :nodoc:
497
521
  if name
498
522
  db_config = ActiveRecord::Base.configurations.configs_for(env_name: env, name: name)
499
- with_temporary_connection(db_config, clobber: clobber, &block)
523
+ with_temporary_pool(db_config, clobber: clobber, &block)
500
524
  else
501
525
  ActiveRecord::Base.configurations.configs_for(env_name: env, name: name).each do |db_config|
502
- with_temporary_connection(db_config, clobber: clobber, &block)
526
+ with_temporary_pool(db_config, clobber: clobber, &block)
503
527
  end
504
528
  end
505
529
  end
506
530
 
507
- def with_temporary_connection(db_config, clobber: false) # :nodoc:
531
+ def with_temporary_connection(db_config, clobber: false, &block) # :nodoc:
508
532
  with_temporary_pool(db_config, clobber: clobber) do |pool|
509
- yield pool.connection
533
+ pool.with_connection(&block)
510
534
  end
511
535
  end
512
536
 
@@ -515,10 +539,25 @@ module ActiveRecord
515
539
  end
516
540
 
517
541
  def migration_connection # :nodoc:
518
- migration_class.connection
542
+ migration_class.lease_connection
543
+ end
544
+
545
+ def migration_connection_pool # :nodoc:
546
+ migration_class.connection_pool
519
547
  end
520
548
 
521
549
  private
550
+ def schema_cache_env
551
+ if ENV["SCHEMA_CACHE"]
552
+ ActiveRecord.deprecator.warn(<<~MSG.squish)
553
+ Setting `ENV["SCHEMA_CACHE"]` is deprecated and will be removed in Rails 8.0.
554
+ Configure the `:schema_cache_path` in the database configuration instead.
555
+ MSG
556
+
557
+ nil
558
+ end
559
+ end
560
+
522
561
  def with_temporary_pool(db_config, clobber: false)
523
562
  original_db_config = migration_class.connection_db_config
524
563
  pool = migration_class.connection_handler.establish_connection(db_config, clobber: clobber)
@@ -614,11 +653,11 @@ module ActiveRecord
614
653
 
615
654
  def check_current_protected_environment!(db_config)
616
655
  with_temporary_pool(db_config) do |pool|
617
- connection = pool.connection
618
- current = connection.migration_context.current_environment
619
- stored = connection.migration_context.last_stored_environment
656
+ migration_context = pool.migration_context
657
+ current = migration_context.current_environment
658
+ stored = migration_context.last_stored_environment
620
659
 
621
- if connection.migration_context.protected_environment?
660
+ if migration_context.protected_environment?
622
661
  raise ActiveRecord::ProtectedEnvironmentError.new(stored)
623
662
  end
624
663
 
@@ -71,7 +71,7 @@ module ActiveRecord
71
71
  attr_reader :db_config, :configuration_hash
72
72
 
73
73
  def connection
74
- ActiveRecord::Base.connection
74
+ ActiveRecord::Base.lease_connection
75
75
  end
76
76
 
77
77
  def establish_connection(config = db_config)
@@ -88,7 +88,7 @@ module ActiveRecord
88
88
  attr_reader :db_config, :configuration_hash
89
89
 
90
90
  def connection
91
- ActiveRecord::Base.connection
91
+ ActiveRecord::Base.lease_connection
92
92
  end
93
93
 
94
94
  def establish_connection(config = db_config)
@@ -66,11 +66,12 @@ module ActiveRecord
66
66
  attr_reader :db_config, :root
67
67
 
68
68
  def connection
69
- ActiveRecord::Base.connection
69
+ ActiveRecord::Base.lease_connection
70
70
  end
71
71
 
72
72
  def establish_connection(config = db_config)
73
73
  ActiveRecord::Base.establish_connection(config)
74
+ connection.connect!
74
75
  end
75
76
 
76
77
  def run_cmd(cmd, args, out)
@@ -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,76 @@ 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
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)
120
105
  end
121
106
 
122
- def run_in_transaction?
123
- use_transactional_tests &&
124
- !self.class.uses_transaction?(name)
125
- end
126
-
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
117
+
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
137
132
 
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]
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
146
140
 
141
+ # Instantiate fixtures for every test if requested.
142
+ instantiate_fixtures if use_instantiated_fixtures
143
+ end
144
+
145
+ def teardown_fixtures
146
+ # Rollback changes if a transaction is active.
147
+ if run_in_transaction?
148
+ teardown_transactional_fixtures
149
+ else
150
+ ActiveRecord::FixtureSet.reset_cache
151
+ invalidate_already_loaded_fixtures
152
+ end
153
+
154
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
155
+ end
156
+
157
+ def invalidate_already_loaded_fixtures
158
+ @@already_loaded_fixtures.clear
159
+ end
160
+
161
+ def setup_transactional_fixtures
162
+ setup_shared_connection_pool
163
+
147
164
  # 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
165
+ @fixture_connection_pools = ActiveRecord::Base.connection_handler.connection_pool_list(:writing)
166
+ @fixture_connection_pools.each do |pool|
167
+ pool.pin_connection!(lock_threads)
168
+ pool.lease_connection
152
169
  end
153
170
 
154
171
  # When connections are established in the future, begin a transaction too
@@ -157,59 +174,31 @@ module ActiveRecord
157
174
  shard = payload[:shard] if payload.key?(:shard)
158
175
 
159
176
  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
177
+ pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(connection_name, shard: shard)
178
+ if pool
167
179
  setup_shared_connection_pool
168
180
 
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
181
+ unless @fixture_connection_pools.include?(pool)
182
+ pool.pin_connection!(lock_threads)
183
+ pool.lease_connection
184
+ @fixture_connection_pools << pool
173
185
  end
174
186
  end
175
187
  end
176
188
  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
189
  end
184
190
 
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?
191
+ def teardown_transactional_fixtures
192
192
  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
193
+ unless @fixture_connection_pools.map(&:unpin_connection!).all?
194
+ # Something caused the transaction to be committed or rolled back
195
+ # We can no longer trust the database is in a clean state.
196
+ @@already_loaded_fixtures.clear
196
197
  end
197
- @fixture_connections.clear
198
+ @fixture_connection_pools.clear
198
199
  teardown_shared_connection_pool
199
- else
200
- ActiveRecord::FixtureSet.reset_cache
201
200
  end
202
201
 
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
202
  # Shares the writing connection pool with connections on
214
203
  # other handlers.
215
204
  #
@@ -272,22 +261,30 @@ module ActiveRecord
272
261
  use_instantiated_fixtures != :no_instances
273
262
  end
274
263
 
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)
264
+ def method_missing(method, ...)
265
+ if fixture_sets.key?(method.name)
266
+ active_record_fixture(method, ...)
278
267
  else
279
268
  super
280
269
  end
281
270
  end
282
271
 
283
- def respond_to_missing?(name, include_private = false)
284
- if include_private && fixture_sets.key?(name.to_s)
272
+ def respond_to_missing?(method, include_private = false)
273
+ if include_private && fixture_sets.key?(method.name)
285
274
  true
286
275
  else
287
276
  super
288
277
  end
289
278
  end
290
279
 
280
+ def active_record_fixture(fixture_set_name, *fixture_names)
281
+ if fs_name = fixture_sets[fixture_set_name.name]
282
+ access_fixture(fs_name, *fixture_names)
283
+ else
284
+ raise StandardError, "No fixture set named '#{fixture_set_name.inspect}'"
285
+ end
286
+ end
287
+
291
288
  def access_fixture(fs_name, *fixture_names)
292
289
  force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
293
290
  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