activerecord 7.0.8.7 → 7.1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1795 -1424
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +16 -16
  5. data/lib/active_record/aggregations.rb +16 -13
  6. data/lib/active_record/association_relation.rb +1 -1
  7. data/lib/active_record/associations/association.rb +20 -4
  8. data/lib/active_record/associations/association_scope.rb +16 -9
  9. data/lib/active_record/associations/belongs_to_association.rb +14 -6
  10. data/lib/active_record/associations/builder/association.rb +3 -3
  11. data/lib/active_record/associations/builder/belongs_to.rb +21 -8
  12. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -5
  13. data/lib/active_record/associations/builder/singular_association.rb +4 -0
  14. data/lib/active_record/associations/collection_association.rb +19 -13
  15. data/lib/active_record/associations/collection_proxy.rb +15 -10
  16. data/lib/active_record/associations/foreign_association.rb +10 -3
  17. data/lib/active_record/associations/has_many_association.rb +20 -13
  18. data/lib/active_record/associations/has_many_through_association.rb +10 -6
  19. data/lib/active_record/associations/has_one_association.rb +10 -3
  20. data/lib/active_record/associations/join_dependency/join_association.rb +3 -2
  21. data/lib/active_record/associations/join_dependency.rb +10 -10
  22. data/lib/active_record/associations/preloader/association.rb +31 -7
  23. data/lib/active_record/associations/preloader.rb +13 -10
  24. data/lib/active_record/associations/singular_association.rb +1 -1
  25. data/lib/active_record/associations/through_association.rb +22 -11
  26. data/lib/active_record/associations.rb +319 -217
  27. data/lib/active_record/attribute_assignment.rb +0 -2
  28. data/lib/active_record/attribute_methods/before_type_cast.rb +17 -0
  29. data/lib/active_record/attribute_methods/dirty.rb +53 -35
  30. data/lib/active_record/attribute_methods/primary_key.rb +76 -24
  31. data/lib/active_record/attribute_methods/query.rb +28 -16
  32. data/lib/active_record/attribute_methods/read.rb +21 -8
  33. data/lib/active_record/attribute_methods/serialization.rb +150 -31
  34. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -0
  35. data/lib/active_record/attribute_methods/write.rb +6 -6
  36. data/lib/active_record/attribute_methods.rb +145 -21
  37. data/lib/active_record/attributes.rb +3 -3
  38. data/lib/active_record/autosave_association.rb +59 -10
  39. data/lib/active_record/base.rb +7 -2
  40. data/lib/active_record/callbacks.rb +10 -24
  41. data/lib/active_record/coders/column_serializer.rb +61 -0
  42. data/lib/active_record/coders/json.rb +1 -1
  43. data/lib/active_record/coders/yaml_column.rb +70 -42
  44. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +163 -88
  45. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -0
  46. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +3 -1
  47. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +80 -50
  48. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  49. data/lib/active_record/connection_adapters/abstract/database_statements.rb +129 -31
  50. data/lib/active_record/connection_adapters/abstract/query_cache.rb +62 -23
  51. data/lib/active_record/connection_adapters/abstract/quoting.rb +41 -6
  52. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  53. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +18 -4
  54. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +137 -11
  55. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +296 -127
  56. data/lib/active_record/connection_adapters/abstract/transaction.rb +287 -58
  57. data/lib/active_record/connection_adapters/abstract_adapter.rb +511 -92
  58. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +244 -121
  59. data/lib/active_record/connection_adapters/column.rb +9 -0
  60. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  61. data/lib/active_record/connection_adapters/mysql/database_statements.rb +22 -143
  62. data/lib/active_record/connection_adapters/mysql/quoting.rb +16 -12
  63. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  64. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +6 -0
  65. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +1 -1
  66. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +19 -13
  67. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +151 -0
  68. data/lib/active_record/connection_adapters/mysql2_adapter.rb +106 -55
  69. data/lib/active_record/connection_adapters/pool_config.rb +14 -5
  70. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  71. data/lib/active_record/connection_adapters/postgresql/column.rb +14 -3
  72. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +74 -40
  73. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
  74. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
  75. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  76. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +1 -1
  77. data/lib/active_record/connection_adapters/postgresql/quoting.rb +10 -6
  78. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +3 -9
  79. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  80. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +131 -2
  81. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +53 -0
  82. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +364 -61
  83. data/lib/active_record/connection_adapters/postgresql_adapter.rb +353 -192
  84. data/lib/active_record/connection_adapters/schema_cache.rb +287 -59
  85. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  86. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +52 -39
  87. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +4 -3
  88. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +1 -0
  89. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +26 -7
  90. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +211 -81
  91. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  92. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  93. data/lib/active_record/connection_adapters/trilogy_adapter.rb +258 -0
  94. data/lib/active_record/connection_adapters.rb +3 -1
  95. data/lib/active_record/connection_handling.rb +72 -95
  96. data/lib/active_record/core.rb +181 -154
  97. data/lib/active_record/counter_cache.rb +52 -27
  98. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
  99. data/lib/active_record/database_configurations/database_config.rb +9 -3
  100. data/lib/active_record/database_configurations/hash_config.rb +28 -14
  101. data/lib/active_record/database_configurations/url_config.rb +17 -11
  102. data/lib/active_record/database_configurations.rb +86 -33
  103. data/lib/active_record/delegated_type.rb +15 -10
  104. data/lib/active_record/deprecator.rb +7 -0
  105. data/lib/active_record/destroy_association_async_job.rb +3 -1
  106. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  107. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  108. data/lib/active_record/encryption/config.rb +25 -1
  109. data/lib/active_record/encryption/configurable.rb +12 -19
  110. data/lib/active_record/encryption/context.rb +10 -3
  111. data/lib/active_record/encryption/contexts.rb +5 -1
  112. data/lib/active_record/encryption/derived_secret_key_provider.rb +8 -2
  113. data/lib/active_record/encryption/encryptable_record.rb +42 -18
  114. data/lib/active_record/encryption/encrypted_attribute_type.rb +23 -8
  115. data/lib/active_record/encryption/extended_deterministic_queries.rb +66 -69
  116. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +3 -3
  117. data/lib/active_record/encryption/key_generator.rb +12 -1
  118. data/lib/active_record/encryption/message_serializer.rb +2 -0
  119. data/lib/active_record/encryption/properties.rb +3 -3
  120. data/lib/active_record/encryption/scheme.rb +22 -21
  121. data/lib/active_record/encryption.rb +3 -0
  122. data/lib/active_record/enum.rb +112 -28
  123. data/lib/active_record/errors.rb +112 -18
  124. data/lib/active_record/explain.rb +23 -3
  125. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  126. data/lib/active_record/fixture_set/render_context.rb +2 -0
  127. data/lib/active_record/fixture_set/table_row.rb +29 -8
  128. data/lib/active_record/fixtures.rb +135 -71
  129. data/lib/active_record/future_result.rb +40 -5
  130. data/lib/active_record/gem_version.rb +4 -4
  131. data/lib/active_record/inheritance.rb +30 -16
  132. data/lib/active_record/insert_all.rb +57 -10
  133. data/lib/active_record/integration.rb +8 -8
  134. data/lib/active_record/internal_metadata.rb +120 -30
  135. data/lib/active_record/locking/optimistic.rb +1 -1
  136. data/lib/active_record/locking/pessimistic.rb +5 -2
  137. data/lib/active_record/log_subscriber.rb +29 -12
  138. data/lib/active_record/marshalling.rb +59 -0
  139. data/lib/active_record/message_pack.rb +124 -0
  140. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  141. data/lib/active_record/middleware/database_selector.rb +6 -8
  142. data/lib/active_record/middleware/shard_selector.rb +3 -1
  143. data/lib/active_record/migration/command_recorder.rb +104 -5
  144. data/lib/active_record/migration/compatibility.rb +145 -5
  145. data/lib/active_record/migration/default_strategy.rb +23 -0
  146. data/lib/active_record/migration/execution_strategy.rb +19 -0
  147. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  148. data/lib/active_record/migration.rb +219 -111
  149. data/lib/active_record/model_schema.rb +69 -44
  150. data/lib/active_record/nested_attributes.rb +37 -8
  151. data/lib/active_record/normalization.rb +167 -0
  152. data/lib/active_record/persistence.rb +188 -37
  153. data/lib/active_record/promise.rb +84 -0
  154. data/lib/active_record/query_cache.rb +4 -22
  155. data/lib/active_record/query_logs.rb +77 -52
  156. data/lib/active_record/query_logs_formatter.rb +41 -0
  157. data/lib/active_record/querying.rb +15 -2
  158. data/lib/active_record/railtie.rb +107 -45
  159. data/lib/active_record/railties/controller_runtime.rb +12 -6
  160. data/lib/active_record/railties/databases.rake +144 -150
  161. data/lib/active_record/railties/job_runtime.rb +23 -0
  162. data/lib/active_record/readonly_attributes.rb +32 -5
  163. data/lib/active_record/reflection.rb +181 -45
  164. data/lib/active_record/relation/batches/batch_enumerator.rb +5 -3
  165. data/lib/active_record/relation/batches.rb +190 -61
  166. data/lib/active_record/relation/calculations.rb +187 -63
  167. data/lib/active_record/relation/delegation.rb +23 -9
  168. data/lib/active_record/relation/finder_methods.rb +77 -16
  169. data/lib/active_record/relation/merger.rb +2 -0
  170. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -2
  171. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +10 -7
  172. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  173. data/lib/active_record/relation/predicate_builder.rb +26 -14
  174. data/lib/active_record/relation/query_attribute.rb +2 -1
  175. data/lib/active_record/relation/query_methods.rb +371 -68
  176. data/lib/active_record/relation/spawn_methods.rb +18 -1
  177. data/lib/active_record/relation.rb +103 -37
  178. data/lib/active_record/result.rb +19 -5
  179. data/lib/active_record/runtime_registry.rb +24 -1
  180. data/lib/active_record/sanitization.rb +51 -11
  181. data/lib/active_record/schema.rb +2 -3
  182. data/lib/active_record/schema_dumper.rb +46 -7
  183. data/lib/active_record/schema_migration.rb +68 -33
  184. data/lib/active_record/scoping/default.rb +15 -5
  185. data/lib/active_record/scoping/named.rb +2 -2
  186. data/lib/active_record/scoping.rb +2 -1
  187. data/lib/active_record/secure_password.rb +60 -0
  188. data/lib/active_record/secure_token.rb +21 -3
  189. data/lib/active_record/signed_id.rb +7 -5
  190. data/lib/active_record/store.rb +8 -8
  191. data/lib/active_record/suppressor.rb +3 -1
  192. data/lib/active_record/table_metadata.rb +10 -1
  193. data/lib/active_record/tasks/database_tasks.rb +152 -108
  194. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  195. data/lib/active_record/tasks/postgresql_database_tasks.rb +16 -13
  196. data/lib/active_record/tasks/sqlite_database_tasks.rb +15 -7
  197. data/lib/active_record/test_fixtures.rb +114 -96
  198. data/lib/active_record/timestamp.rb +30 -16
  199. data/lib/active_record/token_for.rb +113 -0
  200. data/lib/active_record/touch_later.rb +11 -6
  201. data/lib/active_record/transactions.rb +36 -10
  202. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  203. data/lib/active_record/type/internal/timezone.rb +7 -2
  204. data/lib/active_record/type/time.rb +4 -0
  205. data/lib/active_record/validations/absence.rb +1 -1
  206. data/lib/active_record/validations/numericality.rb +5 -4
  207. data/lib/active_record/validations/presence.rb +5 -28
  208. data/lib/active_record/validations/uniqueness.rb +47 -2
  209. data/lib/active_record/validations.rb +8 -4
  210. data/lib/active_record/version.rb +1 -1
  211. data/lib/active_record.rb +122 -17
  212. data/lib/arel/errors.rb +10 -0
  213. data/lib/arel/factory_methods.rb +4 -0
  214. data/lib/arel/nodes/binary.rb +6 -1
  215. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  216. data/lib/arel/nodes/cte.rb +36 -0
  217. data/lib/arel/nodes/fragments.rb +35 -0
  218. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  219. data/lib/arel/nodes/leading_join.rb +8 -0
  220. data/lib/arel/nodes/node.rb +111 -2
  221. data/lib/arel/nodes/sql_literal.rb +6 -0
  222. data/lib/arel/nodes/table_alias.rb +4 -0
  223. data/lib/arel/nodes.rb +4 -0
  224. data/lib/arel/predications.rb +2 -0
  225. data/lib/arel/table.rb +9 -5
  226. data/lib/arel/tree_manager.rb +5 -1
  227. data/lib/arel/visitors/mysql.rb +8 -1
  228. data/lib/arel/visitors/to_sql.rb +83 -18
  229. data/lib/arel/visitors/visitor.rb +2 -2
  230. data/lib/arel.rb +16 -2
  231. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  232. data/lib/rails/generators/active_record/migration.rb +3 -1
  233. data/lib/rails/generators/active_record/model/USAGE +113 -0
  234. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  235. metadata +46 -10
  236. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  237. data/lib/active_record/null_relation.rb +0 -63
