Skip to content

Commit f67cbdd

Browse files
committed
Bug#16587369: HANDLER FOR SQLEXCEPTION NOT CATCHING ERROR
1452 (FK CONSTRAINT) FOR UPDATE Overview -------- This is a bug regarding the handling of fatal errors. A fatal error is an error that should not be caught by a condition handler in a stored procedure. The issues described in the bug report are that: 1. The same error is handled differently for different statements. 2. Seemingly trivial errors are treated as fatal. It should be noted that in the context of this bug report (and bug fix), only handler errors are relevant to consider. There are two important issues regarding handler errors in the source code: 1. Deciding whether a handler error is to be considered fatal. 2. Ensuring that the handler error is indeed treated as fatal. For the first issue, "is_fatal_error()" is provided by the handler api, and returns true for handler errors that are fatal. The second issue can be handled in various ways; the most common is to call "print_error()" with a flag indicating that the handler error is to be considered fatal. Inspecting how the source code handles these two issues, in light of the problems reported, reveals three shortcomings in the implementation: 1. Handler errors are in fact fatal by default, as defined by the "is_fatal_error()" function. 2. When an error is returned from the handler api, the caller does not always check if the error is fatal. 3. The presence of a fatal error is not always flagged when calling "print_error()". Suggested fix ------------- The scope and ambition for the fix is to treat handler errors consistently for DML statements (update, insert, delete) by making sure "is_fatal_error()" is called before calling "print_error()", and to provide the "ME_FATALERROR" flag as appopriate. This alone does not fix the bug that is reported, since handler errors are fatal by default, hence, we also need to extend the function "is_fatal_error()" to recognize more handler errors as being non-fatal. An attempt was made to consider handler errors non-fatal by default, but this made a large number of tests fail in not very obvious ways, so this solution was not pursued further. Instead, a switch for catching non-fatal errors was added to "is_fatal_error()", currently catching: - HA_ERR_NO_REFERENCED_ROW: The error originally addressed by the bug report considered here. - HA_ERR_ROW_IS_REFERENCED: Another FK constraint violation error, an obvious extension of the bug we are addressing. - HA_ERR_LOCK_WAIT_TIMEOUT: See below. Changing the implementation of "is_fatal_error()" has a potential impact on the non-DML source code, which also calls this function; however, no damage has been observed in the testing that has been done. On the other hand, tests failed for other reasons: 1. Some tests expected a fatal outcome in situations where the error is now non-fatal (e.g. for foreign key constraint violations). The fix for this was to modify the test. 2. Some tests expected a non-fatal outcome where the error is now fatal due to the changed implementation of DML statements where we ensure that errors are treated the way they ought to. The fix for this was to extend "is_fatal_error()" to consider the error in question as non fatal (the error here was lock wait timeout). Re-considering which handler errors should be considered fatal is still a relevant task, but is considered beyond the scope of this bug fix. Additionally, such a task might be the responsibility of the storage engine teams.
1 parent 5ded800 commit f67cbdd

File tree

5 files changed

+186
-65
lines changed

5 files changed

+186
-65
lines changed

sql/handler.cc

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3581,6 +3581,41 @@ void print_keydup_error(TABLE *table, KEY *key, myf errflag)
35813581
}
35823582

35833583

