activerecord 8.0.0 → 8.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +703 -248
  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 +1 -1
  7. data/lib/active_record/associations/belongs_to_association.rb +18 -2
  8. data/lib/active_record/associations/builder/association.rb +16 -5
  9. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  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 +3 -3
  14. data/lib/active_record/associations/collection_proxy.rb +22 -4
  15. data/lib/active_record/associations/deprecation.rb +88 -0
  16. data/lib/active_record/associations/errors.rb +3 -0
  17. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  18. data/lib/active_record/associations/join_dependency.rb +4 -2
  19. data/lib/active_record/associations/preloader/batch.rb +7 -1
  20. data/lib/active_record/associations/preloader/branch.rb +1 -0
  21. data/lib/active_record/associations.rb +159 -21
  22. data/lib/active_record/attribute_methods/primary_key.rb +2 -1
  23. data/lib/active_record/attribute_methods/query.rb +34 -0
  24. data/lib/active_record/attribute_methods/serialization.rb +17 -4
  25. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  26. data/lib/active_record/attribute_methods.rb +23 -18
  27. data/lib/active_record/attributes.rb +40 -26
  28. data/lib/active_record/autosave_association.rb +22 -12
  29. data/lib/active_record/base.rb +3 -4
  30. data/lib/active_record/coders/json.rb +14 -5
  31. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +19 -18
  32. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
  33. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  34. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +458 -108
  35. data/lib/active_record/connection_adapters/abstract/database_statements.rb +55 -40
  36. data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -9
  37. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  38. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  39. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +31 -35
  40. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  41. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +89 -23
  42. data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
  43. data/lib/active_record/connection_adapters/abstract_adapter.rb +154 -64
  44. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +57 -20
  45. data/lib/active_record/connection_adapters/column.rb +17 -4
  46. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  47. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -1
  48. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  49. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  50. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +33 -4
  51. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +66 -15
  52. data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -3
  53. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  54. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  55. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +26 -17
  56. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  57. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  58. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  59. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
  60. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -33
  61. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +67 -31
  62. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +82 -49
  63. data/lib/active_record/connection_adapters/postgresql_adapter.rb +38 -10
  64. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  65. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -23
  66. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -8
  67. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +14 -2
  68. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +5 -12
  69. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +65 -36
  70. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  71. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  72. data/lib/active_record/connection_adapters/trilogy_adapter.rb +2 -2
  73. data/lib/active_record/connection_adapters.rb +1 -0
  74. data/lib/active_record/connection_handling.rb +15 -10
  75. data/lib/active_record/core.rb +44 -12
  76. data/lib/active_record/counter_cache.rb +34 -9
  77. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  78. data/lib/active_record/database_configurations/database_config.rb +5 -1
  79. data/lib/active_record/database_configurations/hash_config.rb +59 -9
  80. data/lib/active_record/database_configurations/url_config.rb +13 -3
  81. data/lib/active_record/database_configurations.rb +7 -3
  82. data/lib/active_record/delegated_type.rb +19 -19
  83. data/lib/active_record/dynamic_matchers.rb +54 -69
  84. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  85. data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
  86. data/lib/active_record/encryption/encryptor.rb +39 -25
  87. data/lib/active_record/encryption/scheme.rb +1 -1
  88. data/lib/active_record/enum.rb +37 -20
  89. data/lib/active_record/errors.rb +23 -7
  90. data/lib/active_record/explain.rb +1 -1
  91. data/lib/active_record/explain_registry.rb +51 -2
  92. data/lib/active_record/filter_attribute_handler.rb +73 -0
  93. data/lib/active_record/fixture_set/table_row.rb +19 -2
  94. data/lib/active_record/fixtures.rb +2 -2
  95. data/lib/active_record/future_result.rb +3 -3
  96. data/lib/active_record/gem_version.rb +2 -2
  97. data/lib/active_record/inheritance.rb +1 -1
  98. data/lib/active_record/insert_all.rb +12 -7
  99. data/lib/active_record/locking/optimistic.rb +7 -0
  100. data/lib/active_record/locking/pessimistic.rb +5 -0
  101. data/lib/active_record/log_subscriber.rb +2 -6
  102. data/lib/active_record/middleware/shard_selector.rb +34 -17
  103. data/lib/active_record/migration/command_recorder.rb +19 -3
  104. data/lib/active_record/migration/compatibility.rb +34 -24
  105. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  106. data/lib/active_record/migration.rb +31 -21
  107. data/lib/active_record/model_schema.rb +36 -10
  108. data/lib/active_record/nested_attributes.rb +2 -0
  109. data/lib/active_record/persistence.rb +34 -3
  110. data/lib/active_record/query_cache.rb +22 -15
  111. data/lib/active_record/query_logs.rb +7 -7
  112. data/lib/active_record/querying.rb +4 -4
  113. data/lib/active_record/railtie.rb +35 -6
  114. data/lib/active_record/railties/controller_runtime.rb +11 -6
  115. data/lib/active_record/railties/databases.rake +24 -20
  116. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  117. data/lib/active_record/railties/job_runtime.rb +10 -11
  118. data/lib/active_record/reflection.rb +35 -0
  119. data/lib/active_record/relation/batches.rb +25 -11
  120. data/lib/active_record/relation/calculations.rb +54 -38
  121. data/lib/active_record/relation/delegation.rb +0 -1
  122. data/lib/active_record/relation/finder_methods.rb +42 -25
  123. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -9
  124. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  125. data/lib/active_record/relation/predicate_builder.rb +9 -7
  126. data/lib/active_record/relation/query_attribute.rb +4 -2
  127. data/lib/active_record/relation/query_methods.rb +43 -32
  128. data/lib/active_record/relation/spawn_methods.rb +6 -6
  129. data/lib/active_record/relation/where_clause.rb +10 -11
  130. data/lib/active_record/relation.rb +43 -19
  131. data/lib/active_record/result.rb +44 -21
  132. data/lib/active_record/runtime_registry.rb +42 -58
  133. data/lib/active_record/sanitization.rb +2 -0
  134. data/lib/active_record/schema_dumper.rb +42 -22
  135. data/lib/active_record/scoping.rb +0 -1
  136. data/lib/active_record/secure_token.rb +3 -3
  137. data/lib/active_record/signed_id.rb +47 -18
  138. data/lib/active_record/statement_cache.rb +15 -11
  139. data/lib/active_record/store.rb +44 -19
  140. data/lib/active_record/structured_event_subscriber.rb +85 -0
  141. data/lib/active_record/table_metadata.rb +5 -20
  142. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  143. data/lib/active_record/tasks/database_tasks.rb +44 -45
  144. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  145. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  146. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  147. data/lib/active_record/test_databases.rb +14 -4
  148. data/lib/active_record/test_fixtures.rb +27 -2
  149. data/lib/active_record/testing/query_assertions.rb +8 -2
  150. data/lib/active_record/timestamp.rb +4 -2
  151. data/lib/active_record/transaction.rb +2 -5
  152. data/lib/active_record/transactions.rb +39 -16
  153. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  154. data/lib/active_record/type/internal/timezone.rb +7 -0
  155. data/lib/active_record/type/json.rb +15 -2
  156. data/lib/active_record/type/serialized.rb +11 -4
  157. data/lib/active_record/type/type_map.rb +1 -1
  158. data/lib/active_record/type_caster/connection.rb +2 -1
  159. data/lib/active_record/validations/associated.rb +1 -1
  160. data/lib/active_record.rb +71 -6
  161. data/lib/arel/alias_predication.rb +2 -0
  162. data/lib/arel/collectors/bind.rb +1 -1
  163. data/lib/arel/collectors/sql_string.rb +1 -1
  164. data/lib/arel/collectors/substitute_binds.rb +2 -2
  165. data/lib/arel/crud.rb +8 -11
  166. data/lib/arel/delete_manager.rb +5 -0
  167. data/lib/arel/nodes/binary.rb +1 -1
  168. data/lib/arel/nodes/count.rb +2 -2
  169. data/lib/arel/nodes/delete_statement.rb +4 -2
  170. data/lib/arel/nodes/function.rb +4 -10
  171. data/lib/arel/nodes/named_function.rb +2 -2
  172. data/lib/arel/nodes/node.rb +2 -2
  173. data/lib/arel/nodes/sql_literal.rb +1 -1
  174. data/lib/arel/nodes/update_statement.rb +4 -2
  175. data/lib/arel/nodes.rb +0 -2
  176. data/lib/arel/select_manager.rb +13 -4
  177. data/lib/arel/update_manager.rb +5 -0
  178. data/lib/arel/visitors/dot.rb +2 -3
  179. data/lib/arel/visitors/postgresql.rb +55 -0
  180. data/lib/arel/visitors/sqlite.rb +55 -8
  181. data/lib/arel/visitors/to_sql.rb +6 -22
  182. data/lib/arel.rb +3 -1
  183. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  184. metadata +16 -15
  185. data/lib/active_record/explain_subscriber.rb +0 -34
  186. data/lib/active_record/normalization.rb +0 -163
