Skip to content

Commit 6bd1007

Browse files
committed
Allow SQL Warnings to be ignored using error codes
In rails#46690 the `db_warnings_action` and `db_warnings_ignore` configs where added. The `db_warnings_ignore` can take a list of warning messages to match against. At GitHub we have a subscriber that that does something like this but also filters out error codes. There might also be other applications that filter via error codes and this could be something they can use instead of just the explicit messages. This also refactors the adapter tests in order for mysql2 and postgresql adapters to share the same helper when setting the db_warnings_action and db_warnings_ignore configs
1 parent bc081a5 commit 6bd1007

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)