Skip to content

Commit 05cb63a

Browse files
authored
Merge pull request rails#47650 from nickborromeo/add-error-codes-to-warnings-ignore-list
Allow SQL Warnings to be ignored using error codes
2 parents d53dc86 + 6bd1007 commit 05cb63a

File tree

6 files changed

+103
-81
lines changed

6 files changed

+103
-81
lines changed

activerecord/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
* Allow warning codes to be ignore when reporting SQL warnings.
2+
3+
Active Record config that can ignore warning codes
4+
5+
```ruby
6+
# Configure allowlist of warnings that should always be ignored
7+
config.active_record.db_warnings_ignore = [
8+
"1062", # MySQL Error 1062: Duplicate entry
9+
]
10+
```
11+
12+
This is supported for the MySQL and PostgreSQL adapters.
13+
14+
*Nick Borromeo*
15+
116
* Introduce `:active_record_fixtures` lazy load hook.
217

318
Hooks defined with this name will be run whenever `TestFixtures` is included

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,9 @@ def default_prepared_statements
11971197
end
11981198

11991199
def warning_ignored?(warning)
1200-
ActiveRecord.db_warnings_ignore.any? { |warning_matcher| warning.message.match?(warning_matcher) }
1200+
ActiveRecord.db_warnings_ignore.any? do |warning_matcher|
1201+
warning.message.match?(warning_matcher) || warning.code.to_s.match?(warning_matcher)
1202+
end
12011203
end
12021204
end
12031205
end

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

Lines changed: 59 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -326,55 +326,45 @@ def test_database_timezone_changes_synced_to_connection
326326
end
327327

328328
def test_ignores_warnings_when_behaviour_ignore
329-
ActiveRecord.db_warnings_action = :ignore
330-
331-
result = @conn.execute('SELECT 1 + "foo"')
332-
333-
assert_equal [1], result.to_a.first
334-
ensure
335-
ActiveRecord.db_warnings_action = @original_db_warnings_action
329+
with_db_warnings_action(:ignore) do
330+
result = @conn.execute('SELECT 1 + "foo"')
331+
assert_equal [1], result.to_a.first
332+
end
336333
end
337334

338335
def test_logs_warnings_when_behaviour_log
339-
ActiveRecord.db_warnings_action = :log
340-
341-
mysql_warning = "[ActiveRecord::SQLWarning] Truncated incorrect DOUBLE value: 'foo' (1292)"
336+
with_db_warnings_action(:log) do
337+
mysql_warning = "[ActiveRecord::SQLWarning] Truncated incorrect DOUBLE value: 'foo' (1292)"
342338

343-
assert_called_with(ActiveRecord::Base.logger, :warn, [mysql_warning]) do
344-
@conn.execute('SELECT 1 + "foo"')
339+
assert_called_with(ActiveRecord::Base.logger, :warn, [mysql_warning]) do
340+
@conn.execute('SELECT 1 + "foo"')
341+
end
345342
end
346-
ensure
347-
ActiveRecord.db_warnings_action = @original_db_warnings_action
348343
end
349344

350345
def test_raises_warnings_when_behaviour_raise
351-
ActiveRecord.db_warnings_action = :raise
352-
353-
assert_raises(ActiveRecord::SQLWarning) do
354-
@conn.execute('SELECT 1 + "foo"')
346+
with_db_warnings_action(:raise) do
347+
assert_raises(ActiveRecord::SQLWarning) do
348+
@conn.execute('SELECT 1 + "foo"')
349+
end
355350
end
356-
ensure
357-
ActiveRecord.db_warnings_action = @original_db_warnings_action
358351
end
359352

360353
def test_reports_when_behaviour_report
361-
ActiveRecord.db_warnings_action = :report
362-
363-
error_reporter = ActiveSupport::ErrorReporter.new
364-
subscriber = ActiveSupport::ErrorReporter::TestHelper::ErrorSubscriber.new
354+
with_db_warnings_action(:report) do
355+
error_reporter = ActiveSupport::ErrorReporter.new
356+
subscriber = ActiveSupport::ErrorReporter::TestHelper::ErrorSubscriber.new
365357

366-
Rails.define_singleton_method(:error) { error_reporter }
367-
Rails.error.subscribe(subscriber)
358+
Rails.define_singleton_method(:error) { error_reporter }
359+
Rails.error.subscribe(subscriber)
368360

369-
@conn.execute('SELECT 1 + "foo"')
361+
@conn.execute('SELECT 1 + "foo"')
370362

371-
warning_event, * = subscriber.events.first
363+
warning_event, * = subscriber.events.first
372364

