activerecord 7.1.3.2 → 7.2.0.beta1

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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +507 -2123
  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 +9 -8
  8. data/lib/active_record/associations/belongs_to_association.rb +18 -11
  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 +4 -2
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/has_many_association.rb +3 -3
  17. data/lib/active_record/associations/has_one_association.rb +2 -2
  18. data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
  19. data/lib/active_record/associations/join_dependency.rb +5 -7
  20. data/lib/active_record/associations/nested_error.rb +47 -0
  21. data/lib/active_record/associations/preloader/association.rb +2 -1
  22. data/lib/active_record/associations/preloader/branch.rb +7 -1
  23. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  24. data/lib/active_record/associations/singular_association.rb +6 -0
  25. data/lib/active_record/associations/through_association.rb +1 -1
  26. data/lib/active_record/associations.rb +34 -11
  27. data/lib/active_record/attribute_assignment.rb +1 -11
  28. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  29. data/lib/active_record/attribute_methods/dirty.rb +1 -1
  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 +7 -6
  34. data/lib/active_record/attribute_methods.rb +87 -58
  35. data/lib/active_record/attributes.rb +55 -42
  36. data/lib/active_record/autosave_association.rb +14 -30
  37. data/lib/active_record/base.rb +2 -3
  38. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +24 -107
  39. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -0
  40. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +248 -58
  41. data/lib/active_record/connection_adapters/abstract/database_statements.rb +35 -18
  42. data/lib/active_record/connection_adapters/abstract/query_cache.rb +160 -75
  43. data/lib/active_record/connection_adapters/abstract/quoting.rb +65 -91
  44. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  45. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +22 -9
  46. data/lib/active_record/connection_adapters/abstract/transaction.rb +60 -57
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +32 -61
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +69 -19
  49. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  50. data/lib/active_record/connection_adapters/mysql/quoting.rb +43 -48
  51. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +7 -0
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +11 -5
  53. data/lib/active_record/connection_adapters/mysql2_adapter.rb +20 -32
  54. data/lib/active_record/connection_adapters/pool_config.rb +7 -6
  55. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +27 -4
  56. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
  57. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  58. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  59. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  60. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -0
  61. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +16 -12
  62. data/lib/active_record/connection_adapters/postgresql_adapter.rb +26 -21
  63. data/lib/active_record/connection_adapters/schema_cache.rb +123 -128
  64. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  65. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +10 -6
  66. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +44 -46
  67. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  68. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  69. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  70. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +25 -2
  71. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +109 -77
  72. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +12 -6
  73. data/lib/active_record/connection_adapters/trilogy_adapter.rb +32 -65
  74. data/lib/active_record/connection_adapters.rb +121 -0
  75. data/lib/active_record/connection_handling.rb +56 -41
  76. data/lib/active_record/core.rb +59 -38
  77. data/lib/active_record/counter_cache.rb +23 -10
  78. data/lib/active_record/database_configurations/connection_url_resolver.rb +7 -2
  79. data/lib/active_record/database_configurations/database_config.rb +15 -4
  80. data/lib/active_record/database_configurations/hash_config.rb +44 -36
  81. data/lib/active_record/database_configurations/url_config.rb +20 -1
  82. data/lib/active_record/database_configurations.rb +1 -1
  83. data/lib/active_record/delegated_type.rb +30 -6
  84. data/lib/active_record/destroy_association_async_job.rb +1 -1
  85. data/lib/active_record/dynamic_matchers.rb +2 -2
  86. data/lib/active_record/encryption/encryptable_record.rb +2 -2
  87. data/lib/active_record/encryption/encrypted_attribute_type.rb +24 -4
  88. data/lib/active_record/encryption/encryptor.rb +17 -2
  89. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  90. data/lib/active_record/encryption/message_serializer.rb +4 -0
  91. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  92. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  93. data/lib/active_record/encryption/scheme.rb +8 -4
  94. data/lib/active_record/enum.rb +11 -2
  95. data/lib/active_record/errors.rb +16 -11
  96. data/lib/active_record/explain.rb +13 -24
  97. data/lib/active_record/fixtures.rb +37 -31
  98. data/lib/active_record/future_result.rb +17 -4
  99. data/lib/active_record/gem_version.rb +3 -3
  100. data/lib/active_record/inheritance.rb +4 -2
  101. data/lib/active_record/insert_all.rb +18 -15
  102. data/lib/active_record/integration.rb +4 -1
  103. data/lib/active_record/internal_metadata.rb +48 -34
  104. data/lib/active_record/locking/optimistic.rb +8 -7
  105. data/lib/active_record/log_subscriber.rb +0 -21
  106. data/lib/active_record/marshalling.rb +1 -1
  107. data/lib/active_record/message_pack.rb +2 -2
  108. data/lib/active_record/migration/command_recorder.rb +2 -3
  109. data/lib/active_record/migration/compatibility.rb +11 -3
  110. data/lib/active_record/migration/default_strategy.rb +4 -5
  111. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  112. data/lib/active_record/migration.rb +85 -76
  113. data/lib/active_record/model_schema.rb +34 -69
  114. data/lib/active_record/nested_attributes.rb +11 -3
  115. data/lib/active_record/normalization.rb +3 -7
  116. data/lib/active_record/persistence.rb +32 -354
  117. data/lib/active_record/query_cache.rb +18 -6
  118. data/lib/active_record/query_logs.rb +15 -0
  119. data/lib/active_record/query_logs_formatter.rb +1 -1
  120. data/lib/active_record/querying.rb +21 -9
  121. data/lib/active_record/railtie.rb +52 -64
  122. data/lib/active_record/railties/controller_runtime.rb +13 -4
  123. data/lib/active_record/railties/databases.rake +41 -44
  124. data/lib/active_record/reflection.rb +98 -37
  125. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  126. data/lib/active_record/relation/batches.rb +3 -3
  127. data/lib/active_record/relation/calculations.rb +94 -61
  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/polymorphic_array_value.rb +6 -1
  133. data/lib/active_record/relation/predicate_builder.rb +3 -3
  134. data/lib/active_record/relation/query_methods.rb +196 -43
  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 +500 -66
  139. data/lib/active_record/result.rb +32 -45
  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/signed_id.rb +11 -1
  146. data/lib/active_record/statement_cache.rb +7 -7
  147. data/lib/active_record/table_metadata.rb +1 -10
  148. data/lib/active_record/tasks/database_tasks.rb +70 -42
  149. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  150. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  151. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
  152. data/lib/active_record/test_fixtures.rb +82 -91
  153. data/lib/active_record/testing/query_assertions.rb +121 -0
  154. data/lib/active_record/timestamp.rb +4 -2
  155. data/lib/active_record/token_for.rb +22 -12
  156. data/lib/active_record/touch_later.rb +1 -1
  157. data/lib/active_record/transaction.rb +68 -0
  158. data/lib/active_record/transactions.rb +43 -14
  159. data/lib/active_record/translation.rb +0 -2
  160. data/lib/active_record/type/serialized.rb +1 -3
  161. data/lib/active_record/type_caster/connection.rb +4 -4
  162. data/lib/active_record/validations/associated.rb +9 -3
  163. data/lib/active_record/validations/uniqueness.rb +14 -10
  164. data/lib/active_record/validations.rb +4 -1
  165. data/lib/active_record.rb +149 -40
  166. data/lib/arel/alias_predication.rb +1 -1
  167. data/lib/arel/collectors/bind.rb +2 -0
  168. data/lib/arel/collectors/composite.rb +7 -0
  169. data/lib/arel/collectors/sql_string.rb +1 -1
  170. data/lib/arel/collectors/substitute_binds.rb +1 -1
  171. data/lib/arel/nodes/binary.rb +0 -6
  172. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  173. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  174. data/lib/arel/nodes/node.rb +4 -3
  175. data/lib/arel/nodes/sql_literal.rb +7 -0
  176. data/lib/arel/nodes.rb +2 -2
  177. data/lib/arel/predications.rb +1 -1
  178. data/lib/arel/select_manager.rb +1 -1
  179. data/lib/arel/tree_manager.rb +8 -3
  180. data/lib/arel/update_manager.rb +2 -1
  181. data/lib/arel/visitors/dot.rb +1 -0
  182. data/lib/arel/visitors/mysql.rb +9 -4
  183. data/lib/arel/visitors/postgresql.rb +1 -12
  184. data/lib/arel/visitors/to_sql.rb +31 -17
  185. data/lib/arel.rb +7 -3
  186. metadata +17 -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:
