activerecord 7.1.5.2 → 8.1.2.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 (244) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +567 -2443
  3. data/README.rdoc +16 -16
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +3 -2
  6. data/lib/active_record/associations/alias_tracker.rb +31 -23
  7. data/lib/active_record/associations/association.rb +43 -12
  8. data/lib/active_record/associations/belongs_to_association.rb +31 -8
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/association.rb +23 -11
  11. data/lib/active_record/associations/builder/belongs_to.rb +18 -4
  12. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  13. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  14. data/lib/active_record/associations/builder/has_many.rb +3 -4
  15. data/lib/active_record/associations/builder/has_one.rb +3 -4
  16. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  17. data/lib/active_record/associations/collection_association.rb +17 -9
  18. data/lib/active_record/associations/collection_proxy.rb +36 -5
  19. data/lib/active_record/associations/deprecation.rb +88 -0
  20. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  21. data/lib/active_record/associations/errors.rb +268 -0
  22. data/lib/active_record/associations/has_many_association.rb +1 -1
  23. data/lib/active_record/associations/has_many_through_association.rb +10 -3
  24. data/lib/active_record/associations/join_dependency/join_association.rb +1 -1
  25. data/lib/active_record/associations/join_dependency.rb +4 -2
  26. data/lib/active_record/associations/nested_error.rb +47 -0
  27. data/lib/active_record/associations/preloader/association.rb +4 -3
  28. data/lib/active_record/associations/preloader/batch.rb +7 -1
  29. data/lib/active_record/associations/preloader/branch.rb +8 -1
  30. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  31. data/lib/active_record/associations/singular_association.rb +14 -3
  32. data/lib/active_record/associations/through_association.rb +1 -1
  33. data/lib/active_record/associations.rb +250 -315
  34. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  35. data/lib/active_record/attribute_assignment.rb +0 -2
  36. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  37. data/lib/active_record/attribute_methods/primary_key.rb +25 -61
  38. data/lib/active_record/attribute_methods/query.rb +34 -0
  39. data/lib/active_record/attribute_methods/read.rb +1 -13
  40. data/lib/active_record/attribute_methods/serialization.rb +20 -27
  41. data/lib/active_record/attribute_methods/time_zone_conversion.rb +19 -20
  42. data/lib/active_record/attribute_methods.rb +71 -75
  43. data/lib/active_record/attributes.rb +96 -68
  44. data/lib/active_record/autosave_association.rb +93 -58
  45. data/lib/active_record/base.rb +5 -7
  46. data/lib/active_record/callbacks.rb +1 -1
  47. data/lib/active_record/coders/json.rb +14 -5
  48. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +50 -126
  49. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  50. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +52 -13
  51. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +681 -135
  52. data/lib/active_record/connection_adapters/abstract/database_statements.rb +165 -86
  53. data/lib/active_record/connection_adapters/abstract/query_cache.rb +226 -78
  54. data/lib/active_record/connection_adapters/abstract/quoting.rb +79 -114
  55. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  56. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +37 -36
  57. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  58. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +136 -34
  59. data/lib/active_record/connection_adapters/abstract/transaction.rb +163 -68
  60. data/lib/active_record/connection_adapters/abstract_adapter.rb +197 -129
  61. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +112 -63
  62. data/lib/active_record/connection_adapters/column.rb +17 -4
  63. data/lib/active_record/connection_adapters/mysql/database_statements.rb +11 -3
  64. data/lib/active_record/connection_adapters/mysql/quoting.rb +50 -57
  65. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  66. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  67. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +79 -46
  68. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +97 -101
  69. data/lib/active_record/connection_adapters/mysql2_adapter.rb +14 -33
  70. data/lib/active_record/connection_adapters/pool_config.rb +14 -13
  71. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  72. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +98 -44
  73. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  74. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  75. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  76. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  77. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  78. data/lib/active_record/connection_adapters/postgresql/quoting.rb +79 -68
  79. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  80. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  81. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +44 -41
  82. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +69 -32
  83. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +155 -75
  84. data/lib/active_record/connection_adapters/postgresql_adapter.rb +104 -121
  85. data/lib/active_record/connection_adapters/schema_cache.rb +126 -133
  86. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  87. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +91 -95
  88. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +57 -54
  89. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +16 -0
  90. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  91. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +43 -2
  92. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +38 -15
  93. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +231 -111
  94. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  95. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +40 -69
  96. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -65
  97. data/lib/active_record/connection_adapters.rb +66 -0
  98. data/lib/active_record/connection_handling.rb +88 -46
  99. data/lib/active_record/core.rb +142 -58
  100. data/lib/active_record/counter_cache.rb +52 -18
  101. data/lib/active_record/database_configurations/connection_url_resolver.rb +9 -2
  102. data/lib/active_record/database_configurations/database_config.rb +28 -5
  103. data/lib/active_record/database_configurations/hash_config.rb +103 -41
  104. data/lib/active_record/database_configurations/url_config.rb +30 -1
  105. data/lib/active_record/database_configurations.rb +8 -4
  106. data/lib/active_record/delegated_type.rb +43 -19
  107. data/lib/active_record/dynamic_matchers.rb +56 -71
  108. data/lib/active_record/encryption/config.rb +3 -1
  109. data/lib/active_record/encryption/encryptable_record.rb +12 -12
  110. data/lib/active_record/encryption/encrypted_attribute_type.rb +35 -6
  111. data/lib/active_record/encryption/encryptor.rb +58 -22
  112. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  113. data/lib/active_record/encryption/key_provider.rb +1 -1
  114. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  115. data/lib/active_record/encryption/message_serializer.rb +4 -0
  116. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  117. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  118. data/lib/active_record/encryption/scheme.rb +9 -2
  119. data/lib/active_record/enum.rb +57 -36
  120. data/lib/active_record/errors.rb +76 -26
  121. data/lib/active_record/explain.rb +14 -25
  122. data/lib/active_record/explain_registry.rb +51 -2
  123. data/lib/active_record/filter_attribute_handler.rb +73 -0
  124. data/lib/active_record/fixture_set/table_row.rb +19 -2
  125. data/lib/active_record/fixtures.rb +39 -35
  126. data/lib/active_record/future_result.rb +21 -13
  127. data/lib/active_record/gem_version.rb +3 -3
  128. data/lib/active_record/inheritance.rb +5 -3
  129. data/lib/active_record/insert_all.rb +28 -20
  130. data/lib/active_record/integration.rb +4 -1
  131. data/lib/active_record/internal_metadata.rb +48 -34
  132. data/lib/active_record/locking/optimistic.rb +15 -7
  133. data/lib/active_record/locking/pessimistic.rb +5 -0
  134. data/lib/active_record/log_subscriber.rb +3 -34
  135. data/lib/active_record/message_pack.rb +1 -1
  136. data/lib/active_record/middleware/shard_selector.rb +34 -17
  137. data/lib/active_record/migration/command_recorder.rb +46 -14
  138. data/lib/active_record/migration/compatibility.rb +40 -25
  139. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  140. data/lib/active_record/migration/default_strategy.rb +4 -5
  141. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  142. data/lib/active_record/migration.rb +119 -103
  143. data/lib/active_record/model_schema.rb +66 -78
  144. data/lib/active_record/nested_attributes.rb +17 -9
  145. data/lib/active_record/persistence.rb +161 -454
  146. data/lib/active_record/query_cache.rb +33 -15
  147. data/lib/active_record/query_logs.rb +106 -39
  148. data/lib/active_record/query_logs_formatter.rb +17 -28
  149. data/lib/active_record/querying.rb +28 -16
  150. data/lib/active_record/railtie.rb +61 -74
  151. data/lib/active_record/railties/controller_runtime.rb +19 -5
  152. data/lib/active_record/railties/databases.rake +64 -78
  153. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  154. data/lib/active_record/railties/job_runtime.rb +10 -11
  155. data/lib/active_record/reflection.rb +147 -53
  156. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  157. data/lib/active_record/relation/batches.rb +153 -73
  158. data/lib/active_record/relation/calculations.rb +146 -96
  159. data/lib/active_record/relation/delegation.rb +30 -23
  160. data/lib/active_record/relation/finder_methods.rb +64 -34
  161. data/lib/active_record/relation/merger.rb +12 -14
  162. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  163. data/lib/active_record/relation/predicate_builder/association_query_value.rb +19 -11
  164. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  165. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  166. data/lib/active_record/relation/predicate_builder.rb +25 -10
  167. data/lib/active_record/relation/query_attribute.rb +4 -2
  168. data/lib/active_record/relation/query_methods.rb +351 -124
  169. data/lib/active_record/relation/spawn_methods.rb +9 -25
  170. data/lib/active_record/relation/where_clause.rb +17 -30
  171. data/lib/active_record/relation.rb +590 -124
  172. data/lib/active_record/result.rb +131 -59
  173. data/lib/active_record/runtime_registry.rb +44 -21
  174. data/lib/active_record/sanitization.rb +33 -25
  175. data/lib/active_record/schema.rb +8 -6
  176. data/lib/active_record/schema_dumper.rb +66 -31
  177. data/lib/active_record/schema_migration.rb +31 -14
  178. data/lib/active_record/scoping/named.rb +6 -2
  179. data/lib/active_record/scoping.rb +0 -1
  180. data/lib/active_record/secure_token.rb +3 -3
  181. data/lib/active_record/signed_id.rb +68 -20
  182. data/lib/active_record/statement_cache.rb +29 -25
  183. data/lib/active_record/store.rb +51 -22
  184. data/lib/active_record/structured_event_subscriber.rb +85 -0
  185. data/lib/active_record/table_metadata.rb +7 -33
  186. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  187. data/lib/active_record/tasks/database_tasks.rb +118 -90
  188. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -42
  189. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  190. data/lib/active_record/tasks/sqlite_database_tasks.rb +17 -28
  191. data/lib/active_record/test_databases.rb +14 -4
  192. data/lib/active_record/test_fixtures.rb +123 -89
  193. data/lib/active_record/testing/query_assertions.rb +127 -0
  194. data/lib/active_record/timestamp.rb +6 -4
  195. data/lib/active_record/token_for.rb +22 -12
  196. data/lib/active_record/touch_later.rb +1 -1
  197. data/lib/active_record/transaction.rb +129 -0
  198. data/lib/active_record/transactions.rb +97 -18
  199. data/lib/active_record/translation.rb +0 -2
  200. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  201. data/lib/active_record/type/internal/timezone.rb +7 -0
  202. data/lib/active_record/type/json.rb +15 -2
  203. data/lib/active_record/type/serialized.rb +12 -7
  204. data/lib/active_record/type/type_map.rb +1 -1
  205. data/lib/active_record/type_caster/connection.rb +5 -4
  206. data/lib/active_record/validations/associated.rb +10 -4
  207. data/lib/active_record/validations/uniqueness.rb +23 -18
  208. data/lib/active_record/validations.rb +4 -1
  209. data/lib/active_record.rb +203 -59
  210. data/lib/arel/alias_predication.rb +3 -1
  211. data/lib/arel/collectors/bind.rb +4 -2
  212. data/lib/arel/collectors/composite.rb +7 -0
  213. data/lib/arel/collectors/sql_string.rb +2 -2
  214. data/lib/arel/collectors/substitute_binds.rb +3 -3
  215. data/lib/arel/crud.rb +8 -11
  216. data/lib/arel/delete_manager.rb +5 -0
  217. data/lib/arel/nodes/binary.rb +1 -7
  218. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  219. data/lib/arel/nodes/count.rb +2 -2
  220. data/lib/arel/nodes/delete_statement.rb +4 -2
  221. data/lib/arel/nodes/function.rb +4 -10
  222. data/lib/arel/nodes/named_function.rb +2 -2
  223. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  224. data/lib/arel/nodes/node.rb +6 -5
  225. data/lib/arel/nodes/sql_literal.rb +8 -1
  226. data/lib/arel/nodes/update_statement.rb +4 -2
  227. data/lib/arel/nodes.rb +2 -4
  228. data/lib/arel/predications.rb +1 -1
  229. data/lib/arel/select_manager.rb +14 -5
  230. data/lib/arel/table.rb +3 -7
  231. data/lib/arel/tree_manager.rb +3 -2
  232. data/lib/arel/update_manager.rb +7 -1
  233. data/lib/arel/visitors/dot.rb +3 -3
  234. data/lib/arel/visitors/mysql.rb +9 -4
  235. data/lib/arel/visitors/postgresql.rb +56 -12
  236. data/lib/arel/visitors/sqlite.rb +80 -8
  237. data/lib/arel/visitors/to_sql.rb +34 -37
  238. data/lib/arel.rb +10 -4
  239. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  240. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  241. metadata +23 -14
  242. data/lib/active_record/explain_subscriber.rb +0 -34
  243. data/lib/active_record/normalization.rb +0 -167
  244. data/lib/active_record/relation/record_fetch_warning.rb +0 -49
