activerecord 7.0.0 → 7.1.2

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