activerecord 7.2.2.1 → 8.0.5

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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +523 -677
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -0
  5. data/lib/active_record/associations/alias_tracker.rb +6 -4
  6. data/lib/active_record/associations/association.rb +34 -10
  7. data/lib/active_record/associations/belongs_to_association.rb +18 -2
  8. data/lib/active_record/associations/builder/association.rb +7 -6
  9. data/lib/active_record/associations/collection_association.rb +10 -8
  10. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  11. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  12. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  13. data/lib/active_record/associations/join_dependency.rb +2 -2
  14. data/lib/active_record/associations/preloader/association.rb +2 -2
  15. data/lib/active_record/associations/preloader/batch.rb +7 -1
  16. data/lib/active_record/associations/singular_association.rb +8 -3
  17. data/lib/active_record/associations.rb +34 -4
  18. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  19. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  20. data/lib/active_record/attribute_methods/query.rb +34 -0
  21. data/lib/active_record/attribute_methods/serialization.rb +1 -1
  22. data/lib/active_record/attribute_methods/time_zone_conversion.rb +2 -12
  23. data/lib/active_record/attribute_methods.rb +24 -19
  24. data/lib/active_record/attributes.rb +37 -26
  25. data/lib/active_record/autosave_association.rb +91 -39
  26. data/lib/active_record/base.rb +2 -2
  27. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +34 -25
  28. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  29. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  30. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +55 -47
  31. data/lib/active_record/connection_adapters/abstract/database_statements.rb +91 -44
  32. data/lib/active_record/connection_adapters/abstract/query_cache.rb +25 -8
  33. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  34. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +12 -3
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +40 -10
  37. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  38. data/lib/active_record/connection_adapters/abstract_adapter.rb +92 -70
  39. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +35 -46
  40. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -9
  41. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  42. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +50 -45
  43. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +84 -94
  44. data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -9
  45. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  46. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -2
  47. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +72 -43
  48. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  49. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  50. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  51. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
  52. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -24
  53. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +3 -2
  54. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +60 -17
  55. data/lib/active_record/connection_adapters/postgresql_adapter.rb +61 -99
  56. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  57. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  58. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +80 -100
  59. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -0
  60. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  61. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  62. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +10 -1
  63. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +63 -17
  64. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  65. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  66. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
  67. data/lib/active_record/connection_adapters.rb +0 -56
  68. data/lib/active_record/connection_handling.rb +35 -9
  69. data/lib/active_record/core.rb +57 -22
  70. data/lib/active_record/counter_cache.rb +1 -1
  71. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  72. data/lib/active_record/database_configurations/database_config.rb +4 -0
  73. data/lib/active_record/database_configurations/hash_config.rb +16 -2
  74. data/lib/active_record/delegated_type.rb +18 -18
  75. data/lib/active_record/encryption/config.rb +3 -1
  76. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  77. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  78. data/lib/active_record/encryption/encryptor.rb +37 -28
  79. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  80. data/lib/active_record/encryption/scheme.rb +8 -1
  81. data/lib/active_record/enum.rb +22 -34
  82. data/lib/active_record/errors.rb +16 -8
  83. data/lib/active_record/fixture_set/table_row.rb +19 -2
  84. data/lib/active_record/fixtures.rb +0 -2
  85. data/lib/active_record/future_result.rb +15 -9
  86. data/lib/active_record/gem_version.rb +4 -4
  87. data/lib/active_record/insert_all.rb +3 -3
  88. data/lib/active_record/locking/optimistic.rb +1 -1
  89. data/lib/active_record/log_subscriber.rb +5 -11
  90. data/lib/active_record/migration/command_recorder.rb +32 -12
  91. data/lib/active_record/migration/compatibility.rb +5 -2
  92. data/lib/active_record/migration.rb +40 -43
  93. data/lib/active_record/model_schema.rb +3 -4
  94. data/lib/active_record/nested_attributes.rb +4 -6
  95. data/lib/active_record/persistence.rb +128 -130
  96. data/lib/active_record/query_logs.rb +106 -50
  97. data/lib/active_record/query_logs_formatter.rb +17 -28
  98. data/lib/active_record/querying.rb +12 -12
  99. data/lib/active_record/railtie.rb +4 -28
  100. data/lib/active_record/railties/databases.rake +11 -34
  101. data/lib/active_record/reflection.rb +18 -21
  102. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  103. data/lib/active_record/relation/batches.rb +132 -72
  104. data/lib/active_record/relation/calculations.rb +71 -65
  105. data/lib/active_record/relation/delegation.rb +25 -14
  106. data/lib/active_record/relation/finder_methods.rb +32 -31
  107. data/lib/active_record/relation/merger.rb +8 -8
  108. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  109. data/lib/active_record/relation/predicate_builder/association_query_value.rb +2 -0
  110. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  111. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  112. data/lib/active_record/relation/predicate_builder.rb +13 -0
  113. data/lib/active_record/relation/query_attribute.rb +1 -1
  114. data/lib/active_record/relation/query_methods.rb +121 -70
  115. data/lib/active_record/relation/spawn_methods.rb +7 -7
  116. data/lib/active_record/relation/where_clause.rb +9 -3
  117. data/lib/active_record/relation.rb +95 -67
  118. data/lib/active_record/result.rb +66 -4
  119. data/lib/active_record/sanitization.rb +7 -6
  120. data/lib/active_record/schema_dumper.rb +34 -11
  121. data/lib/active_record/schema_migration.rb +2 -1
  122. data/lib/active_record/scoping/named.rb +5 -2
  123. data/lib/active_record/secure_token.rb +3 -3
  124. data/lib/active_record/signed_id.rb +7 -6
  125. data/lib/active_record/statement_cache.rb +14 -14
  126. data/lib/active_record/store.rb +7 -3
  127. data/lib/active_record/table_metadata.rb +1 -3
  128. data/lib/active_record/tasks/database_tasks.rb +69 -60
  129. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  130. data/lib/active_record/tasks/postgresql_database_tasks.rb +9 -1
  131. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  132. data/lib/active_record/test_databases.rb +1 -1
  133. data/lib/active_record/test_fixtures.rb +12 -0
  134. data/lib/active_record/token_for.rb +1 -1
  135. data/lib/active_record/transactions.rb +8 -7
  136. data/lib/active_record/type/serialized.rb +5 -0
  137. data/lib/active_record/validations/uniqueness.rb +8 -8
  138. data/lib/active_record.rb +22 -49
  139. data/lib/arel/collectors/bind.rb +2 -2
  140. data/lib/arel/collectors/sql_string.rb +1 -1
  141. data/lib/arel/collectors/substitute_binds.rb +2 -2
  142. data/lib/arel/crud.rb +2 -0
  143. data/lib/arel/delete_manager.rb +5 -0
  144. data/lib/arel/nodes/binary.rb +1 -1
  145. data/lib/arel/nodes/delete_statement.rb +4 -2
  146. data/lib/arel/nodes/node.rb +1 -1
  147. data/lib/arel/nodes/sql_literal.rb +1 -1
  148. data/lib/arel/nodes/update_statement.rb +4 -2
  149. data/lib/arel/predications.rb +1 -3
  150. data/lib/arel/select_manager.rb +6 -2
  151. data/lib/arel/table.rb +3 -7
  152. data/lib/arel/update_manager.rb +5 -0
  153. data/lib/arel/visitors/dot.rb +2 -0
  154. data/lib/arel/visitors/to_sql.rb +3 -1
  155. metadata +11 -15
  156. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