@@ -11,16 +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
18
- with_raw_connection do |conn|
19
- result = conn.async_exec(sql).map_types!(@type_map_for_results).values
20
- verified!
21
- result
22
- end
23
- 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
24
17
  end
25
18
 
26
19
  READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
@@ -49,35 +42,6 @@ module ActiveRecord
49
42
  @notice_receiver_sql_warnings = []
50
43
  end
51
44
 
52
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
53
- log(sql, name, async: async) do
54
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
55
- result = conn.async_exec(sql)
56
- verified!
57
- handle_warnings(result)
58
- result
59
- end
60
- end
61
- end
62
-
63
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true) # :nodoc:
64
- execute_and_clear(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |result|
65
- types = {}
66
- fields = result.fields
67
- fields.each_with_index do |fname, i|
68
- ftype = result.ftype i
69
- fmod = result.fmod i
70
- types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
71
- end
72
- build_result(columns: fields, rows: result.values, column_types: types)
73
- end
74
- end
75
-
76
- def exec_delete(sql, name = nil, binds = []) # :nodoc:
77
- execute_and_clear(sql, name, binds) { |result| result.cmd_tuples }
78
- end
79
- alias :exec_update :exec_delete
80
-
81
45
  def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil) # :nodoc:
82
46
  if use_insert_returning? || pk == false
