activerecord 7.2.1.1 → 8.0.0.beta1

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +188 -791
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/associations/association.rb +25 -5
  5. data/lib/active_record/associations/builder/association.rb +7 -6
  6. data/lib/active_record/associations/collection_association.rb +10 -8
  7. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  8. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  9. data/lib/active_record/associations/join_dependency/join_association.rb +3 -2
  10. data/lib/active_record/associations/join_dependency.rb +4 -4
  11. data/lib/active_record/associations/preloader/association.rb +2 -2
  12. data/lib/active_record/associations/singular_association.rb +8 -3
  13. data/lib/active_record/associations.rb +34 -4
  14. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  15. data/lib/active_record/attribute_assignment.rb +9 -1
  16. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -0
  17. data/lib/active_record/attributes.rb +1 -2
  18. data/lib/active_record/autosave_association.rb +69 -27
  19. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  20. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  21. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +0 -1
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +12 -4
  25. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  26. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  27. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +26 -5
  28. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  29. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -25
  30. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +20 -38
  31. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  32. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  33. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +43 -45
  34. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
  35. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -8
  36. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  37. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
  38. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  39. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
  40. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +50 -6
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +38 -90
  42. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  43. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  44. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  45. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  46. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -1
  47. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -12
  48. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  49. data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
  50. data/lib/active_record/connection_handling.rb +22 -0
  51. data/lib/active_record/core.rb +14 -7
  52. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
  53. data/lib/active_record/encryption/config.rb +3 -1
  54. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  55. data/lib/active_record/encryption/encrypted_attribute_type.rb +10 -1
  56. data/lib/active_record/encryption/encryptor.rb +15 -8
  57. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  58. data/lib/active_record/encryption/key_provider.rb +1 -1
  59. data/lib/active_record/encryption/scheme.rb +8 -1
  60. data/lib/active_record/encryption.rb +2 -0
  61. data/lib/active_record/enum.rb +7 -10
  62. data/lib/active_record/errors.rb +13 -5
  63. data/lib/active_record/fixtures.rb +0 -1
  64. data/lib/active_record/future_result.rb +14 -10
  65. data/lib/active_record/gem_version.rb +4 -4
  66. data/lib/active_record/insert_all.rb +1 -1
  67. data/lib/active_record/migration/command_recorder.rb +22 -5
  68. data/lib/active_record/migration/compatibility.rb +5 -2
  69. data/lib/active_record/migration.rb +35 -33
  70. data/lib/active_record/model_schema.rb +2 -3
  71. data/lib/active_record/nested_attributes.rb +11 -2
  72. data/lib/active_record/persistence.rb +128 -130
  73. data/lib/active_record/query_logs.rb +97 -39
  74. data/lib/active_record/query_logs_formatter.rb +17 -28
  75. data/lib/active_record/querying.rb +6 -6
  76. data/lib/active_record/railtie.rb +8 -14
  77. data/lib/active_record/reflection.rb +9 -4
  78. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  79. data/lib/active_record/relation/batches.rb +132 -72
  80. data/lib/active_record/relation/calculations.rb +24 -19
  81. data/lib/active_record/relation/delegation.rb +25 -14
  82. data/lib/active_record/relation/finder_methods.rb +18 -18
  83. data/lib/active_record/relation/merger.rb +8 -8
  84. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  85. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  86. data/lib/active_record/relation/predicate_builder.rb +6 -1
  87. data/lib/active_record/relation/query_methods.rb +58 -37
  88. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  89. data/lib/active_record/relation/spawn_methods.rb +1 -1
  90. data/lib/active_record/relation.rb +72 -61
  91. data/lib/active_record/result.rb +68 -7
  92. data/lib/active_record/sanitization.rb +7 -6
  93. data/lib/active_record/schema_dumper.rb +5 -0
  94. data/lib/active_record/schema_migration.rb +2 -1
  95. data/lib/active_record/scoping/named.rb +5 -2
  96. data/lib/active_record/statement_cache.rb +12 -12
  97. data/lib/active_record/store.rb +7 -3
  98. data/lib/active_record/tasks/database_tasks.rb +36 -16
  99. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  100. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  101. data/lib/active_record/test_fixtures.rb +12 -0
  102. data/lib/active_record/token_for.rb +1 -1
  103. data/lib/active_record/validations/uniqueness.rb +9 -8
  104. data/lib/active_record.rb +15 -0
  105. data/lib/arel/collectors/bind.rb +1 -1
  106. metadata +10 -10