@@ -79,7 +79,7 @@ module ActiveRecord
79
79
 
80
80
  args << config.database
81
81
 
82
- find_cmd_and_exec(["mysql", "mysql5"], *args)
82
+ find_cmd_and_exec(ActiveRecord.database_cli[:mysql], *args)
83
83
  end
84
84
  end
85
85
 
@@ -98,7 +98,7 @@ module ActiveRecord
98
98
  end
99
99
 
100
100
  def supports_index_sort_order?
101
- !mariadb? && database_version >= "8.0.1"
101
+ mariadb? ? database_version >= "10.8.1" : database_version >= "8.0.1"
102
102
  end
103
103
 
104
104
  def supports_expression_index?
@@ -138,7 +138,7 @@ module ActiveRecord
138
138
  end
139
139
 
140
140
  def supports_datetime_with_precision?
141
- mariadb? || database_version >= "5.6.4"
141
+ true
142
142
  end
143
143
 
144
144
  def supports_virtual_columns?
@@ -174,6 +174,10 @@ module ActiveRecord
174
174
  mariadb? && database_version >= "10.5.0"
175
175
  end
176
176
 
177
+ def return_value_after_insert?(column) # :nodoc:
178
+ supports_insert_returning? ? column.auto_populated? : column.auto_increment?
179
+ end
180
+
177
181
  def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
