activerecord 7.2.2.1 → 8.0.5

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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +523 -677
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -0
  5. data/lib/active_record/associations/alias_tracker.rb +6 -4
  6. data/lib/active_record/associations/association.rb +34 -10
  7. data/lib/active_record/associations/belongs_to_association.rb +18 -2
  8. data/lib/active_record/associations/builder/association.rb +7 -6
  9. data/lib/active_record/associations/collection_association.rb +10 -8
  10. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  11. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  12. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  13. data/lib/active_record/associations/join_dependency.rb +2 -2
  14. data/lib/active_record/associations/preloader/association.rb +2 -2
  15. data/lib/active_record/associations/preloader/batch.rb +7 -1
  16. data/lib/active_record/associations/singular_association.rb +8 -3
  17. data/lib/active_record/associations.rb +34 -4
  18. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  19. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  20. data/lib/active_record/attribute_methods/query.rb +34 -0
  21. data/lib/active_record/attribute_methods/serialization.rb +1 -1
  22. data/lib/active_record/attribute_methods/time_zone_conversion.rb +2 -12
  23. data/lib/active_record/attribute_methods.rb +24 -19
  24. data/lib/active_record/attributes.rb +37 -26
  25. data/lib/active_record/autosave_association.rb +91 -39
  26. data/lib/active_record/base.rb +2 -2
  27. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +34 -25
  28. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  29. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  30. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +55 -47
  31. data/lib/active_record/connection_adapters/abstract/database_statements.rb +91 -44
  32. data/lib/active_record/connection_adapters/abstract/query_cache.rb +25 -8
  33. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  34. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +12 -3
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +40 -10
  37. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  38. data/lib/active_record/connection_adapters/abstract_adapter.rb +92 -70
  39. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +35 -46
  40. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -9
  41. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  42. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +50 -45
  43. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +84 -94
  44. data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -9
  45. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  46. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -2
  47. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +72 -43
  48. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  49. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  50. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  51. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
  52. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -24
  53. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +3 -2
  54. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +60 -17
  55. data/lib/active_record/connection_adapters/postgresql_adapter.rb +61 -99
  56. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  57. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  58. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +80 -100
  59. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -0
  60. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  61. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  62. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +10 -1
  63. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +63 -17
  64. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  65. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  66. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
  67. data/lib/active_record/connection_adapters.rb +0 -56
  68. data/lib/active_record/connection_handling.rb +35 -9
  69. data/lib/active_record/core.rb +57 -22
  70. data/lib/active_record/counter_cache.rb +1 -1
  71. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  72. data/lib/active_record/database_configurations/database_config.rb +4 -0
  73. data/lib/active_record/database_configurations/hash_config.rb +16 -2
  74. data/lib/active_record/delegated_type.rb +18 -18
  75. data/lib/active_record/encryption/config.rb +3 -1
  76. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  77. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  78. data/lib/active_record/encryption/encryptor.rb +37 -28
  79. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  80. data/lib/active_record/encryption/scheme.rb +8 -1
  81. data/lib/active_record/enum.rb +22 -34
  82. data/lib/active_record/errors.rb +16 -8
  83. data/lib/active_record/fixture_set/table_row.rb +19 -2
  84. data/lib/active_record/fixtures.rb +0 -2
  85. data/lib/active_record/future_result.rb +15 -9
  86. data/lib/active_record/gem_version.rb +4 -4
  87. data/lib/active_record/insert_all.rb +3 -3
  88. data/lib/active_record/locking/optimistic.rb +1 -1
  89. data/lib/active_record/log_subscriber.rb +5 -11
  90. data/lib/active_record/migration/command_recorder.rb +32 -12
  91. data/lib/active_record/migration/compatibility.rb +5 -2
  92. data/lib/active_record/migration.rb +40 -43
  93. data/lib/active_record/model_schema.rb +3 -4
  94. data/lib/active_record/nested_attributes.rb +4 -6
  95. data/lib/active_record/persistence.rb +128 -130
  96. data/lib/active_record/query_logs.rb +106 -50
  97. data/lib/active_record/query_logs_formatter.rb +17 -28
  98. data/lib/active_record/querying.rb +12 -12
  99. data/lib/active_record/railtie.rb +4 -28
  100. data/lib/active_record/railties/databases.rake +11 -34
  101. data/lib/active_record/reflection.rb +18 -21
  102. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  103. data/lib/active_record/relation/batches.rb +132 -72
  104. data/lib/active_record/relation/calculations.rb +71 -65
  105. data/lib/active_record/relation/delegation.rb +25 -14
  106. data/lib/active_record/relation/finder_methods.rb +32 -31
  107. data/lib/active_record/relation/merger.rb +8 -8
  108. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  109. data/lib/active_record/relation/predicate_builder/association_query_value.rb +2 -0
  110. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  111. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  112. data/lib/active_record/relation/predicate_builder.rb +13 -0
  113. data/lib/active_record/relation/query_attribute.rb +1 -1
  114. data/lib/active_record/relation/query_methods.rb +121 -70
  115. data/lib/active_record/relation/spawn_methods.rb +7 -7
  116. data/lib/active_record/relation/where_clause.rb +9 -3
  117. data/lib/active_record/relation.rb +95 -67
  118. data/lib/active_record/result.rb +66 -4
  119. data/lib/active_record/sanitization.rb +7 -6
  120. data/lib/active_record/schema_dumper.rb +34 -11
  121. data/lib/active_record/schema_migration.rb +2 -1
  122. data/lib/active_record/scoping/named.rb +5 -2
  123. data/lib/active_record/secure_token.rb +3 -3
  124. data/lib/active_record/signed_id.rb +7 -6
  125. data/lib/active_record/statement_cache.rb +14 -14
  126. data/lib/active_record/store.rb +7 -3
  127. data/lib/active_record/table_metadata.rb +1 -3
  128. data/lib/active_record/tasks/database_tasks.rb +69 -60
  129. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  130. data/lib/active_record/tasks/postgresql_database_tasks.rb +9 -1
  131. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  132. data/lib/active_record/test_databases.rb +1 -1
  133. data/lib/active_record/test_fixtures.rb +12 -0
  134. data/lib/active_record/token_for.rb +1 -1
  135. data/lib/active_record/transactions.rb +8 -7
  136. data/lib/active_record/type/serialized.rb +5 -0
  137. data/lib/active_record/validations/uniqueness.rb +8 -8
  138. data/lib/active_record.rb +22 -49
  139. data/lib/arel/collectors/bind.rb +2 -2
  140. data/lib/arel/collectors/sql_string.rb +1 -1
  141. data/lib/arel/collectors/substitute_binds.rb +2 -2
  142. data/lib/arel/crud.rb +2 -0
  143. data/lib/arel/delete_manager.rb +5 -0
  144. data/lib/arel/nodes/binary.rb +1 -1
  145. data/lib/arel/nodes/delete_statement.rb +4 -2
  146. data/lib/arel/nodes/node.rb +1 -1
  147. data/lib/arel/nodes/sql_literal.rb +1 -1
  148. data/lib/arel/nodes/update_statement.rb +4 -2
  149. data/lib/arel/predications.rb +1 -3
  150. data/lib/arel/select_manager.rb +6 -2
  151. data/lib/arel/table.rb +3 -7
  152. data/lib/arel/update_manager.rb +5 -0
  153. data/lib/arel/visitors/dot.rb +2 -0
  154. data/lib/arel/visitors/to_sql.rb +3 -1
  155. metadata +11 -15
  156. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
