diff options
Diffstat (limited to 'sql/sql_error.cc')
-rw-r--r-- | sql/sql_error.cc | 638 |
1 files changed, 546 insertions, 92 deletions
diff --git a/sql/sql_error.cc b/sql/sql_error.cc index 9ea7facbe41..2837e45fb47 100644 --- a/sql/sql_error.cc +++ b/sql/sql_error.cc @@ -1,4 +1,5 @@ -/* Copyright (C) 1995-2002 MySQL AB +/* Copyright (C) 1995-2002 MySQL AB, + Copyright (C) 2008-2009 Sun Microsystems, Inc This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -42,133 +43,577 @@ This file contains the implementation of error and warnings related ***********************************************************************/ #include "mysql_priv.h" +#include "sql_error.h" #include "sp_rcontext.h" /* - Store a new message in an error object - - This is used to in group_concat() to register how many warnings we actually - got after the query has been executed. + Design notes about MYSQL_ERROR::m_message_text. + + The member MYSQL_ERROR::m_message_text contains the text associated with + an error, warning or note (which are all SQL 'conditions') + + Producer of MYSQL_ERROR::m_message_text: + ---------------------------------------- + + (#1) the server implementation itself, when invoking functions like + my_error() or push_warning() + + (#2) user code in stored programs, when using the SIGNAL statement. + + (#3) user code in stored programs, when using the RESIGNAL statement. + + When invoking my_error(), the error number and message is typically + provided like this: + - my_error(ER_WRONG_DB_NAME, MYF(0), ...); + - my_message(ER_SLAVE_IGNORED_TABLE, ER(ER_SLAVE_IGNORED_TABLE), MYF(0)); + + In both cases, the message is retrieved from ER(ER_XXX), which in turn + is read from the resource file errmsg.sys at server startup. + The strings stored in the errmsg.sys file are expressed in the character set + that corresponds to the server --language start option + (see error_message_charset_info). + + When executing: + - a SIGNAL statement, + - a RESIGNAL statement, + the message text is provided by the user logic, and is expressed in UTF8. + + Storage of MYSQL_ERROR::m_message_text: + --------------------------------------- + + (#4) The class MYSQL_ERROR is used to hold the message text member. + This class represents a single SQL condition. + + (#5) The class Warning_info represents a SQL condition area, and contains + a collection of SQL conditions in the Warning_info::m_warn_list + + Consumer of MYSQL_ERROR::m_message_text: + ---------------------------------------- + + (#6) The statements SHOW WARNINGS and SHOW ERRORS display the content of + the warning list. + + (#7) The GET DIAGNOSTICS statement (planned, not implemented yet) will + also read the content of: + - the top level statement condition area (when executed in a query), + - a sub statement (when executed in a stored program) + and return the data stored in a MYSQL_ERROR. + + (#8) The RESIGNAL statement reads the MYSQL_ERROR caught by an exception + handler, to raise a new or modified condition (in #3). + + The big picture + --------------- + -------------- + | ^ + V | + my_error(#1) SIGNAL(#2) RESIGNAL(#3) | + |(#A) |(#B) |(#C) | + | | | | + ----------------------------|---------------------------- | + | | + V | + MYSQL_ERROR(#4) | + | | + | | + V | + Warning_info(#5) | + | | + ----------------------------------------------------- | + | | | | + | | | | + | | | | + V V V | + SHOW WARNINGS(#6) GET DIAGNOSTICS(#7) RESIGNAL(#8) | + | | | | | + | -------- | V | + | | | -------------- + V | | + Connectors | | + | | | + ------------------------- + | + V + Client application + + Current implementation status + ----------------------------- + + (#1) (my_error) produces data in the 'error_message_charset_info' CHARSET + + (#2) and (#3) (SIGNAL, RESIGNAL) produces data internally in UTF8 + + (#6) (SHOW WARNINGS) produces data in the 'error_message_charset_info' CHARSET + + (#7) (GET DIAGNOSTICS) is not implemented. + + (#8) (RESIGNAL) produces data internally in UTF8 (see #3) + + As a result, the design choice for (#4) and (#5) is to store data in + the 'error_message_charset_info' CHARSET, to minimize impact on the code base. + This is implemented by using 'String MYSQL_ERROR::m_message_text'. + + The UTF8 -> error_message_charset_info conversion is implemented in + Signal_common::eval_signal_informations() (for path #B and #C). + + Future work + ----------- + + - Change (#1) (my_error) to generate errors in UTF8. + See WL#751 (Recoding of error messages) + + - Change (#4 and #5) to store message text in UTF8 natively. + In practice, this means changing the type of the message text to + '<UTF8 String 128 class> MYSQL_ERROR::m_message_text', and is a direct + consequence of WL#751. + + - Implement (#9) (GET DIAGNOSTICS). + See WL#2111 (Stored Procedures: Implement GET DIAGNOSTICS) */ -void MYSQL_ERROR::set_msg(THD *thd, const char *msg_arg) +MYSQL_ERROR::MYSQL_ERROR() + : Sql_alloc(), + m_class_origin((const char*) NULL, 0, & my_charset_utf8_bin), + m_subclass_origin((const char*) NULL, 0, & my_charset_utf8_bin), + m_constraint_catalog((const char*) NULL, 0, & my_charset_utf8_bin), + m_constraint_schema((const char*) NULL, 0, & my_charset_utf8_bin), + m_constraint_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_catalog_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_schema_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_table_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_column_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_cursor_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_message_text(), + m_sql_errno(0), + m_level(MYSQL_ERROR::WARN_LEVEL_ERROR), + m_mem_root(NULL) { - msg= strdup_root(&thd->warn_root, msg_arg); + memset(m_returned_sqlstate, 0, sizeof(m_returned_sqlstate)); } +void MYSQL_ERROR::init(MEM_ROOT *mem_root) +{ + DBUG_ASSERT(mem_root != NULL); + DBUG_ASSERT(m_mem_root == NULL); + m_mem_root= mem_root; +} -/* - Reset all warnings for the thread - - SYNOPSIS - mysql_reset_errors() - thd Thread handle - force Reset warnings even if it has been done before +void MYSQL_ERROR::clear() +{ + m_class_origin.length(0); + m_subclass_origin.length(0); + m_constraint_catalog.length(0); + m_constraint_schema.length(0); + m_constraint_name.length(0); + m_catalog_name.length(0); + m_schema_name.length(0); + m_table_name.length(0); + m_column_name.length(0); + m_cursor_name.length(0); + m_message_text.length(0); + m_sql_errno= 0; + m_level= MYSQL_ERROR::WARN_LEVEL_ERROR; +} - IMPLEMENTATION - Don't reset warnings if this has already been called for this query. - This may happen if one gets a warning during the parsing stage, - in which case push_warnings() has already called this function. -*/ +MYSQL_ERROR::MYSQL_ERROR(MEM_ROOT *mem_root) + : Sql_alloc(), + m_class_origin((const char*) NULL, 0, & my_charset_utf8_bin), + m_subclass_origin((const char*) NULL, 0, & my_charset_utf8_bin), + m_constraint_catalog((const char*) NULL, 0, & my_charset_utf8_bin), + m_constraint_schema((const char*) NULL, 0, & my_charset_utf8_bin), + m_constraint_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_catalog_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_schema_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_table_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_column_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_cursor_name((const char*) NULL, 0, & my_charset_utf8_bin), + m_message_text(), + m_sql_errno(0), + m_level(MYSQL_ERROR::WARN_LEVEL_ERROR), + m_mem_root(mem_root) +{ + DBUG_ASSERT(mem_root != NULL); + memset(m_returned_sqlstate, 0, sizeof(m_returned_sqlstate)); +} -void mysql_reset_errors(THD *thd, bool force) +static void copy_string(MEM_ROOT *mem_root, String* dst, const String* src) { - DBUG_ENTER("mysql_reset_errors"); - if (thd->query_id != thd->warn_id || force) + size_t len= src->length(); + if (len) { - thd->warn_id= thd->query_id; - free_root(&thd->warn_root,MYF(0)); - bzero((char*) thd->warn_count, sizeof(thd->warn_count)); - if (force) - thd->total_warn_count= 0; - thd->warn_list.empty(); - thd->row_count= 1; // by default point to row 1 + char* copy= (char*) alloc_root(mem_root, len + 1); + if (copy) + { + memcpy(copy, src->ptr(), len); + copy[len]= '\0'; + dst->set(copy, len, src->charset()); + } } + else + dst->length(0); +} + +void +MYSQL_ERROR::copy_opt_attributes(const MYSQL_ERROR *cond) +{ + DBUG_ASSERT(this != cond); + copy_string(m_mem_root, & m_class_origin, & cond->m_class_origin); + copy_string(m_mem_root, & m_subclass_origin, & cond->m_subclass_origin); + copy_string(m_mem_root, & m_constraint_catalog, & cond->m_constraint_catalog); + copy_string(m_mem_root, & m_constraint_schema, & cond->m_constraint_schema); + copy_string(m_mem_root, & m_constraint_name, & cond->m_constraint_name); + copy_string(m_mem_root, & m_catalog_name, & cond->m_catalog_name); + copy_string(m_mem_root, & m_schema_name, & cond->m_schema_name); + copy_string(m_mem_root, & m_table_name, & cond->m_table_name); + copy_string(m_mem_root, & m_column_name, & cond->m_column_name); + copy_string(m_mem_root, & m_cursor_name, & cond->m_cursor_name); +} + +void +MYSQL_ERROR::set(uint sql_errno, const char* sqlstate, + MYSQL_ERROR::enum_warning_level level, const char* msg) +{ + DBUG_ASSERT(sql_errno != 0); + DBUG_ASSERT(sqlstate != NULL); + DBUG_ASSERT(msg != NULL); + + m_sql_errno= sql_errno; + memcpy(m_returned_sqlstate, sqlstate, SQLSTATE_LENGTH); + m_returned_sqlstate[SQLSTATE_LENGTH]= '\0'; + + set_builtin_message_text(msg); + m_level= level; +} + +void +MYSQL_ERROR::set_builtin_message_text(const char* str) +{ + /* + See the comments + "Design notes about MYSQL_ERROR::m_message_text." + */ + const char* copy; + + copy= strdup_root(m_mem_root, str); + m_message_text.set(copy, strlen(copy), error_message_charset_info); + DBUG_ASSERT(! m_message_text.is_alloced()); +} + +const char* +MYSQL_ERROR::get_message_text() const +{ + return m_message_text.ptr(); +} + +int +MYSQL_ERROR::get_message_octet_length() const +{ + return m_message_text.length(); +} + +void +MYSQL_ERROR::set_sqlstate(const char* sqlstate) +{ + memcpy(m_returned_sqlstate, sqlstate, SQLSTATE_LENGTH); + m_returned_sqlstate[SQLSTATE_LENGTH]= '\0'; +} + +/** + Clear this diagnostics area. + + Normally called at the end of a statement. +*/ + +void +Diagnostics_area::reset_diagnostics_area() +{ + DBUG_ENTER("reset_diagnostics_area"); +#ifdef DBUG_OFF + can_overwrite_status= FALSE; + /** Don't take chances in production */ + m_message[0]= '\0'; + m_sql_errno= 0; + m_server_status= 0; + m_affected_rows= 0; + m_last_insert_id= 0; + m_statement_warn_count= 0; +#endif + is_sent= FALSE; + /** Tiny reset in debug mode to see garbage right away */ + m_status= DA_EMPTY; DBUG_VOID_RETURN; } -/* - Push the warning/error to error list if there is still room in the list +/** + Set OK status -- ends commands that do not return a + result set, e.g. INSERT/UPDATE/DELETE. +*/ - SYNOPSIS - push_warning() - thd Thread handle - level Severity of warning (note, warning, error ...) - code Error number - msg Clear error message - - RETURN - pointer on MYSQL_ERROR object +void +Diagnostics_area::set_ok_status(THD *thd, ulonglong affected_rows_arg, + ulonglong last_insert_id_arg, + const char *message_arg) +{ + DBUG_ENTER("set_ok_status"); + DBUG_ASSERT(! is_set()); + /* + In production, refuse to overwrite an error or a custom response + with an OK packet. + */ + if (is_error() || is_disabled()) + return; + + m_server_status= thd->server_status; + m_statement_warn_count= thd->warning_info->statement_warn_count(); + m_affected_rows= affected_rows_arg; + m_last_insert_id= last_insert_id_arg; + if (message_arg) + strmake(m_message, message_arg, sizeof(m_message) - 1); + else + m_message[0]= '\0'; + m_status= DA_OK; + DBUG_VOID_RETURN; +} + + +/** + Set EOF status. */ -MYSQL_ERROR *push_warning(THD *thd, MYSQL_ERROR::enum_warning_level level, - uint code, const char *msg) +void +Diagnostics_area::set_eof_status(THD *thd) { - MYSQL_ERROR *err= 0; - DBUG_ENTER("push_warning"); - DBUG_PRINT("enter", ("code: %d, msg: %s", code, msg)); + DBUG_ENTER("set_eof_status"); + /* Only allowed to report eof if has not yet reported an error */ + DBUG_ASSERT(! is_set()); + /* + In production, refuse to overwrite an error or a custom response + with an EOF packet. + */ + if (is_error() || is_disabled()) + return; + + m_server_status= thd->server_status; + /* + If inside a stored procedure, do not return the total + number of warnings, since they are not available to the client + anyway. + */ + m_statement_warn_count= (thd->spcont ? + 0 : thd->warning_info->statement_warn_count()); + + m_status= DA_EOF; + DBUG_VOID_RETURN; +} - DBUG_ASSERT(code != 0); - DBUG_ASSERT(msg != NULL); +/** + Set ERROR status. +*/ - if (level == MYSQL_ERROR::WARN_LEVEL_NOTE && - !(thd->options & OPTION_SQL_NOTES)) - DBUG_RETURN(0); +void +Diagnostics_area::set_error_status(THD *thd, uint sql_errno_arg, + const char *message_arg, + const char *sqlstate) +{ + DBUG_ENTER("set_error_status"); + /* + Only allowed to report error if has not yet reported a success + The only exception is when we flush the message to the client, + an error can happen during the flush. + */ + DBUG_ASSERT(! is_set() || can_overwrite_status); +#ifdef DBUG_OFF + /* + In production, refuse to overwrite a custom response with an + ERROR packet. + */ + if (is_disabled()) + return; +#endif + + if (sqlstate == NULL) + sqlstate= mysql_errno_to_sqlstate(sql_errno_arg); + + m_sql_errno= sql_errno_arg; + memcpy(m_sqlstate, sqlstate, SQLSTATE_LENGTH); + m_sqlstate[SQLSTATE_LENGTH]= '\0'; + strmake(m_message, message_arg, sizeof(m_message)-1); + + m_status= DA_ERROR; + DBUG_VOID_RETURN; +} - if (thd->query_id != thd->warn_id && !thd->spcont) - mysql_reset_errors(thd, 0); - thd->got_warning= 1; - /* Abort if we are using strict mode and we are not using IGNORE */ - if ((int) level >= (int) MYSQL_ERROR::WARN_LEVEL_WARN && - thd->really_abort_on_warning()) - { - /* Avoid my_message() calling push_warning */ - bool no_warnings_for_error= thd->no_warnings_for_error; - sp_rcontext *spcont= thd->spcont; +/** + Mark the diagnostics area as 'DISABLED'. + + This is used in rare cases when the COM_ command at hand sends a response + in a custom format. One example is the query cache, another is + COM_STMT_PREPARE. +*/ - thd->no_warnings_for_error= 1; - thd->spcont= NULL; +void +Diagnostics_area::disable_status() +{ + DBUG_ASSERT(! is_set()); + m_status= DA_DISABLED; +} - thd->killed= THD::KILL_BAD_DATA; - my_message(code, msg, MYF(0)); +Warning_info::Warning_info(ulonglong warn_id_arg) + :m_statement_warn_count(0), + m_current_row_for_warning(1), + m_warn_id(warn_id_arg), + m_read_only(FALSE) +{ + /* Initialize sub structures */ + init_sql_alloc(&m_warn_root, WARN_ALLOC_BLOCK_SIZE, WARN_ALLOC_PREALLOC_SIZE); + m_warn_list.empty(); + bzero((char*) m_warn_count, sizeof(m_warn_count)); +} - thd->spcont= spcont; - thd->no_warnings_for_error= no_warnings_for_error; - /* Store error in error list (as my_message() didn't do it) */ - level= MYSQL_ERROR::WARN_LEVEL_ERROR; - } - if (thd->handle_error(code, msg, level)) - DBUG_RETURN(NULL); +Warning_info::~Warning_info() +{ + free_root(&m_warn_root,MYF(0)); +} - if (thd->spcont && - thd->spcont->handle_error(code, level, thd)) + +/** + Reset the warning information of this connection. +*/ + +void Warning_info::clear_warning_info(ulonglong warn_id_arg) +{ + m_warn_id= warn_id_arg; + free_root(&m_warn_root, MYF(0)); + bzero((char*) m_warn_count, sizeof(m_warn_count)); + m_warn_list.empty(); + m_statement_warn_count= 0; + m_current_row_for_warning= 1; /* Start counting from the first row */ +} + +void Warning_info::reserve_space(THD *thd, uint count) +{ + /* Make room for count conditions */ + while ((m_warn_list.elements > 0) && + ((m_warn_list.elements + count) > thd->variables.max_error_count)) + m_warn_list.pop(); +} + +/** + Append warnings only if the original contents of the routine + warning info was replaced. +*/ +void Warning_info::merge_with_routine_info(THD *thd, Warning_info *source) +{ + /* + If a routine body is empty or if a routine did not + generate any warnings (thus m_warn_id didn't change), + do not duplicate our own contents by appending the + contents of the called routine. We know that the called + routine did not change its warning info. + + On the other hand, if the routine body is not empty and + some statement in the routine generates a warning or + uses tables, m_warn_id is guaranteed to have changed. + In this case we know that the routine warning info + contains only new warnings, and thus we perform a copy. + */ + if (m_warn_id != source->m_warn_id) { - DBUG_RETURN(NULL); + /* + If the invocation of the routine was a standalone statement, + rather than a sub-statement, in other words, if it's a CALL + of a procedure, rather than invocation of a function or a + trigger, we need to clear the current contents of the caller's + warning info. + + This is per MySQL rules: if a statement generates a warning, + warnings from the previous statement are flushed. Normally + it's done in push_warning(). However, here we don't use + push_warning() to avoid invocation of condition handlers or + escalation of warnings to errors. + */ + opt_clear_warning_info(thd->query_id); + append_warning_info(thd, source); } - query_cache_abort(&thd->net); +} +/** + Add a warning to the list of warnings. Increment the respective + counters. +*/ +MYSQL_ERROR *Warning_info::push_warning(THD *thd, + uint sql_errno, const char* sqlstate, + MYSQL_ERROR::enum_warning_level level, + const char *msg) +{ + MYSQL_ERROR *cond= NULL; - if (thd->warn_list.elements < thd->variables.max_error_count) + if (! m_read_only) { - /* We have to use warn_root, as mem_root is freed after each query */ - if ((err= new (&thd->warn_root) MYSQL_ERROR(thd, code, level, msg))) - thd->warn_list.push_back(err, &thd->warn_root); + if (m_warn_list.elements < thd->variables.max_error_count) + { + cond= new (& m_warn_root) MYSQL_ERROR(& m_warn_root); + if (cond) + { + cond->set(sql_errno, sqlstate, level, msg); + m_warn_list.push_back(cond, &m_warn_root); + } + } + m_warn_count[(uint) level]++; } - thd->warn_count[(uint) level]++; - thd->total_warn_count++; - DBUG_RETURN(err); + + m_statement_warn_count++; + return cond; } /* - Push the warning/error to error list if there is still room in the list + Push the warning to error list if there is still room in the list + + SYNOPSIS + push_warning() + thd Thread handle + level Severity of warning (note, warning) + code Error number + msg Clear error message +*/ + +void push_warning(THD *thd, MYSQL_ERROR::enum_warning_level level, + uint code, const char *msg) +{ + DBUG_ENTER("push_warning"); + DBUG_PRINT("enter", ("code: %d, msg: %s", code, msg)); + + /* + Calling push_warning/push_warning_printf with a + level of WARN_LEVEL_ERROR *is* a bug. + Either use my_error(), or WARN_LEVEL_WARN. + Please fix the calling code, and do *NOT* + add more work around code in the assert below. + */ + DBUG_ASSERT( (level != MYSQL_ERROR::WARN_LEVEL_ERROR) + || (code == ER_CANT_CREATE_TABLE) /* See Bug#47233 */ + || (code == ER_ILLEGAL_HA_CREATE_OPTION) /* See Bug#47233 */ + ); + + if (level == MYSQL_ERROR::WARN_LEVEL_ERROR) + level= MYSQL_ERROR::WARN_LEVEL_WARN; + + (void) thd->raise_condition(code, NULL, level, msg); + + DBUG_VOID_RETURN; +} + + +/* + Push the warning to error list if there is still room in the list SYNOPSIS push_warning_printf() thd Thread handle - level Severity of warning (note, warning, error ...) + level Severity of warning (note, warning) code Error number msg Clear error message */ @@ -217,10 +662,12 @@ const LEX_STRING warning_level_names[]= }; bool mysqld_show_warnings(THD *thd, ulong levels_to_show) -{ +{ List<Item> field_list; DBUG_ENTER("mysqld_show_warnings"); + DBUG_ASSERT(thd->warning_info->is_read_only()); + field_list.push_back(new Item_empty_string("Level", 7)); field_list.push_back(new Item_return_int("Code",4, MYSQL_TYPE_LONG)); field_list.push_back(new Item_empty_string("Message",MYSQL_ERRMSG_SIZE)); @@ -232,29 +679,36 @@ bool mysqld_show_warnings(THD *thd, ulong levels_to_show) MYSQL_ERROR *err; SELECT_LEX *sel= &thd->lex->select_lex; SELECT_LEX_UNIT *unit= &thd->lex->unit; - ha_rows idx= 0; + ulonglong idx= 0; Protocol *protocol=thd->protocol; unit->set_limit(sel); - List_iterator_fast<MYSQL_ERROR> it(thd->warn_list); + List_iterator_fast<MYSQL_ERROR> it(thd->warning_info->warn_list()); while ((err= it++)) { /* Skip levels that the user is not interested in */ - if (!(levels_to_show & ((ulong) 1 << err->level))) + if (!(levels_to_show & ((ulong) 1 << err->get_level()))) continue; if (++idx <= unit->offset_limit_cnt) continue; if (idx > unit->select_limit_cnt) break; protocol->prepare_for_resend(); - protocol->store(warning_level_names[err->level].str, - warning_level_names[err->level].length, system_charset_info); - protocol->store((uint32) err->code); - protocol->store(err->msg, (uint) strlen(err->msg), system_charset_info); + protocol->store(warning_level_names[err->get_level()].str, + warning_level_names[err->get_level()].length, + system_charset_info); + protocol->store((uint32) err->get_sql_errno()); + protocol->store(err->get_message_text(), + err->get_message_octet_length(), + system_charset_info); if (protocol->write()) DBUG_RETURN(TRUE); } my_eof(thd); + + thd->warning_info->set_read_only(FALSE); + DBUG_RETURN(FALSE); } + |