178
182
  query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
179
183
  end
@@ -197,12 +201,6 @@ module ActiveRecord
197
201
 
198
202
  # HELPER METHODS ===========================================
199
203
 
200
- # The two drivers have slightly different ways of yielding hashes of results, so
201
- # this method must be implemented to provide a uniform interface.
202
- def each_hash(result) # :nodoc:
203
- raise NotImplementedError
204
- end
205
-
206
204
  # Must return the MySQL error number from the exception, if the exception has an
207
205
  # error number.
208
206
  def error_number(exception) # :nodoc:
@@ -226,24 +224,19 @@ module ActiveRecord
226
224
  # DATABASE STATEMENTS ======================================
227
225
  #++
228
226
 
229
- # Mysql2Adapter doesn't have to free a result after using it, but we use this method
230
- # to write stuff in an abstract way without concerning ourselves about whether it
231
- # needs to be explicitly freed or not.
232
- def execute_and_free(sql, name = nil, async: false, allow_retry: false) # :nodoc:
233
- sql = transform_query(sql)
234
- check_if_write_query(sql)
235
-
236
- mark_transaction_written_if_write(sql)
237
- yield raw_execute(sql, name, async: async, allow_retry: allow_retry)
238
- end
239
-
240
227
  def begin_db_transaction # :nodoc:
241
228
  internal_execute("BEGIN", "TRANSACTION", allow_retry: true, materialize_transactions: false)
242
229
  end
243
230
 
244
231
  def begin_isolated_db_transaction(isolation) # :nodoc:
245
- internal_execute("SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}", "TRANSACTION", allow_retry: true, materialize_transactions: false)
246
- begin_db_transaction
232
+ # From MySQL manual: The [SET TRANSACTION] statement applies only to the next single transaction performed within the session.
233
+ # So we don't need to implement #reset_isolation_level
234
+ execute_batch(
235
+ ["SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}", "BEGIN"],
236
+ "TRANSACTION",
237
+ allow_retry: true,
238
+ materialize_transactions: false,
239
+ )
247
240
  end
248
241
 
249
242
  def commit_db_transaction # :nodoc:
@@ -343,7 +336,7 @@ module ActiveRecord
343
336
  rename_table_indexes(table_name, new_name, **options)
344
337
  end
345
338
 
346
- # Drops a table from the database.
339
+ # Drops a table or tables from the database.
347
340
  #
348
341
  # [<tt>:force</tt>]
349
342
  # Set to +:cascade+ to drop dependent objects as well.
@@ -357,10 +350,10 @@ module ActiveRecord
357
350
  #
358
351
  # Although this command ignores most +options+ and the block if one is given,
359
352
  # it can be helpful to provide these in a migration's +change+ method so it can be reverted.
360
- # In that case, +options+ and the block will be used by create_table.
361
- def drop_table(table_name, **options)
362
- schema_cache.clear_data_source_cache!(table_name.to_s)
363
- execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
353
+ # In that case, +options+ and the block will be used by #create_table except if you provide more than one table which is not supported.
354
+ def drop_table(*table_names, **options)
355
+ table_names.each { |table_name| schema_cache.clear_data_source_cache!(table_name.to_s) }
356
+ execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{table_names.map { |table_name| quote_table_name(table_name) }.join(', ')}#{' CASCADE' if options[:force] == :cascade}"
364
357
  end
