activerecord 7.2.4 → 8.1.4

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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1013 -878
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +2 -1
  5. data/lib/active_record/associations/association.rb +69 -27
  6. data/lib/active_record/associations/association_scope.rb +1 -1
  7. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  8. data/lib/active_record/associations/builder/association.rb +23 -11
  9. data/lib/active_record/associations/builder/belongs_to.rb +36 -8
  10. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  11. data/lib/active_record/associations/builder/has_one.rb +1 -1
  12. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  13. data/lib/active_record/associations/collection_association.rb +7 -3
  14. data/lib/active_record/associations/collection_proxy.rb +26 -4
  15. data/lib/active_record/associations/deprecation.rb +88 -0
  16. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  17. data/lib/active_record/associations/errors.rb +3 -0
  18. data/lib/active_record/associations/foreign_association.rb +3 -1
  19. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  20. data/lib/active_record/associations/has_one_association.rb +1 -0
  21. data/lib/active_record/associations/join_dependency.rb +3 -2
  22. data/lib/active_record/associations/preloader/association.rb +3 -3
  23. data/lib/active_record/associations/preloader/batch.rb +7 -1
  24. data/lib/active_record/associations/preloader/branch.rb +1 -0
  25. data/lib/active_record/associations/singular_association.rb +8 -3
  26. data/lib/active_record/associations.rb +192 -24
  27. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  28. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  29. data/lib/active_record/attribute_methods/query.rb +34 -0
  30. data/lib/active_record/attribute_methods/serialization.rb +18 -5
  31. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  32. data/lib/active_record/attributes.rb +3 -0
  33. data/lib/active_record/autosave_association.rb +69 -27
  34. data/lib/active_record/base.rb +1 -2
  35. data/lib/active_record/callbacks.rb +1 -2
  36. data/lib/active_record/coders/column_serializer.rb +1 -1
  37. data/lib/active_record/coders/json.rb +16 -6
  38. data/lib/active_record/coders/yaml_column.rb +2 -2
  39. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +35 -28
  40. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  41. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +68 -13
  42. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +429 -93
  43. data/lib/active_record/connection_adapters/abstract/database_statements.rb +148 -74
  44. data/lib/active_record/connection_adapters/abstract/query_cache.rb +27 -5
  45. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -25
  46. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  47. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +32 -35
  48. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  49. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +124 -34
  50. data/lib/active_record/connection_adapters/abstract/transaction.rb +40 -8
  51. data/lib/active_record/connection_adapters/abstract_adapter.rb +129 -63
  52. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +132 -65
  53. data/lib/active_record/connection_adapters/column.rb +14 -4
  54. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  55. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  56. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  57. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  58. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +79 -66
  59. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +89 -94
  60. data/lib/active_record/connection_adapters/mysql2_adapter.rb +2 -10
  61. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  62. data/lib/active_record/connection_adapters/postgresql/column.rb +8 -2
  63. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +79 -61
  64. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  65. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  66. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
  67. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  68. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  69. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  70. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  71. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +14 -33
  72. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +71 -32
  73. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +156 -68
  74. data/lib/active_record/connection_adapters/postgresql_adapter.rb +78 -105
  75. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  76. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  77. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +90 -98
  78. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  79. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  80. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  81. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +15 -16
  82. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +104 -37
  83. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  84. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +41 -66
  85. data/lib/active_record/connection_adapters/trilogy_adapter.rb +15 -18
  86. data/lib/active_record/connection_adapters.rb +1 -56
  87. data/lib/active_record/connection_handling.rb +25 -2
  88. data/lib/active_record/core.rb +33 -17
  89. data/lib/active_record/counter_cache.rb +35 -8
  90. data/lib/active_record/database_configurations/database_config.rb +9 -1
  91. data/lib/active_record/database_configurations/hash_config.rb +74 -9
  92. data/lib/active_record/database_configurations/url_config.rb +15 -3
  93. data/lib/active_record/database_configurations.rb +7 -3
  94. data/lib/active_record/delegated_type.rb +1 -1
  95. data/lib/active_record/disable_joins_association_relation.rb +5 -1
  96. data/lib/active_record/dynamic_matchers.rb +54 -69
  97. data/lib/active_record/encryption/config.rb +3 -1
  98. data/lib/active_record/encryption/encryptable_record.rb +8 -8
  99. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  100. data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
  101. data/lib/active_record/encryption/encryptor.rb +28 -8
  102. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  103. data/lib/active_record/encryption/scheme.rb +9 -2
  104. data/lib/active_record/enum.rb +33 -30
  105. data/lib/active_record/errors.rb +33 -9
  106. data/lib/active_record/explain.rb +1 -1
  107. data/lib/active_record/explain_registry.rb +51 -2
  108. data/lib/active_record/filter_attribute_handler.rb +76 -0
  109. data/lib/active_record/fixtures.rb +2 -4
  110. data/lib/active_record/future_result.rb +15 -9
  111. data/lib/active_record/gem_version.rb +2 -2
  112. data/lib/active_record/inheritance.rb +9 -4
  113. data/lib/active_record/insert_all.rb +14 -9
  114. data/lib/active_record/locking/optimistic.rb +8 -1
  115. data/lib/active_record/locking/pessimistic.rb +5 -0
  116. data/lib/active_record/log_subscriber.rb +31 -29
  117. data/lib/active_record/message_pack.rb +13 -0
  118. data/lib/active_record/middleware/shard_selector.rb +34 -17
  119. data/lib/active_record/migration/command_recorder.rb +56 -13
  120. data/lib/active_record/migration/compatibility.rb +37 -24
  121. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  122. data/lib/active_record/migration.rb +48 -42
  123. data/lib/active_record/model_schema.rb +40 -15
  124. data/lib/active_record/nested_attributes.rb +14 -12
  125. data/lib/active_record/persistence.rb +163 -133
  126. data/lib/active_record/query_cache.rb +22 -15
  127. data/lib/active_record/query_logs.rb +100 -52
  128. data/lib/active_record/query_logs_formatter.rb +17 -28
  129. data/lib/active_record/querying.rb +21 -9
  130. data/lib/active_record/railtie.rb +35 -30
  131. data/lib/active_record/railties/controller_runtime.rb +11 -6
  132. data/lib/active_record/railties/databases.rake +26 -38
  133. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  134. data/lib/active_record/railties/job_runtime.rb +10 -11
  135. data/lib/active_record/reflection.rb +54 -22
  136. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  137. data/lib/active_record/relation/batches.rb +147 -73
  138. data/lib/active_record/relation/calculations.rb +60 -43
  139. data/lib/active_record/relation/delegation.rb +25 -15
  140. data/lib/active_record/relation/finder_methods.rb +73 -43
  141. data/lib/active_record/relation/merger.rb +12 -10
  142. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  143. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  144. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  145. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  146. data/lib/active_record/relation/predicate_builder.rb +23 -8
  147. data/lib/active_record/relation/query_attribute.rb +3 -1
  148. data/lib/active_record/relation/query_methods.rb +200 -96
  149. data/lib/active_record/relation/spawn_methods.rb +7 -7
  150. data/lib/active_record/relation/where_clause.rb +2 -9
  151. data/lib/active_record/relation.rb +127 -87
  152. data/lib/active_record/result.rb +109 -24
  153. data/lib/active_record/runtime_registry.rb +42 -58
  154. data/lib/active_record/sanitization.rb +9 -6
  155. data/lib/active_record/schema.rb +1 -1
  156. data/lib/active_record/schema_dumper.rb +17 -10
  157. data/lib/active_record/schema_migration.rb +2 -1
  158. data/lib/active_record/scoping/named.rb +5 -2
  159. data/lib/active_record/scoping.rb +0 -1
  160. data/lib/active_record/signed_id.rb +44 -16
  161. data/lib/active_record/statement_cache.rb +24 -20
  162. data/lib/active_record/store.rb +53 -22
  163. data/lib/active_record/structured_event_subscriber.rb +85 -0
  164. data/lib/active_record/table_metadata.rb +6 -23
  165. data/lib/active_record/tasks/abstract_tasks.rb +83 -0
  166. data/lib/active_record/tasks/database_tasks.rb +85 -85
  167. data/lib/active_record/tasks/mysql_database_tasks.rb +4 -58
  168. data/lib/active_record/tasks/postgresql_database_tasks.rb +7 -40
  169. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -28
  170. data/lib/active_record/test_databases.rb +18 -4
  171. data/lib/active_record/test_fixtures.rb +52 -6
  172. data/lib/active_record/testing/query_assertions.rb +8 -2
  173. data/lib/active_record/timestamp.rb +4 -2
  174. data/lib/active_record/token_for.rb +1 -1
  175. data/lib/active_record/transaction.rb +2 -5
  176. data/lib/active_record/transactions.rb +56 -32
  177. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  178. data/lib/active_record/type/internal/timezone.rb +7 -0
  179. data/lib/active_record/type/json.rb +13 -2
  180. data/lib/active_record/type/serialized.rb +16 -4
  181. data/lib/active_record/type/type_map.rb +1 -1
  182. data/lib/active_record/type_caster/connection.rb +2 -1
  183. data/lib/active_record/validations/associated.rb +1 -1
  184. data/lib/active_record/validations/uniqueness.rb +8 -8
  185. data/lib/active_record.rb +85 -50
  186. data/lib/arel/alias_predication.rb +2 -0
  187. data/lib/arel/collectors/bind.rb +2 -2
  188. data/lib/arel/collectors/sql_string.rb +1 -1
  189. data/lib/arel/collectors/substitute_binds.rb +2 -2
  190. data/lib/arel/crud.rb +6 -11
  191. data/lib/arel/nodes/binary.rb +1 -1
  192. data/lib/arel/nodes/count.rb +2 -2
  193. data/lib/arel/nodes/function.rb +4 -10
  194. data/lib/arel/nodes/named_function.rb +2 -2
  195. data/lib/arel/nodes/node.rb +2 -2
  196. data/lib/arel/nodes/sql_literal.rb +1 -1
  197. data/lib/arel/nodes.rb +0 -2
  198. data/lib/arel/predications.rb +1 -3
  199. data/lib/arel/select_manager.rb +7 -2
  200. data/lib/arel/table.rb +3 -7
  201. data/lib/arel/visitors/dot.rb +0 -3
  202. data/lib/arel/visitors/postgresql.rb +55 -0
  203. data/lib/arel/visitors/sqlite.rb +55 -8
  204. data/lib/arel/visitors/to_sql.rb +4 -22
  205. data/lib/arel.rb +3 -1
  206. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  207. metadata +15 -12
  208. data/lib/active_record/explain_subscriber.rb +0 -34
  209. data/lib/active_record/normalization.rb +0 -163
  210. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