@@ -17,16 +17,22 @@ module ActiveRecord
17
17
  # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
18
18
  # +sql_type_metadata+ is various information about the type of the column
19
19
  # +null+ determines if this column allows +NULL+ values.
20
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **)
20
+ def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **)
21
21
  @name = name.freeze
22
+ @cast_type = cast_type
22
23
  @sql_type_metadata = sql_type_metadata
23
24
  @null = null
24
- @default = default
25
+ @default = default.nil? || cast_type.mutable? ? default : cast_type.deserialize(default)
25
26
  @default_function = default_function
26
27
  @collation = collation
27
28
  @comment = comment
28
29
  end
29
30
 
31
+ def fetch_cast_type(connection) # :nodoc:
32
+ # TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
33
+ @cast_type || connection.lookup_cast_type(sql_type)
34
+ end
35
+
30
36
  def has_default?
31
37
  !default.nil? || default_function
32
38
  end
@@ -45,6 +51,7 @@ module ActiveRecord
45
51
 
46
52
  def init_with(coder)
47
53
  @name = coder["name"]
54
+ @cast_type = coder["cast_type"]
48
55
  @sql_type_metadata = coder["sql_type_metadata"]
49
56
  @null = coder["null"]
50
57
  @default = coder["default"]
@@ -55,6 +62,7 @@ module ActiveRecord
55
62
 