365
358
 
366
359
  def rename_index(table_name, old_name, new_name)
@@ -414,7 +407,11 @@ module ActiveRecord
414
407
  type ||= column.sql_type
415
408
 
416
409
  unless options.key?(:default)
417
- options[:default] = column.default
410
+ options[:default] = if column.default_function
411
+ -> { column.default_function }
412
+ else
413
+ column.default
414
+ end
418
415
  end
419
416
 
420
417
  unless options.key?(:null)
@@ -580,7 +577,7 @@ module ActiveRecord
580
577
 
581
578
  # SHOW VARIABLES LIKE 'name'
582
579
  def show_variable(name)
583
- query_value("SELECT @@#{name}", "SCHEMA")
580
+ query_value("SELECT @@#{name}", "SCHEMA", materialize_transactions: false, allow_retry: true)
584
581
  rescue ActiveRecord::StatementInvalid
585
582
  nil
586
583
  end
@@ -639,24 +636,26 @@ module ActiveRecord
639
636
  end
640
637
 
641
638
  def build_insert_sql(insert) # :nodoc:
639
+ # Can use any column as it will be assigned to itself.
642
640
  no_op_column = quote_column_name(insert.keys.first) if insert.keys.first
643
641
 
644
642
  # MySQL 8.0.19 replaces `VALUES(<expression>)` clauses with row and column alias names, see https://dev.mysql.com/worklog/task/?id=6312 .
645
643
  # then MySQL 8.0.20 deprecates the `VALUES(<expression>)` see https://dev.mysql.com/worklog/task/?id=13325 .
646
644
  if supports_insert_raw_alias_syntax?
645
+ quoted_table_name = insert.model.quoted_table_name
647
646
  values_alias = quote_table_name("#{insert.model.table_name.parameterize}_values")
648
647
  sql = +"INSERT #{insert.into} #{insert.values_list} AS #{values_alias}"
649
648
 
650
649
  if insert.skip_duplicates?
651
650
  if no_op_column
652
- sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{values_alias}.#{no_op_column}"
651
+ sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{quoted_table_name}.#{no_op_column}"
653
652
  end
654
653
  elsif insert.update_duplicates?
655
654
  if insert.raw_update_sql?
656
655
  sql = +"INSERT #{insert.into} #{insert.values_list} ON DUPLICATE KEY UPDATE #{insert.raw_update_sql}"
657
656
  else
658
657
  sql << " ON DUPLICATE KEY UPDATE "
659
- sql << insert.touch_model_timestamps_unless { |column| "#{insert.model.quoted_table_name}.#{column}<=>#{values_alias}.#{column}" }
658
+ sql << insert.touch_model_timestamps_unless { |column| "#{quoted_table_name}.#{column}<=>#{values_alias}.#{column}" }
660
659
  sql << insert.updatable_columns.map { |column| "#{column}=#{values_alias}.#{column}" }.join(",")
661
660
  end
662
661
  end
@@ -683,8 +682,8 @@ module ActiveRecord
683
682
  end
684
683
 
685
684
  def check_version # :nodoc:
686
- if database_version < "5.5.8"
687
- raise DatabaseVersionError, "Your version of MySQL (#{database_version}) is too old. Active Record supports MySQL >= 5.5.8."
685
+ if database_version < "5.6.4"
686
+ raise DatabaseVersionError, "Your version of MySQL (#{database_version}) is too old. Active Record supports MySQL >= 5.6.4."
688
687
  end
689
688
  end
690
689
 
@@ -757,9 +756,7 @@ module ActiveRecord
757
756
 
758
757
  private
759
758
  def strip_whitespace_characters(expression)