@@ -179,7 +179,7 @@ module ActiveRecord
179
179
  each_current_configuration(env) do |db_config|
180
180
  with_temporary_pool(db_config) do
181
181
  begin
182
- database_initialized = migration_connection.schema_migration.table_exists?
182
+ database_initialized = migration_connection_pool.schema_migration.table_exists?
183
183
  rescue ActiveRecord::NoDatabaseError
184
184
  create(db_config)
185
185
  retry
@@ -240,7 +240,7 @@ module ActiveRecord
240
240
 
241
241
  check_target_version
242
242
 
243
- migration_connection.migration_context.migrate(target_version) do |migration|
243
+ migration_connection_pool.migration_context.migrate(target_version) do |migration|
244
244
  if version.blank?
245
245
  scope.blank? || scope == migration.scope
246
246
  else
@@ -250,17 +250,17 @@ module ActiveRecord
250
250
  Migration.write("No migrations ran. (using #{scope} scope)") if scope.present? && migrations_ran.empty?
251
251
  end
252
252
 
253
- migration_connection.schema_cache.clear!
253
+ migration_connection_pool.schema_cache.clear!
254
254
  ensure
255
255
  Migration.verbose = verbose_was
256
256
  end
257
257
 
258
- def db_configs_with_versions(db_configs) # :nodoc:
258
+ def db_configs_with_versions # :nodoc:
259
259
  db_configs_with_versions = Hash.new { |h, k| h[k] = [] }