56
63
  def encode_with(coder)
57
64
  coder["name"] = @name
65
+ coder["cast_type"] = @cast_type
58
66
  coder["sql_type_metadata"] = @sql_type_metadata
59
67
  coder["null"] = @null
60
68
  coder["default"] = @default
@@ -75,6 +83,7 @@ module ActiveRecord
75
83
  def ==(other)
76
84
  other.is_a?(Column) &&
77
85
  name == other.name &&
86
+ cast_type == other.cast_type &&
78
87
  default == other.default &&
79
88
  sql_type_metadata == other.sql_type_metadata &&
80
89
  null == other.null &&
@@ -88,6 +97,7 @@ module ActiveRecord
88
97
  Column.hash ^
89
98
  name.hash ^
90
99
  name.encoding.hash ^
100
+ cast_type.hash ^
91
101
  default.hash ^
92
102
  sql_type_metadata.hash ^
93
103
  null.hash ^
@@ -100,11 +110,14 @@ module ActiveRecord
100
110
  false
101
111
  end
102
112
 
113
+ protected
114
+ attr_reader :cast_type
115
+
103
116
  private
104
117
  def deduplicated
105
118
  @name = -name
106
119
  @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
107
- @default = -default if default
120
+ @default = -default if String === default
108
121
  @default_function = -default_function if default_function
109
122
  @collation = -collation if collation
110
123
  @comment = -comment if comment
@@ -114,7 +127,7 @@ module ActiveRecord
114
127
 
115
128
  class NullColumn < Column
116
129
  def initialize(name, **)
117
- super(name, nil)
130
+ super(name, nil, nil)
118
131
  end
119
132
  end
120
133
  end
@@ -45,16 +45,16 @@ module ActiveRecord
45
45
  end
46
46
  end
47
47
 
48
+ def default_insert_value(column) # :nodoc:
49
+ super unless column.auto_increment?
50
+ end
51
+
48
52
  private
49
53
  # https://mariadb.com/kb/en/analyze-statement/
50
54
  def analyze_without_explain?
51
55
  mariadb? && database_version >= "10.1.0"
52
56
  end
53
57
 
54
- def default_insert_value(column)
55
- super unless column.auto_increment?
56
- end
57
-
58
58
  def returning_column_values(result)
59
59
  if supports_insert_returning?
60
60
  result.rows.first
@@ -102,7 +102,13 @@ module ActiveRecord
102
102
  else
103
103
  value.getlocal
104
104
  end
