activerecord 8.0.0 → 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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1381 -241
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/alias_tracker.rb +6 -4
  6. data/lib/active_record/associations/association.rb +35 -17
  7. data/lib/active_record/associations/association_scope.rb +1 -1
  8. data/lib/active_record/associations/belongs_to_association.rb +20 -4
  9. data/lib/active_record/associations/builder/association.rb +16 -5
  10. data/lib/active_record/associations/builder/belongs_to.rb +36 -8
  11. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  12. data/lib/active_record/associations/builder/has_one.rb +1 -1
  13. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  14. data/lib/active_record/associations/collection_association.rb +9 -5
  15. data/lib/active_record/associations/collection_proxy.rb +26 -4
  16. data/lib/active_record/associations/deprecation.rb +88 -0
  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_one_association.rb +1 -0
  20. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  21. data/lib/active_record/associations/join_dependency.rb +5 -4
  22. data/lib/active_record/associations/preloader/association.rb +1 -1
  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.rb +159 -21
  26. data/lib/active_record/attribute_methods/primary_key.rb +2 -1
  27. data/lib/active_record/attribute_methods/query.rb +34 -0
  28. data/lib/active_record/attribute_methods/serialization.rb +19 -6
  29. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  30. data/lib/active_record/attribute_methods.rb +24 -17
  31. data/lib/active_record/attributes.rb +40 -26
  32. data/lib/active_record/autosave_association.rb +22 -12
  33. data/lib/active_record/base.rb +3 -4
  34. data/lib/active_record/callbacks.rb +1 -2
  35. data/lib/active_record/coders/column_serializer.rb +1 -1
  36. data/lib/active_record/coders/json.rb +16 -6
  37. data/lib/active_record/coders/yaml_column.rb +2 -2
  38. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +19 -18
  39. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
  40. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +68 -12
  41. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +474 -112
  42. data/lib/active_record/connection_adapters/abstract/database_statements.rb +68 -41
  43. data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -9
  44. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  45. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  46. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +31 -35
  47. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  48. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +94 -28
  49. data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
  50. data/lib/active_record/connection_adapters/abstract_adapter.rb +166 -64
  51. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +133 -40
  52. data/lib/active_record/connection_adapters/column.rb +14 -4
  53. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  54. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -1
  55. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  56. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  57. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +40 -25
  58. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +66 -15
  59. data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -3
  60. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  61. data/lib/active_record/connection_adapters/postgresql/column.rb +8 -2
  62. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +29 -33
  63. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  64. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
  65. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  66. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  67. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
  68. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
  69. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -33
  70. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +70 -32
  71. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +99 -54
  72. data/lib/active_record/connection_adapters/postgresql_adapter.rb +40 -12
  73. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  74. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -23
  75. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -8
  76. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +15 -3
  77. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +7 -14
  78. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +68 -37
  79. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  80. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +8 -3
  81. data/lib/active_record/connection_adapters/trilogy_adapter.rb +16 -2
  82. data/lib/active_record/connection_adapters.rb +1 -0
  83. data/lib/active_record/connection_handling.rb +15 -10
  84. data/lib/active_record/core.rb +44 -12
  85. data/lib/active_record/counter_cache.rb +36 -9
  86. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  87. data/lib/active_record/database_configurations/database_config.rb +5 -1
  88. data/lib/active_record/database_configurations/hash_config.rb +66 -9
  89. data/lib/active_record/database_configurations/url_config.rb +15 -3
  90. data/lib/active_record/database_configurations.rb +7 -3
  91. data/lib/active_record/delegated_type.rb +19 -19
  92. data/lib/active_record/disable_joins_association_relation.rb +5 -1
  93. data/lib/active_record/dynamic_matchers.rb +54 -69
  94. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  95. data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
  96. data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
  97. data/lib/active_record/encryption/encryptor.rb +39 -25
  98. data/lib/active_record/encryption/scheme.rb +1 -1
  99. data/lib/active_record/enum.rb +37 -20
  100. data/lib/active_record/errors.rb +23 -7
  101. data/lib/active_record/explain.rb +1 -1
  102. data/lib/active_record/explain_registry.rb +51 -2
  103. data/lib/active_record/filter_attribute_handler.rb +76 -0
  104. data/lib/active_record/fixture_set/table_row.rb +19 -2
  105. data/lib/active_record/fixtures.rb +2 -2
  106. data/lib/active_record/future_result.rb +5 -3
  107. data/lib/active_record/gem_version.rb +2 -2
  108. data/lib/active_record/inheritance.rb +9 -4
  109. data/lib/active_record/insert_all.rb +14 -9
  110. data/lib/active_record/locking/optimistic.rb +7 -0
  111. data/lib/active_record/locking/pessimistic.rb +5 -0
  112. data/lib/active_record/log_subscriber.rb +30 -22
  113. data/lib/active_record/message_pack.rb +13 -0
  114. data/lib/active_record/middleware/shard_selector.rb +34 -17
  115. data/lib/active_record/migration/command_recorder.rb +32 -6
  116. data/lib/active_record/migration/compatibility.rb +34 -24
  117. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  118. data/lib/active_record/migration.rb +31 -21
  119. data/lib/active_record/model_schema.rb +38 -12
  120. data/lib/active_record/nested_attributes.rb +10 -6
  121. data/lib/active_record/persistence.rb +35 -3
  122. data/lib/active_record/query_cache.rb +22 -15
  123. data/lib/active_record/query_logs.rb +7 -7
  124. data/lib/active_record/querying.rb +17 -5
  125. data/lib/active_record/railtie.rb +35 -6
  126. data/lib/active_record/railties/controller_runtime.rb +11 -6
  127. data/lib/active_record/railties/databases.rake +24 -20
  128. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  129. data/lib/active_record/railties/job_runtime.rb +10 -11
  130. data/lib/active_record/reflection.rb +36 -1
  131. data/lib/active_record/relation/batches.rb +25 -11
  132. data/lib/active_record/relation/calculations.rb +62 -41
  133. data/lib/active_record/relation/delegation.rb +0 -1
  134. data/lib/active_record/relation/finder_methods.rb +76 -45
  135. data/lib/active_record/relation/merger.rb +4 -2
  136. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  137. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -9
  138. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  139. data/lib/active_record/relation/predicate_builder.rb +10 -8
  140. data/lib/active_record/relation/query_attribute.rb +4 -2
  141. data/lib/active_record/relation/query_methods.rb +106 -45
  142. data/lib/active_record/relation/spawn_methods.rb +6 -6
  143. data/lib/active_record/relation/where_clause.rb +10 -11
  144. data/lib/active_record/relation.rb +63 -31
  145. data/lib/active_record/result.rb +44 -21
  146. data/lib/active_record/runtime_registry.rb +42 -58
  147. data/lib/active_record/sanitization.rb +2 -0
  148. data/lib/active_record/schema.rb +1 -1
  149. data/lib/active_record/schema_dumper.rb +41 -21
  150. data/lib/active_record/scoping.rb +0 -1
  151. data/lib/active_record/secure_token.rb +3 -3
  152. data/lib/active_record/signed_id.rb +48 -19
  153. data/lib/active_record/statement_cache.rb +16 -12
  154. data/lib/active_record/store.rb +46 -19
  155. data/lib/active_record/structured_event_subscriber.rb +85 -0
  156. data/lib/active_record/table_metadata.rb +5 -20
  157. data/lib/active_record/tasks/abstract_tasks.rb +83 -0
  158. data/lib/active_record/tasks/database_tasks.rb +44 -45
  159. data/lib/active_record/tasks/mysql_database_tasks.rb +4 -56
  160. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  161. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  162. data/lib/active_record/test_databases.rb +18 -4
  163. data/lib/active_record/test_fixtures.rb +40 -6
  164. data/lib/active_record/testing/query_assertions.rb +8 -2
  165. data/lib/active_record/timestamp.rb +4 -2
  166. data/lib/active_record/transaction.rb +2 -5
  167. data/lib/active_record/transactions.rb +58 -32
  168. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  169. data/lib/active_record/type/internal/timezone.rb +7 -0
  170. data/lib/active_record/type/json.rb +13 -2
  171. data/lib/active_record/type/serialized.rb +16 -4
  172. data/lib/active_record/type/type_map.rb +1 -1
  173. data/lib/active_record/type_caster/connection.rb +2 -1
  174. data/lib/active_record/validations/associated.rb +1 -1
  175. data/lib/active_record.rb +72 -7
  176. data/lib/arel/alias_predication.rb +2 -0
  177. data/lib/arel/collectors/bind.rb +1 -1
  178. data/lib/arel/collectors/sql_string.rb +1 -1
  179. data/lib/arel/collectors/substitute_binds.rb +2 -2
  180. data/lib/arel/crud.rb +8 -11
  181. data/lib/arel/delete_manager.rb +5 -0
  182. data/lib/arel/nodes/binary.rb +1 -1
  183. data/lib/arel/nodes/count.rb +2 -2
  184. data/lib/arel/nodes/delete_statement.rb +4 -2
  185. data/lib/arel/nodes/function.rb +4 -10
  186. data/lib/arel/nodes/named_function.rb +2 -2
  187. data/lib/arel/nodes/node.rb +2 -2
  188. data/lib/arel/nodes/sql_literal.rb +1 -1
  189. data/lib/arel/nodes/update_statement.rb +4 -2
  190. data/lib/arel/nodes.rb +0 -2
  191. data/lib/arel/predications.rb +1 -3
  192. data/lib/arel/select_manager.rb +13 -4
  193. data/lib/arel/update_manager.rb +5 -0
  194. data/lib/arel/visitors/dot.rb +2 -3
  195. data/lib/arel/visitors/postgresql.rb +55 -0
  196. data/lib/arel/visitors/sqlite.rb +55 -8
  197. data/lib/arel/visitors/to_sql.rb +7 -23
  198. data/lib/arel.rb +3 -1
  199. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  200. metadata +16 -15
  201. data/lib/active_record/explain_subscriber.rb +0 -34
  202. data/lib/active_record/normalization.rb +0 -163