@@ -74,11 +74,11 @@ module ActiveRecord
74
74
  FROM pg_class t
75
75
  INNER JOIN pg_index d ON t.oid = d.indrelid
76
76
  INNER JOIN pg_class i ON d.indexrelid = i.oid
77
- LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
77
+ LEFT JOIN pg_namespace n ON n.oid = t.relnamespace
78
78
  WHERE i.relkind IN ('i', 'I')
79
79
  AND i.relname = #{index[:name]}
80
80
  AND t.relname = #{table[:name]}
81
- AND n.nspname = #{index[:schema]}
81
+ AND n.nspname = #{table[:schema]}
82
82
  SQL
83
83
  end
84
84
 
@@ -88,11 +88,11 @@ module ActiveRecord
88
88
 
89
89
  result = query(<<~SQL, "SCHEMA")
90
90
  SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
91
- pg_catalog.obj_description(i.oid, 'pg_class') AS comment
91
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment, d.indisvalid
92
92
  FROM pg_class t
93
93
  INNER JOIN pg_index d ON t.oid = d.indrelid
94
94
  INNER JOIN pg_class i ON d.indexrelid = i.oid
95
- LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
95
+ LEFT JOIN pg_namespace n ON n.oid = t.relnamespace
96
96
  WHERE i.relkind IN ('i', 'I')