@@ -11,17 +11,9 @@ module ActiveRecord
11
11
  end
12
12
 
13
13
  # Queries the database and returns the results in an Array-like object
14
- def query(sql, name = nil) # :nodoc:
15
- mark_transaction_written_if_write(sql)
16
-
17
- log(sql, name) do |notification_payload|
18
- with_raw_connection do |conn|
19
- result = conn.async_exec(sql).map_types!(@type_map_for_results).values
20
- verified!
21
- notification_payload[:row_count] = result.count
22
- result
23
- end
24
- end
14
+ def query(sql, name = nil, allow_retry: true, materialize_transactions: true) # :nodoc:
15
+ result = internal_execute(sql, name, allow_retry:, materialize_transactions:)
16
+ result.map_types!(@type_map_for_results).values
25
17
  end
26
18
 
27
19
  READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
@@ -35,51 +27,6 @@ module ActiveRecord
35
27
  !READ_QUERY.match?(sql.b)
36
28
  end
37
29
 
38
- # Executes an SQL statement, returning a PG::Result object on success
39
- # or raising a PG::Error exception otherwise.
40
- #
41
- # Setting +allow_retry+ to true causes the db to reconnect and retry
42
- # executing the SQL statement in case of a connection-related exception.
43
- # This option should only be enabled for known idempotent queries.
44
- #
45
- # Note: the PG::Result object is manually memory managed; if you don't
46
- # need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
47
- def execute(...) # :nodoc:
48
- super
49
- ensure
50
- @notice_receiver_sql_warnings = []
51
- end
52
-
53
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
54
- log(sql, name, async: async) do |notification_payload|
55
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
56
- result = conn.async_exec(sql)
57
- verified!
58
- handle_warnings(result)
59
- notification_payload[:row_count] = result.count
60
- result
61
- end
62
- end
63
- end
64
-
65
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true) # :nodoc:
66
- execute_and_clear(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |result|
67
- types = {}
68
- fields = result.fields
69
- fields.each_with_index do |fname, i|
70
- ftype = result.ftype i
71
- fmod = result.fmod i
72
- types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
73
- end
74
- build_result(columns: fields, rows: result.values, column_types: types.freeze)
75
- end
76
- end
77
-
78
- def exec_delete(sql, name = nil, binds = []) # :nodoc:
79
- execute_and_clear(sql, name, binds) { |result| result.cmd_tuples }
80
- end
81
- alias :exec_update :exec_delete
82
-
83
30
  def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil) # :nodoc:
84
31
  if use_insert_returning? || pk == false
85
32
  super
@@ -165,13 +112,82 @@ module ActiveRecord
165
112
  def cancel_any_running_query
166
113
  return if @raw_connection.nil? || IDLE_TRANSACTION_STATUSES.include?(@raw_connection.transaction_status)
167
114
 
168
- @raw_connection.cancel
115
+ # Skip @raw_connection.cancel (PG::Connection#cancel) when using libpq >= 18 with pg < 1.6.0,
116
+ # because the pg gem cannot obtain the backend_key in that case.
117
+ # This method is only called from exec_rollback_db_transaction and exec_restart_db_transaction.
118
+ # Even without cancel, rollback will still run. However, since any running
119
+ # query must finish first, the rollback may take longer.
120
+ if !(PG.library_version >= 18_00_00 && Gem::Version.new(PG::VERSION) < Gem::Version.new("1.6.0"))
121
+ @raw_connection.cancel
122
+ end
169
123
  @raw_connection.block
170
124
  rescue PG::Error
171
125
  end
172
126
 
173
- def execute_batch(statements, name = nil)
174
- execute(combine_multi_statements(statements))
127
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
128
+ update_typemap_for_default_timezone
129
+ result = if prepare
130
+ begin
131
+ stmt_key = prepare_statement(sql, binds, raw_connection)
132
+ notification_payload[:statement_name] = stmt_key
133
+ raw_connection.exec_prepared(stmt_key, type_casted_binds)
134
+ rescue PG::FeatureNotSupported => error
135
+ if is_cached_plan_failure?(error)
136
+ # Nothing we can do if we are in a transaction because all commands
137
+ # will raise InFailedSQLTransaction
138
+ if in_transaction?
139
+ raise PreparedStatementCacheExpired.new(error.message, connection_pool: @pool)
140
+ else
141
+ @lock.synchronize do
142
+ # outside of transactions we can simply flush this query and retry
143
+ @statements.delete sql_key(sql)
144
+ end
145
+ retry
146
+ end
147
+ end
148
+
149
+ raise
150
+ end
151
+ elsif binds.nil? || binds.empty?
152
+ raw_connection.async_exec(sql)
153
+ else
154
+ raw_connection.exec_params(sql, type_casted_binds)
155
+ end
156
+
157
+ verified!
158
+
159
+ notification_payload[:affected_rows] = result.cmd_tuples
160
+ notification_payload[:row_count] = result.ntuples
161
+ result
162
+ end
163
+
164
+ def cast_result(result)
165
+ ar_result = if result.fields.empty?
166
+ ActiveRecord::Result.empty(affected_rows: result.cmd_tuples)
167
+ else
168
+ fields = result.fields
169
+ types = Array.new(fields.size)
170
+ fields.size.times do |index|
171
+ ftype = result.ftype(index)
172
+ fmod = result.fmod(index)
173
+ types[index] = get_oid_type(ftype, fmod, fields[index])
174
+ end
175
+
176
+ ActiveRecord::Result.new(fields, result.values, types.freeze, affected_rows: result.cmd_tuples)
177
+ end
178
+
179
+ result.clear
180
+ ar_result
181
+ end
182
+
183
+ def affected_rows(result)
184
+ affected_rows = result.cmd_tuples
185
+ result.clear
186
+ affected_rows
187
+ end
188
+
189
+ def execute_batch(statements, name = nil, **kwargs)
190
+ raw_execute(combine_multi_statements(statements), name, batch: true, **kwargs)
175
191
  end