260
260
 
261
- with_temporary_connection_for_each do |conn|
262
- db_config = conn.pool.db_config
263
- versions_to_run = conn.migration_context.pending_migration_versions
261
+ with_temporary_pool_for_each do |pool|
262
+ db_config = pool.db_config
263
+ versions_to_run = pool.migration_context.pending_migration_versions
264
264
  target_version = ActiveRecord::Tasks::DatabaseTasks.target_version
265
265
 
266
266
  versions_to_run.each do |version|
@@ -273,15 +273,15 @@ module ActiveRecord
273
273
  end
274
274
 
275
275
  def migrate_status
276
- unless migration_connection.schema_migration.table_exists?
276
+ unless migration_connection_pool.schema_migration.table_exists?
277
277
  Kernel.abort "Schema migrations table does not exist yet."
278
278
  end
279
279
 
280
280
  # output
281
- puts "\ndatabase: #{migration_connection.pool.db_config.database}\n\n"
281
+ puts "\ndatabase: #{migration_connection_pool.db_config.database}\n\n"
282
282
  puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
283
283
  puts "-" * 50
284
- migration_connection.migration_context.migrations_status.each do |status, version, name|
284
+ migration_connection_pool.migration_context.migrations_status.each do |status, version, name|
285
285
  puts "#{status.center(8)} #{version.ljust(14)} #{name}"
286
286
  end
287
287
  puts
@@ -362,7 +362,7 @@ module ActiveRecord
362
362
  raise ArgumentError, "unknown format #{format.inspect}"
363
363
  end
364
364
 
365
- migration_connection.internal_metadata.create_table_and_set_flags(db_config.env_name, schema_sha1(file))
365
+ migration_connection_pool.internal_metadata.create_table_and_set_flags(db_config.env_name, schema_sha1(file))
366
366
  ensure
367
367
  Migration.verbose = verbose_was
368
368
  end
@@ -374,11 +374,12 @@ module ActiveRecord
374
374
 
375
375
  return true unless file && File.exist?(file)
376
376
 
377
- with_temporary_connection(db_config) do |connection|
378
- return false unless connection.internal_metadata.enabled?
379
- return false unless connection.internal_metadata.table_exists?
377
+ with_temporary_pool(db_config) do |pool|
378
+ internal_metadata = pool.internal_metadata
379
+ return false unless internal_metadata.enabled?
380
+ return false unless internal_metadata.table_exists?
380
381
 
381
- connection.internal_metadata[:schema_sha1] == schema_sha1(file)
382
+ internal_metadata[:schema_sha1] == schema_sha1(file)
382
383
  end
383
384
  end
384
385
 
@@ -389,7 +390,7 @@ module ActiveRecord
389
390
 