@@ -48,30 +48,64 @@ module ActiveRecord
48
48
  # made since we established the connection
49
49
  raw_connection.query_options[:database_timezone] = default_timezone
50
50
 
51
- result = if prepare
52
- stmt = @statements[sql] ||= raw_connection.prepare(sql)
51
+ result = nil
52
+ if binds.nil? || binds.empty?
53
+ result = raw_connection.query(sql)
54
+ # Ref: https://github.com/brianmario/mysql2/pull/1383
55
+ # As of mysql2 0.5.6 `#affected_rows` might raise Mysql2::Error if a prepared statement
56
+ # from that same connection was GCed while `#query` released the GVL.
57
+ # By avoiding to call `#affected_rows` when we have a result, we reduce the likeliness
58
+ # of hitting the bug.
59
+ @affected_rows_before_warnings = result&.size || raw_connection.affected_rows
60
+ elsif prepare
61
+ retry_count = 1
62
+ begin
63
+ stmt = @statements[sql] ||= raw_connection.prepare(sql)
64
+ result = stmt.execute(*type_casted_binds)
65
+ @affected_rows_before_warnings = stmt.affected_rows
66
+ rescue ::Mysql2::Error => error
67
+ @statements.delete(sql)
68
+ # Sometimes for an unknown reason, we get that error.
69
+ # It suggest somehow that the prepared statement was deallocated
70
+ # but the client doesn't know it.
71
+ # But we know that this error is safe to retry, so we do so after
72
+ # getting rid of the originally cached statement.
73
+ if error.error_number == Mysql2Adapter::ER_UNKNOWN_STMT_HANDLER
74
+ if retry_count.positive?
75
+ retry_count -= 1
76
+ retry
77
+ end
78
+ end
79
+ raise
80
+ end
81
+ else
82
+ stmt = raw_connection.prepare(sql)
53
83
 
