activerecord 7.2.0 → 8.0.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +189 -745
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/associations/association.rb +25 -5
  5. data/lib/active_record/associations/builder/association.rb +7 -6
  6. data/lib/active_record/associations/collection_association.rb +10 -8
  7. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  8. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  9. data/lib/active_record/associations/join_dependency/join_association.rb +3 -2
  10. data/lib/active_record/associations/join_dependency.rb +5 -5
  11. data/lib/active_record/associations/preloader/association.rb +2 -2
  12. data/lib/active_record/associations/singular_association.rb +8 -3
  13. data/lib/active_record/associations.rb +34 -4
  14. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  15. data/lib/active_record/attribute_assignment.rb +9 -1
  16. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -0
  17. data/lib/active_record/attributes.rb +6 -5
  18. data/lib/active_record/autosave_association.rb +69 -27
  19. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  20. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  21. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +23 -44
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +53 -18
  25. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  26. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  27. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +26 -5
  28. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  29. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -25
  30. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +20 -38
  31. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  32. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  33. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +44 -46
  34. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
  35. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -8
  36. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  37. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
  38. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  39. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
  40. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +50 -6
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +38 -90
  42. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  43. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  44. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  45. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  46. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -1
  47. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -12
  48. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  49. data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
  50. data/lib/active_record/connection_handling.rb +22 -0
  51. data/lib/active_record/core.rb +16 -9
  52. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
  53. data/lib/active_record/encryption/config.rb +3 -1
  54. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  55. data/lib/active_record/encryption/encrypted_attribute_type.rb +12 -3
  56. data/lib/active_record/encryption/encryptor.rb +16 -9
  57. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  58. data/lib/active_record/encryption/key_provider.rb +1 -1
  59. data/lib/active_record/encryption/scheme.rb +8 -1
  60. data/lib/active_record/encryption.rb +2 -0
  61. data/lib/active_record/enum.rb +8 -0
  62. data/lib/active_record/errors.rb +13 -5
  63. data/lib/active_record/fixtures.rb +0 -1
  64. data/lib/active_record/future_result.rb +14 -10
  65. data/lib/active_record/gem_version.rb +3 -3
  66. data/lib/active_record/insert_all.rb +1 -1
  67. data/lib/active_record/migration/command_recorder.rb +22 -5
  68. data/lib/active_record/migration/compatibility.rb +5 -2
  69. data/lib/active_record/migration.rb +35 -33
  70. data/lib/active_record/model_schema.rb +6 -3
  71. data/lib/active_record/nested_attributes.rb +11 -2
  72. data/lib/active_record/persistence.rb +128 -130
  73. data/lib/active_record/query_logs.rb +97 -39
  74. data/lib/active_record/query_logs_formatter.rb +17 -28
  75. data/lib/active_record/querying.rb +6 -6
  76. data/lib/active_record/railtie.rb +8 -14
  77. data/lib/active_record/reflection.rb +19 -10
  78. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  79. data/lib/active_record/relation/batches.rb +135 -75
  80. data/lib/active_record/relation/calculations.rb +24 -19
  81. data/lib/active_record/relation/delegation.rb +25 -14
  82. data/lib/active_record/relation/finder_methods.rb +18 -18
  83. data/lib/active_record/relation/merger.rb +8 -8
  84. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  85. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  86. data/lib/active_record/relation/predicate_builder.rb +6 -1
  87. data/lib/active_record/relation/query_methods.rb +58 -37
  88. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  89. data/lib/active_record/relation/spawn_methods.rb +1 -1
  90. data/lib/active_record/relation.rb +72 -61
  91. data/lib/active_record/result.rb +68 -7
  92. data/lib/active_record/sanitization.rb +7 -6
  93. data/lib/active_record/schema_dumper.rb +5 -0
  94. data/lib/active_record/schema_migration.rb +2 -1
  95. data/lib/active_record/scoping/named.rb +6 -2
  96. data/lib/active_record/statement_cache.rb +12 -12
  97. data/lib/active_record/store.rb +7 -3
  98. data/lib/active_record/tasks/database_tasks.rb +36 -16
  99. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  100. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  101. data/lib/active_record/test_fixtures.rb +12 -0
  102. data/lib/active_record/token_for.rb +1 -1
  103. data/lib/active_record/validations/uniqueness.rb +9 -8
  104. data/lib/active_record.rb +15 -0
  105. data/lib/arel/collectors/bind.rb +1 -1
  106. metadata +14 -14
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
3
  require "concurrent/map"