373-
assert_kind_of ActiveRecord::SQLWarning, warning_event
374-
assert_equal "Truncated incorrect DOUBLE value: 'foo'", warning_event.message
375-
ensure
376-
Rails.singleton_class.remove_method(:error)
377-
ActiveRecord.db_warnings_action = @original_db_warnings_action
365+
assert_kind_of ActiveRecord::SQLWarning, warning_event
366+
assert_equal "Truncated incorrect DOUBLE value: 'foo'", warning_event.message
367+
end
378368
end
379369

380370
def test_warnings_behaviour_can_be_customized_with_a_proc
@@ -391,64 +381,68 @@ def test_warnings_behaviour_can_be_customized_with_a_proc
391381
end
392382

393383
def test_allowlist_of_warnings_to_ignore
394-
old_ignored_warnings = ActiveRecord.db_warnings_ignore
395-
ActiveRecord.db_warnings_action = :raise
396-
ActiveRecord.db_warnings_ignore = [/Truncated incorrect DOUBLE value/]
384+
with_db_warnings_action(:raise, [/Truncated incorrect DOUBLE value/]) do
385+
result = @conn.execute('SELECT 1 + "foo"')
397386

398-
result = @conn.execute('SELECT 1 + "foo"')
387+
assert_equal [1], result.to_a.first
388+
end
389+
end
399390

400-
assert_equal [1], result.to_a.first
401-
ensure
402-
ActiveRecord.db_warnings_action = @original_db_warnings_action
403-
ActiveRecord.db_warnings_ignore = old_ignored_warnings
391+
def test_allowlist_of_warning_codes_to_ignore
392+
with_db_warnings_action(:raise, ["1062"]) do
393+
row_id = @conn.insert("INSERT INTO posts (title, body) VALUES('Title', 'Body')")
394+
result = @conn.execute("INSERT IGNORE INTO posts (id, title, body) VALUES(#{row_id}, 'Title', 'Body')")
395+
396+
assert_nil result
397+
end
404398
end
405399

406400
def test_does_not_raise_note_level_warnings
407-
ActiveRecord.db_warnings_action = :raise
408-
409-
result = @conn.execute("DROP TABLE IF EXISTS non_existent_table")
401+
with_db_warnings_action(:raise) do
402+
result = @conn.execute("DROP TABLE IF EXISTS non_existent_table")
410403

411-
assert_equal [], result.to_a
412-
ensure
413-
ActiveRecord.db_warnings_action = @original_db_warnings_action
404+
assert_equal [], result.to_a
405+
end
414406
end
415407

416408
def test_warnings_do_not_change_returned_value_of_exec_update
417409
previous_logger = ActiveRecord::Base.logger
418-
ActiveRecord::Base.logger = ActiveSupport::Logger.new(nil)
419-
ActiveRecord.db_warnings_action = :log
420-
421-
# Mysql2 will raise an error when attempting to perform an update that warns if the sql_mode is set to strict
422410
old_sql_mode = @conn.query_value("SELECT @@SESSION.sql_mode")
423-
@conn.execute("SET @@SESSION.sql_mode=''")
424411

425-
@conn.execute("INSERT INTO posts (title, body) VALUES('Title', 'Body')")
426-
result = @conn.update("UPDATE posts SET title = 'Updated' WHERE id > (0+'foo') LIMIT 1")
412+
with_db_warnings_action(:log) do
413+
ActiveRecord::Base.logger = ActiveSupport::Logger.new(nil)
414+
415+
# Mysql2 will raise an error when attempting to perform an update that warns if the sql_mode is set to strict
416+
@conn.execute("SET @@SESSION.sql_mode=''")
417+
418+
@conn.execute("INSERT INTO posts (title, body) VALUES('Title', 'Body')")
419+
result = @conn.update("UPDATE posts SET title = 'Updated' WHERE id > (0+'foo') LIMIT 1")
427420

428-
assert_equal 1, result
421+
assert_equal 1, result
422+
end
429423
ensure
430424
@conn.execute("SET @@SESSION.sql_mode='#{old_sql_mode}'")
431425
ActiveRecord::Base.logger = previous_logger
432-
ActiveRecord.db_warnings_action = @original_db_warnings_action
433426
end
434427

435428
def test_warnings_do_not_change_returned_value_of_exec_delete
436429
previous_logger = ActiveRecord::Base.logger
437-
ActiveRecord::Base.logger = ActiveSupport::Logger.new(nil)
438-
ActiveRecord.db_warnings_action = :log
439-
440-
# Mysql2 will raise an error when attempting to perform a delete that warns if the sql_mode is set to strict
441430
old_sql_mode = @conn.query_value("SELECT @@SESSION.sql_mode")
442-
@conn.execute("SET @@SESSION.sql_mode=''")
443431