@@ -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
 
@@ -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?
@@ -197,12 +197,6 @@ module ActiveRecord
197
197
 
198
198
  # HELPER METHODS ===========================================
199
199
 
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
200
  # Must return the MySQL error number from the exception, if the exception has an
207
201
  # error number.
208
202
  def error_number(exception) # :nodoc:
@@ -226,24 +220,19 @@ module ActiveRecord
226
220
  # DATABASE STATEMENTS ======================================
227
221
  #++
228
222
 
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
223
  def begin_db_transaction # :nodoc:
241
224
  internal_execute("BEGIN", "TRANSACTION", allow_retry: true, materialize_transactions: false)
242
225
  end
243
226
 
244
227
  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
228
+ # From MySQL manual: The [SET TRANSACTION] statement applies only to the next single transaction performed within the session.
229
+ # 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
+ )
247
236
  end
248
237
 
249
238
  def commit_db_transaction # :nodoc:
@@ -343,7 +332,7 @@ module ActiveRecord
343
332
  rename_table_indexes(table_name, new_name, **options)
344
333
  end
345
334
 
346
- # Drops a table from the database.
335
+ # Drops a table or tables from the database.
347
336
  #
348
337
  # [<tt>:force</tt>]
349
338
  # Set to +:cascade+ to drop dependent objects as well.
@@ -357,10 +346,10 @@ module ActiveRecord
357
346
  #
358
347
  # Although this command ignores most +options+ and the block if one is given,
359
348
  # 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}"
349
+ # 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.
350
+ def drop_table(*table_names, **options)
351
+ table_names.each { |table_name| schema_cache.clear_data_source_cache!(table_name.to_s) }
352
+ 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
353
  end
365
354
 
366
355
  def rename_index(table_name, old_name, new_name)
@@ -580,7 +569,7 @@ module ActiveRecord
580
569
 
581
570
  # SHOW VARIABLES LIKE 'name'
582
571
  def show_variable(name)
583
- query_value("SELECT @@#{name}", "SCHEMA")
572
+ query_value("SELECT @@#{name}", "SCHEMA", materialize_transactions: false, allow_retry: true)
584
573
  rescue ActiveRecord::StatementInvalid
585
574
  nil
586
575
  end
@@ -679,8 +668,8 @@ module ActiveRecord
679
668
  end
680
669
 
681
670
  def check_version # :nodoc:
682
- if database_version < "5.5.8"
683
- raise DatabaseVersionError, "Your version of MySQL (#{database_version}) is too old. Active Record supports MySQL >= 5.5.8."
671
+ if database_version < "5.6.4"
672
+ raise DatabaseVersionError, "Your version of MySQL (#{database_version}) is too old. Active Record supports MySQL >= 5.6.4."
684
673
  end
685
674
  end
686
675
 
@@ -787,11 +776,6 @@ module ActiveRecord
787
776
  warning.level == "Note" || super
788
777
  end
789
778
 
790
- # Make sure we carry over any changes to ActiveRecord.default_timezone that have been
791
- # made since we established the connection
792
- def sync_timezone_changes(raw_connection)
793
- end
794
-
795
779
  # See https://dev.mysql.com/doc/mysql-errors/en/server-error-reference.html
796
780
  ER_DB_CREATE_EXISTS = 1007
797
781
  ER_FILSORT_ABORT = 1028
@@ -961,13 +945,11 @@ module ActiveRecord
961
945
  end.join(", ")
962
946
 
963
947
  # ...and send them all in one query
964
- internal_execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}")
948
+ raw_execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA")
965
949
  end
966
950
 
967
951
  def column_definitions(table_name) # :nodoc:
968
- execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
969
- each_hash(result)
970
- end
952
+ internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA")
971
953
  end
972
954
 
973
955
  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
@@ -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
 
@@ -182,12 +180,12 @@ module ActiveRecord
182
180
  end
183
181
 
184
182
  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
183
+ field_name = field.fetch("Field")
184
+ type_metadata = fetch_type_metadata(field["Type"], field["Extra"])
185
+ default, default_function = field["Default"], nil
188
186
 
189
187
  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])
188
+ default = "#{default} ON UPDATE #{default}" if /on update CURRENT_TIMESTAMP/i.match?(field["Extra"])
191
189
  default, default_function = nil, default