105
- when Date, Time
105
+ when Time
106
+ if default_timezone == :utc
107
+ value.utc? ? value : value.getutc
108
+ else
109
+ value.utc? ? value.getlocal : value
110
+ end
111
+ when Date
106
112
  value
107
113
  else
108
114
  super
@@ -49,6 +49,8 @@ module ActiveRecord
49
49
  sql << "USING #{o.using}" if o.using
50
50
  sql << "ON #{quote_table_name(o.table)}" if create
51
51
  sql << "(#{quoted_columns(o)})"
52
+ sql << "INVISIBLE" if o.disabled? && !mariadb?
53
+ sql << "IGNORED" if o.disabled? && mariadb?
52
54
 
53
55
  add_sql_comment!(sql.join(" "), o.comment)
54
56
  end
@@ -5,6 +5,7 @@ module ActiveRecord
5
5
  module MySQL
6
6
  module ColumnMethods
7
7
  extend ActiveSupport::Concern
8
+ extend ConnectionAdapters::ColumnMethods::ClassMethods
8
9
 
9
10
  ##
10
11
  # :method: blob
@@ -42,12 +43,26 @@ module ActiveRecord
42
43
  # :method: unsigned_bigint
43
44
  # :call-seq: unsigned_bigint(*names, **options)
44
45
 
45
- included do
46
- define_column_methods :blob, :tinyblob, :mediumblob, :longblob,
47
- :tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint,
48
- :unsigned_float, :unsigned_decimal
46
+ define_column_methods :blob, :tinyblob, :mediumblob, :longblob,
47
+ :tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint
48
+ end
49
+
50
+ # = Active Record MySQL Adapter \Index Definition
51
+ class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition # :nodoc:
52
+ attr_accessor :enabled
53
+
54
+ def initialize(*args, **kwargs)
55
+ @enabled = kwargs.key?(:enabled) ? kwargs.delete(:enabled) : true
56
+ super
57
+ end
49
58
 
50
- deprecate :unsigned_float, :unsigned_decimal, deprecator: ActiveRecord.deprecator
59
+ def defined_for?(columns = nil, name: nil, unique: nil, valid: nil, include: nil, nulls_not_distinct: nil, enabled: nil, **options)
60
+ super(columns, name:, unique:, valid:, include:, nulls_not_distinct:, **options) &&
61
+ (enabled.nil? || self.enabled == enabled)
62
+ end
63
+
64
+ def disabled?
65
+ !@enabled
51
66
  end
52
67
  end
53
68
 
@@ -100,6 +115,28 @@ module ActiveRecord
100
115
  # = Active Record MySQL Adapter \Table
101
116
  class Table < ActiveRecord::ConnectionAdapters::Table
102
117
  include ColumnMethods
118
+
119
+ # Enables an index to be used by query optimizers.
120
+ #
121
+ # t.enable_index(:email)
122
+ #
123
+ # Note: only supported by MySQL version 8.0.0 and greater, and MariaDB version 10.6.0 and greater.
124
+ #
125
+ # See {connection.enable_index}[rdoc-ref:SchemaStatements#enable_index]
126
+ def enable_index(index_name)
127
+ @base.enable_index(name, index_name)
128
+ end
129
+
130
+ # Disables an index not to be used by query optimizers.
131
+ #
132
+ # t.disable_index(:email)
133
+ #
134
+ # Note: only supported by MySQL version 8.0.0 and greater, and MariaDB version 10.6.0 and greater.
135
+ #
136
+ # See {connection.disable_index}[rdoc-ref:SchemaStatements#disable_index]
137
+ def disable_index(index_name)
138
+ @base.disable_index(name, index_name)
139
+ end
103
140
  end
104
141
  end
105
142
  end
@@ -21,7 +21,7 @@ module ActiveRecord
21
21
  index_using = mysql_index_type
22
22
  end
23
23
 
24
- indexes << [
24
+ index = [
25
25
  row["Table"],
26
26
  row["Key_name"],
27
27
  row["Non_unique"].to_i == 0,
@@ -30,8 +30,14 @@ module ActiveRecord
30
30
  orders: {},
31
31
  type: index_type,
32
32
  using: index_using,
33
- comment: row["Index_comment"].presence
33
+ comment: row["Index_comment"].presence,
34
34
  ]
35
+
36
+ if supports_disabling_indexes?
37
+ index[-1][:enabled] = mariadb? ? row["Ignored"] == "NO" : row["Visible"] == "YES"
38
+ end
39
+
40
+ indexes << index
35
41
  end
36
42
 
37
43
  if expression = row["Expression"]
@@ -63,8 +69,7 @@ module ActiveRecord
63
69
  columns, order: orders, length: lengths
64
70
  ).values.join(", ")
