Skip to content

Commit 6be9ba2

Browse files
authored
Merge pull request rails#50049 from kmcphillips/mysql-warnings-empty
Handle case in MySQL where the `ActiveRecord.db_warnings_action` is not called even when a DB query has warnings
2 parents a5d243f + db3d692 commit 6be9ba2

File tree

5 files changed

+120
-86
lines changed

5 files changed

+120
-86
lines changed

activerecord/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
* In cases where MySQL returns `warning_count` greater than zero, but returns no warnings when
2+
the `SHOW WARNINGS` query is executed, `ActiveRecord.db_warnings_action` proc will still be
3+
called with a generic warning message rather than silently ignoring the warning(s).
4+
5+
*Kevin McPhillips*
6+
17
* `DatabaseConfigurations#configs_for` can accept a symbol in the `name` parameter.
28

39
*Andrew Novoselac*

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,11 @@ def handle_warnings(sql)
750750
return if ActiveRecord.db_warnings_action.nil? || @raw_connection.warning_count == 0
751751

752752
@affected_rows_before_warnings = @raw_connection.affected_rows
753+
warning_count = @raw_connection.warning_count
753754
result = @raw_connection.query("SHOW WARNINGS")
755+
result = [
756+
["Warning", nil, "Query had warning_count=#{warning_count} but ‘SHOW WARNINGS’ did not return the warnings. Check MySQL logs or database configuration."],
757+
] if result.count == 0
754758
result.each do |level, code, message|
755759
warning = SQLWarning.new(message, code, level, sql, @pool)
756760
next if warning_ignored?(warning)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# frozen_string_literal: true
2+
3+
require "cases/helper"
4+
require "active_support/error_reporter/test_helper"
5+
6+
class WarningsTest < ActiveRecord::AbstractMysqlTestCase
7+
def setup
8+
@connection = ActiveRecord::Base.connection
9+
@original_db_warnings_action = :ignore
10+
end
11+
12+
test "db_warnings_action :raise on warning" do
13+
with_db_warnings_action(:raise) do
14+
error = assert_raises(ActiveRecord::SQLWarning) do
15+
@connection.execute('SELECT 1 + "foo"')
16+
end
17+
18+
assert_equal @connection.pool, error.connection_pool
19+
end
20+
end
21+
22+
test "db_warnings_action :ignore on warning" do
23+
with_db_warnings_action(:ignore) do
24+
result = @connection.execute('SELECT 1 + "foo"')
25+
assert_equal [1], result.to_a.first
26+
end
27+
end
28+
29+
test "db_warnings_action :log on warning" do
30+
with_db_warnings_action(:log) do
31+
mysql_warning = "[ActiveRecord::SQLWarning] Truncated incorrect DOUBLE value: 'foo' (1292)"
32+
33+
assert_called_with(ActiveRecord::Base.logger, :warn, [mysql_warning]) do
34+
@connection.execute('SELECT 1 + "foo"')
35+
end
36+
end
37+
end
38+
39+
test "db_warnings_action :report on warning" do
40+
with_db_warnings_action(:report) do
41+
error_reporter = ActiveSupport::ErrorReporter.new
42+
subscriber = ActiveSupport::ErrorReporter::TestHelper::ErrorSubscriber.new
43+
44+
Rails.define_singleton_method(:error) { error_reporter }
45+
Rails.error.subscribe(subscriber)
46+
47+
@connection.execute('SELECT 1 + "foo"')
48+
49+
warning_event, * = subscriber.events.first
50+
51+
assert_kind_of ActiveRecord::SQLWarning, warning_event
52+
assert_equal "Truncated incorrect DOUBLE value: 'foo'", warning_event.message
53+
end
54+
end
55+
56+
test "db_warnings_action custom proc on warning" do
57+
warning_message = nil
58+
warning_level = nil
59+
warning_action = ->(warning) do
60+
warning_message = warning.message
61+
warning_level = warning.level
62+
end
63+
64+
with_db_warnings_action(warning_action) do
65+
@connection.execute('SELECT 1 + "foo"')
66+
67+
assert_equal "Truncated incorrect DOUBLE value: 'foo'", warning_message
68+
assert_equal "Warning", warning_level
69+
end
70+
end
71+
72+
test "db_warnings_action allows a list of warnings to ignore" do
73+
with_db_warnings_action(:raise, [/Truncated incorrect DOUBLE value/]) do
74+
result = @connection.execute('SELECT 1 + "foo"')
75+
76+
assert_equal [1], result.to_a.first
77+
end
78+
end
79+
80+
test "db_warnings_action allows a list of codes to ignore" do
81+
with_db_warnings_action(:raise, ["1062"]) do
82+
row_id = @connection.insert("INSERT INTO posts (title, body) VALUES('Title', 'Body')")
83+
result = @connection.execute("INSERT IGNORE INTO posts (id, title, body) VALUES(#{row_id}, 'Title', 'Body')")
84+
85+
assert_equal [], result.to_a
86+
end
87+
end
88+
89+
test "db_warnings_action ignores note level warnings" do
90+
with_db_warnings_action(:raise) do
91+
result = @connection.execute("DROP TABLE IF EXISTS non_existent_table")
92+
93+
assert_equal [], result.to_a
94+
end
95+
end
96+
97+
test "db_warnings_action handles when warning_count does not match returned warnings" do
98+
with_db_warnings_action(:raise) do
99+
# force warnings to 1, but SHOW WARNINGS will return [].
100+
@connection.raw_connection.stub(:warning_count, 1) do
101+
error = assert_raises(ActiveRecord::SQLWarning) do
102+
@connection.execute('SELECT "x"')
103+
end
104+
105+
expected = "Query had warning_count=1 but ‘SHOW WARNINGS’ did not return the warnings. Check MySQL logs or database configuration."
106+
assert_equal expected, error.message
107+
end
108+
end
109+
end
110+
end

activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
require "cases/helper"
44
require "support/ddl_helper"
55

6-
require "active_support/error_reporter/test_helper"
7-
86
class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
97
include DdlHelper
108

@@ -304,88 +302,6 @@ def test_database_timezone_changes_synced_to_connection
304302
end
305303
end
306304

307-
def test_ignores_warnings_when_behaviour_ignore
308-
with_db_warnings_action(:ignore) do
309-
result = @conn.execute('SELECT 1 + "foo"')
310-
assert_equal [1], result.to_a.first
311-
end
312-
end
313-
314-
def test_logs_warnings_when_behaviour_log
315-
with_db_warnings_action(:log) do
316-
mysql_warning = "[ActiveRecord::SQLWarning] Truncated incorrect DOUBLE value: 'foo' (1292)"
317-
318-
assert_called_with(ActiveRecord::Base.logger, :warn, [mysql_warning]) do
319-
@conn.execute('SELECT 1 + "foo"')
320-
end
321-
end
322-
end
323-
324-
def test_raises_warnings_when_behaviour_raise
325-
with_db_warnings_action(:raise) do
326-
error = assert_raises(ActiveRecord::SQLWarning) do
327-
@conn.execute('SELECT 1 + "foo"')
328-
end
329-
330-
assert_equal @conn.pool, error.connection_pool
331-
end
332-
end
333-
334-
def test_reports_when_behaviour_report
335-
with_db_warnings_action(:report) do
336-
error_reporter = ActiveSupport::ErrorReporter.new
337-
subscriber = ActiveSupport::ErrorReporter::TestHelper::ErrorSubscriber.new
338-
339-
Rails.define_singleton_method(:error) { error_reporter }
340-
Rails.error.subscribe(subscriber)
341-
342-
@conn.execute('SELECT 1 + "foo"')
343-
344-
warning_event, * = subscriber.events.first
345-
346-
assert_kind_of ActiveRecord::SQLWarning, warning_event
347-
assert_equal "Truncated incorrect DOUBLE value: 'foo'", warning_event.message
348-
end
349-
end
350-
351-
def test_warnings_behaviour_can_be_customized_with_a_proc
352-
warning_code = nil
353-
ActiveRecord.db_warnings_action = ->(warning) do
354-
warning_code = warning.code
355-
end
356-
357-
@conn.execute('SELECT 1 + "foo"')
358-
359-
assert_equal 1292, warning_code
360-
ensure
361-
ActiveRecord.db_warnings_action = @original_db_warnings_action
362-
end
363-
364-
def test_allowlist_of_warnings_to_ignore
365-
with_db_warnings_action(:raise, [/Truncated incorrect DOUBLE value/]) do
366-
result = @conn.execute('SELECT 1 + "foo"')
367-
368-
assert_equal [1], result.to_a.first
369-
end
370-
end
371-
372-
def test_allowlist_of_warning_codes_to_ignore
373-
with_db_warnings_action(:raise, ["1062"]) do
374-
row_id = @conn.insert("INSERT INTO posts (title, body) VALUES('Title', 'Body')")
375-
result = @conn.execute("INSERT IGNORE INTO posts (id, title, body) VALUES(#{row_id}, 'Title', 'Body')")
376-
377-
assert_nil result
378-
end
379-
end
380-
381-
def test_does_not_raise_note_level_warnings
382-
with_db_warnings_action(:raise) do
383-
result = @conn.execute("DROP TABLE IF EXISTS non_existent_table")
384-
385-
assert_equal [], result.to_a
386-
end
387-
end
388-
389305
def test_warnings_do_not_change_returned_value_of_exec_update
390306
previous_logger = ActiveRecord::Base.logger
391307
old_sql_mode = @conn.query_value("SELECT @@SESSION.sql_mode")

activerecord/test/cases/adapters/trilogy/trilogy_adapter_test.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
require "models/book"
66
require "models/post"
77

8-
require "active_support/error_reporter/test_helper"
9-
108
class TrilogyAdapterTest < ActiveRecord::TrilogyTestCase
119
setup do
1210
@conn = ActiveRecord::Base.connection

0 commit comments

Comments
 (0)