54
84
  begin
55
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
56
- stmt.execute(*type_casted_binds)
85
+ result = stmt.execute(*type_casted_binds)
86
+ @affected_rows_before_warnings = stmt.affected_rows
87
+
88
+ # Ref: https://github.com/brianmario/mysql2/pull/1383
89
+ # by eagerly closing uncached prepared statements, we also reduce the chances of
90
+ # that bug happening. It can still happen if `#execute` is used as we have no callback
91
+ # to eagerly close the statement.
92
+ if result
93
+ result.instance_variable_set(:@_ar_stmt_to_close, stmt)
94
+ else
95
+ stmt.close
57
96
  end
58
97
  rescue ::Mysql2::Error
59
- @statements.delete(sql)
60
98
  stmt.close
61
99
  raise
62
100
  end
63
- verified!
64
- else
65
- raw_connection.query(sql)
66
101
  end
67
102
 
103
+ notification_payload[:affected_rows] = @affected_rows_before_warnings
68
104
  notification_payload[:row_count] = result&.size || 0
69
105
 
70
- @affected_rows_before_warnings = raw_connection.affected_rows
71
106
  raw_connection.abandon_results!
72
107
 
73
108
  verified!
74
- handle_warnings(sql)
75
109
  result
76
110
  ensure
77
111
  if reset_multi_statement && active?