760
- expression = expression.gsub(/\\n|\\\\/, "")
761
- expression = expression.gsub(/\s{2,}/, " ")
762
- expression
759
+ expression.gsub('\\\n', "").gsub("x0A", "").squish
763
760
  end
764
761
 
765
762
  def extended_type_map_key
@@ -773,7 +770,6 @@ module ActiveRecord
773
770
  def handle_warnings(sql)
774
771
  return if ActiveRecord.db_warnings_action.nil? || @raw_connection.warning_count == 0
775
772
 
776
- @affected_rows_before_warnings = @raw_connection.affected_rows
777
773
  warning_count = @raw_connection.warning_count
778
774
  result = @raw_connection.query("SHOW WARNINGS")
779
775
  result = [
@@ -791,11 +787,6 @@ module ActiveRecord
791
787
  warning.level == "Note" || super
792
788
  end
793
789
 
794
- # Make sure we carry over any changes to ActiveRecord.default_timezone that have been
795
- # made since we established the connection
796
- def sync_timezone_changes(raw_connection)
797
- end
798
-
799
790
  # See https://dev.mysql.com/doc/mysql-errors/en/server-error-reference.html
800
791
  ER_DB_CREATE_EXISTS = 1007
801
792
  ER_FILSORT_ABORT = 1028
@@ -965,13 +956,11 @@ module ActiveRecord
965
956
  end.join(", ")
966
957
 
967
958
  # ...and send them all in one query
968
- internal_execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}")
959
+ raw_execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA")
969
960
  end
970
961
 
971
962
  def column_definitions(table_name) # :nodoc:
972
- execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
973
- each_hash(result)
974
- end
963
+ internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA")
975
964
  end
976
965
 
977
966
  def create_table_info(table_name) # :nodoc:
@@ -77,14 +77,6 @@ module ActiveRecord
77
77
  0
78
78
  end
79
79
 
80
- def quoted_date(value)
81
- if supports_datetime_with_precision?
82
- super
83
- else
84
- super.sub(/\.\d{6}\z/, "")
85
- end
86
- end
87
-
88
80
  def quoted_binary(value)
89
81
  "x'#{value.hex}'"
90
82
  end
@@ -110,7 +102,13 @@ module ActiveRecord
110
102
  else
111
103
  value.getlocal
112
104
  end
113
- 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
114
112
  value
115
113
  else
116
114
  super
@@ -42,18 +42,12 @@ module ActiveRecord
42
42
  # :method: unsigned_bigint
43
43
  # :call-seq: unsigned_bigint(*names, **options)
44
44
 
45
- ##
46
- # :method: unsigned_float
47
- # :call-seq: unsigned_float(*names, **options)
48
-
49
- ##
50
- # :method: unsigned_decimal
51
- # :call-seq: unsigned_decimal(*names, **options)
52
-
53
45
  included do
54
46
  define_column_methods :blob, :tinyblob, :mediumblob, :longblob,
55
47
  :tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint,
56
48
  :unsigned_float, :unsigned_decimal
49
+
50
+ deprecate :unsigned_float, :unsigned_decimal, deprecator: ActiveRecord.deprecator
57
51
  end
58
52
  end
59
53
 
@@ -8,45 +8,43 @@ module ActiveRecord
8
8
  def indexes(table_name)
9
9
  indexes = []
10
10
  current_index = nil
11
- execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
12
- each_hash(result) do |row|
13
- if current_index != row[:Key_name]
14
- next if row[:Key_name] == "PRIMARY" # skip the primary key
15
- current_index = row[:Key_name]
16
-
17
- mysql_index_type = row[:Index_type].downcase.to_sym
18
- case mysql_index_type
19
- when :fulltext, :spatial
20
- index_type = mysql_index_type
21
- when :btree, :hash
22
- index_using = mysql_index_type
23
- end
24
-
25
- indexes << [
26
- row[:Table],
27
- row[:Key_name],
28
- row[:Non_unique].to_i == 0,
29
- [],
30
- lengths: {},
31
- orders: {},
32
- type: index_type,
33
- using: index_using,
34
- comment: row[:Index_comment].presence
35
- ]
11
+ internal_exec_query("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA").each do |row|
12
+ if current_index != row["Key_name"]
13
+ next if row["Key_name"] == "PRIMARY" # skip the primary key
14
+ current_index = row["Key_name"]
15
+
16
+ mysql_index_type = row["Index_type"].downcase.to_sym
17
+ case mysql_index_type
18
+ when :fulltext, :spatial
19
+ index_type = mysql_index_type
20
+ when :btree, :hash
21
+ index_using = mysql_index_type
36
22
  end