176
192
 
177
193
  def build_truncate_statements(table_names)
@@ -191,8 +207,10 @@ module ActiveRecord
191
207
  pk unless pk.is_a?(Array)
192
208
  end
193
209
 
194
- def handle_warnings(sql)
195
- @notice_receiver_sql_warnings.each do |warning|
210
+ def handle_warnings(result, sql)
211
+ warnings, @notice_receiver_sql_warnings = @notice_receiver_sql_warnings, []
212
+
213
+ warnings.each do |warning|
196
214
  next if warning_ignored?(warning)
197
215
 
198
216
  warning.sql = sql
@@ -16,8 +16,8 @@ module ActiveRecord
16
16
  @subtype = subtype
17
17
  @delimiter = delimiter
18
18
 
19
- @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter
20
- @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter
19
+ @pg_encoder = PG::TextEncoder::Array.new(name: "#{type}[]".freeze, delimiter: delimiter).freeze
20
+ @pg_decoder = PG::TextDecoder::Array.new(name: "#{type}[]".freeze, delimiter: delimiter).freeze
21
21
  end
22
22
 
23
23
  def deserialize(value)
@@ -65,7 +65,7 @@ module ActiveRecord
65
65
  end
66
66
 
67
67
  def map(value, &block)
68
- value.map { |v| subtype.map(v, &block) }
68
+ value.is_a?(::Array) ? value.map(&block) : subtype.map(value, &block)
69
69
  end