@@ -79,17 +113,34 @@ module ActiveRecord
79
113
  end
80
114
  end
81
115
 
82
- def cast_result(result)
83
- if result.nil? || result.fields.empty?
84
- ActiveRecord::Result.empty
116
+ def cast_result(raw_result)
117
+ return ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings) if raw_result.nil?
118
+
119
+ fields = raw_result.fields
120
+
121
+ result = if fields.empty?
122
+ ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings)
85
123
  else
86
- ActiveRecord::Result.new(result.fields, result.to_a)
124
+ ActiveRecord::Result.new(fields, raw_result.to_a)
87
125
  end
126
+
127
+ free_raw_result(raw_result)
128
+
129
+ result
88
130
  end
89
131
 
90
- def affected_rows(result)
132
+ def affected_rows(raw_result)
133
+ free_raw_result(raw_result) if raw_result
134
+
91
135
  @affected_rows_before_warnings
92
136
  end
137
+
138
+ def free_raw_result(raw_result)
139
+ raw_result.free
140
+ if stmt = raw_result.instance_variable_get(:@_ar_stmt_to_close)
141
+ stmt.close
142
+ end
143
+ end
93
144
  end
94
145
  end
95
146
  end
@@ -13,6 +13,7 @@ module ActiveRecord
13
13
  ER_BAD_DB_ERROR = 1049
14
14
  ER_DBACCESS_DENIED_ERROR = 1044
15
15
  ER_ACCESS_DENIED_ERROR = 1045
16
+ ER_UNKNOWN_STMT_HANDLER = 1243
16
17
  ER_CONN_HOST_ERROR = 2003
17
18
  ER_UNKNOWN_HOST_ERROR = 2005
18
19
 
@@ -91,8 +92,6 @@ module ActiveRecord
91
92
  true
92
93
  end
93
94
 
94
- # HELPER METHODS ===========================================
95
-
96
95
  def error_number(exception)
97
96
  exception.error_number if exception.respond_to?(:error_number)
98
97
  end
@@ -106,7 +105,14 @@ module ActiveRecord
106
105
  end
107
106
 
108
107
  def active?
109
- connected? && @lock.synchronize { @raw_connection&.ping } || false
108
+ if connected?
109
+ @lock.synchronize do
110
+ if @raw_connection&.ping
111
+ verified!
112
+ true
113
+ end
114
+ end
115
+ end || false
110
116
  end
111
117
 
112
118
  alias :reset! :reconnect!
@@ -5,9 +5,8 @@ module ActiveRecord
5
5
  class PoolConfig # :nodoc:
6
6
  include MonitorMixin
7
7
 
8
- attr_reader :db_config, :role, :shard
8
+ attr_reader :db_config, :role, :shard, :connection_descriptor
9
9
  attr_writer :schema_reflection, :server_version
10
- attr_accessor :connection_class
11
10
 
12
11
  def schema_reflection
13
12
  @schema_reflection ||= SchemaReflection.new(db_config.lazy_schema_cache_path)
@@ -29,7 +28,7 @@ module ActiveRecord
29
28
  def initialize(connection_class, db_config, role, shard)
30
29
  super()
31
30
  @server_version = nil
32
- @connection_class = connection_class
31
+ self.connection_descriptor = connection_class
33
32
  @db_config = db_config
34
33
  @role = role
35
34
  @shard = shard
@@ -41,11 +40,12 @@ module ActiveRecord
41
40
  @server_version || synchronize { @server_version ||= connection.get_database_version }
42
41
  end
43
42
 
44
- def connection_name
45
- if connection_class.primary_class?
46
- "ActiveRecord::Base"
43
+ def connection_descriptor=(connection_descriptor)
44
+ case connection_descriptor
45
+ when ConnectionHandler::ConnectionDescriptor
46
+ @connection_descriptor = connection_descriptor
47
47
  else
48
- connection_class.name
48
+ @connection_descriptor = ConnectionHandler::ConnectionDescriptor.new(connection_descriptor.name, connection_descriptor.primary_class?)
49
49
  end
50
50
  end
51
51
 
@@ -30,6 +30,10 @@ module ActiveRecord
30
30
  @generated.present?
31
31
  end
32
32
 
33
+ def virtual_stored?
34
+ @generated == "s"
35
+ end
36
+
33
37
  def has_default?
34
38
  super && !virtual?
35
39
  end