37
23
 
38
- if row[:Expression]
39
- expression = row[:Expression].gsub("\\'", "'")
40
- expression = +"(#{expression})" unless expression.start_with?("(")
41
- indexes.last[-2] << expression
42
- indexes.last[-1][:expressions] ||= {}
43
- indexes.last[-1][:expressions][expression] = expression
44
- indexes.last[-1][:orders][expression] = :desc if row[:Collation] == "D"
45
- else
46
- indexes.last[-2] << row[:Column_name]
47
- indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part]
48
- indexes.last[-1][:orders][row[:Column_name]] = :desc if row[:Collation] == "D"
49
- end
24
+ indexes << [
25
+ row["Table"],
26
+ row["Key_name"],
27
+ row["Non_unique"].to_i == 0,
28
+ [],
29
+ lengths: {},
30
+ orders: {},
31
+ type: index_type,
32
+ using: index_using,
33
+ comment: row["Index_comment"].presence
34
+ ]
35
+ end
36
+
37
+ if expression = row["Expression"]
38
+ expression = expression.gsub("\\'", "'")
39
+ expression = +"(#{expression})" unless expression.start_with?("(")
40
+ indexes.last[-2] << expression
41
+ indexes.last[-1][:expressions] ||= {}
42
+ indexes.last[-1][:expressions][expression] = expression
43
+ indexes.last[-1][:orders][expression] = :desc if row["Collation"] == "D"
44
+ else
45
+ indexes.last[-2] << row["Column_name"]
46
+ indexes.last[-1][:lengths][row["Column_name"]] = row["Sub_part"].to_i if row["Sub_part"]
47
+ indexes.last[-1][:orders][row["Column_name"]] = :desc if row["Collation"] == "D"
50
48
  end
51
49
  end
52
50
 
@@ -87,6 +85,13 @@ module ActiveRecord
87
85
  super
88
86
  end
89
87
 
88
+ def remove_foreign_key(from_table, to_table = nil, **options)
89
+ # RESTRICT is by default in MySQL.
90
+ options.delete(:on_update) if options[:on_update] == :restrict
91
+ options.delete(:on_delete) if options[:on_delete] == :restrict
92
+ super
93
+ end
94
+
90
95
  def internal_string_options_for_primary_key
91
96
  super.tap do |options|
92
97
  if !row_format_dynamic_by_default? && CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
@@ -182,12 +187,12 @@ module ActiveRecord
182
187
  end
183
188
 
184
189
  def new_column_from_field(table_name, field, _definitions)
185
- field_name = field.fetch(:Field)
186
- type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
187
- default, default_function = field[:Default], nil
190
+ field_name = field.fetch("Field")
191
+ type_metadata = fetch_type_metadata(field["Type"], field["Extra"])
192
+ default, default_function = field["Default"], nil
188
193
 
189
194
  if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default)
190
- default = "#{default} ON UPDATE #{default}" if /on update CURRENT_TIMESTAMP/i.match?(field[:Extra])
195
+ default = "#{default} ON UPDATE #{default}" if /on update CURRENT_TIMESTAMP/i.match?(field["Extra"])
191
196
  default, default_function = nil, default
192
197
  elsif type_metadata.extra == "DEFAULT_GENERATED"
193
198
  default = +"(#{default})" unless default.start_with?("(")
@@ -203,13 +208,13 @@ module ActiveRecord
203
208
  end
204
209
 