3584+
/**
3585+
This method is used to analyse the error to see whether the error
3586+
is ignorable or not. Further comments in header file.
3587+
*/
3588+
3589+
bool handler::is_fatal_error(int error, uint flags)
3590+
{
3591+
DBUG_ENTER("is_fatal_error");
3592+
// Error code 0 is not fatal, dup key errors may be explicitly ignored
3593+
if (!error || ((flags & HA_CHECK_DUP_KEY) &&
3594+
(error == HA_ERR_FOUND_DUPP_KEY ||
3595+
error == HA_ERR_FOUND_DUPP_UNIQUE)))
3596+
DBUG_RETURN(false);
3597+
3598+
// Catch errors that are not fatal
3599+
switch (error)
3600+
{
3601+
/*
3602+
Lock wait timeout was treated as 'non-fatal' by e.g. insert code,
3603+
and as 'fatal' by update code prior to fix of bug#16587369.
3604+
In order to change current behavior as little as possible, we
3605+
for now treat lock wait timeout as non-fatal rather than fatal.
3606+
*/
3607+
case HA_ERR_LOCK_WAIT_TIMEOUT:
3608+
// Foreign key constraint violations are not fatal:
3609+
case HA_ERR_ROW_IS_REFERENCED:
3610+
case HA_ERR_NO_REFERENCED_ROW:
3611+
DBUG_RETURN(false);
3612+
}
3613+
3614+
// Default is that an error is fatal
3615+
DBUG_RETURN(true);
3616+
}
3617+
3618+
35843619
/**
35853620
Print error that we got from handler function.
35863621

sql/handler.h

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,24 +2193,26 @@ class handler :public Sql_alloc
21932193
virtual uint extra_rec_buf_length() const { return 0; }
21942194

21952195
/**
2196-
This method is used to analyse the error to see whether the error
2197-
is ignorable or not, certain handlers can have more error that are
2198-
ignorable than others. E.g. the partition handler can get inserts
2199-
into a range where there is no partition and this is an ignorable
2200-
error.
2196+
@brief Determine whether an error is fatal or not.
2197+
2198+
@details This method is used to analyze the error to see whether the
2199+
error is fatal or not. Handlers can have different sets of fatal errors,
2200+
e.g. the partition handler can get inserts into a range where there
2201+
is no partition, and this is an ignorable error.
2202+
22012203
HA_ERR_FOUND_DUP_UNIQUE is a special case in MyISAM that means the
2202-
same thing as HA_ERR_FOUND_DUP_KEY but can in some cases lead to
2204+
same thing as HA_ERR_FOUND_DUP_KEY, but can in some cases lead to
22032205
a slightly different error message.
2206+
2207+
@param error error code received from the handler interface (HA_ERR_...)
2208+
@param flags indicate whether duplicate key errors should be ignorable
2209+
2210+
@return whether the error is fatal or not
2211+
@retval true the error is fatal
2212+
@retval false the error is not fatal
22042213
*/
2205-
virtual bool is_fatal_error(int error, uint flags)
2206-
{
2207-
if (!error ||
2208-
((flags & HA_CHECK_DUP_KEY) &&
2209-
(error == HA_ERR_FOUND_DUPP_KEY ||
2210-
error == HA_ERR_FOUND_DUPP_UNIQUE)))
2211-
return FALSE;
2212-
return TRUE;
2213-
}
2214+
2215+
virtual bool is_fatal_error(int error, uint flags);
22142216

22152217
/**
22162218
Number of rows in table. It will only be called if

sql/sql_delete.cc

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
bool mysql_delete(THD *thd, TABLE_LIST *table_list, Item *conds,
5050
SQL_I_List<ORDER> *order_list, ha_rows limit, ulonglong options)
5151
{
52+
myf error_flags= MYF(0); /**< Flag for fatal errors */
5253
bool will_batch;
5354
int error, loc_error;
5455
TABLE *table;
@@ -205,7 +206,10 @@ bool mysql_delete(THD *thd, TABLE_LIST *table_list, Item *conds,
205206
}
206207
if (error != HA_ERR_WRONG_COMMAND)
207208
{
208-
table->file->print_error(error,MYF(0));
209+
if (table->file->is_fatal_error(error, HA_CHECK_DUP_KEY))
210+
error_flags|= ME_FATALERROR;
211+
212+
table->file->print_error(error, error_flags);
209213
error=0;
210214
goto cleanup;
211215
}
@@ -383,7 +387,10 @@ bool mysql_delete(THD *thd, TABLE_LIST *table_list, Item *conds,
383387
/* If quick select is used, initialize it before retrieving rows. */
384388
if (select && select->quick && (error= select->quick->reset()))
385389
{
386-
table->file->print_error(error, MYF(0));
390+
if (table->file->is_fatal_error(error, HA_CHECK_DUP_KEY))
391+
error_flags|= ME_FATALERROR;
392+
393+
table->file->print_error(error, error_flags);
387394
err= true;
388395
goto exit_without_my_ok;
389396
}
@@ -458,7 +465,10 @@ bool mysql_delete(THD *thd, TABLE_LIST *table_list, Item *conds,
458465
}
459466
else
460467
{
461-
table->file->print_error(error,MYF(0));
468+
if (table->file->is_fatal_error(error, HA_CHECK_DUP_KEY))
469+
error_flags|= ME_FATALERROR;
470+
471+
table->file->print_error(error, error_flags);
462472
/*
463473
In < 4.0.14 we set the error number to 0 here, but that
464474
was not sensible, because then MySQL would not roll back the
@@ -487,7 +497,12 @@ bool mysql_delete(THD *thd, TABLE_LIST *table_list, Item *conds,
487497
{
488498
/* purecov: begin inspected */
489499
if (error != 1)
490-
table->file->print_error(loc_error,MYF(0));
500+
{
501+
if (table->file->is_fatal_error(loc_error, HA_CHECK_DUP_KEY))
502+
error_flags|= ME_FATALERROR;
503+
504+
table->file->print_error(loc_error, error_flags);
505+
}
491506
error=1;
492507
/* purecov: end */
493508
}
@@ -891,7 +906,11 @@ bool multi_delete::send_data(List<Item> &values)
891906
If the IGNORE option is used errors caused by ha_delete_row don't
892907
have to stop the iteration.
893908
*/
894-
table->file->print_error(error,MYF(0));
909+
myf error_flags= MYF(0);
910+
if (table->file->is_fatal_error(error, HA_CHECK_DUP_KEY))
911+
error_flags|= ME_FATALERROR;
912+
913+
table->file->print_error(error, error_flags);
895914
DBUG_RETURN(1);
896915
}
897916
}
@@ -1035,6 +1054,7 @@ int multi_delete::do_deletes()
10351054
*/
10361055
int multi_delete::do_table_deletes(TABLE *table, bool ignore)
10371056
{
1057+
myf error_flags= MYF(0); /**< Flag for fatal errors */
10381058
int local_error= 0;
10391059
READ_RECORD info;
10401060
ha_rows last_deleted= deleted;
@@ -1060,7 +1080,10 @@ int multi_delete::do_table_deletes(TABLE *table, bool ignore)
10601080
local_error= table->file->ha_delete_row(table->record[0]);
10611081
if (local_error && !ignore)
10621082
{
1063-
table->file->print_error(local_error, MYF(0));
1083+
if (table->file->is_fatal_error(local_error, HA_CHECK_DUP_KEY))
1084+
error_flags|= ME_FATALERROR;
1085+
1086+
table->file->print_error(local_error, error_flags);
10641087
break;
10651088
}
10661089

@@ -1087,7 +1110,10 @@ int multi_delete::do_table_deletes(TABLE *table, bool ignore)
10871110
if (tmp_error && !local_error)
10881111
{
10891112
local_error= tmp_error;
1090-
table->file->print_error(local_error, MYF(0));
1113+
if (table->file->is_fatal_error(local_error, HA_CHECK_DUP_KEY))
1114+
error_flags|= ME_FATALERROR;
1115+
1116+
table->file->print_error(local_error, error_flags);
10911117
}
10921118
}
10931119
if (last_deleted != deleted && !table->file->has_transactions())