5
4
 
6
5
  module ActiveRecord
@@ -210,18 +209,25 @@ module ActiveRecord
210
209
  # This makes retrieving the connection pool O(1) once the process is warm.
211
210
  # When a connection is established or removed, we invalidate the cache.
212
211
  def retrieve_connection_pool(connection_name, role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard, strict: false)
213
- pool = get_pool_manager(connection_name)&.get_pool_config(role, shard)&.pool
212
+ pool_manager = get_pool_manager(connection_name)
213
+ pool = pool_manager&.get_pool_config(role, shard)&.pool
214
214
 
215
215
  if strict && !pool
216
- if shard != ActiveRecord::Base.default_shard
217
- message = "No connection pool for '#{connection_name}' found for the '#{shard}' shard."
218
- elsif role != ActiveRecord::Base.default_role
219
- message = "No connection pool for '#{connection_name}' found for the '#{role}' role."
220
- else
221
- message = "No connection pool for '#{connection_name}' found."
222
- end
216
+ selector = [
217
+ ("'#{shard}' shard" unless shard == ActiveRecord::Base.default_shard),
218
+ ("'#{role}' role" unless role == ActiveRecord::Base.default_role),
219
+ ].compact.join(" and ")
220
+
221
+ selector = [
222
+ (connection_name unless connection_name == "ActiveRecord::Base"),
223
+ selector.presence,
224
+ ].compact.join(" with ")
225
+
226
+ selector = " for #{selector}" if selector.present?
227
+
228
+ message = "No database connection defined#{selector}."
223
229
 
224
- raise ConnectionNotEstablished, message
230
+ raise ConnectionNotDefined.new(message, connection_name: connection_name, shard: shard, role: role)
225
231
  end
226
232
 
227
233
  pool
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
3
  require "monitor"
5
4
 
6
5
  module ActiveRecord
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
3
  require "weakref"
5
4
 
6
5
  module ActiveRecord
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
3
  require "concurrent/map"
5
4
  require "monitor"
6
5
 
@@ -118,6 +117,27 @@ module ActiveRecord
118
117
  # * private methods that require being called in a +synchronize+ blocks
119
118
  # are now explicitly documented
120
119
  class ConnectionPool
120
+ class WeakThreadKeyMap # :nodoc:
121
+ # FIXME: On 3.3 we could use ObjectSpace::WeakKeyMap
122
+ # but it currently cause GC crashes: https://github.com/byroot/rails/pull/3
123
+ def initialize
124
+ @map = {}
125
+ end
126
+
127
+ def clear
128
+ @map.clear
129
+ end
130
+
131
+ def [](key)
132
+ @map[key]
133
+ end
134
+
135
+ def []=(key, value)
136
+ @map.select! { |c, _| c.alive? }
137
+ @map[key] = value
138
+ end
139
+ end
140
+
121
141
  class Lease # :nodoc:
122
142
  attr_accessor :connection, :sticky
123
143
 
@@ -145,48 +165,9 @@ module ActiveRecord
145
165
  end
146
166
 
147
167
  class LeaseRegistry # :nodoc:
148
- if ObjectSpace.const_defined?(:WeakKeyMap) # RUBY_VERSION >= 3.3
149
- WeakKeyMap = ::ObjectSpace::WeakKeyMap # :nodoc:
150
- else
151
- class WeakKeyMap # :nodoc:
152
- def initialize
153
- @map = ObjectSpace::WeakMap.new
154
- @values = nil
155
- @size = 0
156
- end
157
-
158
- alias_method :clear, :initialize
159
-
160
- def [](key)
161
- prune if @map.size != @size
162
- @map[key]
163
- end
164
-
165
- def []=(key, value)
166
- @map[key] = value
167
- prune if @map.size != @size
168
- value
169
- end
170
-
171
- def delete(key)
172
- if value = self[key]
173
- self[key] = nil
174
- prune
175
- end
176
- value
177
- end
178
-
179
- private
180
- def prune(force = false)
181
- @values = @map.values
182
- @size = @map.size
183
- end
184
- end
185
- end
186
-
187
168
  def initialize