@@ -5,9 +5,8 @@ module ActiveRecord
5
5
  class PoolConfig # :nodoc:
6
6
  include MonitorMixin
7
7
 
8
- attr_reader :db_config, :role, :shard
8
+ attr_reader :db_config, :role, :shard, :connection_descriptor
9
9
  attr_writer :schema_reflection, :server_version
10
- attr_accessor :connection_class
11
10
 
12
11
  def schema_reflection
13
12
  @schema_reflection ||= SchemaReflection.new(db_config.lazy_schema_cache_path)
@@ -29,7 +28,7 @@ module ActiveRecord
29
28
  def initialize(connection_class, db_config, role, shard)
30
29
  super()
31
30
  @server_version = nil
32
- @connection_class = connection_class
31
+ self.connection_descriptor = connection_class
33
32
  @db_config = db_config
34
33
  @role = role
35
34
  @shard = shard
@@ -41,11 +40,12 @@ module ActiveRecord
41
40
  @server_version || synchronize { @server_version ||= connection.get_database_version }
42
41
  end
43
42
 
44
- def connection_name
45
- if connection_class.primary_class?
46
- "ActiveRecord::Base"
43
+ def connection_descriptor=(connection_descriptor)
44
+ case connection_descriptor
45
+ when ConnectionHandler::ConnectionDescriptor
46
+ @connection_descriptor = connection_descriptor
47
47
  else