70
70
 
71
71
  def changed_in_place?(raw_old_value, new_value)
@@ -25,6 +25,10 @@ module ActiveRecord
25
25
  build_point(x, y)
26
26
  when ::Array
27
27
  build_point(*value)
28
+ when ::Hash
29
+ return if value.blank?
30
+
31
+ build_point(*values_array_from_hash(value))
28
32
  else
29
33
  value
30
34
  end
@@ -36,6 +40,8 @@ module ActiveRecord
36
40
  "(#{number_for_point(value.x)},#{number_for_point(value.y)})"
37
41
  when ::Array
38
42
  serialize(build_point(*value))
43
+ when ::Hash
44
+ serialize(build_point(*values_array_from_hash(value)))
39
45
  else
40
46
  super
41
47
  end
@@ -57,6 +63,10 @@ module ActiveRecord
57
63
  def build_point(x, y)
58
64
  ActiveRecord::Point.new(Float(x), Float(y))
59
65
  end
66
+
67
+ def values_array_from_hash(value)
68
+ [value.values_at(:x, "x").compact.first, value.values_at(:y, "y").compact.first]
69
+ end
60
70
  end
61
71
  end
62
72
  end
@@ -14,7 +14,10 @@ module ActiveRecord
14
14
  end
15
15
 
16
16
  def type_cast_for_schema(value)
17
- value.inspect.gsub("Infinity", "::Float::INFINITY")
17
+ from = bound_for_schema(value.begin)
18
+ to = bound_for_schema(value.end)
19
+ op = value.exclude_end? ? "..." : ".."
20
+ "#{from}#{op}#{to}"
18
21
  end
19
22
 
20
23
  def cast_value(value)
@@ -58,6 +61,19 @@ module ActiveRecord
58
61
  end
59
62
 
60
63
  private
64
+ def bound_for_schema(bound)
65
+ case bound
66
+ when nil
67
+ "nil"
68
+ when ::Float::INFINITY
69
+ "::Float::INFINITY"
70
+ when -::Float::INFINITY
71
+ "-::Float::INFINITY"
72
+ else
73
+ @subtype.type_cast_for_schema(bound)
74
+ end
75
+ end
76
+
61
77
  def type_cast_single(value)
62
78
  infinity?(value) ? value : @subtype.deserialize(value)
63
79
  end
@@ -67,7 +83,7 @@ module ActiveRecord
67
83
  end
68
84
 
69
85
  def extract_bounds(value)
70
- from, to = value[1..-2].split(",", 2)
86
+ from, to = split_bounds(value[1..-2])
71
87
  {
72
88
  from: (from == "" || from == "-infinity") ? infinity(negative: true) : unquote(from),
73
89
  to: (to == "" || to == "infinity") ? infinity : unquote(to),
@@ -76,6 +92,33 @@ module ActiveRecord
76
92
  }
77
93
  end
78
94
 
95
+ # Matches the comma-separated lower and upper bounds of a range's
96
+ # textual representation when a bound is double-quoted. A quoted
97
+ # bound can itself contain a comma, so a naive split on the first
98
+ # comma would corrupt such values. Within a double-quoted bound,
99
+ # literal " and \ are escaped (as "" / \" and \\ respectively), so
100
+ # those escapes are skipped while scanning for the separating comma.
101
+ #
102
+ # An unquoted bound never contains a comma or a double-quote
103
+ # (PostgreSQL quotes the whole bound when it would), so the
104
+ # alternation matches an unquoted run with [^,"] rather than [^,].
105
+ # Excluding " keeps the two alternatives mutually exclusive, which
106
+ # removes any ambiguity over how a "-prefixed bound is consumed. The
107
+ # /m flag lets the captured upper bound (and quoted bounds) span
108
+ # newlines.
109
+ BOUNDS = /\A((?:"(?:[^"\\]|""|\\.)*"|[^,"])*),(.*)\z/m # :nodoc:
110
+
111
+ def split_bounds(value)
112
+ # Fast path: an unquoted representation (every built-in range
113
+ # type -- int/num/date/timestamp) has no embedded comma, so a
114
+ # plain split is correct and avoids the regexp. Only quoted
115
+ # bounds (custom text/varchar/money/... ranges) need the
116
+ # comma-skipping scan.
117
+ return value.split(",", 2) unless value.include?('"')
118
+
119
+ (match = BOUNDS.match(value)) ? [match[1], match[2]] : [value, nil]
120
+ end
121
+
79
122
  INFINITE_FLOAT_RANGE = (-::Float::INFINITY)..(::Float::INFINITY) # :nodoc:
80
123
 
81
124
  def sanitize_bounds(from, to)
@@ -94,7 +137,7 @@ module ActiveRecord
94
137
  # * https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-IO
95
138
  # * https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-IO-SYNTAX
96
139
  def unquote(value)
97
- if value.start_with?('"') && value.end_with?('"')
140
+ if value && value.start_with?('"') && value.end_with?('"')
98
141
  unquoted_value = value[1..-2]
99
142
  unquoted_value.gsub!('""', '"')
100
143
  unquoted_value.gsub!("\\\\", "\\")
@@ -68,7 +68,7 @@ module ActiveRecord
68
68
 
69
69
  def register_array_type(row)
70
70
  register_with_subtype(row["oid"], row["typelem"].to_i) do |subtype|
71
- OID::Array.new(subtype, row["typdelim"])
71
+ OID::Array.new(subtype, row["typdelim"].freeze)
72
72
  end
73
73
  end
74
74
 
@@ -153,14 +153,15 @@ module ActiveRecord
153
153
  "'#{escape_bytea(value.to_s)}'"
154
154
  end
155
155
 
156
+ # `column` may be either an instance of Column or ColumnDefinition.
156
157
  def quote_default_expression(value, column) # :nodoc:
157
158
  if value.is_a?(Proc)
158
159
  value.call
159
160
  elsif column.type == :uuid && value.is_a?(String) && value.include?("()")
160
161
  value # Does not quote function default values for UUID columns
161
162
  elsif column.respond_to?(:array?)
162
- type = lookup_cast_type_from_column(column)
163
- quote(type.serialize(value))
163
+ # TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
164
+ quote(column.fetch_cast_type(self).serialize(value))
164
165
  else
165
166
  super
166
167
  end
@@ -186,16 +187,12 @@ module ActiveRecord
186
187
  end
187
188
  end
188
189
 
189
- def lookup_cast_type_from_column(column) # :nodoc:
190
- verify! if type_map.nil?
191
- type_map.lookup(column.oid, column.fmod, column.sql_type)
190
+ # TODO: Make this method private after we release 8.1.
191
+ def lookup_cast_type(sql_type) # :nodoc:
192
+ super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
192
193
  end
193
194
 
194
195
  private
195
- def lookup_cast_type(sql_type)
196
- super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
197
- end
198
-
199
196
  def encode_array(array_data)
200
197
  encoder = array_data.encoder
201
198
  values = type_cast_array(array_data.values)
@@ -208,7 +205,17 @@ module ActiveRecord
208
205
  end
209
206
 
210
207
  def encode_range(range)
211
- "[#{type_cast_range_value(range.begin)},#{type_cast_range_value(range.end)}#{range.exclude_end? ? ')' : ']'}"
208
+ lower_bound = type_cast_range_value(range.begin)
209
+ upper_bound = if date_or_time_range?(range)
210
+ # Postgres will convert `[today,]` to `[today,)`, making it exclusive.
211
+ # We can use the special timestamp value `infinity` to force inclusion.
212
+ # https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-INFINITE
213
+ range.end.nil? ? "infinity" : type_cast(range.end)
214
+ else
215
+ type_cast_range_value(range.end)
216
+ end
217
+
218
+ "[#{lower_bound},#{upper_bound}#{range.exclude_end? ? ')' : ']'}"
212
219
  end
213
220
 
214
221
  def determine_encoding_of_strings_in_array(value)
@@ -232,6 +239,10 @@ module ActiveRecord
232
239
  def infinity?(value)
233
240
  value.respond_to?(:infinite?) && value.infinite?
234
241
  end
242
+
243
+ def date_or_time_range?(range)
244
+ [range.begin.class, range.end.class].intersect?([Date, DateTime, Time])
245
+ end
235
246
  end
236
247
  end
237
248
  end
@@ -45,12 +45,10 @@ Rails needs superuser privileges to disable referential integrity.
45
45
  BEGIN