83
47
  super
@@ -122,7 +86,7 @@ module ActiveRecord
122
86
  end
123
87
 
124
88
  # From https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT
125
- HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP").freeze # :nodoc:
89
+ HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP", retryable: true).freeze # :nodoc:
126
90
  private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
127
91
 
128
92
  def high_precision_current_timestamp
@@ -135,6 +99,27 @@ module ActiveRecord
135
99
  "EXPLAIN (#{options.join(", ").upcase})"
136
100
  end
137
101
 
102
+ # Set when constraints will be checked for the current transaction.
103
+ #
104
+ # Not passing any specific constraint names will set the value for all deferrable constraints.
105
+ #
106
+ # [<tt>deferred</tt>]
107
+ # Valid values are +:deferred+ or +:immediate+.
108
+ #
109
+ # See https://www.postgresql.org/docs/current/sql-set-constraints.html
110
+ def set_constraints(deferred, *constraints)
111
+ unless %i[deferred immediate].include?(deferred)
112
+ raise ArgumentError, "deferred must be :deferred or :immediate"
113
+ end
114
+
115
+ constraints = if constraints.empty?
116
+ "ALL"
117
+ else
118
+ constraints.map { |c| quote_table_name(c) }.join(", ")
119
+ end
120
+ execute("SET CONSTRAINTS #{constraints} #{deferred.to_s.upcase}")
121
+ end
122
+
138
123
  private