48
- connection_class.name
48
+ @connection_descriptor = ConnectionHandler::ConnectionDescriptor.new(connection_descriptor.name, connection_descriptor.primary_class?)
49
49
  end
50
50
  end
51
51
 
@@ -65,7 +65,8 @@ module ActiveRecord
65
65
  other.is_a?(Column) &&
66
66
  super &&
67
67
  identity? == other.identity? &&
68
- serial? == other.serial?
68
+ serial? == other.serial? &&
69
+ virtual? == other.virtual?
69
70
  end
70
71
  alias :eql? :==
71
72
 
@@ -73,7 +74,8 @@ module ActiveRecord
73
74
  Column.hash ^
74
75
  super.hash ^
75
76
  identity?.hash ^
76
- serial?.hash
77
+ serial?.hash ^
78
+ virtual?.hash
77
79
  end
78
80
  end
79
81
  end
@@ -12,16 +12,8 @@ module ActiveRecord
12
12
 
13
13
  # Queries the database and returns the results in an Array-like object
14
14
  def query(sql, name = nil) # :nodoc:
15
- mark_transaction_written_if_write(sql)
16
-
17
- log(sql, name) do |notification_payload|
18
- with_raw_connection do |conn|
19
- result = conn.async_exec(sql).map_types!(@type_map_for_results).values
20
- verified!
21
- notification_payload[:row_count] = result.count
22
- result
23
- end
24
- end
15
+ result = internal_execute(sql, name)
16
+ result.map_types!(@type_map_for_results).values
25
17
  end
26
18
 
27
19
  READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
@@ -50,36 +42,6 @@ module ActiveRecord
50
42
  @notice_receiver_sql_warnings = []
51
43
  end
52
44
 
53
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
54
- log(sql, name, async: async) do |notification_payload|
55
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
56
- result = conn.async_exec(sql)
57
- verified!
58
- handle_warnings(result)
59
- notification_payload[:row_count] = result.count
60
- result
61
- end
62
- end
63
- end
64
-
65
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true) # :nodoc:
66
- execute_and_clear(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |result|
67
- types = {}
68
- fields = result.fields
69
- fields.each_with_index do |fname, i|
70
- ftype = result.ftype i
71
- fmod = result.fmod i
72
- types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
73
- end
74
- build_result(columns: fields, rows: result.values, column_types: types.freeze)
75
- end
76
- end
77
-
78
- def exec_delete(sql, name = nil, binds = []) # :nodoc:
79
- execute_and_clear(sql, name, binds) { |result| result.cmd_tuples }
80
- end
81
- alias :exec_update :exec_delete
82
-
83
45
  def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil) # :nodoc:
84
46
  if use_insert_returning? || pk == false
85
47
  super
@@ -165,13 +127,80 @@ module ActiveRecord
165
127
  def cancel_any_running_query
166
128
  return if @raw_connection.nil? || IDLE_TRANSACTION_STATUSES.include?(@raw_connection.transaction_status)
167
129
 
168
- @raw_connection.cancel
130
+ # Skip @raw_connection.cancel (PG::Connection#cancel) when using libpq >= 18 with pg < 1.6.0,
131
+ # because the pg gem cannot obtain the backend_key in that case.
132
+ # This method is only called from exec_rollback_db_transaction and exec_restart_db_transaction.
133
+ # Even without cancel, rollback will still run. However, since any running
134
+ # query must finish first, the rollback may take longer.
135
+ if !(PG.library_version >= 18_00_00 && Gem::Version.new(PG::VERSION) < Gem::Version.new("1.6.0"))
136
+ @raw_connection.cancel
137
+ end
169
138
  @raw_connection.block
170
139
  rescue PG::Error
171
140
  end
172
141
 