390
391
  with_temporary_pool(db_config, clobber: true) do
391
392
  if schema_up_to_date?(db_config, format, file)
392
- truncate_tables(db_config)
393
+ truncate_tables(db_config) unless ENV["SKIP_TEST_DATABASE_TRUNCATE"]
393
394
  else
394
395
  purge(db_config)
395
396
  load_schema(db_config, format, file)
@@ -411,11 +412,11 @@ module ActiveRecord
411
412
  case format
412
413
  when :ruby
413
414
  File.open(filename, "w:utf-8") do |file|
414
- ActiveRecord::SchemaDumper.dump(migration_connection, file)
415
+ ActiveRecord::SchemaDumper.dump(migration_connection_pool, file)
415
416
  end
416
417
  when :sql
417
418
  structure_dump(db_config, filename)
418
- if migration_connection.schema_migration.table_exists?
419
+ if migration_connection_pool.schema_migration.table_exists?
419
420
  File.open(filename, "a") do |f|
420
421
  f.puts migration_connection.dump_schema_information
421
422
  f.print "\n"
@@ -437,14 +438,26 @@ module ActiveRecord
437
438
  end
438
439
  end
439
440
 
440
- def cache_dump_filename(db_config_name, schema_cache_path: nil)
441
- filename = if ActiveRecord::Base.configurations.primary?(db_config_name)
442
- "schema_cache.yml"
441
+ def cache_dump_filename(db_config_or_name, schema_cache_path: nil)
442
+ if db_config_or_name.is_a?(DatabaseConfigurations::DatabaseConfig)
443
+ schema_cache_path ||
444
+ db_config_or_name.schema_cache_path ||
445
+ schema_cache_env ||
446
+ db_config_or_name.default_schema_cache_path(ActiveRecord::Tasks::DatabaseTasks.db_dir)
443
447
  else
444
- "#{db_config_name}_schema_cache.yml"
445
- end
448
+ ActiveRecord.deprecator.warn(<<~MSG.squish)
449
+ Passing a database name to `cache_dump_filename` is deprecated and will be removed in Rails 7.3. Pass a
450
+ `ActiveRecord::DatabaseConfigurations::DatabaseConfig` object instead.
451
+ MSG
446
452
 