192
190
  elsif type_metadata.extra == "DEFAULT_GENERATED"
193
191
  default = +"(#{default})" unless default.start_with?("(")
@@ -203,13 +201,13 @@ module ActiveRecord
203
201
  end
204
202
 
205
203
  MySQL::Column.new(
206
- field[:Field],
204
+ field["Field"],
207
205
  default,
208
206
  type_metadata,
209
- field[:Null] == "YES",
207
+ field["Null"] == "YES",
210
208
  default_function,
211
- collation: field[:Collation],
212
- comment: field[:Comment].presence
209
+ collation: field["Collation"],
210
+ comment: field["Comment"].presence
213
211
  )
214
212
  end
215
213
 
@@ -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,75 +38,58 @@ 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 = if prepare
52
+ stmt = @statements[sql] ||= raw_connection.prepare(sql)
93
53
 
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
54
+ begin
55
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
56
+ stmt.execute(*type_casted_binds)
57
+ end
58
+ rescue ::Mysql2::Error
59
+ @statements.delete(sql)
60
+ stmt.close
61
+ raise
104
62
  end
63
+ verified!
64
+ else
65
+ raw_connection.query(sql)
105
66
  end
106
- end
107
-
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
67
 
112
- mark_transaction_written_if_write(sql)
68
+ notification_payload[:row_count] = result&.size || 0
113
69
 
114
- type_casted_binds = type_casted_binds(binds)
70
+ @affected_rows_before_warnings = raw_connection.affected_rows
71
+ raw_connection.abandon_results!
115
72
 
116
- log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
117
- with_raw_connection do |conn|
118
- sync_timezone_changes(conn)
119
-
120
- if cache_stmt
121
- stmt = @statements[sql] ||= conn.prepare(sql)
122
- else
123
- stmt = conn.prepare(sql)
124
- end
125
-
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
73
+ verified!
74
+ handle_warnings(sql)
75
+ result
76
+ ensure
77
+ if reset_multi_statement && active?
78
+ raw_connection.set_server_option(::Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF)
79
+ end
80
+ end
140
81
 
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
82
+ def cast_result(result)
83
+ if result.nil? || result.fields.empty?
84
+ ActiveRecord::Result.empty
85
+ else
86
+ ActiveRecord::Result.new(result.fields, result.to_a)
147
87
  end
148
88
  end
89
+
90
+ def affected_rows(result)
91
+ @affected_rows_before_warnings
92
+ end
149
93
  end
150
94
  end
151
95
  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
@@ -12,16 +12,8 @@ module ActiveRecord
12
12
 
13
13
  # Queries the database and returns the results in an Array-like object
14
14
  def query(sql, name = nil) # :nodoc:
15
- mark_transaction_written_if_write(sql)
16
-
17
- log(sql, name) do |notification_payload|
18
- with_raw_connection do |conn|
19
- result = conn.async_exec(sql).map_types!(@type_map_for_results).values
20
- verified!
21
- notification_payload[:row_count] = result.count
22
- result
23
- end
24
- end
15
+ result = internal_execute(sql, name)
16
+ result.map_types!(@type_map_for_results).values
25
17
  end
26
18
 
27
19
  READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
@@ -50,36 +42,6 @@ module ActiveRecord
50
42
  @notice_receiver_sql_warnings = []
51
43
  end
52
44
 
53
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
54
- log(sql, name, async: async) do |notification_payload|
55
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
56
- result = conn.async_exec(sql)
57
- verified!
58
- handle_warnings(result)
59
- notification_payload[:row_count] = result.count
60
- result
61
- end
62
- end
63
- end
64
-
65
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true) # :nodoc:
66
- execute_and_clear(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |result|
67
- types = {}
68
- fields = result.fields
69
- fields.each_with_index do |fname, i|
70
- ftype = result.ftype i
71
- fmod = result.fmod i
72
- types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
73
- end
74
- build_result(columns: fields, rows: result.values, column_types: types.freeze)
75
- end
76
- end
77
-
78
- def exec_delete(sql, name = nil, binds = []) # :nodoc:
79
- execute_and_clear(sql, name, binds) { |result| result.cmd_tuples }
80
- end
81
- alias :exec_update :exec_delete
82
-
83
45
  def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil) # :nodoc:
84
46
  if use_insert_returning? || pk == false
85
47
  super
@@ -170,8 +132,68 @@ module ActiveRecord
170
132
  rescue PG::Error