173
- def execute_batch(statements, name = nil)
174
- execute(combine_multi_statements(statements))
142
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
143
+ update_typemap_for_default_timezone
144
+ result = if prepare
145
+ begin
146
+ stmt_key = prepare_statement(sql, binds, raw_connection)
147
+ notification_payload[:statement_name] = stmt_key
148
+ raw_connection.exec_prepared(stmt_key, type_casted_binds)
149
+ rescue PG::FeatureNotSupported => error
150
+ if is_cached_plan_failure?(error)
151
+ # Nothing we can do if we are in a transaction because all commands
152
+ # will raise InFailedSQLTransaction
153
+ if in_transaction?
154
+ raise PreparedStatementCacheExpired.new(error.message, connection_pool: @pool)
155
+ else
156
+ @lock.synchronize do
157
+ # outside of transactions we can simply flush this query and retry
158
+ @statements.delete sql_key(sql)
159
+ end
160
+ retry
161
+ end
162
+ end
163
+
164
+ raise
165
+ end
166
+ elsif binds.nil? || binds.empty?
167
+ raw_connection.async_exec(sql)
168
+ else
169
+ raw_connection.exec_params(sql, type_casted_binds)
170
+ end
171
+
172
+ verified!
173
+ handle_warnings(result)
174
+ notification_payload[:row_count] = result.ntuples
175
+ result
176
+ end
177
+
178
+ def cast_result(result)
179
+ if result.fields.empty?
180
+ result.clear
181
+ return ActiveRecord::Result.empty
182
+ end
183
+
184
+ types = {}
185
+ fields = result.fields
186
+ fields.each_with_index do |fname, i|
187
+ ftype = result.ftype i
188
+ fmod = result.fmod i
189
+ types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
190
+ end
191
+ ar_result = ActiveRecord::Result.new(fields, result.values, types.freeze)
192
+ result.clear
193
+ ar_result
194
+ end
195
+
196
+ def affected_rows(result)
197
+ affected_rows = result.cmd_tuples
198
+ result.clear
199
+ affected_rows
200
+ end
201
+
202
+ def execute_batch(statements, name = nil, **kwargs)
203
+ raw_execute(combine_multi_statements(statements), name, batch: true, **kwargs)
175
204
  end
176
205
 
177
206
  def build_truncate_statements(table_names)
@@ -65,7 +65,7 @@ module ActiveRecord
65
65
  end
66
66
 
67
67
  def map(value, &block)
68
- value.map { |v| subtype.map(v, &block) }
68
+ value.is_a?(::Array) ? value.map(&block) : subtype.map(value, &block)
69
69
  end
70
70
 
71
71
  def changed_in_place?(raw_old_value, new_value)
@@ -25,6 +25,10 @@ module ActiveRecord
25
25
  build_point(x, y)
26
26
  when ::Array
27
27
  build_point(*value)
28
+ when ::Hash
29
+ return if value.blank?
30
+
31
+ build_point(*values_array_from_hash(value))
28
32
  else
29
33
  value
30
34
  end
@@ -36,6 +40,8 @@ module ActiveRecord
36
40
  "(#{number_for_point(value.x)},#{number_for_point(value.y)})"
37
41
  when ::Array
38
42
  serialize(build_point(*value))
43
+ when ::Hash
44
+ serialize(build_point(*values_array_from_hash(value)))
39
45
  else
40
46
  super
41
47
  end
@@ -57,6 +63,10 @@ module ActiveRecord
57
63
  def build_point(x, y)
58
64
  ActiveRecord::Point.new(Float(x), Float(y))
59
65
  end
66
+
67
+ def values_array_from_hash(value)
68
+ [value.values_at(:x, "x").compact.first, value.values_at(:y, "y").compact.first]
69
+ end
60
70
  end
61
71
  end
62
72
  end
@@ -45,12 +45,10 @@ Rails needs superuser privileges to disable referential integrity.
45
45
  BEGIN