97
97
  AND d.indisprimary = 'f'
98
98
  AND t.relname = #{scope[:name]}
@@ -107,25 +107,24 @@ module ActiveRecord
107
107
  inddef = row[3]
108
108
  oid = row[4]
109
109
  comment = row[5]
110
-
111
- using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten
110
+ valid = row[6]
111
+ using, expressions, include, nulls_not_distinct, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: INCLUDE \((.+?)\))?( NULLS NOT DISTINCT)?(?: WHERE (.+))?\z/m).flatten
112
112
 
113
113
  orders = {}
114
114
  opclasses = {}
115
+ include_columns = include ? include.split(",").map(&:strip) : []
115
116
 
116
117
  if indkey.include?(0)
117
118
  columns = expressions
118
119
  else
119
- columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact
120
- SELECT a.attnum, a.attname
121
- FROM pg_attribute a
122
- WHERE a.attrelid = #{oid}
123
- AND a.attnum IN (#{indkey.join(",")})
124
- SQL
120
+ columns = column_names_from_column_numbers(oid, indkey)
121
+
122
+ # prevent INCLUDE columns from being matched
123
+ columns.reject! { |c| include_columns.include?(c) }
125
124
 
126
125
  # add info on sort order (only desc order is explicitly specified, asc is the default)
127
126
  # and non-default opclasses
128
- expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
127
+ expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops(_\w+)?)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
129
128
  opclasses[column] = opclass.to_sym if opclass