188
169
  @mutex = Mutex.new
189
- @map = WeakKeyMap.new
170
+ @map = WeakThreadKeyMap.new
190
171
  end
191
172
 
192
173
  def [](context)
@@ -197,7 +178,7 @@ module ActiveRecord
197
178
 
198
179
  def clear
199
180
  @mutex.synchronize do
200
- @map = WeakKeyMap.new
181
+ @map.clear
201
182
  end
202
183
  end
203
184
  end
@@ -630,8 +611,6 @@ module ActiveRecord
630
611
  remove conn
631
612
  end
632
613
  end
633
-
634
- prune_thread_cache
635
614
  end
636
615
 
637
616
  # Disconnect all connections that have been idle for at least
@@ -102,16 +102,16 @@ module ActiveRecord
102
102
  select_all(arel, name, binds, async: async).then(&:rows)
103
103
  end
104
104
 
105
- def query_value(sql, name = nil) # :nodoc:
106
- single_value_from_rows(query(sql, name))
105
+ def query_value(...) # :nodoc:
106
+ single_value_from_rows(query(...))
107
107
  end
108
108
 
109
- def query_values(sql, name = nil) # :nodoc:
110
- query(sql, name).map(&:first)
109
+ def query_values(...) # :nodoc:
110
+ query(...).map(&:first)
111
111
  end
112
112
 
113
- def query(sql, name = nil) # :nodoc:
114
- internal_exec_query(sql, name).rows
113
+ def query(...) # :nodoc:
114
+ internal_exec_query(...).rows
115
115
  end
116
116
 
117
117
  # Determines whether the SQL statement is a write query.
@@ -163,14 +163,14 @@ module ActiveRecord
163
163
  # +binds+ as the bind substitutes. +name+ is logged along with
164
164
  # the executed +sql+ statement.
165
165
  def exec_delete(sql, name = nil, binds = [])
166
- internal_exec_query(sql, name, binds)
166
+ affected_rows(internal_execute(sql, name, binds))
167
167
  end
168
168
 
169
169
  # Executes update +sql+ statement in the context of this connection using
170
170
  # +binds+ as the bind substitutes. +name+ is logged along with
171
171
  # the executed +sql+ statement.
172
172
  def exec_update(sql, name = nil, binds = [])
173
- internal_exec_query(sql, name, binds)
173
+ affected_rows(internal_execute(sql, name, binds))
174
174
  end
175
175
 
176
176
  def exec_insert_all(sql, name) # :nodoc:
@@ -224,11 +224,9 @@ module ActiveRecord
224
224
 
225
225
  return if table_names.empty?
226
226
 
227
- with_multi_statements do
228
- disable_referential_integrity do
229
- statements = build_truncate_statements(table_names)
230
- execute_batch(statements, "Truncate Tables")
231
- end
227
+ disable_referential_integrity do
228
+ statements = build_truncate_statements(table_names)
229
+ execute_batch(statements, "Truncate Tables")
232
230
  end
233
231
  end
234
232
 
@@ -358,7 +356,7 @@ module ActiveRecord
358
356
  end
359
357
  yield current_transaction.user_transaction
360
358
  else
361
- transaction_manager.within_new_transaction(isolation: isolation, joinable: joinable, &block)
359
+ within_new_transaction(isolation: isolation, joinable: joinable, &block)
362
360
  end
363
361
  rescue ActiveRecord::Rollback
364
362
  # rollbacks are silently swallowed
@@ -411,6 +409,14 @@ module ActiveRecord
411
409
  # Begins the transaction (and turns off auto-committing).
412
410
  def begin_db_transaction() end