46
46
  FOR r IN (
47
47
  SELECT FORMAT(
48
- 'UPDATE pg_constraint SET convalidated=false WHERE conname = ''%I'' AND connamespace::regnamespace = ''%I''::regnamespace; ALTER TABLE %I.%I VALIDATE CONSTRAINT %I;',
48
+ 'UPDATE pg_catalog.pg_constraint SET convalidated=false WHERE conname = ''%1$I'' AND connamespace::regnamespace = ''%2$I''::regnamespace; ALTER TABLE %2$I.%3$I VALIDATE CONSTRAINT %1$I;',
49
49
  constraint_name,
50
50
  table_schema,
51
- table_schema,
52
- table_name,
53
- constraint_name
51
+ table_name
54
52
  ) AS constraint_check
55
53
  FROM information_schema.table_constraints WHERE constraint_type = 'FOREIGN KEY'
56
54
  )
@@ -11,14 +11,11 @@ module ActiveRecord
11
11
  sql = super
12
12
  sql << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ")
13
13
  sql << o.exclusion_constraint_adds.map { |con| visit_AddExclusionConstraint con }.join(" ")
14
- sql << o.exclusion_constraint_drops.map { |con| visit_DropExclusionConstraint con }.join(" ")
15
14
  sql << o.unique_constraint_adds.map { |con| visit_AddUniqueConstraint con }.join(" ")
16
- sql << o.unique_constraint_drops.map { |con| visit_DropUniqueConstraint con }.join(" ")
17
15
  end
18
16
 
19
17
  def visit_AddForeignKey(o)
20
18
  super.dup.tap do |sql|
21
- sql << " DEFERRABLE INITIALLY #{o.options[:deferrable].to_s.upcase}" if o.deferrable
22
19
  sql << " NOT VALID" unless o.validate?
23
20
  end
24
21
  end
@@ -55,6 +52,7 @@ module ActiveRecord
55
52
  sql = ["CONSTRAINT"]
56
53
  sql << quote_column_name(o.name)
57
54
  sql << "UNIQUE"
55
+ sql << "NULLS NOT DISTINCT" if supports_nulls_not_distinct? && o.nulls_not_distinct
58
56
 
59
57
  if o.using_index
60
58
  sql << "USING INDEX #{quote_column_name(o.using_index)}"
@@ -73,18 +71,10 @@ module ActiveRecord
73
71
  "ADD #{accept(o)}"
74
72
  end
75
73
 
76
- def visit_DropExclusionConstraint(name)
77
- "DROP CONSTRAINT #{quote_column_name(name)}"
78
- end
79
-
80
74
  def visit_AddUniqueConstraint(o)
81
75
  "ADD #{accept(o)}"
82
76
  end
83
77
 
84
- def visit_DropUniqueConstraint(name)
85
- "DROP CONSTRAINT #{quote_column_name(name)}"
86
- end
87
-
88
78
  def visit_ChangeColumnDefinition(o)
89
79
  column = o.column
90
80
  column.sql_type = type_to_sql(column.type, **column.options)
@@ -224,11 +224,17 @@ module ActiveRecord
224
224
  options[:using_index]
225
225
  end
226
226
 
227
+ def nulls_not_distinct
228
+ options[:nulls_not_distinct]
229
+ end
230
+
227
231
  def export_name_on_schema_dump?
228
232
  !ActiveRecord::SchemaDumper.unique_ignore_pattern.match?(name) if name
229
233
  end
230
234
 
231
235
  def defined_for?(name: nil, column: nil, **options)
236
+ options = options.slice(*self.options.keys)
237
+
232
238
  (name.nil? || self.name == name.to_s) &&
233
239
  (column.nil? || Array(self.column) == Array(column).map(&:to_s)) &&
234
240
  options.all? { |k, v| self.options[k].to_s == v.to_s }
@@ -302,8 +308,8 @@ module ActiveRecord
302
308
  # t.exclusion_constraint("price WITH =, availability_range WITH &&", using: :gist, name: "price_check")
303
309
  #
304
310
  # See {connection.add_exclusion_constraint}[rdoc-ref:SchemaStatements#add_exclusion_constraint]
305
- def exclusion_constraint(*args)
306
- @base.add_exclusion_constraint(name, *args)
311
+ def exclusion_constraint(...)
312
+ @base.add_exclusion_constraint(name, ...)
307
313
  end
308
314
 
309
315
  # Removes the given exclusion constraint from the table.
@@ -311,17 +317,17 @@ module ActiveRecord
311
317
  # t.remove_exclusion_constraint(name: "price_check")
312
318
  #