139
124
  IDLE_TRANSACTION_STATUSES = [PG::PQTRANS_IDLE, PG::PQTRANS_INTRANS, PG::PQTRANS_INERROR]
140
125
  private_constant :IDLE_TRANSACTION_STATUSES
@@ -142,13 +127,82 @@ module ActiveRecord
142
127
  def cancel_any_running_query
143
128
  return if @raw_connection.nil? || IDLE_TRANSACTION_STATUSES.include?(@raw_connection.transaction_status)
144
129
 
145
- @raw_connection.cancel
130
+ # Skip @raw_connection.cancel (PG::Connection#cancel) when using libpq >= 18 with pg < 1.6.0,
131
+ # because the pg gem cannot obtain the backend_key in that case.
132
+ # This method is only called from exec_rollback_db_transaction and exec_restart_db_transaction.
133
+ # Even without cancel, rollback will still run. However, since any running
134
+ # query must finish first, the rollback may take longer.
135
+ if !(PG.library_version >= 18_00_00 && Gem::Version.new(PG::VERSION) < Gem::Version.new("1.6.0"))
136
+ @raw_connection.cancel
137
+ end
146
138
  @raw_connection.block
147
139
  rescue PG::Error
148
140
  end
149
141
 
150
- def execute_batch(statements, name = nil)
151
- execute(combine_multi_statements(statements))
142
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
143
+ update_typemap_for_default_timezone
144
+ result = if prepare
145
+ begin
146
+ stmt_key = prepare_statement(sql, binds, raw_connection)
147
+ notification_payload[:statement_name] = stmt_key
148
+ raw_connection.exec_prepared(stmt_key, type_casted_binds)
149
+ rescue PG::FeatureNotSupported => error
150
+ if is_cached_plan_failure?(error)
151
+ # Nothing we can do if we are in a transaction because all commands
152
+ # will raise InFailedSQLTransaction
153
+ if in_transaction?
154
+ raise PreparedStatementCacheExpired.new(error.message, connection_pool: @pool)
155
+ else
156
+ @lock.synchronize do
157
+ # outside of transactions we can simply flush this query and retry
158
+ @statements.delete sql_key(sql)
159
+ end
160
+ retry
161
+ end
162
+ end
163
+
164
+ raise
165
+ end
166
+ elsif binds.nil? || binds.empty?
167
+ raw_connection.async_exec(sql)
168
+ else
169
+ raw_connection.exec_params(sql, type_casted_binds)
170
+ end
171
+
172
+ verified!
173
+
174
+ notification_payload[:affected_rows] = result.cmd_tuples
175
+ notification_payload[:row_count] = result.ntuples
176
+ result
177
+ end
178
+
179
+ def cast_result(result)
180
+ ar_result = if result.fields.empty?
181
+ ActiveRecord::Result.empty(affected_rows: result.cmd_tuples)
182
+ else
183
+ fields = result.fields
184
+ types = Array.new(fields.size)
185
+ fields.size.times do |index|
186
+ ftype = result.ftype(index)
187
+ fmod = result.fmod(index)
188
+ types[index] = get_oid_type(ftype, fmod, fields[index])
189
+ end
190
+
191
+ ActiveRecord::Result.new(fields, result.values, types.freeze, affected_rows: result.cmd_tuples)
192
+ end
193
+
194
+ result.clear
195
+ ar_result
196
+ end
197
+
198
+ def affected_rows(result)
199
+ affected_rows = result.cmd_tuples
200
+ result.clear
201
+ affected_rows
202
+ end
203
+
204
+ def execute_batch(statements, name = nil, **kwargs)
205
+ raw_execute(combine_multi_statements(statements), name, batch: true, **kwargs)
152
206
  end