205
210
  MySQL::Column.new(
206
- field[:Field],
211
+ field["Field"],
207
212
  default,
208
213
  type_metadata,
209
- field[:Null] == "YES",
214
+ field["Null"] == "YES",
210
215
  default_function,
211
- collation: field[:Collation],
212
- comment: field[:Comment].presence
216
+ collation: field["Collation"],
217
+ comment: field["Comment"].presence
213
218
  )
214
219
  end
215
220
 
@@ -13,49 +13,10 @@ module ActiveRecord
13
13
  end
14
14
  end
15
15
 
16
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
17
- if without_prepared_statement?(binds)
18
- execute_and_free(sql, name, async: async, allow_retry: allow_retry) do |result|
19
- if result
20
- build_result(columns: result.fields, rows: result.to_a)
21
- else
22
- build_result(columns: [], rows: [])
23
- end
24
- end
25
- else
26
- exec_stmt_and_free(sql, name, binds, cache_stmt: prepare, async: async) do |_, result|
27
- if result
28
- build_result(columns: result.fields, rows: result.to_a)
29
- else
30
- build_result(columns: [], rows: [])
31
- end
32
- end
33
- end
34
- end
35
-
36
- def exec_delete(sql, name = nil, binds = []) # :nodoc:
37
- if without_prepared_statement?(binds)
38
- with_raw_connection do |conn|
39
- @affected_rows_before_warnings = nil
40
- execute_and_free(sql, name) { @affected_rows_before_warnings || conn.affected_rows }
41
- end
42
- else
43
- exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows }
44
- end
45
- end
46
- alias :exec_update :exec_delete
47
-
48
16
  private
49
- def sync_timezone_changes(raw_connection)
50
- raw_connection.query_options[:database_timezone] = default_timezone
51
- end
52
-
53
- def execute_batch(statements, name = nil)
54
- statements = statements.map { |sql| transform_query(sql) }
17
+ def execute_batch(statements, name = nil, **kwargs)
55
18
  combine_multi_statements(statements).each do |statement|
56
- with_raw_connection do |conn|
57
- raw_execute(statement, name)
58
- end
19
+ raw_execute(statement, name, batch: true, **kwargs)
59
20
  end
60
21
  end
61
22
 
@@ -77,73 +38,102 @@ module ActiveRecord
77
38
  end
78
39
  end
79
40
 
80
- def with_multi_statements
81
- if multi_statements_enabled?
82
- return yield
41
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
42
+ reset_multi_statement = if batch && !multi_statements_enabled?
43
+ raw_connection.set_server_option(::Mysql2::Client::OPTION_MULTI_STATEMENTS_ON)
44
+ true
83
45
  end
84
46
 
85
- with_raw_connection do |conn|
86
- conn.set_server_option(::Mysql2::Client::OPTION_MULTI_STATEMENTS_ON)
47
+ # Make sure we carry over any changes to ActiveRecord.default_timezone that have been
48
+ # made since we established the connection
49
+ raw_connection.query_options[:database_timezone] = default_timezone
87
50
 
88
- yield
89
- ensure
90
- conn.set_server_option(::Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF)
91
- end
92
- end
51
+ result = nil
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
62
+ elsif prepare
63
+ stmt = @statements[sql] ||= raw_connection.prepare(sql)
64
+ 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
70
+ @statements.delete(sql)
71
+ raise
72
+ end
73
+ else
74
+ stmt = raw_connection.prepare(sql)
75
+
76
+ 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
93
81
 
94
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
95
- log(sql, name, async: async) do |notification_payload|
96
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
97
- sync_timezone_changes(conn)
98
- result = conn.query(sql)
99
- conn.abandon_results!
100
- verified!
101
- handle_warnings(sql)
102
- notification_payload[:row_count] = result&.size || 0
103
- result
82
+ # Ref: https://github.com/brianmario/mysql2/pull/1383
83
+ # by eagerly closing uncached prepared statements, we also reduce the chances of
84
+ # that bug happening. It can still happen if `#execute` is used as we have no callback
85
+ # to eagerly close the statement.
86
+ if result
87
+ result.instance_variable_set(:@_ar_stmt_to_close, stmt)
88
+ else
89
+ stmt.close
90
+ end
91
+ rescue ::Mysql2::Error
92
+ stmt.close
93
+ raise
104
94
  end