46
46
  FOR r IN (
47
47
  SELECT FORMAT(
48
- 'UPDATE pg_constraint SET convalidated=false WHERE conname = ''%I'' AND connamespace::regnamespace = ''%I''::regnamespace; ALTER TABLE %I.%I VALIDATE CONSTRAINT %I;',
48
+ 'UPDATE pg_catalog.pg_constraint SET convalidated=false WHERE conname = ''%1$I'' AND connamespace::regnamespace = ''%2$I''::regnamespace AND conrelid::regclass = ''%3$I''::regclass; ALTER TABLE %2$I.%3$I VALIDATE CONSTRAINT %1$I;',
49
49
  constraint_name,
50
50
  table_schema,
51
- table_schema,
52
- table_name,
53
- constraint_name
51
+ table_name
54
52
  ) AS constraint_check
55
53
  FROM information_schema.table_constraints WHERE constraint_type = 'FOREIGN KEY'
56
54
  )
@@ -6,19 +6,17 @@ module ActiveRecord
6
6
  class SchemaCreation < SchemaCreation # :nodoc:
7
7
  private
8
8
  delegate :quoted_include_columns_for_index, to: :@conn
9
+ delegate :database_version, to: :@conn
9
10
 
10
11
  def visit_AlterTable(o)
11
12
  sql = super
12
13
  sql << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ")
13
14
  sql << o.exclusion_constraint_adds.map { |con| visit_AddExclusionConstraint con }.join(" ")
14
- sql << o.exclusion_constraint_drops.map { |con| visit_DropExclusionConstraint con }.join(" ")
15
15
  sql << o.unique_constraint_adds.map { |con| visit_AddUniqueConstraint con }.join(" ")
16
- sql << o.unique_constraint_drops.map { |con| visit_DropUniqueConstraint con }.join(" ")
17
16
  end
18
17
 
19
18
  def visit_AddForeignKey(o)
20
19
  super.dup.tap do |sql|
21
- sql << " DEFERRABLE INITIALLY #{o.options[:deferrable].to_s.upcase}" if o.deferrable
22
20
  sql << " NOT VALID" unless o.validate?
23
21
  end
24
22
  end
@@ -55,6 +53,7 @@ module ActiveRecord
55
53
  sql = ["CONSTRAINT"]
56
54
  sql << quote_column_name(o.name)
57
55
  sql << "UNIQUE"
56
+ sql << "NULLS NOT DISTINCT" if supports_nulls_not_distinct? && o.nulls_not_distinct
58
57
 
59
58
  if o.using_index
60
59
  sql << "USING INDEX #{quote_column_name(o.using_index)}"
@@ -73,18 +72,10 @@ module ActiveRecord
73
72
  "ADD #{accept(o)}"
74
73
  end
75
74
 
76
- def visit_DropExclusionConstraint(name)
77
- "DROP CONSTRAINT #{quote_column_name(name)}"
78
- end
79
-
80
75
  def visit_AddUniqueConstraint(o)
81
76
  "ADD #{accept(o)}"
82
77
  end
83
78
 
84
- def visit_DropUniqueConstraint(name)
85
- "DROP CONSTRAINT #{quote_column_name(name)}"
86
- end
87
-
88
79
  def visit_ChangeColumnDefinition(o)
89
80
  column = o.column
90
81
  column.sql_type = type_to_sql(column.type, **column.options)
@@ -109,7 +100,7 @@ module ActiveRecord
109
100
  if options[:default].nil?
110
101
  change_column_sql << ", ALTER COLUMN #{quoted_column_name} DROP DEFAULT"
111
102
  else
112
- quoted_default = quote_default_expression(options[:default], column)
103
+ quoted_default = quote_default_expression_for_column_definition(options[:default], column)
113
104
  change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}"
114
105
  end
115
106
  end
@@ -136,16 +127,17 @@ module ActiveRecord
136
127
  end
137
128
 
138
129
  if as = options[:as]
139
- sql << " GENERATED ALWAYS AS (#{as})"
130
+ stored = options[:stored]
140
131
 
141
- if options[:stored]
142
- sql << " STORED"
143
- else
132
+ if stored != true && database_version < 18_00_00
144
133
  raise ArgumentError, <<~MSG
145
- PostgreSQL currently does not support VIRTUAL (not persisted) generated columns.
134
+ PostgreSQL versions before 18 do not support VIRTUAL (not persisted) generated columns.
146
135
  Specify 'stored: true' option for '#{options[:column].name}'
147
136
  MSG
148
137
  end
138
+
139
+ sql << " GENERATED ALWAYS AS (#{as})"
140
+ sql << (stored ? " STORED" : " VIRTUAL")
149
141
  end
150
142
  super
151
143
  end
@@ -5,6 +5,7 @@ module ActiveRecord
5
5
  module PostgreSQL