@@ -65,7 +69,8 @@ module ActiveRecord
65
69
  other.is_a?(Column) &&
66
70
  super &&
67
71
  identity? == other.identity? &&
68
- serial? == other.serial?
72
+ serial? == other.serial? &&
73
+ virtual? == other.virtual?
69
74
  end
70
75
  alias :eql? :==
71
76
 
@@ -73,7 +78,8 @@ module ActiveRecord
73
78
  Column.hash ^
74
79
  super.hash ^
75
80
  identity?.hash ^
76
- serial?.hash
81
+ serial?.hash ^
82
+ virtual?.hash
77
83
  end
78
84
  end
79
85
  end
@@ -11,8 +11,8 @@ 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
- result = internal_execute(sql, name)
14
+ def query(sql, name = nil, allow_retry: true, materialize_transactions: true) # :nodoc:
15
+ result = internal_execute(sql, name, allow_retry:, materialize_transactions:)
16
16
  result.map_types!(@type_map_for_results).values
17
17
  end
18
18
 
@@ -27,21 +27,6 @@ module ActiveRecord
27
27
  !READ_QUERY.match?(sql.b)
28
28
  end
29
29
 
30
- # Executes an SQL statement, returning a PG::Result object on success
31
- # or raising a PG::Error exception otherwise.
32
- #
33
- # Setting +allow_retry+ to true causes the db to reconnect and retry
34
- # executing the SQL statement in case of a connection-related exception.
35
- # This option should only be enabled for known idempotent queries.
36
- #
37
- # Note: the PG::Result object is manually memory managed; if you don't
38
- # need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
39
- def execute(...) # :nodoc:
40
- super
41
- ensure
42
- @notice_receiver_sql_warnings = []
43
- end
44
-
45
30
  def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil) # :nodoc:
46
31
  if use_insert_returning? || pk == false
47
32
  super
@@ -127,7 +112,14 @@ module ActiveRecord
127
112
  def cancel_any_running_query
128
113
  return if @raw_connection.nil? || IDLE_TRANSACTION_STATUSES.include?(@raw_connection.transaction_status)
129
114
 
130
- @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
131
123
  @raw_connection.block
132
124
  rescue PG::Error
133
125
  end
@@ -163,25 +155,27 @@ module ActiveRecord
163
155
  end
164
156
 
165
157
  verified!
166
- handle_warnings(result)
167
- notification_payload[:row_count] = result.count
158
+
159
+ notification_payload[:affected_rows] = result.cmd_tuples
160
+ notification_payload[:row_count] = result.ntuples
168
161
  result
169
162
  end
170
163
 
171
164
  def cast_result(result)
172
- if result.fields.empty?
173
- result.clear
174
- return ActiveRecord::Result.empty
175
- end
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
176
175
 
177
- types = {}
178
- fields = result.fields
179
- fields.each_with_index do |fname, i|
180
- ftype = result.ftype i
181
- fmod = result.fmod i
182
- types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
176
+ ActiveRecord::Result.new(fields, result.values, types.freeze, affected_rows: result.cmd_tuples)
183
177
  end
184
- ar_result = ActiveRecord::Result.new(fields, result.values, types.freeze)
178
+
185
179
  result.clear
186
180
  ar_result
187
181
  end
@@ -213,8 +207,10 @@ module ActiveRecord
213
207
  pk unless pk.is_a?(Array)
214
208
  end
215
209
 
216
- def handle_warnings(sql)
217
- @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|
218
214
  next if warning_ignored?(warning)
219
215
 
220
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)
@@ -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,7 +45,7 @@ Rails needs superuser privileges to disable referential integrity.
45
45
  BEGIN
