activerecord 8.0.3 → 8.1.0.rc1

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +520 -514
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/association.rb +1 -1
  6. data/lib/active_record/associations/belongs_to_association.rb +2 -0
  7. data/lib/active_record/associations/builder/association.rb +16 -5
  8. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  9. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  10. data/lib/active_record/associations/builder/has_one.rb +1 -1
  11. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  12. data/lib/active_record/associations/collection_proxy.rb +22 -4
  13. data/lib/active_record/associations/deprecation.rb +88 -0
  14. data/lib/active_record/associations/errors.rb +3 -0
  15. data/lib/active_record/associations/join_dependency.rb +2 -0
  16. data/lib/active_record/associations/preloader/branch.rb +1 -0
  17. data/lib/active_record/associations.rb +159 -21
  18. data/lib/active_record/attribute_methods/serialization.rb +16 -3
  19. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  20. data/lib/active_record/attributes.rb +3 -0
  21. data/lib/active_record/autosave_association.rb +1 -1
  22. data/lib/active_record/base.rb +0 -2
  23. data/lib/active_record/coders/json.rb +14 -5
  24. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +1 -3
  25. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
  26. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  27. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +405 -72
  28. data/lib/active_record/connection_adapters/abstract/database_statements.rb +55 -40
  29. data/lib/active_record/connection_adapters/abstract/query_cache.rb +19 -3
  30. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  31. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  32. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
  33. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  34. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +85 -22
  35. data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
  36. data/lib/active_record/connection_adapters/abstract_adapter.rb +86 -20
  37. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -13
  38. data/lib/active_record/connection_adapters/column.rb +17 -4
  39. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  40. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  41. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  42. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
  43. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
  44. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -2
  45. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  46. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +17 -15
  47. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  48. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  49. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  50. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
  51. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  52. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +66 -31
  53. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +81 -48
  54. data/lib/active_record/connection_adapters/postgresql_adapter.rb +23 -7
  55. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  56. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +37 -25
  57. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  58. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  59. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +54 -30
  60. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  61. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  62. data/lib/active_record/connection_adapters.rb +1 -0
  63. data/lib/active_record/connection_handling.rb +2 -1
  64. data/lib/active_record/core.rb +5 -4
  65. data/lib/active_record/counter_cache.rb +33 -8
  66. data/lib/active_record/database_configurations/database_config.rb +5 -1
  67. data/lib/active_record/database_configurations/hash_config.rb +53 -9
  68. data/lib/active_record/database_configurations/url_config.rb +13 -3
  69. data/lib/active_record/database_configurations.rb +7 -3
  70. data/lib/active_record/delegated_type.rb +1 -1
  71. data/lib/active_record/dynamic_matchers.rb +54 -69
  72. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  73. data/lib/active_record/encryption/encrypted_attribute_type.rb +1 -1
  74. data/lib/active_record/encryption/encryptor.rb +12 -0
  75. data/lib/active_record/encryption/scheme.rb +1 -1
  76. data/lib/active_record/enum.rb +24 -8
  77. data/lib/active_record/errors.rb +20 -4
  78. data/lib/active_record/explain.rb +1 -1
  79. data/lib/active_record/explain_registry.rb +51 -2
  80. data/lib/active_record/filter_attribute_handler.rb +73 -0
  81. data/lib/active_record/fixtures.rb +2 -2
  82. data/lib/active_record/gem_version.rb +3 -3
  83. data/lib/active_record/inheritance.rb +1 -1
  84. data/lib/active_record/insert_all.rb +12 -7
  85. data/lib/active_record/locking/optimistic.rb +7 -0
  86. data/lib/active_record/locking/pessimistic.rb +5 -0
  87. data/lib/active_record/log_subscriber.rb +2 -6
  88. data/lib/active_record/middleware/shard_selector.rb +34 -17
  89. data/lib/active_record/migration/command_recorder.rb +14 -1
  90. data/lib/active_record/migration/compatibility.rb +34 -24
  91. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  92. data/lib/active_record/migration.rb +26 -16
  93. data/lib/active_record/model_schema.rb +36 -10
  94. data/lib/active_record/nested_attributes.rb +2 -0
  95. data/lib/active_record/persistence.rb +34 -3
  96. data/lib/active_record/query_cache.rb +22 -15
  97. data/lib/active_record/query_logs.rb +3 -7
  98. data/lib/active_record/railtie.rb +32 -3
  99. data/lib/active_record/railties/controller_runtime.rb +11 -6
  100. data/lib/active_record/railties/databases.rake +15 -3
  101. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  102. data/lib/active_record/railties/job_runtime.rb +10 -11
  103. data/lib/active_record/reflection.rb +42 -3
  104. data/lib/active_record/relation/batches.rb +25 -11
  105. data/lib/active_record/relation/calculations.rb +20 -9
  106. data/lib/active_record/relation/delegation.rb +0 -1
  107. data/lib/active_record/relation/finder_methods.rb +27 -11
  108. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  109. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  110. data/lib/active_record/relation/predicate_builder.rb +9 -7
  111. data/lib/active_record/relation/query_attribute.rb +3 -1
  112. data/lib/active_record/relation/query_methods.rb +38 -28
  113. data/lib/active_record/relation/where_clause.rb +1 -8
  114. data/lib/active_record/relation.rb +24 -12
  115. data/lib/active_record/result.rb +44 -21
  116. data/lib/active_record/runtime_registry.rb +41 -58
  117. data/lib/active_record/sanitization.rb +2 -0
  118. data/lib/active_record/schema_dumper.rb +12 -10
  119. data/lib/active_record/scoping.rb +0 -1
  120. data/lib/active_record/signed_id.rb +43 -15
  121. data/lib/active_record/statement_cache.rb +13 -9
  122. data/lib/active_record/store.rb +44 -19
  123. data/lib/active_record/structured_event_subscriber.rb +85 -0
  124. data/lib/active_record/table_metadata.rb +5 -20
  125. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  126. data/lib/active_record/tasks/database_tasks.rb +25 -34
  127. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  128. data/lib/active_record/tasks/postgresql_database_tasks.rb +5 -39
  129. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  130. data/lib/active_record/test_databases.rb +14 -4
  131. data/lib/active_record/test_fixtures.rb +27 -2
  132. data/lib/active_record/testing/query_assertions.rb +8 -2
  133. data/lib/active_record/timestamp.rb +4 -2
  134. data/lib/active_record/transaction.rb +2 -5
  135. data/lib/active_record/transactions.rb +32 -10
  136. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  137. data/lib/active_record/type/internal/timezone.rb +7 -0
  138. data/lib/active_record/type/json.rb +15 -2
  139. data/lib/active_record/type/serialized.rb +11 -4
  140. data/lib/active_record/type/type_map.rb +1 -1
  141. data/lib/active_record/type_caster/connection.rb +2 -1
  142. data/lib/active_record/validations/associated.rb +1 -1
  143. data/lib/active_record.rb +65 -3
  144. data/lib/arel/alias_predication.rb +2 -0
  145. data/lib/arel/crud.rb +6 -11
  146. data/lib/arel/nodes/count.rb +2 -2
  147. data/lib/arel/nodes/function.rb +4 -10
  148. data/lib/arel/nodes/named_function.rb +2 -2
  149. data/lib/arel/nodes/node.rb +1 -1
  150. data/lib/arel/nodes.rb +0 -2
  151. data/lib/arel/select_manager.rb +7 -2
  152. data/lib/arel/visitors/dot.rb +0 -3
  153. data/lib/arel/visitors/postgresql.rb +55 -0
  154. data/lib/arel/visitors/sqlite.rb +55 -8
  155. data/lib/arel/visitors/to_sql.rb +3 -21
  156. data/lib/arel.rb +3 -1
  157. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  158. metadata +14 -10
  159. data/lib/active_record/explain_subscriber.rb +0 -34
  160. data/lib/active_record/normalization.rb +0 -163