6
6
  module ColumnMethods
7
7
  extend ActiveSupport::Concern
8
+ extend ConnectionAdapters::ColumnMethods::ClassMethods
8
9
 
9
10
  # Defines the primary key field.
10
11
  # Use of the native PostgreSQL UUID type is supported, and can be used
@@ -15,22 +16,10 @@ module ActiveRecord
15
16
  # t.timestamps
16
17
  # end
17
18
  #
18
- # By default, this will use the <tt>gen_random_uuid()</tt> function from the
19
- # +pgcrypto+ extension. As that extension is only available in
20
- # PostgreSQL 9.4+, for earlier versions an explicit default can be set
21
- # to use <tt>uuid_generate_v4()</tt> from the +uuid-ossp+ extension instead:
19
+ # By default, this will use the <tt>gen_random_uuid()</tt> function.
22
20
  #
23
- # create_table :stuffs, id: false do |t|
24
- # t.primary_key :id, :uuid, default: "uuid_generate_v4()"
25
- # t.uuid :foo_id
26
- # t.timestamps
27
- # end
28
- #
29
- # To enable the appropriate extension, which is a requirement, use
30
- # the +enable_extension+ method in your migrations.
31
- #
32
- # To use a UUID primary key without any of the extensions, set the
33
- # +:default+ option to +nil+:
21
+ # To use a UUID primary key without any defaults, set the +:default+
22
+ # option to +nil+:
34
23
  #
35
24
  # create_table :stuffs, id: false do |t|
36
25
  # t.primary_key :id, :uuid, default: nil
@@ -181,12 +170,10 @@ module ActiveRecord
181
170
  # :method: enum
182
171
  # :call-seq: enum(*names, **options)
183
172
 
184
- included do
185
- define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange,
186
- :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr,
187
- :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle,
188
- :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml, :timestamptz, :enum
189
- end
173
+ define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange,
174
+ :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr,
175
+ :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle,
176
+ :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml, :timestamptz, :enum
190
177
  end
191
178
 
192
179
  ExclusionConstraintDefinition = Struct.new(:table_name, :expression, :options) do
@@ -224,6 +211,10 @@ module ActiveRecord
224
211
  options[:using_index]
225
212
  end
226
213
 
214
+ def nulls_not_distinct
215
+ options[:nulls_not_distinct]
216
+ end
217
+
227
218
  def export_name_on_schema_dump?
228
219
  !ActiveRecord::SchemaDumper.unique_ignore_pattern.match?(name) if name
229
220
  end
@@ -319,7 +310,7 @@ module ActiveRecord
319
310
 
320
311
  # Adds a unique constraint.
321
312
  #
322
- # t.unique_constraint(:position, name: 'unique_position', deferrable: :deferred)
313
+ # t.unique_constraint(:position, name: 'unique_position', deferrable: :deferred, nulls_not_distinct: true)
323
314
  #
324
315
  # See {connection.add_unique_constraint}[rdoc-ref:SchemaStatements#add_unique_constraint]
325
316
  def unique_constraint(...)
@@ -358,15 +349,13 @@ module ActiveRecord
358
349
 
359
350
  # = Active Record PostgreSQL Adapter Alter \Table
360
351
  class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable
361
- attr_reader :constraint_validations, :exclusion_constraint_adds, :exclusion_constraint_drops, :unique_constraint_adds, :unique_constraint_drops
352
+ attr_reader :constraint_validations, :exclusion_constraint_adds, :unique_constraint_adds
362
353
 
363
354
  def initialize(td)
364
355
  super
365
356
  @constraint_validations = []
366
357
  @exclusion_constraint_adds = []
367
- @exclusion_constraint_drops = []
368
358
  @unique_constraint_adds = []
369
- @unique_constraint_drops = []
370
359
  end
371
360
 
372
361
  def validate_constraint(name)
@@ -377,17 +366,9 @@ module ActiveRecord
377
366
  @exclusion_constraint_adds << @td.new_exclusion_constraint_definition(expression, options)
378
367
  end
379
368
 
380
- def drop_exclusion_constraint(constraint_name)
381
- @exclusion_constraint_drops << constraint_name
382
- end
383
-
384
369
  def add_unique_constraint(column_name, options)
385
370
  @unique_constraint_adds << @td.new_unique_constraint_definition(column_name, options)
386
371
  end
387
-
388
- def drop_unique_constraint(unique_constraint_name)
389
- @unique_constraint_drops << unique_constraint_name
390
- end
391
372
  end
392
373
  end
393
374
  end