413
411
 
412
+ def begin_deferred_transaction(isolation_level = nil) # :nodoc:
413
+ if isolation_level
414
+ begin_isolated_db_transaction(isolation_level)
415
+ else
416
+ begin_db_transaction
417
+ end
418
+ end
419
+
414
420
  def transaction_isolation_levels
415
421
  {
416
422
  read_uncommitted: "READ UNCOMMITTED",
@@ -427,6 +433,15 @@ module ActiveRecord
427
433
  raise ActiveRecord::TransactionIsolationError, "adapter does not support setting transaction isolation"
428
434
  end
429
435
 
436
+ # Hook point called after an isolated DB transaction is committed
437
+ # or rolled back.
438
+ # Most adapters don't need to implement anything because the isolation
439
+ # level is set on a per transaction basis.
440
+ # But some databases like SQLite set it on a per connection level
441
+ # and need to explicitly reset it after commit or rollback.
442
+ def reset_isolation_level
443
+ end
444
+
430
445
  # Commits the transaction (and turns on auto-committing).
431
446
  def commit_db_transaction() end
432
447
 
@@ -473,11 +488,9 @@ module ActiveRecord
473
488
  table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name(table)}" }
474
489
  statements = table_deletes + fixture_inserts
475
490
 
476
- with_multi_statements do
477
- transaction(requires_new: true) do
478
- disable_referential_integrity do
479
- execute_batch(statements, "Fixtures Load")
480
- end
491
+ transaction(requires_new: true) do
492
+ disable_referential_integrity do
493
+ execute_batch(statements, "Fixtures Load")
481
494
  end
482
495
  end
483
496
  end
@@ -524,28 +537,64 @@ module ActiveRecord
524
537
  HIGH_PRECISION_CURRENT_TIMESTAMP
525
538
  end
526
539
 
527
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
528
- raise NotImplementedError
540
+ # Same as raw_execute but returns an ActiveRecord::Result object.
541
+ def raw_exec_query(...) # :nodoc:
542
+ cast_result(raw_execute(...))
543
+ end
544
+
545
+ # Execute a query and returns an ActiveRecord::Result
546
+ def internal_exec_query(...) # :nodoc:
547
+ cast_result(internal_execute(...))
529
548
  end
530
549
 
531
550
  private
532
- def internal_execute(sql, name = "SCHEMA", allow_retry: false, materialize_transactions: true)
533
- sql = transform_query(sql)
534
- check_if_write_query(sql)
551
+ # Lowest level way to execute a query. Doesn't check for illegal writes, doesn't annotate queries, yields a native result object.
552
+ def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, batch: false)
553
+ type_casted_binds = type_casted_binds(binds)
554
+ log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
555
+ with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
556
+ perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch)
557
+ end
558
+ end
559
+ end
535
560
 
536
- mark_transaction_written_if_write(sql)
561
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch:)
562
+ raise NotImplementedError
563
+ end
537
564
 