153
207
 
154
208
  def build_truncate_statements(table_names)
@@ -168,7 +222,7 @@ module ActiveRecord
168
222
  pk unless pk.is_a?(Array)
169
223
  end
170
224
 
171
- def handle_warnings(sql)
225
+ def handle_warnings(result, sql)
172
226
  @notice_receiver_sql_warnings.each do |warning|
173
227
  next if warning_ignored?(warning)
174
228
 
@@ -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)
@@ -33,7 +33,7 @@ module ActiveRecord
33
33
  when ::Numeric
34
34
  # Sometimes operations on Times returns just float number of seconds so we need to handle that.
35
35
  # Example: Time.current - (Time.current + 1.hour) # => -3600.000001776 (Float)
36
- value.seconds.iso8601(precision: self.precision)
36
+ ActiveSupport::Duration.build(value).iso8601(precision: self.precision)
37
37
  else
38
38
  super
39
39
  end
@@ -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
@@ -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
 
@@ -6,6 +6,7 @@ module ActiveRecord
6
6
  module OID # :nodoc:
7
7
  class Uuid < Type::Value # :nodoc:
8
8
  ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z}
9
+ CANONICAL_UUID = %r{\A[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\z}
9
10
 
10
11
  alias :serialize :deserialize
11
12
 
@@ -15,18 +16,27 @@ module ActiveRecord
15
16
 
16
17
  def changed?(old_value, new_value, _new_value_before_type_cast)
17
18
  old_value.class != new_value.class ||
18
- new_value && old_value.casecmp(new_value) != 0
19
+ new_value != old_value
19
20
  end
20
21
 
21
22
  def changed_in_place?(raw_old_value, new_value)
22
23
  raw_old_value.class != new_value.class ||
23
- new_value && raw_old_value.casecmp(new_value) != 0
24
+ new_value != raw_old_value
24
25
  end
25
26
 
26
27
  private
27
28
  def cast_value(value)
28
- casted = value.to_s
29
- casted if casted.match?(ACCEPTABLE_UUID)
29
+ value = value.to_s
30
+ format_uuid(value) if value.match?(ACCEPTABLE_UUID)
31
+ end
32
+
33
+ def format_uuid(uuid)
34
+ if uuid.match?(CANONICAL_UUID)
35
+ uuid
36
+ else
37
+ uuid = uuid.delete("{}-").downcase
38
+ "#{uuid[..7]}-#{uuid[8..11]}-#{uuid[12..15]}-#{uuid[16..19]}-#{uuid[20..]}"
39
+ end
30
40
  end
31
41
  end
32
42
  end
@@ -4,9 +4,62 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module PostgreSQL
6
6
  module Quoting
7
+ extend ActiveSupport::Concern
8
+
7
9
  QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
8
10
  QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
9
11
 
12
+ module ClassMethods # :nodoc:
13
+ def column_name_matcher
14
+ /
15
+ \A
16
+ (
17
+ (?:
18
+ # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
19
+ ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)? | \w+\((?:|\g<2>)\)(?:::\w+)?)
20
+ )
21
+ (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
22
+ )
23
+ (?:\s*,\s*\g<1>)*
24
+ \z
25
+ /ix
26
+ end
27
+
28
+ def column_name_with_order_matcher
29
+ /
30
+ \A
31
+ (
32
+ (?:
33
+ # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
34
+ ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)? | \w+\((?:|\g<2>)\)(?:::\w+)?)
35
+ )
36
+ (?:\s+COLLATE\s+"\w+")?
37
+ (?:\s+ASC|\s+DESC)?
38
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
39
+ )
40
+ (?:\s*,\s*\g<1>)*
41
+ \z
42
+ /ix
43
+ end
44
+
45
+ # Quotes column names for use in SQL queries.
46
+ def quote_column_name(name) # :nodoc:
47
+ QUOTED_COLUMN_NAMES[name] ||= PG::Connection.quote_ident(name.to_s).freeze
48
+ end
49
+
50
+ # Checks the following cases:
51
+ #
52
+ # - table_name
53
+ # - "table.name"
54
+ # - schema_name.table_name
55
+ # - schema_name."table.name"
56
+ # - "schema.name".table_name
57
+ # - "schema.name"."table.name"
58
+ def quote_table_name(name) # :nodoc:
59
+ QUOTED_TABLE_NAMES[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
60
+ end
61
+ end
62
+
10
63
  class IntegerOutOf64BitRange < StandardError
11
64
  def initialize(msg)
12
65
  super(msg)
@@ -77,30 +130,13 @@ module ActiveRecord
77
130
  end
78
131
  end
79
132
 
80
- # Checks the following cases:
81
- #
82
- # - table_name
83
- # - "table.name"
84
- # - schema_name.table_name
85
- # - schema_name."table.name"
86
- # - "schema.name".table_name
87
- # - "schema.name"."table.name"
88
- def quote_table_name(name) # :nodoc:
89
- QUOTED_TABLE_NAMES[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
90
- end
91
-
92
- # Quotes schema names for use in SQL queries.
93
- def quote_schema_name(name)
94
- PG::Connection.quote_ident(name)
95
- end
96
-
97
133
  def quote_table_name_for_assignment(table, attr)
98
134
  quote_column_name(attr)
99
135
  end
100
136
 
101
- # Quotes column names for use in SQL queries.
102
- def quote_column_name(name) # :nodoc:
103
- QUOTED_COLUMN_NAMES[name] ||= PG::Connection.quote_ident(super).freeze
137
+ # Quotes schema names for use in SQL queries.
138
+ def quote_schema_name(schema_name)
139
+ quote_column_name(schema_name)
104
140
  end
105
141
 
106
142
  # Quote date/time values for use in SQL input.
@@ -117,14 +153,15 @@ module ActiveRecord
117
153
  "'#{escape_bytea(value.to_s)}'"
118
154
  end
119
155
 
156
+ # `column` may be either an instance of Column or ColumnDefinition.
120
157
  def quote_default_expression(value, column) # :nodoc:
121
158
  if value.is_a?(Proc)
122
159
  value.call
123
160
  elsif column.type == :uuid && value.is_a?(String) && value.include?("()")
124
161
  value # Does not quote function default values for UUID columns
125
162
  elsif column.respond_to?(:array?)
126
- type = lookup_cast_type_from_column(column)
127
- 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))
128
165
  else