65
71
  end
66
-
67
- IndexDefinition.new(*index, **options)
72
+ MySQL::IndexDefinition.new(*index, **options)
68
73
  end
69
74
  rescue StatementInvalid => e
70
75
  if e.message.match?(/Table '.+' doesn't exist/)
@@ -74,6 +79,16 @@ module ActiveRecord
74
79
  end
75
80
  end
76
81
 
82
+ def create_index_definition(table_name, name, unique, columns, **options)
83
+ MySQL::IndexDefinition.new(table_name, name, unique, columns, **options)
84
+ end
85
+
86
+ def add_index_options(table_name, column_name, name: nil, if_not_exists: false, internal: false, **options) # :nodoc:
87
+ index, algorithm, if_not_exists = super
88
+ index.enabled = options[:enabled] unless options[:enabled].nil?
89
+ [index, algorithm, if_not_exists]
90
+ end
91
+
77
92
  def remove_column(table_name, column_name, type = nil, **options)
78
93
  if foreign_key_exists?(table_name, column: column_name)
79
94
  remove_foreign_key(table_name, column: column_name)
@@ -85,6 +100,13 @@ module ActiveRecord
85
100
  super
86
101
  end
87
102
 
103
+ def remove_foreign_key(from_table, to_table = nil, **options)
104
+ # RESTRICT is by default in MySQL.
105
+ options.delete(:on_update) if options[:on_update] == :restrict
106
+ options.delete(:on_delete) if options[:on_delete] == :restrict
107
+ super
108
+ end
109
+
88
110
  def internal_string_options_for_primary_key
89
111
  super.tap do |options|
90
112
  if !row_format_dynamic_by_default? && CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
@@ -202,6 +224,7 @@ module ActiveRecord
202
224
 
203
225
  MySQL::Column.new(
204
226
  field["Field"],
227
+ lookup_cast_type(type_metadata.sql_type),
205
228
  default,
206
229
  type_metadata,
207
230
  field["Null"] == "YES",
@@ -226,6 +249,12 @@ module ActiveRecord
226
249
  end
227
250
  end
228
251
 
252
+ def valid_index_options
253
+ index_options = super
254
+ index_options << :enabled if supports_disabling_indexes?
255
+ index_options
256
+ end
257
+
229
258
  def add_options_for_index_columns(quoted_columns, **options)
230
259
  quoted_columns = add_index_length(quoted_columns, **options)
231
260
  super
@@ -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
@@ -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
 
@@ -127,7 +127,14 @@ module ActiveRecord
127
127
  def cancel_any_running_query
128
128
  return if @raw_connection.nil? || IDLE_TRANSACTION_STATUSES.include?(@raw_connection.transaction_status)
129
129
 
130
- @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
131
138
  @raw_connection.block
132
139
  rescue PG::Error
133
140
  end
@@ -163,25 +170,27 @@ module ActiveRecord
163
170
  end
164
171
 
165
172
  verified!
166
- handle_warnings(result)
167
- notification_payload[:row_count] = result.count
173
+
174
+ notification_payload[:affected_rows] = result.cmd_tuples
175
+ notification_payload[:row_count] = result.ntuples
168
176
  result
169
177
  end
170
178
 
171
179
  def cast_result(result)
172
- if result.fields.empty?
173
- result.clear
174
- return ActiveRecord::Result.empty
175
- end
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
176
190
 
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)
191
+ ActiveRecord::Result.new(fields, result.values, types.freeze, affected_rows: result.cmd_tuples)
183
192
  end
184
- ar_result = ActiveRecord::Result.new(fields, result.values, types.freeze)
193
+
185
194
  result.clear
186
195
  ar_result
187
196
  end
@@ -213,7 +222,7 @@ module ActiveRecord
213
222
  pk unless pk.is_a?(Array)
214
223
  end
215
224
 
216
- def handle_warnings(sql)
225
+ def handle_warnings(result, sql)
217
226
  @notice_receiver_sql_warnings.each do |warning|
218
227
  next if warning_ignored?(warning)
219
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)
@@ -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