46
46
  FOR r IN (
47
47
  SELECT FORMAT(
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;',
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
51
  table_name
@@ -6,6 +6,7 @@ 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
@@ -99,7 +100,7 @@ module ActiveRecord
99
100
  if options[:default].nil?
100
101
  change_column_sql << ", ALTER COLUMN #{quoted_column_name} DROP DEFAULT"
101
102
  else
102
- quoted_default = quote_default_expression(options[:default], column)
103
+ quoted_default = quote_default_expression_for_column_definition(options[:default], column)
103
104
  change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}"
104
105
  end
105
106
  end
@@ -126,16 +127,17 @@ module ActiveRecord
126
127
  end
127
128
 
128
129
  if as = options[:as]
129
- sql << " GENERATED ALWAYS AS (#{as})"
130
+ stored = options[:stored]
130
131
 
131
- if options[:stored]
132
- sql << " STORED"
133
- else
132
+ if stored != true && database_version < 18_00_00
134
133
  raise ArgumentError, <<~MSG
135
- PostgreSQL currently does not support VIRTUAL (not persisted) generated columns.
134
+ PostgreSQL versions before 18 do not support VIRTUAL (not persisted) generated columns.
136
135
  Specify 'stored: true' option for '#{options[:column].name}'
137
136
  MSG
138
137
  end
138
+
139
+ sql << " GENERATED ALWAYS AS (#{as})"
140
+ sql << (stored ? " STORED" : " VIRTUAL")
139
141
  end
140
142
  super
141
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
@@ -233,6 +220,8 @@ module ActiveRecord
233
220
  end
234
221
 
235
222
  def defined_for?(name: nil, column: nil, **options)
223
+ options = options.slice(*self.options.keys)
224
+
236
225
  (name.nil? || self.name == name.to_s) &&
237
226
  (column.nil? || Array(self.column) == Array(column).map(&:to_s)) &&
238
227
  options.all? { |k, v| self.options[k].to_s == v.to_s }
@@ -306,8 +295,8 @@ module ActiveRecord
306
295
  # t.exclusion_constraint("price WITH =, availability_range WITH &&", using: :gist, name: "price_check")
307
296
  #
308
297
  # See {connection.add_exclusion_constraint}[rdoc-ref:SchemaStatements#add_exclusion_constraint]
309
- def exclusion_constraint(*args)
310
- @base.add_exclusion_constraint(name, *args)
298
+ def exclusion_constraint(...)
299
+ @base.add_exclusion_constraint(name, ...)
311
300
  end
312
301
 
313
302
  # Removes the given exclusion constraint from the table.
@@ -315,8 +304,8 @@ module ActiveRecord
315
304
  # t.remove_exclusion_constraint(name: "price_check")
316
305
  #
317
306
  # See {connection.remove_exclusion_constraint}[rdoc-ref:SchemaStatements#remove_exclusion_constraint]
318
- def remove_exclusion_constraint(*args)
319
- @base.remove_exclusion_constraint(name, *args)
307
+ def remove_exclusion_constraint(...)
308
+ @base.remove_exclusion_constraint(name, ...)
320
309
  end
321
310
 
322
311
  # Adds a unique constraint.
@@ -324,8 +313,8 @@ module ActiveRecord
324
313
  # t.unique_constraint(:position, name: 'unique_position', deferrable: :deferred, nulls_not_distinct: true)
325
314
  #
326
315
  # See {connection.add_unique_constraint}[rdoc-ref:SchemaStatements#add_unique_constraint]
327
- def unique_constraint(*args)
328
- @base.add_unique_constraint(name, *args)
316
+ def unique_constraint(...)
317
+ @base.add_unique_constraint(name, ...)
329
318
  end
330
319
 
331
320
  # Removes the given unique constraint from the table.
@@ -333,8 +322,8 @@ module ActiveRecord
333
322
  # t.remove_unique_constraint(name: "unique_position")
334
323
  #
335
324
  # See {connection.remove_unique_constraint}[rdoc-ref:SchemaStatements#remove_unique_constraint]
336
- def remove_unique_constraint(*args)
337
- @base.remove_unique_constraint(name, *args)
325
+ def remove_unique_constraint(...)
326
+ @base.remove_unique_constraint(name, ...)
338
327
  end
339
328
 
340
329
  # Validates the given constraint on the table.
@@ -343,8 +332,8 @@ module ActiveRecord
343
332
  # t.validate_constraint "price_check"
344
333
  #
345
334
  # See {connection.validate_constraint}[rdoc-ref:SchemaStatements#validate_constraint]
346
- def validate_constraint(*args)
347
- @base.validate_constraint(name, *args)
335
+ def validate_constraint(...)
336
+ @base.validate_constraint(name, ...)
348
337
  end
349
338
 
350
339
  # Validates the given check constraint on the table
@@ -353,8 +342,8 @@ module ActiveRecord
353
342
  # t.validate_check_constraint name: "price_check"
354
343
  #
355
344
  # See {connection.validate_check_constraint}[rdoc-ref:SchemaStatements#validate_check_constraint]
356
- def validate_check_constraint(*args)
357
- @base.validate_check_constraint(name, *args)
345
+ def validate_check_constraint(...)
346
+ @base.validate_check_constraint(name, ...)
358
347
  end
359
348
  end
360
349