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
@@ -28,6 +28,21 @@ module ActiveRecord
28
28
  # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false
29
29
  class_attribute :emulate_booleans, default: true
30
30
 
31
+ class_attribute :cli_arg_map, instance_accessor: false, default: {
32
+ host: "--host",
33
+ port: "--port",
34
+ socket: "--socket",
35
+ username: "--user",
36
+ password: "--password",
37
+ encoding: "--default-character-set",
38
+ sslca: "--ssl-ca",
39
+ sslcert: "--ssl-cert",
40
+ sslcapath: "--ssl-capath",
41
+ sslcipher: "--ssl-cipher",
42
+ sslkey: "--ssl-key",
43
+ ssl_mode: "--ssl-mode"
44
+ }.freeze
45
+
31
46
  NATIVE_DATABASE_TYPES = {
32
47
  primary_key: "bigint auto_increment PRIMARY KEY",
33
48
  string: { name: "varchar", limit: 255 },
@@ -42,7 +57,7 @@ module ActiveRecord
42
57
  date: { name: "date" },
43
58
  binary: { name: "blob" },
44
59
  blob: { name: "blob" },
45
- boolean: { name: "tinyint", limit: 1 },
60
+ boolean: { name: "boolean" },
46
61
  json: { name: "json" },
47
62
  }
48
63
 
@@ -54,22 +69,14 @@ module ActiveRecord
54
69
  end
55
70
 
56
71
  class << self
72
+ def cli_args(config) # :nodoc:
73
+ cli_arg_map.filter_map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }
74
+ end
75
+
57
76
  def dbconsole(config, options = {})
58
77
  mysql_config = config.configuration_hash
59
78
 
60
- args = {
61
- host: "--host",
62
- port: "--port",
63
- socket: "--socket",
64
- username: "--user",
65
- encoding: "--default-character-set",
66
- sslca: "--ssl-ca",
67
- sslcert: "--ssl-cert",
68
- sslcapath: "--ssl-capath",
69
- sslcipher: "--ssl-cipher",
70
- sslkey: "--ssl-key",
71
- ssl_mode: "--ssl-mode"
72
- }.filter_map { |opt, arg| "#{arg}=#{mysql_config[opt]}" if mysql_config[opt] }
79
+ args = cli_args(mysql_config.except(:password))
73
80
 
74
81
  if mysql_config[:password] && options[:include_password]
75
82
  args << "--password=#{mysql_config[:password]}"
@@ -81,6 +88,10 @@ module ActiveRecord
81
88
 
82
89
  find_cmd_and_exec(ActiveRecord.database_cli[:mysql], *args)
83
90
  end
91
+
92
+ def native_database_types # :nodoc:
93
+ NATIVE_DATABASE_TYPES
94
+ end
84
95
  end
85
96
 
86
97
  def get_database_version # :nodoc:
@@ -174,6 +185,30 @@ module ActiveRecord
174
185
  mariadb? && database_version >= "10.5.0"
175
186
  end
176
187
 
188
+ def return_value_after_insert?(column) # :nodoc:
189
+ supports_insert_returning? ? column.auto_populated? : column.auto_increment?
190
+ end
191
+
192
+ # See https://dev.mysql.com/doc/refman/8.0/en/invisible-indexes.html for more details on MySQL feature.
193
+ # See https://mariadb.com/kb/en/ignored-indexes/ for more details on the MariaDB feature.
194
+ def supports_disabling_indexes?
195
+ if mariadb?
196
+ database_version >= "10.6.0"
197
+ else
198
+ database_version >= "8.0.0"
199
+ end
200
+ end
201
+
202
+ def create_or_find_by_lock # :nodoc:
203
+ # A shared lock provides a current read under REPEATABLE READ without
204
+ # upgrading the shared record lock acquired by a duplicate INSERT.
205
+ if mariadb? || database_version < "8.0.1"
206
+ "LOCK IN SHARE MODE"
207
+ else
208
+ "FOR SHARE"
209
+ end
210
+ end
211
+
177
212
  def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
178
213
  query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
179
214
  end
@@ -182,10 +217,6 @@ module ActiveRecord
182
217
  query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1
183
218
  end
184
219
 
185
- def native_database_types
186
- NATIVE_DATABASE_TYPES
187
- end
188
-
189
220
  def index_algorithms
190
221
  {
191
222
  default: "ALGORITHM = DEFAULT",
@@ -195,8 +226,6 @@ module ActiveRecord
195
226
  }
196
227
  end
197
228
 
198
- # HELPER METHODS ===========================================
199
-
200
229
  # Must return the MySQL error number from the exception, if the exception has an
201
230
  # error number.
202
231
  def error_number(exception) # :nodoc:
@@ -227,12 +256,24 @@ module ActiveRecord
227
256
  def begin_isolated_db_transaction(isolation) # :nodoc:
228
257
  # From MySQL manual: The [SET TRANSACTION] statement applies only to the next single transaction performed within the session.
229
258
  # So we don't need to implement #reset_isolation_level
230
- execute_batch(
231
- ["SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}", "BEGIN"],
232
- "TRANSACTION",
233
- allow_retry: true,
234
- materialize_transactions: false,
235
- )
259
+ if multi_statements_enabled?
260
+ execute_batch(
261
+ ["SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}", "BEGIN"],
262
+ "TRANSACTION",
263
+ allow_retry: true,
264
+ materialize_transactions: false,
265
+ )
266
+ else
267
+ with_raw_connection(allow_retry: true, materialize_transactions: false) do
268
+ internal_execute(
269
+ "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}",
270
+ "TRANSACTION",
271
+ allow_retry: false,
272
+ materialize_transactions: false,
273
+ )
274
+ internal_execute("BEGIN", "TRANSACTION", allow_retry: false, materialize_transactions: false)
275
+ end
276
+ end
236
277
  end
237
278
 
238
279
  def commit_db_transaction # :nodoc:
@@ -403,7 +444,11 @@ module ActiveRecord
403
444
  type ||= column.sql_type
404
445
 
405
446
  unless options.key?(:default)
406
- options[:default] = column.default
447
+ options[:default] = if column.default_function
448
+ -> { column.default_function }
449
+ else
450
+ column.default
451
+ end
407
452
  end
408
453
 
409
454
  unless options.key?(:null)
@@ -449,6 +494,24 @@ module ActiveRecord
449
494
  CreateIndexDefinition.new(index, algorithm)
450
495
  end
451
496
 
497
+ def enable_index(table_name, index_name) # :nodoc:
498
+ raise NotImplementedError unless supports_disabling_indexes?
499
+
500
+ query = <<~SQL
501
+ ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} #{mariadb? ? "NOT IGNORED" : "VISIBLE"}
502
+ SQL
503
+ execute(query)
504
+ end
505
+
506
+ def disable_index(table_name, index_name) # :nodoc:
507
+ raise NotImplementedError unless supports_disabling_indexes?
508
+
509
+ query = <<~SQL
510
+ ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} #{mariadb? ? "IGNORED" : "INVISIBLE"}
511
+ SQL
512
+ execute(query)
513
+ end
514
+
452
515
  def add_sql_comment!(sql, comment) # :nodoc:
453
516
  sql << " COMMENT #{quote(comment)}" if comment.present?
454
517
  sql
@@ -628,24 +691,26 @@ module ActiveRecord
628
691
  end
629
692
 
630
693
  def build_insert_sql(insert) # :nodoc:
694
+ # Can use any column as it will be assigned to itself.
631
695
  no_op_column = quote_column_name(insert.keys.first) if insert.keys.first
632
696
 
633
697
  # MySQL 8.0.19 replaces `VALUES(<expression>)` clauses with row and column alias names, see https://dev.mysql.com/worklog/task/?id=6312 .
634
698
  # then MySQL 8.0.20 deprecates the `VALUES(<expression>)` see https://dev.mysql.com/worklog/task/?id=13325 .
635
699
  if supports_insert_raw_alias_syntax?
700
+ quoted_table_name = insert.model.quoted_table_name
636
701
  values_alias = quote_table_name("#{insert.model.table_name.parameterize}_values")
637
702
  sql = +"INSERT #{insert.into} #{insert.values_list} AS #{values_alias}"
638
703
 
639
704
  if insert.skip_duplicates?
640
705
  if no_op_column
641
- sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{values_alias}.#{no_op_column}"
706
+ sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{quoted_table_name}.#{no_op_column}"
642
707
  end
643
708
  elsif insert.update_duplicates?
644
709
  if insert.raw_update_sql?
645
710
  sql = +"INSERT #{insert.into} #{insert.values_list} ON DUPLICATE KEY UPDATE #{insert.raw_update_sql}"
646
711
  else
647
712
  sql << " ON DUPLICATE KEY UPDATE "
648
- sql << insert.touch_model_timestamps_unless { |column| "#{insert.model.quoted_table_name}.#{column}<=>#{values_alias}.#{column}" }
713
+ sql << insert.touch_model_timestamps_unless { |column| "#{quoted_table_name}.#{column}<=>#{values_alias}.#{column}" }
649
714
  sql << insert.updatable_columns.map { |column| "#{column}=#{values_alias}.#{column}" }.join(",")
650
715
  end
651
716
  end
@@ -722,12 +787,12 @@ module ActiveRecord
722
787
  m.alias_type %r(bit)i, "binary"
723
788
  end
724
789
 