447
- schema_cache_path || ENV["SCHEMA_CACHE"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
453
+ filename = if ActiveRecord::Base.configurations.primary?(db_config_or_name)
454
+ "schema_cache.yml"
455
+ else
456
+ "#{db_config_or_name}_schema_cache.yml"
457
+ end
458
+
459
+ schema_cache_path || schema_cache_env || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
460
+ end
448
461
  end
449
462
 
450
463
  def load_schema_current(format = ActiveRecord.schema_format, file = nil, environment = env)
@@ -476,29 +489,29 @@ module ActiveRecord
476
489
  # Dumps the schema cache in YAML format for the connection into the file
477
490
  #
478
491
  # ==== Examples
479
- # ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, "tmp/schema_dump.yaml")
480
- def dump_schema_cache(conn, filename)
481
- conn.schema_cache.dump_to(filename)
492
+ # ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.lease_connection, "tmp/schema_dump.yaml")
493
+ def dump_schema_cache(conn_or_pool, filename)
494
+ conn_or_pool.schema_cache.dump_to(filename)
482
495
  end
483
496
 
484
497
  def clear_schema_cache(filename)
485
498
  FileUtils.rm_f filename, verbose: false
486
499
  end
487
500
 
488
- def with_temporary_connection_for_each(env: ActiveRecord::Tasks::DatabaseTasks.env, name: nil, clobber: false, &block) # :nodoc:
501
+ def with_temporary_pool_for_each(env: ActiveRecord::Tasks::DatabaseTasks.env, name: nil, clobber: false, &block) # :nodoc:
489
502
  if name
490
503
  db_config = ActiveRecord::Base.configurations.configs_for(env_name: env, name: name)
491
- with_temporary_connection(db_config, clobber: clobber, &block)
504
+ with_temporary_pool(db_config, clobber: clobber, &block)
492
505
  else
493
506
  ActiveRecord::Base.configurations.configs_for(env_name: env, name: name).each do |db_config|
494
- with_temporary_connection(db_config, clobber: clobber, &block)
507
+ with_temporary_pool(db_config, clobber: clobber, &block)
495
508
  end
496
509
  end
497
510
  end
498
511
 
499
- def with_temporary_connection(db_config, clobber: false) # :nodoc:
512
+ def with_temporary_connection(db_config, clobber: false, &block) # :nodoc:
500
513
  with_temporary_pool(db_config, clobber: clobber) do |pool|
501
- yield pool.connection
514
+ pool.with_connection(&block)
502
515
  end
503
516
  end
504
517
 
@@ -507,10 +520,25 @@ module ActiveRecord
507
520
  end
508
521
 
509
522
  def migration_connection # :nodoc:
510
- migration_class.connection
523
+ migration_class.lease_connection
524
+ end
525
+
526
+ def migration_connection_pool # :nodoc:
527
+ migration_class.connection_pool
511
528
  end
512
529
 
513
530
  private
531
+ def schema_cache_env
532
+ if ENV["SCHEMA_CACHE"]
533
+ ActiveRecord.deprecator.warn(<<~MSG.squish)
534
+ Setting `ENV["SCHEMA_CACHE"]` is deprecated and will be removed in Rails 7.3.
535
+ Configure the `:schema_cache_path` in the database configuration instead.
536
+ MSG
537
+
538
+ nil
539
+ end
540
+ end
541
+
514
542
  def with_temporary_pool(db_config, clobber: false)
515
543
  original_db_config = migration_class.connection_db_config
516
544
  pool = migration_class.connection_handler.establish_connection(db_config, clobber: clobber)
@@ -603,11 +631,11 @@ module ActiveRecord
603
631
 
604
632
  def check_current_protected_environment!(db_config)
605
633
  with_temporary_pool(db_config) do |pool|
606
- connection = pool.connection
607
- current = connection.migration_context.current_environment
608
- stored = connection.migration_context.last_stored_environment
634
+ migration_context = pool.migration_context
635
+ current = migration_context.current_environment
636
+ stored = migration_context.last_stored_environment
609
637
 
610
- if connection.migration_context.protected_environment?
638
+ if migration_context.protected_environment?
611
639
  raise ActiveRecord::ProtectedEnvironmentError.new(stored)
612
640
  end
613
641
 
@@ -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)
@@ -13,6 +13,7 @@ module ActiveRecord
13
13
 
14
14
  def after_teardown # :nodoc:
15
15
  super
16
+ ensure
16
17
  teardown_fixtures
17
18
  end
18
19
 
@@ -52,20 +53,6 @@ module ActiveRecord
52
53
  self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys)
53
54
  end
54
55
 
55
- def fixture_path # :nodoc:
56
- ActiveRecord.deprecator.warn(<<~WARNING)
57
- TestFixtures.fixture_path is deprecated and will be removed in Rails 7.2. Use .fixture_paths instead.
58
- If multiple fixture paths have been configured with .fixture_paths, then .fixture_path will just return
59
- the first path.
60
- WARNING
61
- fixture_paths.first
62
- end
63
-
64
- def fixture_path=(path) # :nodoc:
65
- ActiveRecord.deprecator.warn("TestFixtures.fixture_path= is deprecated and will be removed in Rails 7.2. Use .fixture_paths= instead.")
66
- self.fixture_paths = Array(path)
67
- end
68
-
69
56
  def fixtures(*fixture_set_names)
70
57
  if fixture_set_names.first == :all
71
58
  raise StandardError, "No fixture path found. Please set `#{self}.fixture_paths`." if fixture_paths.blank?
@@ -78,7 +65,7 @@ module ActiveRecord
78
65
  fixture_set_names = fixture_set_names.flatten.map(&:to_s)
79
66
  end
80
67
 
81
- self.fixture_table_names |= fixture_set_names
68
+ self.fixture_table_names = (fixture_table_names | fixture_set_names).sort
82
69
  setup_fixture_accessors(fixture_set_names)
83
70
  end
84
71
 
@@ -109,45 +96,68 @@ module ActiveRecord
109
96
  end
110
97
  end
111
98
 