313
319
  # See {connection.remove_exclusion_constraint}[rdoc-ref:SchemaStatements#remove_exclusion_constraint]
314
- def remove_exclusion_constraint(*args)
315
- @base.remove_exclusion_constraint(name, *args)
320
+ def remove_exclusion_constraint(...)
321
+ @base.remove_exclusion_constraint(name, ...)
316
322
  end
317
323
 
318
324
  # Adds a unique constraint.
319
325
  #
320
- # t.unique_constraint(:position, name: 'unique_position', deferrable: :deferred)
326
+ # t.unique_constraint(:position, name: 'unique_position', deferrable: :deferred, nulls_not_distinct: true)
321
327
  #
322
328
  # See {connection.add_unique_constraint}[rdoc-ref:SchemaStatements#add_unique_constraint]
323
- def unique_constraint(*args)
324
- @base.add_unique_constraint(name, *args)
329
+ def unique_constraint(...)
330
+ @base.add_unique_constraint(name, ...)
325
331
  end
326
332
 
327
333
  # Removes the given unique constraint from the table.
@@ -329,8 +335,8 @@ module ActiveRecord
329
335
  # t.remove_unique_constraint(name: "unique_position")
330
336
  #
331
337
  # See {connection.remove_unique_constraint}[rdoc-ref:SchemaStatements#remove_unique_constraint]
332
- def remove_unique_constraint(*args)
333
- @base.remove_unique_constraint(name, *args)
338
+ def remove_unique_constraint(...)
339
+ @base.remove_unique_constraint(name, ...)
334
340
  end
335
341
 
336
342
  # Validates the given constraint on the table.
@@ -339,8 +345,8 @@ module ActiveRecord
339
345
  # t.validate_constraint "price_check"
340
346
  #
341
347
  # See {connection.validate_constraint}[rdoc-ref:SchemaStatements#validate_constraint]
342
- def validate_constraint(*args)
343
- @base.validate_constraint(name, *args)
348
+ def validate_constraint(...)
349
+ @base.validate_constraint(name, ...)
344
350
  end
345
351
 
346
352
  # Validates the given check constraint on the table
@@ -349,22 +355,20 @@ module ActiveRecord
349
355
  # t.validate_check_constraint name: "price_check"
350
356
  #
351
357
  # See {connection.validate_check_constraint}[rdoc-ref:SchemaStatements#validate_check_constraint]
352
- def validate_check_constraint(*args)
353
- @base.validate_check_constraint(name, *args)
358
+ def validate_check_constraint(...)
359
+ @base.validate_check_constraint(name, ...)
354
360
  end
355
361
  end
356
362
 
357
363
  # = Active Record PostgreSQL Adapter Alter \Table
358
364
  class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable
359
- attr_reader :constraint_validations, :exclusion_constraint_adds, :exclusion_constraint_drops, :unique_constraint_adds, :unique_constraint_drops
365
+ attr_reader :constraint_validations, :exclusion_constraint_adds, :unique_constraint_adds
360
366
 
361
367
  def initialize(td)
362
368
  super
363
369
  @constraint_validations = []
364
370
  @exclusion_constraint_adds = []
365
- @exclusion_constraint_drops = []
366
371
  @unique_constraint_adds = []
367
- @unique_constraint_drops = []
368
372
  end
369
373
 
370
374
  def validate_constraint(name)
@@ -375,17 +379,9 @@ module ActiveRecord
375
379
  @exclusion_constraint_adds << @td.new_exclusion_constraint_definition(expression, options)
376
380
  end
377
381
 
378
- def drop_exclusion_constraint(constraint_name)
379
- @exclusion_constraint_drops << constraint_name
380
- end
381
-
382
382
  def add_unique_constraint(column_name, options)
383
383
  @unique_constraint_adds << @td.new_unique_constraint_definition(column_name, options)
384
384
  end
385
-
386
- def drop_unique_constraint(unique_constraint_name)
387
- @unique_constraint_drops << unique_constraint_name
388
- end
389
385
  end
390
386
  end
391
387
  end
@@ -22,7 +22,7 @@ module ActiveRecord
22
22
  stream.puts " # Custom types defined in this database."
23
23
  stream.puts " # Note that some types may not work with other database engines. Be careful if changing database."
24
24
  types.sort.each do |name, values|