538
- raw_execute(sql, name, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
565
+ # Receive a native adapter result object and returns an ActiveRecord::Result object.
566
+ def cast_result(raw_result)
567
+ raise NotImplementedError
539
568
  end
540
569
 
541
- def execute_batch(statements, name = nil)
542
- statements.each do |statement|
543
- internal_execute(statement, name)
570
+ def affected_rows(raw_result)
571
+ raise NotImplementedError
572
+ end
573
+
574
+ def preprocess_query(sql)
575
+ check_if_write_query(sql)
576
+ mark_transaction_written_if_write(sql)
577
+
578
+ # We call tranformers after the write checks so we don't add extra parsing work.
579
+ # This means we assume no transformer whille change a read for a write
580
+ # but it would be insane to do such a thing.
581
+ ActiveRecord.query_transformers.each do |transformer|
582
+ sql = transformer.call(sql, self)
544
583
  end
584
+
585
+ sql
545
586
  end
546
587
 
547
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
548
- raise NotImplementedError
588
+ # Same as #internal_exec_query, but yields a native adapter result
589
+ def internal_execute(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, &block)
590
+ sql = preprocess_query(sql)
591
+ raw_execute(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions, &block)
592
+ end
593
+
594
+ def execute_batch(statements, name = nil, **kwargs)
595
+ statements.each do |statement|
596
+ raw_execute(statement, name, **kwargs)
597
+ end
549
598
  end
550
599
 
551
600
  DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
@@ -614,10 +663,6 @@ module ActiveRecord
614
663
  end
615
664
  end
616
665
 
617
- def with_multi_statements
618
- yield
619
- end
620
-
621
666
  def combine_multi_statements(total_sql)
622
667
  total_sql.join(";\n")
623
668
  end
@@ -629,6 +674,8 @@ module ActiveRecord
629
674
  raise AsynchronousQueryInsideTransactionError, "Asynchronous queries are not allowed inside transactions"
630
675
  end
631
676
 
677
+ # We make sure to run query transformers on the orignal thread
678
+ sql = preprocess_query(sql)
632
679
  future_result = async.new(
633
680
  pool,
634
681
  sql,
@@ -636,19 +683,19 @@ module ActiveRecord
636
683
  binds,
637
684
  prepare: prepare,
638
685
  )
639
- if supports_concurrent_connections? && current_transaction.closed?
686
+ if supports_concurrent_connections? && !current_transaction.joinable?
640
687
  future_result.schedule!(ActiveRecord::Base.asynchronous_queries_session)
641
688
  else
642
689
  future_result.execute!(self)
643
690
  end
644
- return future_result
645
- end
646
-
647
- result = internal_exec_query(sql, name, binds, prepare: prepare, allow_retry: allow_retry)
648
- if async
649
- FutureResult.wrap(result)
691
+ future_result
650
692
  else
651
- result
693
+ result = internal_exec_query(sql, name, binds, prepare: prepare, allow_retry: allow_retry)
694
+ if async
695
+ FutureResult.wrap(result)
696
+ else
697
+ result
698
+ end
652
699
  end
653
700
  end
654
701
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "concurrent/map"
4
+ require "concurrent/atomic/atomic_fixnum"
4
5
 
5
6
  module ActiveRecord
6
7
  module ConnectionAdapters # :nodoc:
@@ -35,7 +36,9 @@ module ActiveRecord
35
36
  alias_method :enabled?, :enabled
36
37
  alias_method :dirties?, :dirties
37
38
 
38
- def initialize(max_size)
39
+ def initialize(version, max_size)
40
+ @version = version
41
+ @current_version = version.value
39
42
  @map = {}
40
43
  @max_size = max_size
41
44
  @enabled = false
@@ -43,14 +46,17 @@ module ActiveRecord
43
46
  end
44
47
 
45
48
  def size
49
+ check_version
46
50
  @map.size
47
51
  end
48
52
 
49
53
  def empty?
54
+ check_version
50
55
  @map.empty?
51
56
  end
52
57
 
53
58
  def [](key)
59
+ check_version
54
60
  return unless @enabled
55
61
 
56
62
  if entry = @map.delete(key)
@@ -59,6 +65,8 @@ module ActiveRecord
59
65
  end
60
66
 
61
67
  def compute_if_absent(key)
68
+ check_version
69
+
62
70
  return yield unless @enabled
63
71
 
64
72
  if entry = @map.delete(key)
@@ -76,12 +84,40 @@ module ActiveRecord
76
84
  @map.clear
77
85
  self
78
86
  end
87
+
88
+ private
89
+ def check_version
90
+ if @current_version != @version.value
91
+ @map.clear
92
+ @current_version = @version.value
93
+ end
94
+ end
95
+ end
96
+
97
+ class QueryCacheRegistry # :nodoc:
98
+ def initialize
99
+ @mutex = Mutex.new
100
+ @map = ConnectionPool::WeakThreadKeyMap.new
101
+ end
102
+
103
+ def compute_if_absent(context)
104
+ @map[context] || @mutex.synchronize do
105
+ @map[context] ||= yield
106
+ end
107
+ end
108
+
109
+ def clear
110
+ @map.synchronize do
111
+ @map.clear
112
+ end
113
+ end
79
114
  end
80
115
 
81
116
  module ConnectionPoolConfiguration # :nodoc:
82
117
  def initialize(...)
83
118
  super
84
- @thread_query_caches = Concurrent::Map.new(initial_capacity: @size)
119
+ @query_cache_version = Concurrent::AtomicFixnum.new
120
+ @thread_query_caches = QueryCacheRegistry.new
85
121
  @query_cache_max_size = \
86
122
  case query_cache = db_config&.query_cache
87
123
  when 0, false
@@ -121,11 +157,13 @@ module ActiveRecord
121
157
  end
122
158
 
123
159
  def enable_query_cache!
124
- query_cache.enabled, query_cache.dirties = true, true
160
+ query_cache.enabled = true
161
+ query_cache.dirties = true
125
162
  end
126
163
 
127
164
  def disable_query_cache!
128
- query_cache.enabled, query_cache.dirties = false, true
165
+ query_cache.enabled = false
166
+ query_cache.dirties = true
129
167
  end
130
168
 
131
169
  def query_cache_enabled
@@ -141,25 +179,16 @@ module ActiveRecord
141
179
  # With transactional fixtures, and especially systems test
142
180
  # another thread may use the same connection, but with a different
143
181
  # query cache. So we must clear them all.
144
- @thread_query_caches.each_value(&:clear)
145
- else
146
- query_cache.clear
182
+ @query_cache_version.increment
147
183
  end
184
+ query_cache.clear
148
185
  end
149
186
 
150
187
  def query_cache
151
188
  @thread_query_caches.compute_if_absent(ActiveSupport::IsolatedExecutionState.context) do
152
- Store.new(@query_cache_max_size)
189
+ Store.new(@query_cache_version, @query_cache_max_size)
153
190
  end
154
191
  end
155
-
156
- private
157
- def prune_thread_cache
158
- dead_threads = @thread_query_caches.keys.reject(&:alive?)
159
- dead_threads.each do |dead_thread|
160
- @thread_query_caches.delete(dead_thread)
161
- end
162
- end
163
192
  end
164
193
 
165
194
  attr_accessor :query_cache
@@ -239,7 +268,7 @@ module ActiveRecord
239
268
  if result
240
269
  ActiveSupport::Notifications.instrument(
241
270
  "sql.active_record",
242
- cache_notification_info(sql, name, binds)
271
+ cache_notification_info_result(sql, name, binds, result)
243
272
  )
244
273
  end
245
274
 
@@ -261,13 +290,19 @@ module ActiveRecord
261
290
  if hit
262
291
  ActiveSupport::Notifications.instrument(
263
292
  "sql.active_record",
264
- cache_notification_info(sql, name, binds)
293
+ cache_notification_info_result(sql, name, binds, result)
265
294
  )
266
295
  end
267
296
 
268
297
  result.dup
269
298
  end
270
299
 
300
+ def cache_notification_info_result(sql, name, binds, result)
301
+ payload = cache_notification_info(sql, name, binds)
302
+ payload[:row_count] = result.length
303
+ payload
304
+ end
305
+
271
306
  # Database adapters can override this method to
272
307
  # provide custom cache information.
273
308
  def cache_notification_info(sql, name, binds)
@@ -222,7 +222,7 @@ module ActiveRecord
222
222
 
223
223
  private
224
224
  def type_casted_binds(binds)
225
- binds.map do |value|
225
+ binds&.map do |value|
226
226
  if ActiveModel::Attribute === value
227
227
  type_cast(value.value_for_database)
228
228
  else
@@ -348,7 +348,7 @@ module ActiveRecord
348
348
  # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table]
349
349
  # is actually of this type:
350
350
  #
351
- # class SomeMigration < ActiveRecord::Migration[7.2]
351
+ # class SomeMigration < ActiveRecord::Migration[8.0]
352
352
  # def up
353
353
  # create_table :foo do |t|
354
354
  # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
@@ -345,6 +345,15 @@ module ActiveRecord
345
345
  # # Creates a table called 'assemblies_parts' with no id.
346
346
  # create_join_table(:assemblies, :parts)
347
347
  #
348
+ # # Creates a table called 'paper_boxes_papers' with no id.
349
+ # create_join_table('papers', 'paper_boxes')
350
+ #
351
+ # A duplicate prefix is combined into a single prefix. This is useful for
352
+ # namespaced models like Music::Artist and Music::Record:
353
+ #
354
+ # # Creates a table called 'music_artists_records' with no id.
355
+ # create_join_table('music_artists', 'music_records')
356
+ #
348
357
  # You can pass an +options+ hash which can include the following keys:
349
358
  # [<tt>:table_name</tt>]
350
359
  # Sets the table name, overriding the default.
@@ -516,7 +525,7 @@ module ActiveRecord
516
525
  raise NotImplementedError, "rename_table is not implemented"
517
526
  end
518
527
 
519
- # Drops a table from the database.
528
+ # Drops a table or tables from the database.
520
529
  #
521
530
  # [<tt>:force</tt>]
522
531
  # Set to +:cascade+ to drop dependent objects as well.
@@ -527,10 +536,12 @@ module ActiveRecord
527
536
  #
528
537
  # Although this command ignores most +options+ and the block if one is given,
529
538
  # it can be helpful to provide these in a migration's +change+ method so it can be reverted.
530
- # In that case, +options+ and the block will be used by #create_table.
531
- def drop_table(table_name, **options)
532
- schema_cache.clear_data_source_cache!(table_name.to_s)
533
- execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
539
+ # In that case, +options+ and the block will be used by #create_table except if you provide more than one table which is not supported.
540
+ def drop_table(*table_names, **options)
541
+ table_names.each do |table_name|
542
+ schema_cache.clear_data_source_cache!(table_name.to_s)
543
+ execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
544
+ end
534
545
  end
535
546
 
536
547
  # Add a new +type+ column named +column_name+ to +table_name+.
@@ -844,6 +855,16 @@ module ActiveRecord
844
855
  #
845
856
  # Note: only supported by PostgreSQL.
846
857
  #
858
+ # ====== Creating an index where NULLs are treated equally
859
+ #
860
+ # add_index(:people, :last_name, nulls_not_distinct: true)
861
+ #
862
+ # generates:
863
+ #
864
+ # CREATE INDEX index_people_on_last_name ON people (last_name) NULLS NOT DISTINCT
865
+ #
866
+ # Note: only supported by PostgreSQL version 15.0.0 and greater.
867
+ #
847
868
  # ====== Creating an index with a specific method
848
869
  #
849
870
  # add_index(:developers, :name, using: 'btree')
@@ -448,10 +448,14 @@ module ActiveRecord
448
448
  # = Active Record Real \Transaction
449
449
  class RealTransaction < Transaction
450
450
  def materialize!
451
- if isolation_level
452
- connection.begin_isolated_db_transaction(isolation_level)
451
+ if joinable?
452
+ if isolation_level
453
+ connection.begin_isolated_db_transaction(isolation_level)
454
+ else
455
+ connection.begin_db_transaction
456
+ end
453
457
  else
454
- connection.begin_db_transaction
458
+ connection.begin_deferred_transaction(isolation_level)
455
459
  end
456
460
 
457
461
  super
@@ -472,13 +476,19 @@ module ActiveRecord
472
476
  end
473
477
 
474
478
  def rollback
475
- connection.rollback_db_transaction if materialized?
479
+ if materialized?
480
+ connection.rollback_db_transaction
481
+ connection.reset_isolation_level if isolation_level
482
+ end
476
483
  @state.full_rollback!
477
484
  @instrumenter.finish(:rollback) if materialized?
478
485
  end
479
486
 
480
487
  def commit
481
- connection.commit_db_transaction if materialized?
488
+ if materialized?
489
+ connection.commit_db_transaction
490
+ connection.reset_isolation_level if isolation_level
491
+ end
482
492
  @state.full_commit!
483
493
  @instrumenter.finish(:commit) if materialized?
484
494
  end