112
- def fixture_path # :nodoc:
113
- ActiveRecord.deprecator.warn(<<~WARNING)
114
- TestFixtures#fixture_path is deprecated and will be removed in Rails 7.2. Use #fixture_paths instead.
115
- If multiple fixture paths have been configured with #fixture_paths, then #fixture_path will just return
116
- the first path.
117
- WARNING
118
- fixture_paths.first
119
- end
120
-
121
- def run_in_transaction?
122
- use_transactional_tests &&
123
- !self.class.uses_transaction?(name)
124
- end
125
-
126
- def setup_fixtures(config = ActiveRecord::Base)
127
- if pre_loaded_fixtures && !use_transactional_tests
128
- raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
99
+ private
100
+ def run_in_transaction?
101
+ use_transactional_tests &&
102
+ !self.class.uses_transaction?(name)
129
103
  end
130
104
 
131
- @fixture_cache = {}
132
- @fixture_connections = []
133
- @@already_loaded_fixtures ||= {}
134
- @connection_subscriber = nil
135
- @saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
105
+ def setup_fixtures(config = ActiveRecord::Base)
106
+ if pre_loaded_fixtures && !use_transactional_tests
107
+ raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
108
+ end
136
109
 
137
- # Load fixtures once and begin transaction.
138
- if run_in_transaction?
139
- if @@already_loaded_fixtures[self.class]
140
- @loaded_fixtures = @@already_loaded_fixtures[self.class]
110
+ @fixture_cache = {}
111
+ @fixture_cache_key = [self.class.fixture_table_names.dup, self.class.fixture_paths.dup, self.class.fixture_class_names.dup]
112
+ @fixture_connection_pools = []
113
+ @@already_loaded_fixtures ||= {}
114
+ @connection_subscriber = nil
115
+ @saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
116
+
117
+ if run_in_transaction?
118
+ # Load fixtures once and begin transaction.
119
+ @loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key]
120
+ unless @loaded_fixtures
121
+ @@already_loaded_fixtures.clear
122
+ @loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key] = load_fixtures(config)
123
+ end
124
+
125
+ setup_transactional_fixtures
141
126
  else
127
+ # Load fixtures for every test.
128
+ ActiveRecord::FixtureSet.reset_cache
129
+ invalidate_already_loaded_fixtures
142
130
  @loaded_fixtures = load_fixtures(config)
143
- @@already_loaded_fixtures[self.class] = @loaded_fixtures
144
131
  end
145
132
 
133
+ # Instantiate fixtures for every test if requested.
134
+ instantiate_fixtures if use_instantiated_fixtures
135
+ end
136
+
137
+ def teardown_fixtures
138
+ # Rollback changes if a transaction is active.
139
+ if run_in_transaction?
140
+ teardown_transactional_fixtures
141
+ else
142
+ ActiveRecord::FixtureSet.reset_cache
143
+ invalidate_already_loaded_fixtures
144
+ end
145
+
146
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
147
+ end
148
+
149
+ def invalidate_already_loaded_fixtures
150
+ @@already_loaded_fixtures.clear
151
+ end
152
+
153
+ def setup_transactional_fixtures
154
+ setup_shared_connection_pool
155
+
146
156
  # Begin transactions for connections already established
147
- @fixture_connections = enlist_fixture_connections
148
- @fixture_connections.each do |connection|
149
- connection.begin_transaction joinable: false, _lazy: false
150
- connection.pool.lock_thread = true if lock_threads
157
+ @fixture_connection_pools = ActiveRecord::Base.connection_handler.connection_pool_list(:writing)
158
+ @fixture_connection_pools.each do |pool|
159
+ pool.pin_connection!(lock_threads)
160
+ pool.lease_connection
151
161
  end
152
162
 
153
163
  # When connections are established in the future, begin a transaction too
@@ -156,59 +166,31 @@ module ActiveRecord
156
166
  shard = payload[:shard] if payload.key?(:shard)
157
167
 
158
168
  if connection_name
159
- begin
160
- connection = ActiveRecord::Base.connection_handler.retrieve_connection(connection_name, shard: shard)
161
- rescue ConnectionNotEstablished
162
- connection = nil
163
- end
164
-
165
- if connection
169
+ pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(connection_name, shard: shard)
170
+ if pool
166
171
  setup_shared_connection_pool
167
172
 