25
- stream.puts " create_enum #{name.inspect}, #{values.split(",").inspect}"
25
+ stream.puts " create_enum #{name.inspect}, #{values.inspect}"
26
26
  end
27
27
  stream.puts
28
28
  end
@@ -68,6 +68,7 @@ module ActiveRecord
68
68
  "t.unique_constraint #{unique_constraint.column.inspect}"
69
69
  ]
70
70
 
71
+ parts << "nulls_not_distinct: #{unique_constraint.nulls_not_distinct.inspect}" if unique_constraint.nulls_not_distinct
71
72
  parts << "deferrable: #{unique_constraint.deferrable.inspect}" if unique_constraint.deferrable
72
73
 
73
74
  if unique_constraint.export_name_on_schema_dump?
@@ -91,7 +92,7 @@ module ActiveRecord
91
92
  spec = { type: schema_type(column).inspect }.merge!(spec)
92
93
  end
93
94
 
94
- spec[:enum_type] = "\"#{column.sql_type}\"" if column.enum?
95
+ spec[:enum_type] = column.sql_type.inspect if column.enum?
95
96
 
96
97
  spec
97
98
  end
@@ -54,9 +54,9 @@ module ActiveRecord
54
54
  execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
55
55
  end
56
56
 
57
- def drop_table(table_name, **options) # :nodoc:
58
- schema_cache.clear_data_source_cache!(table_name.to_s)
59
- execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
57
+ def drop_table(*table_names, **options) # :nodoc:
58
+ table_names.each { |table_name| schema_cache.clear_data_source_cache!(table_name.to_s) }
59
+ execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{table_names.map { |table_name| quote_table_name(table_name) }.join(', ')}#{' CASCADE' if options[:force] == :cascade}"
60
60
  end
61
61
 
62
62
  # Returns true if schema exists.
@@ -152,9 +152,23 @@ module ActiveRecord
152
152
  end
153
153
 
154
154
  def table_options(table_name) # :nodoc:
155
- if comment = table_comment(table_name)
156
- { comment: comment }
155
+ options = {}
156
+
157
+ comment = table_comment(table_name)
158
+
159
+ options[:comment] = comment if comment
160
+
161
+ inherited_table_names = inherited_table_names(table_name).presence
162
+
163
+ options[:options] = "INHERITS (#{inherited_table_names.join(", ")})" if inherited_table_names
164
+
165
+ if !options[:options] && supports_native_partitioning?
166
+ partition_definition = table_partition_definition(table_name)
167
+
168
+ options[:options] = "PARTITION BY #{partition_definition}" if partition_definition
157
169
  end
170
+
171
+ options
158
172
  end
159
173
 
160
174
  # Returns a comment stored in database for given table
@@ -172,6 +186,36 @@ module ActiveRecord
172
186
  end
173
187
  end
174
188
 