@@ -42,7 +42,7 @@ module ActiveRecord
42
42
  date: { name: "date" },
43
43
  binary: { name: "blob" },
44
44
  blob: { name: "blob" },
45
- boolean: { name: "tinyint", limit: 1 },
45
+ boolean: { name: "boolean" },
46
46
  json: { name: "json" },
47
47
  }
48
48
 
@@ -81,6 +81,10 @@ module ActiveRecord
81
81
 
82
82
  find_cmd_and_exec(ActiveRecord.database_cli[:mysql], *args)
83
83
  end
84
+
85
+ def native_database_types # :nodoc:
86
+ NATIVE_DATABASE_TYPES
87
+ end
84
88
  end
85
89
 
86
90
  def get_database_version # :nodoc:
@@ -178,6 +182,16 @@ module ActiveRecord
178
182
  supports_insert_returning? ? column.auto_populated? : column.auto_increment?
179
183
  end
180
184
 
185
+ # See https://dev.mysql.com/doc/refman/8.0/en/invisible-indexes.html for more details on MySQL feature.
186
+ # See https://mariadb.com/kb/en/ignored-indexes/ for more details on the MariaDB feature.
187
+ def supports_disabling_indexes?
188
+ if mariadb?
189
+ database_version >= "10.6.0"
190
+ else
191
+ database_version >= "8.0.0"
192
+ end
193
+ end
194
+
181
195
  def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