130
129
  if nulls
131
130
  orders[column] = [desc, nulls].compact.join(" ")
@@ -144,7 +143,10 @@ module ActiveRecord
144
143
  opclasses: opclasses,
145
144
  where: where,
146
145
  using: using.to_sym,
147
- comment: comment.presence
146
+ include: include_columns.presence,
147
+ nulls_not_distinct: nulls_not_distinct.present?,
148
+ comment: comment.presence,
149
+ valid: valid
148
150
  )
149
151
  end
150
152
  end
@@ -223,7 +225,7 @@ module ActiveRecord
223
225
  # This should be not be called manually but set in database.yml.
224
226
  def schema_search_path=(schema_csv)
225
227
  if schema_csv
226
- execute("SET search_path TO #{schema_csv}", "SCHEMA")
228
+ internal_execute("SET search_path TO #{schema_csv}")
227
229
  @schema_search_path = schema_csv
228
230
  end
229
231
  end
@@ -240,11 +242,13 @@ module ActiveRecord
240
242
 
241
243
  # Set the client message level.
242
244
  def client_min_messages=(level)
243
- execute("SET client_min_messages TO '#{level}'", "SCHEMA")
245
+ internal_execute("SET client_min_messages TO '#{level}'")
244
246
  end
245
247
 
246
248
  # Returns the sequence name for a table's primary key or some other specified key.
247
249
  def default_sequence_name(table_name, pk = "id") # :nodoc:
250
+ return nil if pk.is_a?(Array)
251
+
248
252
  result = serial_sequence(table_name, pk)
249
253
  return nil unless result
250
254
  Utils.extract_schema_qualified_name(result).to_s
@@ -288,14 +292,14 @@ module ActiveRecord
288
292
  quoted_sequence = quote_table_name(sequence)
289
293
  max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
290
294
  if max_pk.nil?
291
- if database_version >= 100000
295
+ if database_version >= 10_00_00
292
296
  minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
293
297
  else
294
298
  minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
295
299
  end
296
300
  end
297
301
 
298
- query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
302
+ query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA")
299
303
  end
300
304
  end
301
305
 
@@ -339,7 +343,7 @@ module ActiveRecord
339
343
  JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
340
344
  WHERE t.oid = #{quote(quote_table_name(table))}::regclass
341
345
  AND cons.contype = 'p'
342
- AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
346
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate|gen_random_uuid'
343
347
  SQL
344
348
  end
345
349
 
@@ -375,22 +379,30 @@ module ActiveRecord
375
379
  #
376
380
  # Example:
377
381
  # rename_table('octopuses', 'octopi')
378
- def rename_table(table_name, new_name)
382
+ def rename_table(table_name, new_name, **options)
383
+ validate_table_length!(new_name) unless options[:_uses_legacy_table_name]
379
384
  clear_cache!
380
385
  schema_cache.clear_data_source_cache!(table_name.to_s)
381
386
  schema_cache.clear_data_source_cache!(new_name.to_s)
382
387
  execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
383
388
  pk, seq = pk_and_sequence_for(new_name)
384
389
  if pk
385
- idx = "#{table_name}_pkey"
386
- new_idx = "#{new_name}_pkey"
390
+ # PostgreSQL automatically creates an index for PRIMARY KEY with name consisting of
391
+ # truncated table name and "_pkey" suffix fitting into max_identifier_length number of characters.
392
+ max_pkey_prefix = max_identifier_length - "_pkey".size
393
+ idx = "#{table_name[0, max_pkey_prefix]}_pkey"
394
+ new_idx = "#{new_name[0, max_pkey_prefix]}_pkey"
387
395
  execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
388
- if seq && seq.identifier == "#{table_name}_#{pk}_seq"
389
- new_seq = "#{new_name}_#{pk}_seq"
396
+
397
+ # PostgreSQL automatically creates a sequence for PRIMARY KEY with name consisting of
398
+ # truncated table name and "#{primary_key}_seq" suffix fitting into max_identifier_length number of characters.
399
+ max_seq_prefix = max_identifier_length - "_#{pk}_seq".size
400
+ if seq && seq.identifier == "#{table_name[0, max_seq_prefix]}_#{pk}_seq"
401
+ new_seq = "#{new_name[0, max_seq_prefix]}_#{pk}_seq"
390
402
  execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}"
391
403
  end
392
404
  end
393
- rename_table_indexes(table_name, new_name)
405
+ rename_table_indexes(table_name, new_name, **options)
394
406
  end
395
407
 
396
408
  def add_column(table_name, column_name, type, **options) # :nodoc:
@@ -406,18 +418,39 @@ module ActiveRecord
406
418
  procs.each(&:call)
407
419
  end
408
420
 
421
+ # Builds a ChangeColumnDefinition object.
422
+ #
423
+ # This definition object contains information about the column change that would occur
424
+ # if the same arguments were passed to #change_column. See #change_column for information about
425
+ # passing a +table_name+, +column_name+, +type+ and other options that can be passed.
426
+ def build_change_column_definition(table_name, column_name, type, **options) # :nodoc:
427
+ td = create_table_definition(table_name)
428
+ cd = td.new_column_definition(column_name, type, **options)
429
+ ChangeColumnDefinition.new(cd, column_name)
430
+ end
431
+
409
432
  # Changes the default value of a table column.