189
+ # Returns the partition definition of a given table
190
+ def table_partition_definition(table_name) # :nodoc:
191
+ scope = quoted_scope(table_name, type: "BASE TABLE")
192
+
193
+ query_value(<<~SQL, "SCHEMA")
194
+ SELECT pg_catalog.pg_get_partkeydef(c.oid)
195
+ FROM pg_catalog.pg_class c
196
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
197
+ WHERE c.relname = #{scope[:name]}
198
+ AND c.relkind IN (#{scope[:type]})
199
+ AND n.nspname = #{scope[:schema]}
200
+ SQL
201
+ end
202
+
203
+ # Returns the inherited table name of a given table
204
+ def inherited_table_names(table_name) # :nodoc:
205
+ scope = quoted_scope(table_name, type: "BASE TABLE")
206
+
207
+ query_values(<<~SQL, "SCHEMA")
208
+ SELECT parent.relname
209
+ FROM pg_catalog.pg_inherits i
210
+ JOIN pg_catalog.pg_class child ON i.inhrelid = child.oid
211
+ JOIN pg_catalog.pg_class parent ON i.inhparent = parent.oid
212
+ LEFT JOIN pg_namespace n ON n.oid = child.relnamespace
213
+ WHERE child.relname = #{scope[:name]}
214
+ AND child.relkind IN (#{scope[:type]})
215
+ AND n.nspname = #{scope[:schema]}
216
+ SQL
217
+ end
218
+
175
219
  # Returns the current database name.
176
220
  def current_database
177
221
  query_value("SELECT current_database()", "SCHEMA")
@@ -250,7 +294,7 @@ module ActiveRecord
250
294
 
251
295
  # Set the client message level.
252
296
  def client_min_messages=(level)
253
- internal_execute("SET client_min_messages TO '#{level}'")
297
+ internal_execute("SET client_min_messages TO '#{level}'", "SCHEMA")
254
298
  end
255
299
 
256
300
  # Returns the sequence name for a table's primary key or some other specified key.
@@ -654,7 +698,7 @@ module ActiveRecord
654
698
  scope = quoted_scope(table_name)
655
699
 
656
700
  unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
657
- SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred
701
+ SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef
658
702
  FROM pg_constraint c
659
703
  JOIN pg_class t ON c.conrelid = t.oid
660
704
  JOIN pg_namespace n ON n.oid = c.connamespace
@@ -667,10 +711,12 @@ module ActiveRecord
667
711
  conkey = row["conkey"].delete("{}").split(",").map(&:to_i)
668
712
  columns = column_names_from_column_numbers(row["conrelid"], conkey)
669
713
 
714
+ nulls_not_distinct = row["constraintdef"].start_with?("UNIQUE NULLS NOT DISTINCT")
670
715
  deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
671
716
 
672
717
  options = {
673
718
  name: row["conname"],
719
+ nulls_not_distinct: nulls_not_distinct,
674
720
  deferrable: deferrable
675
721
  }
676
722
 
@@ -722,15 +768,12 @@ module ActiveRecord
722
768
  def remove_exclusion_constraint(table_name, expression = nil, **options)
723
769
  excl_name_to_delete = exclusion_constraint_for!(table_name, expression: expression, **options).name
724
770
 
725
- at = create_alter_table(table_name)
726
- at.drop_exclusion_constraint(excl_name_to_delete)
727
-
728
- execute schema_creation.accept(at)
771
+ remove_constraint(table_name, excl_name_to_delete)
729
772
  end
730
773
 
731
774
  # Adds a new unique constraint to the table.
732
775
  #
733
- # add_unique_constraint :sections, [:position], deferrable: :deferred, name: "unique_position"
776
+ # add_unique_constraint :sections, [:position], deferrable: :deferred, name: "unique_position", nulls_not_distinct: true
734
777
  #
735
778
  # generates:
736
779
  #
@@ -747,6 +790,9 @@ module ActiveRecord
747
790
  # Specify whether or not the unique constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+.
748
791
  # [<tt>:using_index</tt>]
749
792
  # To specify an existing unique index name. Defaults to +nil+.
793
+ # [<tt>:nulls_not_distinct</tt>]
794
+ # Create a unique constraint where NULLs are treated equally.
795
+ # Note: only supported by PostgreSQL version 15.0.0 and greater.
750
796
  def add_unique_constraint(table_name, column_name = nil, **options)
751
797
  options = unique_constraint_options(table_name, column_name, options)
752
798
  at = create_alter_table(table_name)
@@ -777,10 +823,7 @@ module ActiveRecord
777
823
  def remove_unique_constraint(table_name, column_name = nil, **options)
778
824
  unique_name_to_delete = unique_constraint_for!(table_name, column: column_name, **options).name
779
825
 
780
- at = create_alter_table(table_name)
781
- at.drop_unique_constraint(unique_name_to_delete)
782
-
783
- execute schema_creation.accept(at)
826
+ remove_constraint(table_name, unique_name_to_delete)
784
827
  end
785
828
 
786
829
  # Maps logical Rails types to PostgreSQL-specific data types.
@@ -879,7 +922,7 @@ module ActiveRecord
879
922
  #
880
923
  # validate_check_constraint :products, name: "price_check"
881
924
  #
882
- # The +options+ hash accepts the same keys as add_check_constraint[rdoc-ref:ConnectionAdapters::SchemaStatements#add_check_constraint].
925
+ # The +options+ hash accepts the same keys as {add_check_constraint}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_check_constraint].
883
926
  def validate_check_constraint(table_name, **options)
884
927
  chk_name_to_validate = check_constraint_for!(table_name, **options).name
885
928