105
95
  end
96
+
97
+ notification_payload[:affected_rows] = @affected_rows_before_warnings
98
+ notification_payload[:row_count] = result&.size || 0
99
+
100
+ raw_connection.abandon_results!
101
+
102
+ verified!
103
+ handle_warnings(sql)
104
+ result
105
+ ensure
106
+ if reset_multi_statement && active?
107
+ raw_connection.set_server_option(::Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF)
108
+ end
106
109
  end
107
110
 
108
- def exec_stmt_and_free(sql, name, binds, cache_stmt: false, async: false)
109
- sql = transform_query(sql)
110
- check_if_write_query(sql)
111
+ def cast_result(raw_result)
112
+ return ActiveRecord::Result.empty if raw_result.nil?
111
113
 
112
- mark_transaction_written_if_write(sql)
114
+ fields = raw_result.fields
113
115
 
114
- type_casted_binds = type_casted_binds(binds)
116
+ result = if fields.empty?
117
+ ActiveRecord::Result.empty
118
+ else
119
+ ActiveRecord::Result.new(fields, raw_result.to_a)
120
+ end
115
121
 
116
- log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
117
- with_raw_connection do |conn|
118
- sync_timezone_changes(conn)
122
+ free_raw_result(raw_result)
119
123
 
120
- if cache_stmt
121
- stmt = @statements[sql] ||= conn.prepare(sql)
122
- else
123
- stmt = conn.prepare(sql)
124
- end
124
+ result
125
+ end
125
126
 
126
- begin
127
- result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
128
- stmt.execute(*type_casted_binds)
129
- end
130
- verified!
131
- result
132
- rescue ::Mysql2::Error => e
133
- if cache_stmt
134
- @statements.delete(sql)
135
- else
136
- stmt.close
137
- end
138
- raise e
139
- end
127
+ def affected_rows(raw_result)
128
+ free_raw_result(raw_result) if raw_result
140
129
 
141
- ret = yield stmt, result
142
- notification_payload[:row_count] = result&.size || 0
143
- result.free if result
144
- stmt.close unless cache_stmt
145
- ret
146
- end
130
+ @affected_rows_before_warnings
131
+ end
132
+
133
+ def free_raw_result(raw_result)
134
+ raw_result.free
135
+ if stmt = raw_result.instance_variable_get(:@_ar_stmt_to_close)
136
+ stmt.close
147
137
  end
148
138
  end
149
139
  end
@@ -55,6 +55,7 @@ module ActiveRecord
55
55
  def initialize(...)
56
56
  super
57
57
 
58
+ @affected_rows_before_warnings = nil
58
59
  @config[:flags] ||= 0
59
60
 
60
61
  if @config[:flags].kind_of? Array
@@ -92,14 +93,6 @@ module ActiveRecord
92
93
 
93
94
  # HELPER METHODS ===========================================
94
95
 
95
- def each_hash(result, &block) # :nodoc:
96
- if block_given?
97
- result.each(as: :hash, symbolize_keys: true, &block)
98
- else
99
- to_enum(:each_hash, result)
100
- end
101
- end
102
-
103
96
  def error_number(exception)
104
97
  exception.error_number if exception.respond_to?(:error_number)
105
98
  end
@@ -113,7 +106,14 @@ module ActiveRecord
113
106
  end
114
107
 
115
108
  def active?
116
- connected? && @lock.synchronize { @raw_connection&.ping } || false
109
+ if connected?
110
+ @lock.synchronize do
111
+ if @raw_connection&.ping
112
+ verified!
113
+ true
114
+ end
115
+ end
116
+ end || false
117
117
  end
118
118
 
119
119
  alias :reset! :reconnect!