410
433
  def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
411
434
  execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}"
412
435
  end
413
436
 
437
+ def build_change_column_default_definition(table_name, column_name, default_or_changes) # :nodoc:
438
+ column = column_for(table_name, column_name)
439
+ return unless column
440
+
441
+ default = extract_new_default_value(default_or_changes)
442
+ ChangeColumnDefaultDefinition.new(column, default)
443
+ end
444
+
414
445
  def change_column_null(table_name, column_name, null, default = nil) # :nodoc:
446
+ validate_change_column_null_argument!(null)
447
+
415
448
  clear_cache!
416
449
  unless null || default.nil?
417
450
  column = column_for(table_name, column_name)
418
451
  execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL" if column
419
452
  end
420
- execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}"
453
+ execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
421
454
  end
422
455
 
423
456
  # Adds comment for given table column or drops it if +comment+ is a +nil+
@@ -442,15 +475,19 @@ module ActiveRecord
442
475
  end
443
476
 
444
477
  def add_index(table_name, column_name, **options) # :nodoc:
445
- index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
446
-
447
- create_index = CreateIndexDefinition.new(index, algorithm, if_not_exists)
478
+ create_index = build_create_index_definition(table_name, column_name, **options)
448
479
  result = execute schema_creation.accept(create_index)
449
480
 
481
+ index = create_index.index
450
482
  execute "COMMENT ON INDEX #{quote_column_name(index.name)} IS #{quote(index.comment)}" if index.comment
451
483
  result
452
484
  end
453
485
 
486
+ def build_create_index_definition(table_name, column_name, **options) # :nodoc:
487
+ index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
488
+ CreateIndexDefinition.new(index, algorithm, if_not_exists)
489
+ end
490
+
454
491
  def remove_index(table_name, column_name = nil, **options) # :nodoc:
455
492
  table = Utils.extract_schema_qualified_name(table_name.to_s)
456
493
 
@@ -477,13 +514,33 @@ module ActiveRecord
477
514
  def rename_index(table_name, old_name, new_name)
478
515
  validate_index_length!(table_name, new_name)
479
516
 
480
- execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
517
+ schema, = extract_schema_qualified_name(table_name)
518
+ execute "ALTER INDEX #{quote_table_name(schema) + '.' if schema}#{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
519
+ end
520
+
521
+ def index_name(table_name, options) # :nodoc:
522
+ _schema, table_name = extract_schema_qualified_name(table_name.to_s)
523
+ super
524
+ end
525
+
526
+ def add_foreign_key(from_table, to_table, **options)
527
+ if options[:deferrable] == true
528
+ ActiveRecord.deprecator.warn(<<~MSG)
529
+ `deferrable: true` is deprecated in favor of `deferrable: :immediate`, and will be removed in Rails 7.2.
530
+ MSG
531
+
532
+ options[:deferrable] = :immediate
533
+ end
534
+
535
+ assert_valid_deferrable(options[:deferrable])
536
+
537
+ super
481
538
  end
482
539
 
483
540
  def foreign_keys(table_name)
484
541
  scope = quoted_scope(table_name)
485
- fk_info = exec_query(<<~SQL, "SCHEMA")
486
- SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred
542
+ fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
543
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conkey, c.confkey, c.conrelid, c.confrelid
487
544
  FROM pg_constraint c
488
545
  JOIN pg_class t1 ON c.conrelid = t1.oid
489
546
  JOIN pg_class t2 ON c.confrelid = t2.oid
@@ -497,18 +554,29 @@ module ActiveRecord
497
554
  SQL
498
555
 
499
556
  fk_info.map do |row|
557
+ to_table = Utils.unquote_identifier(row["to_table"])
558
+ conkey = row["conkey"].scan(/\d+/).map(&:to_i)
559
+ confkey = row["confkey"].scan(/\d+/).map(&:to_i)
560
+
561
+ if conkey.size > 1
562
+ column = column_names_from_column_numbers(row["conrelid"], conkey)
563
+ primary_key = column_names_from_column_numbers(row["confrelid"], confkey)
564
+ else
565
+ column = Utils.unquote_identifier(row["column"])
566
+ primary_key = row["primary_key"]
567
+ end
568
+
500
569
  options = {
501
- column: Utils.unquote_identifier(row["column"]),
570
+ column: column,
502
571
  name: row["name"],
503
- primary_key: row["primary_key"]
572
+ primary_key: primary_key
504
573
  }
505
574
 
506
575
  options[:on_delete] = extract_foreign_key_action(row["on_delete"])
507
576
  options[:on_update] = extract_foreign_key_action(row["on_update"])
508
- options[:deferrable] = extract_foreign_key_deferrable(row["deferrable"], row["deferred"])
577
+ options[:deferrable] = extract_constraint_deferrable(row["deferrable"], row["deferred"])
509
578
 
510
579
  options[:validate] = row["valid"]
511
- to_table = Utils.unquote_identifier(row["to_table"])
512
580
 
513
581
  ForeignKeyDefinition.new(table_name, to_table, options)
514
582
  end
@@ -525,7 +593,7 @@ module ActiveRecord
525
593
  def check_constraints(table_name) # :nodoc:
526
594
  scope = quoted_scope(table_name)
527
595
 
528
- check_info = exec_query(<<-SQL, "SCHEMA")
596
+ check_info = internal_exec_query(<<-SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
529
597
  SELECT conname, pg_get_constraintdef(c.oid, true) AS constraintdef, c.convalidated AS valid
530
598
  FROM pg_constraint c
531
599
  JOIN pg_class t ON c.conrelid = t.oid
@@ -546,6 +614,171 @@ module ActiveRecord
546
614
  end
547
615
  end
548
616
 
617
+ # Returns an array of exclusion constraints for the given table.
618
+ # The exclusion constraints are represented as ExclusionConstraintDefinition objects.
619
+ def exclusion_constraints(table_name)
620
+ scope = quoted_scope(table_name)
621
+
622
+ exclusion_info = internal_exec_query(<<-SQL, "SCHEMA")
623
+ SELECT conname, pg_get_constraintdef(c.oid) AS constraintdef, c.condeferrable, c.condeferred
624
+ FROM pg_constraint c
625
+ JOIN pg_class t ON c.conrelid = t.oid
626
+ JOIN pg_namespace n ON n.oid = c.connamespace
627
+ WHERE c.contype = 'x'
628
+ AND t.relname = #{scope[:name]}
629
+ AND n.nspname = #{scope[:schema]}
630
+ SQL
631
+
632
+ exclusion_info.map do |row|
633
+ method_and_elements, predicate = row["constraintdef"].split(" WHERE ")
634
+ method_and_elements_parts = method_and_elements.match(/EXCLUDE(?: USING (?<using>\S+))? \((?<expression>.+)\)/)
635
+ predicate.remove!(/ DEFERRABLE(?: INITIALLY (?:IMMEDIATE|DEFERRED))?/) if predicate
636
+ predicate = predicate.from(2).to(-3) if predicate # strip 2 opening and closing parentheses
637
+
638
+ deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
639
+
640
+ options = {
641
+ name: row["conname"],
642
+ using: method_and_elements_parts["using"].to_sym,
643
+ where: predicate,
644
+ deferrable: deferrable
645
+ }
646
+
647
+ ExclusionConstraintDefinition.new(table_name, method_and_elements_parts["expression"], options)
648
+ end
649
+ end
650
+
651
+ # Returns an array of unique constraints for the given table.
652
+ # The unique constraints are represented as UniqueConstraintDefinition objects.
653
+ def unique_constraints(table_name)
654
+ scope = quoted_scope(table_name)
655
+
656
+ 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
658
+ FROM pg_constraint c
659
+ JOIN pg_class t ON c.conrelid = t.oid
660
+ JOIN pg_namespace n ON n.oid = c.connamespace
661
+ WHERE c.contype = 'u'
662
+ AND t.relname = #{scope[:name]}
663
+ AND n.nspname = #{scope[:schema]}
664
+ SQL
665
+
666
+ unique_info.map do |row|
667
+ conkey = row["conkey"].delete("{}").split(",").map(&:to_i)
668
+ columns = column_names_from_column_numbers(row["conrelid"], conkey)
669
+
670
+ deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
671
+
672
+ options = {
673
+ name: row["conname"],
674
+ deferrable: deferrable
675
+ }
676
+
677
+ UniqueConstraintDefinition.new(table_name, columns, options)
678
+ end
679
+ end
680
+
681
+ # Adds a new exclusion constraint to the table. +expression+ is a String
682
+ # representation of a list of exclusion elements and operators.
683
+ #
684
+ # add_exclusion_constraint :products, "price WITH =, availability_range WITH &&", using: :gist, name: "price_check"
685
+ #
686
+ # generates:
687
+ #
688
+ # ALTER TABLE "products" ADD CONSTRAINT price_check EXCLUDE USING gist (price WITH =, availability_range WITH &&)
689
+ #
690
+ # The +options+ hash can include the following keys:
691
+ # [<tt>:name</tt>]
692
+ # The constraint name. Defaults to <tt>excl_rails_<identifier></tt>.
693
+ # [<tt>:deferrable</tt>]
694
+ # Specify whether or not the exclusion constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+.
695
+ def add_exclusion_constraint(table_name, expression, **options)
696
+ options = exclusion_constraint_options(table_name, expression, options)
697
+ at = create_alter_table(table_name)
698
+ at.add_exclusion_constraint(expression, options)
699
+
700
+ execute schema_creation.accept(at)
701
+ end
702
+
703
+ def exclusion_constraint_options(table_name, expression, options) # :nodoc:
704
+ assert_valid_deferrable(options[:deferrable])
705
+
706
+ options = options.dup
707
+ options[:name] ||= exclusion_constraint_name(table_name, expression: expression, **options)
708
+ options
709
+ end
710
+
711
+ # Removes the given exclusion constraint from the table.
712
+ #
713
+ # remove_exclusion_constraint :products, name: "price_check"
714
+ #
715
+ # The +expression+ parameter will be ignored if present. It can be helpful
716
+ # to provide this in a migration's +change+ method so it can be reverted.
717
+ # In that case, +expression+ will be used by #add_exclusion_constraint.
718
+ def remove_exclusion_constraint(table_name, expression = nil, **options)
719
+ excl_name_to_delete = exclusion_constraint_for!(table_name, expression: expression, **options).name
720
+
721
+ at = create_alter_table(table_name)
722
+ at.drop_exclusion_constraint(excl_name_to_delete)
723
+
724
+ execute schema_creation.accept(at)
725
+ end
726
+
727
+ # Adds a new unique constraint to the table.
728
+ #
729
+ # add_unique_constraint :sections, [:position], deferrable: :deferred, name: "unique_position"
730
+ #
731
+ # generates:
732
+ #
733
+ # ALTER TABLE "sections" ADD CONSTRAINT unique_position UNIQUE (position) DEFERRABLE INITIALLY DEFERRED
734
+ #
735
+ # If you want to change an existing unique index to deferrable, you can use :using_index to create deferrable unique constraints.
736
+ #
737
+ # add_unique_constraint :sections, deferrable: :deferred, name: "unique_position", using_index: "index_sections_on_position"
738
+ #
739
+ # The +options+ hash can include the following keys:
740
+ # [<tt>:name</tt>]
741
+ # The constraint name. Defaults to <tt>uniq_rails_<identifier></tt>.
742
+ # [<tt>:deferrable</tt>]
743
+ # 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+.
744
+ # [<tt>:using_index</tt>]
745
+ # To specify an existing unique index name. Defaults to +nil+.
746
+ def add_unique_constraint(table_name, column_name = nil, **options)
747
+ options = unique_constraint_options(table_name, column_name, options)
748
+ at = create_alter_table(table_name)
749
+ at.add_unique_constraint(column_name, options)
750
+
751
+ execute schema_creation.accept(at)
752
+ end
753
+
754
+ def unique_constraint_options(table_name, column_name, options) # :nodoc:
755
+ assert_valid_deferrable(options[:deferrable])
756
+
757
+ if column_name && options[:using_index]
758
+ raise ArgumentError, "Cannot specify both column_name and :using_index options."
759
+ end
760
+
761
+ options = options.dup
762
+ options[:name] ||= unique_constraint_name(table_name, column: column_name, **options)
763
+ options
764
+ end
765
+
766
+ # Removes the given unique constraint from the table.
767
+ #
768
+ # remove_unique_constraint :sections, name: "unique_position"
769
+ #
770
+ # The +column_name+ parameter will be ignored if present. It can be helpful
771
+ # to provide this in a migration's +change+ method so it can be reverted.
772
+ # In that case, +column_name+ will be used by #add_unique_constraint.
773
+ def remove_unique_constraint(table_name, column_name = nil, **options)
774
+ unique_name_to_delete = unique_constraint_for!(table_name, column: column_name, **options).name
775
+
776
+ at = create_alter_table(table_name)
777
+ at.drop_unique_constraint(unique_name_to_delete)
778
+
779
+ execute schema_creation.accept(at)
780
+ end
781
+
549
782
  # Maps logical Rails types to PostgreSQL-specific data types.
550
783
  def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, enum_type: nil, **) # :nodoc:
551
784
  sql = \
@@ -649,11 +882,32 @@ module ActiveRecord
649
882
  validate_constraint table_name, chk_name_to_validate
650
883
  end
651
884
 
652
- private
653
- def schema_creation
654
- PostgreSQL::SchemaCreation.new(self)
885
+ def foreign_key_column_for(table_name, column_name) # :nodoc:
886
+ _schema, table_name = extract_schema_qualified_name(table_name)
887
+ super
888
+ end
889
+
890
+ def add_index_options(table_name, column_name, **options) # :nodoc:
891
+ if (where = options[:where]) && table_exists?(table_name) && column_exists?(table_name, where)
892
+ options[:where] = quote_column_name(where)
893
+ end
894
+ super
895
+ end
896
+
897
+ def quoted_include_columns_for_index(column_names) # :nodoc:
898
+ return quote_column_name(column_names) if column_names.is_a?(Symbol)
899
+
900
+ quoted_columns = column_names.each_with_object({}) do |name, result|
901
+ result[name.to_sym] = quote_column_name(name).dup
655
902
  end
903
+ add_options_for_index_columns(quoted_columns).values.join(", ")
904
+ end
656
905
 
906
+ def schema_creation # :nodoc:
907
+ PostgreSQL::SchemaCreation.new(self)
908
+ end
909
+
910
+ private
657
911
  def create_table_definition(name, **options)
658
912
  PostgreSQL::TableDefinition.new(self, name, **options)
659
913
  end
@@ -662,8 +916,8 @@ module ActiveRecord
662
916
  PostgreSQL::AlterTable.new create_table_definition(name)
663
917
  end
664
918
 
665
- def new_column_from_field(table_name, field)
666
- column_name, type, default, notnull, oid, fmod, collation, comment, attgenerated = field
919
+ def new_column_from_field(table_name, field, _definitions)
920
+ column_name, type, default, notnull, oid, fmod, collation, comment, identity, attgenerated = field
667
921
  type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
668
922
  default_value = extract_value_from_default(default)
669
923
 
@@ -686,6 +940,7 @@ module ActiveRecord
686
940
  collation: collation,
687
941
  comment: comment.presence,
688
942
  serial: serial,
943
+ identity: identity.presence,
689
944
  generated: attgenerated
690
945
  )
691
946
  end
@@ -726,8 +981,19 @@ module ActiveRecord
726
981
  end