182
196
  query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
183
197
  end
@@ -186,10 +200,6 @@ module ActiveRecord
186
200
  query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1
187
201
  end
188
202
 
189
- def native_database_types
190
- NATIVE_DATABASE_TYPES
191
- end
192
-
193
203
  def index_algorithms
194
204
  {
195
205
  default: "ALGORITHM = DEFAULT",
@@ -199,8 +209,6 @@ module ActiveRecord
199
209
  }
200
210
  end
201
211
 
202
- # HELPER METHODS ===========================================
203
-
204
212
  # Must return the MySQL error number from the exception, if the exception has an
205
213
  # error number.
206
214
  def error_number(exception) # :nodoc:
@@ -457,6 +465,24 @@ module ActiveRecord
457
465
  CreateIndexDefinition.new(index, algorithm)
458
466
  end
459
467
 
468
+ def enable_index(table_name, index_name) # :nodoc:
469
+ raise NotImplementedError unless supports_disabling_indexes?
470
+
471
+ query = <<~SQL
472
+ ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "NOT IGNORED" : "VISIBLE"}
473
+ SQL
474
+ execute(query)
475
+ end
476
+
477
+ def disable_index(table_name, index_name) # :nodoc:
478
+ raise NotImplementedError unless supports_disabling_indexes?
479
+
480
+ query = <<~SQL
481
+ ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "IGNORED" : "INVISIBLE"}
482
+ SQL
483
+ execute(query)
484
+ end
485
+
460
486
  def add_sql_comment!(sql, comment) # :nodoc:
461
487
  sql << " COMMENT #{quote(comment)}" if comment.present?
462
488
  sql
@@ -732,12 +758,12 @@ module ActiveRecord
732
758
  m.alias_type %r(bit)i, "binary"
733
759
  end
734
760
 
735
- def register_integer_type(mapping, key, **options)
761
+ def register_integer_type(mapping, key, limit:)
736
762
  mapping.register_type(key) do |sql_type|
737
763
  if /\bunsigned\b/.match?(sql_type)
738
- Type::UnsignedInteger.new(**options)
764
+ Type::UnsignedInteger.new(limit: limit)
739
765
  else
740
- Type::Integer.new(**options)
766
+ Type::Integer.new(limit: limit)
741
767
  end
742
768
  end
743
769
  end
@@ -767,13 +793,13 @@ module ActiveRecord
767
793
  end
768
794
  end
769
795
 
770
- def handle_warnings(sql)
796
+ def handle_warnings(_initial_result, sql)
771
797
  return if ActiveRecord.db_warnings_action.nil? || @raw_connection.warning_count == 0
772
798
 
773
799
  warning_count = @raw_connection.warning_count
774
800
  result = @raw_connection.query("SHOW WARNINGS")
775
801
  result = [
776
- ["Warning", nil, "Query had warning_count=#{warning_count} but SHOW WARNINGS did not return the warnings. Check MySQL logs or database configuration."],
802
+ ["Warning", nil, "Query had warning_count=#{warning_count} but `SHOW WARNINGS` did not return the warnings. Check MySQL logs or database configuration."],
777
803
  ] if result.count == 0
778
804
  result.each do |level, code, message|
779
805
  warning = SQLWarning.new(message, code, level, sql, @pool)
@@ -810,6 +836,8 @@ module ActiveRecord
810
836
  CR_SERVER_LOST = 2013
811
837
  ER_QUERY_TIMEOUT = 3024
812
838
  ER_FK_INCOMPATIBLE_COLUMNS = 3780
839
+ ER_CHECK_CONSTRAINT_VIOLATED = 3819
840
+ ER_CONSTRAINT_FAILED = 4025
813
841
  ER_CLIENT_INTERACTION_TIMEOUT = 4031
814
842
 
815
843
  def translate_exception(exception, message:, sql:, binds:)
@@ -842,6 +870,8 @@ module ActiveRecord
842
870
  RangeError.new(message, sql: sql, binds: binds, connection_pool: @pool)
843
871
  when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT
844
872
  NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
873
+ when ER_CHECK_CONSTRAINT_VIOLATED, ER_CONSTRAINT_FAILED
874
+ CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
845
875
  when ER_LOCK_DEADLOCK
846
876
  Deadlocked.new(message, sql: sql, binds: binds, connection_pool: @pool)
847
877
  when ER_LOCK_WAIT_TIMEOUT
@@ -960,7 +990,7 @@ module ActiveRecord
960
990
  end
961
991
 
962
992
  def column_definitions(table_name) # :nodoc:
963
- internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA")
993
+ internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA", allow_retry: true)
964
994
  end