129
166
  super
130
167
  end
@@ -143,59 +180,19 @@ module ActiveRecord
143
180
  encode_array(value)
144
181
  when Range
145
182
  encode_range(value)
183
+ when Rational
184
+ value.to_f
146
185
  else
147
186
  super
148
187
  end
149
188
  end
150
189
 
151
- def lookup_cast_type_from_column(column) # :nodoc:
152
- verify! if type_map.nil?
153
- type_map.lookup(column.oid, column.fmod, column.sql_type)
154
- end
155
-
156
- def column_name_matcher
157
- COLUMN_NAME
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)
158
193
  end
159
194
 
160
- def column_name_with_order_matcher
161
- COLUMN_NAME_WITH_ORDER
162
- end
163
-
164
- COLUMN_NAME = /
165
- \A
166
- (
167
- (?:
168
- # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
169
- ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)? | \w+\((?:|\g<2>)\)(?:::\w+)?)
170
- )
171
- (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
172
- )
173
- (?:\s*,\s*\g<1>)*
174
- \z
175
- /ix
176
-
177
- COLUMN_NAME_WITH_ORDER = /
178
- \A
179
- (
180
- (?:
181
- # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
182
- ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)? | \w+\((?:|\g<2>)\)(?:::\w+)?)
183
- )
184
- (?:\s+COLLATE\s+"\w+")?
185
- (?:\s+ASC|\s+DESC)?
186
- (?:\s+NULLS\s+(?:FIRST|LAST))?
187
- )
188
- (?:\s*,\s*\g<1>)*
189
- \z
190
- /ix
191
-
192
- private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
193
-
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; 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