727
982
  end
728
983
 
729
- def extract_foreign_key_deferrable(deferrable, deferred)
730
- deferrable && (deferred ? :deferred : true)
984
+ def assert_valid_deferrable(deferrable)
985
+ return if !deferrable || %i(immediate deferred).include?(deferrable)
986
+
987
+ raise ArgumentError, "deferrable must be `:immediate` or `:deferred`, got: `#{deferrable.inspect}`"
988
+ end
989
+
990
+ def extract_constraint_deferrable(deferrable, deferred)
991
+ deferrable && (deferred ? :deferred : :immediate)
992
+ end
993
+
994
+ def reference_name_for_table(table_name)
995
+ _schema, table_name = extract_schema_qualified_name(table_name.to_s)
996
+ table_name.singularize
731
997
  end
732
998
 
733
999
  def add_column_for_alter(table_name, column_name, type, **options)
@@ -736,32 +1002,20 @@ module ActiveRecord
736
1002
  end
737
1003
 
738
1004
  def change_column_for_alter(table_name, column_name, type, **options)
739
- td = create_table_definition(table_name)
740
- cd = td.new_column_definition(column_name, type, **options)
741
- sqls = [schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))]
1005
+ change_col_def = build_change_column_definition(table_name, column_name, type, **options)
1006
+ sqls = [schema_creation.accept(change_col_def)]
742
1007
  sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
743
1008
  sqls
744
1009
  end
745
1010
 
746
- def change_column_default_for_alter(table_name, column_name, default_or_changes)
747
- column = column_for(table_name, column_name)
748
- return unless column
749
-
750
- default = extract_new_default_value(default_or_changes)
751
- alter_column_query = "ALTER COLUMN #{quote_column_name(column_name)} %s"
1011
+ def change_column_null_for_alter(table_name, column_name, null, default = nil)
752
1012
  if default.nil?
753
- # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
754
- # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
755
- alter_column_query % "DROP DEFAULT"
1013
+ "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
756
1014
  else
757
- alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
1015
+ Proc.new { change_column_null(table_name, column_name, null, default) }
758
1016
  end
759
1017
  end
760
1018
 
761
- def change_column_null_for_alter(table_name, column_name, null, default = nil)
762
- "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
763
- end
764
-
765
1019
  def add_index_opclass(quoted_columns, **options)
766
1020
  opclasses = options_for_index_columns(options[:opclass])
767
1021
  quoted_columns.each do |name, column|
@@ -774,6 +1028,46 @@ module ActiveRecord
774
1028
  super
775
1029
  end
776
1030
 
1031
+ def exclusion_constraint_name(table_name, **options)
1032
+ options.fetch(:name) do
1033
+ expression = options.fetch(:expression)
1034
+ identifier = "#{table_name}_#{expression}_excl"
1035
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
1036
+
1037
+ "excl_rails_#{hashed_identifier}"
1038
+ end
1039
+ end
1040
+
1041
+ def exclusion_constraint_for(table_name, **options)
1042
+ excl_name = exclusion_constraint_name(table_name, **options)
1043
+ exclusion_constraints(table_name).detect { |excl| excl.name == excl_name }
1044
+ end
1045
+
1046
+ def exclusion_constraint_for!(table_name, expression: nil, **options)
1047
+ exclusion_constraint_for(table_name, expression: expression, **options) ||
1048
+ raise(ArgumentError, "Table '#{table_name}' has no exclusion constraint for #{expression || options}")
1049
+ end
1050
+
1051
+ def unique_constraint_name(table_name, **options)
1052
+ options.fetch(:name) do
1053
+ column_or_index = Array(options[:column] || options[:using_index]).map(&:to_s)
1054
+ identifier = "#{table_name}_#{column_or_index * '_and_'}_unique"
1055
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
1056
+
1057
+ "uniq_rails_#{hashed_identifier}"
1058
+ end
1059
+ end
1060
+
1061
+ def unique_constraint_for(table_name, **options)
1062
+ name = unique_constraint_name(table_name, **options) unless options.key?(:column)
1063
+ unique_constraints(table_name).detect { |unique_constraint| unique_constraint.defined_for?(name: name, **options) }
1064
+ end
1065
+
1066
+ def unique_constraint_for!(table_name, column: nil, **options)
1067
+ unique_constraint_for(table_name, column: column, **options) ||
1068
+ raise(ArgumentError, "Table '#{table_name}' has no unique constraint for #{column || options}")
1069
+ end
1070
+
777
1071
  def data_source_sql(name = nil, type: nil)
778
1072
  scope = quoted_scope(name, type: type)
779
1073
  scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table
@@ -807,6 +1101,15 @@ module ActiveRecord
807
1101
  name = Utils.extract_schema_qualified_name(string.to_s)
808
1102
  [name.schema, name.identifier]
809
1103
  end
1104
+
1105
+ def column_names_from_column_numbers(table_oid, column_numbers)
1106
+ Hash[query(<<~SQL, "SCHEMA")].values_at(*column_numbers).compact
1107
+ SELECT a.attnum, a.attname
1108
+ FROM pg_attribute a
1109
+ WHERE a.attrelid = #{table_oid}
1110
+ AND a.attnum IN (#{column_numbers.join(", ")})
1111
+ SQL
1112
+ end
810
1113
  end
811
1114
  end
812
1115
  end