444-
@conn.execute("INSERT INTO posts (title, body) VALUES('Title', 'Body')")
445-
result = @conn.delete("DELETE FROM posts WHERE id > (0+'foo') LIMIT 1")
432+
with_db_warnings_action(:log) do
433+
ActiveRecord::Base.logger = ActiveSupport::Logger.new(nil)
434+
435+
# Mysql2 will raise an error when attempting to perform a delete that warns if the sql_mode is set to strict
436+
@conn.execute("SET @@SESSION.sql_mode=''")
437+
438+
@conn.execute("INSERT INTO posts (title, body) VALUES('Title', 'Body')")
439+
result = @conn.delete("DELETE FROM posts WHERE id > (0+'foo') LIMIT 1")
446440

447-
assert_equal 1, result
441+
assert_equal 1, result
442+
end
448443
ensure
449444
@conn.execute("SET @@SESSION.sql_mode='#{old_sql_mode}'")
450445
ActiveRecord::Base.logger = previous_logger
451-
ActiveRecord.db_warnings_action = @original_db_warnings_action
452446
end
453447

454448
private

activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,15 @@ def test_allowlist_of_warnings_to_ignore
589589
end
590590
end
591591

592+
def test_allowlist_of_warning_codes_to_ignore
593+
with_example_table do
594+
with_db_warnings_action(:raise, ["01000"]) do
595+
result = @connection.execute("do $$ BEGIN RAISE WARNING 'PostgreSQL SQL warning'; END; $$")
596+
assert_equal [], result.to_a
597+
end
598+
end
599+
end
600+
592601
def test_does_not_raise_notice_level_warnings
593602
with_db_warnings_action(:raise, [/PostgreSQL SQL warning/]) do
594603
result = @connection.execute("DROP TABLE IF EXISTS non_existent_table")
@@ -606,20 +615,6 @@ def connection_without_insert_returning
606615
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
607616
ActiveRecord::Base.postgresql_connection(db_config.configuration_hash.merge(insert_returning: false))
608617
end
609-
610-
def with_db_warnings_action(action, warnings_to_ignore = [])
611-
original_db_warnings_ignore = ActiveRecord.db_warnings_ignore
612-
613-
ActiveRecord.db_warnings_action = action
614-
ActiveRecord.db_warnings_ignore = warnings_to_ignore
615-
@connection.disconnect! # Disconnect from the db so that we reconfigure the connection
616-
617-
yield
618-
ensure
619-
ActiveRecord.db_warnings_action = @original_db_warnings_action
620-
ActiveRecord.db_warnings_ignore = original_db_warnings_ignore
621-
@connection.disconnect!
622-
end
623618
end
624619
end
625620
end

activerecord/test/cases/test_case.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ def with_automatic_scope_inversing(*reflections)
115115
end
116116
end
117117

118+
def with_db_warnings_action(action, warnings_to_ignore = [])
119+
original_db_warnings_ignore = ActiveRecord.db_warnings_ignore
120+
121+
ActiveRecord.db_warnings_action = action
122+
ActiveRecord.db_warnings_ignore = warnings_to_ignore
123+
124+
ActiveRecord::Base.connection.disconnect! # Disconnect from the db so that we reconfigure the connection
125+
126+
yield
127+
ensure
128+
ActiveRecord.db_warnings_action = @original_db_warnings_action
129+
ActiveRecord.db_warnings_ignore = original_db_warnings_ignore
130+
ActiveRecord::Base.connection.disconnect!
131+
end
132+
118133
def reset_callbacks(klass, kind)
119134
old_callbacks = {}
120135
old_callbacks[klass] = klass.send("_#{kind}_callbacks").dup

guides/source/configuring.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1023,7 +1023,7 @@ Controls the action to be taken when a SQL query produces a warning. The followi
10231023

10241024
#### `config.active_record.db_warnings_ignore`
10251025

1026-
Specifies an allowlist of warnings that will be ignored, regardless of the configured `db_warnings_action`.
1026+
Specifies an allowlist of warning codes and messages that will be ignored, regardless of the configured `db_warnings_action`.
10271027
The default behavior is to report all warnings. Warnings to ignore can be specified as Strings or Regexps. For example:
10281028

10291029
```ruby
@@ -1032,6 +1032,7 @@ The default behavior is to report all warnings. Warnings to ignore can be specif
10321032
config.active_record.db_warnings_ignore = [
10331033
/Invalid utf8mb4 character string/,
10341034
"An exact warning message",
1035+
"1062", # MySQL Error 1062: Duplicate entry
10351036
]
10361037
```
10371038

0 commit comments

Comments
 (0)