168
- if !@fixture_connections.include?(connection)
169
- connection.begin_transaction joinable: false, _lazy: false
170
- connection.pool.lock_thread = true if lock_threads
171
- @fixture_connections << connection
173
+ unless @fixture_connection_pools.include?(pool)
174
+ pool.pin_connection!(lock_threads)
175
+ pool.lease_connection
176
+ @fixture_connection_pools << pool
172
177
  end
173
178
  end
174
179
  end
175
180
  end
176
-
177
- # Load fixtures for every test.
178
- else
179
- ActiveRecord::FixtureSet.reset_cache
180
- @@already_loaded_fixtures[self.class] = nil
181
- @loaded_fixtures = load_fixtures(config)
182
181
  end
183
182
 
184
- # Instantiate fixtures for every test if requested.
185
- instantiate_fixtures if use_instantiated_fixtures
186
- end
187
-
188
- def teardown_fixtures
189
- # Rollback changes if a transaction is active.
190
- if run_in_transaction?
183
+ def teardown_transactional_fixtures
191
184
  ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
192
- @fixture_connections.each do |connection|
193
- connection.rollback_transaction if connection.transaction_open?
194
- connection.pool.lock_thread = false
185
+ unless @fixture_connection_pools.map(&:unpin_connection!).all?
186
+ # Something caused the transaction to be committed or rolled back
187
+ # We can no longer trust the database is in a clean state.
188
+ @@already_loaded_fixtures.clear
195
189
  end
196
- @fixture_connections.clear
190
+ @fixture_connection_pools.clear
197
191
  teardown_shared_connection_pool
198
- else
199
- ActiveRecord::FixtureSet.reset_cache
200
192
  end
201
193
 
202
- ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
203
- end
204
-
205
- def enlist_fixture_connections
206
- setup_shared_connection_pool
207
-
208
- ActiveRecord::Base.connection_handler.connection_pool_list(:writing).map(&:connection)
209
- end
210
-
211
- private
212
194
  # Shares the writing connection pool with connections on
213
195
  # other handlers.
214
196
  #
@@ -271,22 +253,31 @@ module ActiveRecord
271
253
  use_instantiated_fixtures != :no_instances
272
254
  end
273
255
 
274
- def method_missing(name, *args, **kwargs, &block)
275
- if fs_name = fixture_sets[name.to_s]
276
- access_fixture(fs_name, *args, **kwargs, &block)
256
+ def method_missing(method, ...)
257
+ if fixture_sets.key?(method.name)
258
+ _active_record_fixture(method, ...)
277
259
  else
278
260
  super
279
261
  end
280
262
  end
281
263
 
282
- def respond_to_missing?(name, include_private = false)
283
- if include_private && fixture_sets.key?(name.to_s)
264
+ def respond_to_missing?(method, include_private = false)
265
+ if include_private && fixture_sets.key?(method.name)
284
266
  true
285
267
  else
286
268
  super
287
269
  end
288
270
  end
289
271
 
272
+ def _active_record_fixture(fixture_set_name, *fixture_names)
273
+ if fs_name = fixture_sets[fixture_set_name.name]
274
+ access_fixture(fs_name, *fixture_names)
275
+ else
276
+ raise StandardError, "No fixture set named '#{fixture_set_name.inspect}'"
277
+ end
278
+ end
279
+ alias_method :fixture, :_active_record_fixture
280
+
290
281
  def access_fixture(fs_name, *fixture_names)
291
282
  force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
292
283
  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
@@ -54,8 +54,10 @@ module ActiveRecord
54
54
 
55
55
  module ClassMethods # :nodoc:
56
56
  def touch_attributes_with_time(*names, time: nil)
57
+ names = names.map(&:to_s)
58
+ names = names.map { |name| attribute_aliases[name] || name }
57
59
  attribute_names = timestamp_attributes_for_update_in_model
58
- attribute_names |= names.map(&:to_s)
60
+ attribute_names |= names
59
61
  attribute_names.index_with(time || current_time_from_proper_timezone)
60
62
  end
61
63
 
@@ -75,7 +77,7 @@ module ActiveRecord
75
77
  end
76
78
 
77
79
  def current_time_from_proper_timezone
78
- connection.default_timezone == :utc ? Time.now.utc : Time.now
80
+ with_connection { |c| c.default_timezone == :utc ? Time.now.utc : Time.now }
79
81
  end
80
82
 
81
83
  protected