sql/sql_insert.cc

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -783,11 +783,14 @@ bool mysql_insert(THD *thd,TABLE_LIST *table_list,
783783
*/
784784
{
785785
table->file->ha_release_auto_increment();
786-
if (thd->locked_tables_mode <= LTM_LOCK_TABLES &&
787-
table->file->ha_end_bulk_insert() && !error)
786+
if (thd->locked_tables_mode <= LTM_LOCK_TABLES && !error &&
787+
(error= table->file->ha_end_bulk_insert()))
788788
{
789-
table->file->print_error(my_errno,MYF(0));
790-
error=1;
789+
myf error_flags= MYF(0);
790+
if (table->file->is_fatal_error(error, HA_CHECK_DUP_KEY))
791+
error_flags|= ME_FATALERROR;
792+
793+
table->file->print_error(error, error_flags);
791794
}
792795
if (duplic != DUP_ERROR || ignore)
793796
table->file->extra(HA_EXTRA_NO_IGNORE_DUP_KEY);
@@ -1613,11 +1616,17 @@ int write_record(THD *thd, TABLE *table, COPY_INFO *info, COPY_INFO *update)
16131616
DBUG_RETURN(trg_error);
16141617

16151618
err:
1616-
info->last_errno= error;
1617-
DBUG_ASSERT(thd->lex->current_select() != NULL);
1619+
{
1620+
myf error_flags= MYF(0); /**< Flag for fatal errors */
1621+
info->last_errno= error;
1622+
DBUG_ASSERT(thd->lex->current_select() != NULL);
16181623
thd->lex->current_select()->no_error= 0; // Give error
1619-
table->file->print_error(error,MYF(0));
1620-
1624+
if (table->file->is_fatal_error(error, HA_CHECK_DUP_KEY))
1625+
error_flags|= ME_FATALERROR;
1626+
1627+
table->file->print_error(error, error_flags);
1628+
}
1629+
16211630
before_trg_err:
16221631
table->file->restore_auto_increment(prev_insert_id);
16231632
if (key)
@@ -2089,7 +2098,11 @@ bool select_insert::send_eof()
20892098

20902099
if (error)
20912100
{
2092-
table->file->print_error(error,MYF(0));
2101+
myf error_flags= MYF(0);
2102+
if (table->file->is_fatal_error(my_errno, HA_CHECK_DUP_KEY))
2103+
error_flags|= ME_FATALERROR;
2104+
2105+
table->file->print_error(my_errno, error_flags);
20932106
DBUG_RETURN(1);
20942107
}
20952108

0 commit comments

Comments
 (0)