725
- def register_integer_type(mapping, key, **options)
790
+ def register_integer_type(mapping, key, limit:)
726
791
  mapping.register_type(key) do |sql_type|
727
792
  if /\bunsigned\b/.match?(sql_type)
728
- Type::UnsignedInteger.new(**options)
793
+ Type::UnsignedInteger.new(limit: limit)
729
794
  else
730
- Type::Integer.new(**options)
795
+ Type::Integer.new(limit: limit)
731
796
  end
732
797
  end
733
798
  end
@@ -746,9 +811,7 @@ module ActiveRecord
746
811
 
747
812
  private
748
813
  def strip_whitespace_characters(expression)
749
- expression = expression.gsub(/\\n|\\\\/, "")
750
- expression = expression.gsub(/\s{2,}/, " ")
751
- expression
814
+ expression.gsub('\\\n', "").gsub("x0A", "").squish
752
815
  end
753
816
 
754
817
  def extended_type_map_key
@@ -759,14 +822,13 @@ module ActiveRecord
759
822
  end
760
823
  end
761
824
 
762
- def handle_warnings(sql)
825
+ def handle_warnings(_initial_result, sql)
763
826
  return if ActiveRecord.db_warnings_action.nil? || @raw_connection.warning_count == 0
764
827
 
765
- @affected_rows_before_warnings = @raw_connection.affected_rows
766
828
  warning_count = @raw_connection.warning_count
767
829
  result = @raw_connection.query("SHOW WARNINGS")
768
830
  result = [
769
- ["Warning", nil, "Query had warning_count=#{warning_count} but ‘SHOW WARNINGS’ did not return the warnings. Check MySQL logs or database configuration."],
831
+ ["Warning", nil, "Query had warning_count=#{warning_count} but `SHOW WARNINGS` did not return the warnings. Check MySQL logs or database configuration."],
770
832
  ] if result.count == 0
771
833
  result.each do |level, code, message|
772
834
  warning = SQLWarning.new(message, code, level, sql, @pool)
@@ -783,6 +845,7 @@ module ActiveRecord
783
845
  # See https://dev.mysql.com/doc/mysql-errors/en/server-error-reference.html
784
846
  ER_DB_CREATE_EXISTS = 1007
785
847
  ER_FILSORT_ABORT = 1028
848
+ ER_NO_DB_ERROR = 1046
786
849
  ER_DUP_ENTRY = 1062
787
850
  ER_SERVER_SHUTDOWN = 1053
788
851
  ER_NOT_NULL_VIOLATION = 1048
@@ -803,6 +866,8 @@ module ActiveRecord
803
866
  CR_SERVER_LOST = 2013
804
867
  ER_QUERY_TIMEOUT = 3024
805
868
  ER_FK_INCOMPATIBLE_COLUMNS = 3780
869
+ ER_CHECK_CONSTRAINT_VIOLATED = 3819
870
+ ER_CONSTRAINT_FAILED = 4025
806
871
  ER_CLIENT_INTERACTION_TIMEOUT = 4031
807
872
 
808
873
  def translate_exception(exception, message:, sql:, binds:)
@@ -813,7 +878,7 @@ module ActiveRecord
813
878
  else
814
879
  super
815
880
  end
816
- when ER_CONNECTION_KILLED, ER_SERVER_SHUTDOWN, CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ER_CLIENT_INTERACTION_TIMEOUT
881
+ when ER_CONNECTION_KILLED, ER_SERVER_SHUTDOWN, CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ER_CLIENT_INTERACTION_TIMEOUT, ER_NO_DB_ERROR
817
882
  ConnectionFailed.new(message, sql: sql, binds: binds, connection_pool: @pool)
818
883
  when ER_DB_CREATE_EXISTS
819
884
  DatabaseAlreadyExists.new(message, sql: sql, binds: binds, connection_pool: @pool)
@@ -835,6 +900,8 @@ module ActiveRecord
835
900
  RangeError.new(message, sql: sql, binds: binds, connection_pool: @pool)
836
901
  when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT
837
902
  NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
903
+ when ER_CHECK_CONSTRAINT_VIOLATED, ER_CONSTRAINT_FAILED
904
+ CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
838
905
  when ER_LOCK_DEADLOCK
839
906
  Deadlocked.new(message, sql: sql, binds: binds, connection_pool: @pool)
840
907
  when ER_LOCK_WAIT_TIMEOUT
@@ -953,7 +1020,33 @@ module ActiveRecord
953
1020
  end
954
1021
 
955
1022
  def column_definitions(table_name) # :nodoc:
956
- internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA")
1023
+ fields = internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA", allow_retry: true)
1024
+
1025
+ update_fields_for_mariadb(table_name, fields) if mariadb?
1026
+
1027
+ fields
1028
+ end
1029
+
1030
+ def update_fields_for_mariadb(table_name, fields)
1031
+ has_function_default_candidate = fields.any? do |field|
1032
+ default = field["Default"]
1033
+ default&.match?(/[a-zA-Z_]\w*\(/) && !/\ACURRENT_TIMESTAMP/i.match?(default)
1034
+ end
1035
+
1036
+ if has_function_default_candidate
1037
+ table_info = create_table_info(table_name)
1038
+ fields.each do |field|
1039
+ default = field["Default"]
1040
+ next unless default&.match?(/[a-zA-Z_]\w*\(/)
1041
+ next if /\ACURRENT_TIMESTAMP/i.match?(default)
1042
+
1043
+ field_name = field["Field"]
1044
+ match = table_info&.match(/`#{field_name}` .+ DEFAULT ('|\d+|[A-z]+)/)
1045
+ if match && match[1].match?(/\A[A-z]/)
1046
+ field["Extra"] = "DEFAULT_GENERATED"
1047
+ end
1048
+ end
1049
+ end
957
1050
  end
958
1051
 
959
1052
  def create_table_info(table_name) # :nodoc:
@@ -7,7 +7,7 @@ module ActiveRecord
7
7
  class Column
8
8
  include Deduplicable
9
9
 
10
- attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment
10
+ attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment, :cast_type
11
11
 
12
12
  delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
13
13
 
@@ -17,8 +17,9 @@ 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
25
  @default = default
@@ -27,6 +28,11 @@ module ActiveRecord
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 ^
@@ -104,7 +114,7 @@ module ActiveRecord
104
114
  def deduplicated
105
115
  @name = -name
106
116
  @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
107
- @default = -default if default
117
+ @default = -default if String === default
108
118
  @default_function = -default_function if default_function
109
119
  @collation = -collation if collation
110
120
  @comment = -comment if comment
@@ -114,7 +124,7 @@ module ActiveRecord
114
124
 
115
125
  class NullColumn < Column
116
126
  def initialize(name, **)
117
- super(name, nil)
127
+ super(name, nil, nil)
118
128
  end
119
129
  end
120
130
  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)
@@ -166,21 +188,7 @@ module ActiveRecord
166
188
  MySQL::TableDefinition.new(self, name, **options)
167
189
  end
168
190
 
169
- def default_type(table_name, field_name)
170
- match = create_table_info(table_name)&.match(/`#{field_name}` (.+) DEFAULT ('|\d+|[A-z]+)/)
171
- default_pre = match[2] if match
172
-
173
- if default_pre == "'"
174
- :string
175
- elsif default_pre&.match?(/^\d+$/)
176
- :integer
177
- elsif default_pre&.match?(/^[A-z]+$/)
178
- :function
179
- end
180
- end
181
-
182
191
  def new_column_from_field(table_name, field, _definitions)
183
- field_name = field.fetch("Field")
184
192
  type_metadata = fetch_type_metadata(field["Type"], field["Extra"])
185
193
  default, default_function = field["Default"], nil
186
194
 
@@ -188,20 +196,21 @@ module ActiveRecord
188
196
  default = "#{default} ON UPDATE #{default}" if /on update CURRENT_TIMESTAMP/i.match?(field["Extra"])
189
197
  default, default_function = nil, default
190
198
  elsif type_metadata.extra == "DEFAULT_GENERATED"
191
- default = +"(#{default})" unless default.start_with?("(")
192
- default = default.gsub("\\'", "'")
193
- default, default_function = nil, default
199
+ if mariadb?
200
+ default, default_function = nil, default
201
+ else
202
+ default = "(#{default})" unless default.start_with?("(")
203
+ default = default.gsub("\\'", "'")
204
+ default, default_function = nil, default
205
+ end
194
206
  elsif type_metadata.type == :text && default&.start_with?("'")
195
207
  # strip and unescape quotes
196
208
  default = default[1...-1].gsub("\\'", "'")
197
- elsif default&.match?(/\A\d/)
198
- # Its a number so we can skip the query to check if it is a function
199
- elsif default && default_type(table_name, field_name) == :function
200
- default, default_function = nil, default
201
209
  end
202
210
 
203
211
  MySQL::Column.new(
204
212
  field["Field"],
213
+ lookup_cast_type(type_metadata.sql_type),
205
214
  default,
206
215
  type_metadata,
207
216
  field["Null"] == "YES",
@@ -226,6 +235,12 @@ module ActiveRecord
226
235
  end
227
236
  end
228
237
 
238
+ def valid_index_options
239
+ index_options = super
240
+ index_options << :enabled if supports_disabling_indexes?
241
+ index_options
242
+ end
243
+
229
244
  def add_options_for_index_columns(quoted_columns, **options)
230
245
  quoted_columns = add_index_length(quoted_columns, **options)
231
246
  super