965
995
 
966
996
  def create_table_info(table_name) # :nodoc:
@@ -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
@@ -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)
@@ -209,6 +224,7 @@ module ActiveRecord
209
224
 
210
225
  MySQL::Column.new(
211
226
  field["Field"],
227
+ lookup_cast_type(type_metadata.sql_type),
212
228
  default,
213
229
  type_metadata,
214
230
  field["Null"] == "YES",
@@ -233,6 +249,12 @@ module ActiveRecord
233
249
  end
234
250
  end
235
251
 
252
+ def valid_index_options
253
+ index_options = super
254
+ index_options << :enabled if supports_disabling_indexes?
255
+ index_options
256
+ end
257
+
236
258
  def add_options_for_index_columns(quoted_columns, **options)
237
259
  quoted_columns = add_index_length(quoted_columns, **options)
238
260
  super
@@ -50,34 +50,40 @@ module ActiveRecord
50
50
 
51
51
  result = nil
52
52
  if binds.nil? || binds.empty?
53
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
54
- result = raw_connection.query(sql)
55
- # Ref: https://github.com/brianmario/mysql2/pull/1383
56
- # As of mysql2 0.5.6 `#affected_rows` might raise Mysql2::Error if a prepared statement
57
- # from that same connection was GCed while `#query` released the GVL.
58
- # By avoiding to call `#affected_rows` when we have a result, we reduce the likeliness
59
- # of hitting the bug.
60
- @affected_rows_before_warnings = result&.size || raw_connection.affected_rows
61
- end
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
62
60
  elsif prepare
63
- stmt = @statements[sql] ||= raw_connection.prepare(sql)
61
+ retry_count = 1
64
62
  begin
65
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
66
- result = stmt.execute(*type_casted_binds)
67
- @affected_rows_before_warnings = stmt.affected_rows
68
- end
69
- rescue ::Mysql2::Error
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
70
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
71
79
  raise
72
80
  end
73
81
  else
74
82
  stmt = raw_connection.prepare(sql)
75
83
 
76
84
  begin
77
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
78
- result = stmt.execute(*type_casted_binds)
79
- @affected_rows_before_warnings = stmt.affected_rows
80
- end
85
+ result = stmt.execute(*type_casted_binds)
86
+ @affected_rows_before_warnings = stmt.affected_rows
81
87
 
82
88
  # Ref: https://github.com/brianmario/mysql2/pull/1383
83
89
  # by eagerly closing uncached prepared statements, we also reduce the chances of
@@ -100,7 +106,6 @@ module ActiveRecord
100
106
  raw_connection.abandon_results!
101
107
 
102
108
  verified!
103
- handle_warnings(sql)
104
109
  result
105
110
  ensure
106
111
  if reset_multi_statement && active?
@@ -109,12 +114,12 @@ module ActiveRecord
109
114
  end
110
115
 
111
116
  def cast_result(raw_result)
112
- return ActiveRecord::Result.empty if raw_result.nil?
117
+ return ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings) if raw_result.nil?
113
118
 
114
119
  fields = raw_result.fields
115
120
 
116
121
  result = if fields.empty?
117
- ActiveRecord::Result.empty
122
+ ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings)
118
123
  else
119
124
  ActiveRecord::Result.new(fields, raw_result.to_a)
120
125
  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
@@ -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
 
@@ -170,25 +170,27 @@ module ActiveRecord
170
170
  end
171
171
 
172
172
  verified!
173
- handle_warnings(result)
173
+
174
+ notification_payload[:affected_rows] = result.cmd_tuples
174
175
  notification_payload[:row_count] = result.ntuples
175
176
  result
176
177
  end
177
178
 
178
179
  def cast_result(result)
179
- if result.fields.empty?
180
- result.clear
181
- return ActiveRecord::Result.empty
182
- 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
183
190
 
184
- types = {}
185
- fields = result.fields
186
- fields.each_with_index do |fname, i|
187
- ftype = result.ftype i
188
- fmod = result.fmod i
189
- types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
191
+ ActiveRecord::Result.new(fields, result.values, types.freeze, affected_rows: result.cmd_tuples)
190
192
  end
191
- ar_result = ActiveRecord::Result.new(fields, result.values, types.freeze)
193
+
192
194
  result.clear
193
195
  ar_result
194
196
  end
@@ -220,7 +222,7 @@ module ActiveRecord
220
222
  pk unless pk.is_a?(Array)
221
223
  end
222
224
 
223
- def handle_warnings(sql)
225
+ def handle_warnings(result, sql)
224
226
  @notice_receiver_sql_warnings.each do |warning|
225
227
  next if warning_ignored?(warning)
226
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