171
133
  end
172
134
 
173
- def execute_batch(statements, name = nil)
174
- execute(combine_multi_statements(statements))
135
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
136
+ update_typemap_for_default_timezone
137
+ result = if prepare
138
+ begin
139
+ stmt_key = prepare_statement(sql, binds, raw_connection)
140
+ notification_payload[:statement_name] = stmt_key
141
+ raw_connection.exec_prepared(stmt_key, type_casted_binds)
142
+ rescue PG::FeatureNotSupported => error
143
+ if is_cached_plan_failure?(error)
144
+ # Nothing we can do if we are in a transaction because all commands
145
+ # will raise InFailedSQLTransaction
146
+ if in_transaction?
147
+ raise PreparedStatementCacheExpired.new(error.message, connection_pool: @pool)
148
+ else
149
+ @lock.synchronize do
150
+ # outside of transactions we can simply flush this query and retry
151
+ @statements.delete sql_key(sql)
152
+ end
153
+ retry
154
+ end
155
+ end
156
+
157
+ raise
158
+ end
159
+ elsif binds.nil? || binds.empty?
160
+ raw_connection.async_exec(sql)
161
+ else
162
+ raw_connection.exec_params(sql, type_casted_binds)
163
+ end
164
+
165
+ verified!
166
+ handle_warnings(result)
167
+ notification_payload[:row_count] = result.count
168
+ result
169
+ end
170
+
171
+ def cast_result(result)
172
+ if result.fields.empty?
173
+ result.clear
174
+ return ActiveRecord::Result.empty
175
+ end
176
+
177
+ types = {}
178
+ fields = result.fields
179
+ fields.each_with_index do |fname, i|
180
+ ftype = result.ftype i
181
+ fmod = result.fmod i
182
+ types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
183
+ end
184
+ ar_result = ActiveRecord::Result.new(fields, result.values, types.freeze)
185
+ result.clear
186
+ ar_result
187
+ end
188
+
189
+ def affected_rows(result)
190
+ affected_rows = result.cmd_tuples
191
+ result.clear
192
+ affected_rows
193
+ end
194
+
195
+ def execute_batch(statements, name = nil, **kwargs)
196
+ raw_execute(combine_multi_statements(statements), name, batch: true, **kwargs)
175
197
  end
176
198
 
177
199
  def build_truncate_statements(table_names)
@@ -31,7 +31,7 @@ module ActiveRecord
31
31
  # TODO: Remove when IPAddr#== compares IPAddr#prefix. See
32
32
  # https://github.com/ruby/ipaddr/issues/21
33
33
  def changed?(old_value, new_value, _new_value_before_type_cast)
34
- super || !old_value.nil? && old_value.prefix != new_value.prefix
34
+ !old_value.eql?(new_value) || !old_value.nil? && old_value.prefix != new_value.prefix
35
35
  end
36
36
 
37
37
  def cast_value(value)
@@ -25,6 +25,10 @@ module ActiveRecord
25
25
  build_point(x, y)
26
26
  when ::Array
27
27
  build_point(*value)
28
+ when ::Hash
29
+ return if value.blank?
30
+
31
+ build_point(*values_array_from_hash(value))
28
32
  else
29
33
  value
30
34
  end
@@ -36,6 +40,8 @@ module ActiveRecord
36
40
  "(#{number_for_point(value.x)},#{number_for_point(value.y)})"
37
41
  when ::Array
38
42
  serialize(build_point(*value))
43
+ when ::Hash
44
+ serialize(build_point(*values_array_from_hash(value)))
39
45
  else
40
46
  super
41
47
  end
@@ -57,6 +63,10 @@ module ActiveRecord
57
63
  def build_point(x, y)
58
64
  ActiveRecord::Point.new(Float(x), Float(y))
59
65
  end
66
+
67
+ def values_array_from_hash(value)
68
+ [value.values_at(:x, "x").compact.first, value.values_at(:y, "y").compact.first]
69
+ end
60
70
  end
61
71
  end
62
72
  end
@@ -18,7 +18,6 @@ module ActiveRecord
18
18
 
19
19
  def visit_AddForeignKey(o)
20
20
  super.dup.tap do |sql|
21
- sql << " DEFERRABLE INITIALLY #{o.options[:deferrable].to_s.upcase}" if o.deferrable
22
21
  sql << " NOT VALID" unless o.validate?
23
22
  end
24
23
  end