From efa696d93e74566d98305e2036b72789b643b113 Mon Sep 17 00:00:00 2001 From: Pali Date: Fri, 11 Jan 2019 16:56:27 +0100 Subject: [PATCH] Add support for reporting warnings and information messages Reporting warnings can be enabled/disabled via standard DBI attribute PrintWarn. Both warnings and information messages can be retrieved via standard DBI methods ->err, ->errstr and ->state. Information messages are collected from MySQL "Note" messages and mysql_info() message. This change fixes also reporting mariadb_warning_count attribute for multi result statements. --- dbdimp.c | 529 ++++++++++++++++++++++++++++++++++--- dbdimp.h | 4 + t/29warnings.t | 25 +- t/40server_prepare_crash.t | 5 +- t/41int_min_max.t | 18 +- t/55utf8.t | 14 +- t/76multi_statement.t | 34 ++- 7 files changed, 583 insertions(+), 46 deletions(-) diff --git a/dbdimp.c b/dbdimp.c index 2402b2c2..ecef6e87 100644 --- a/dbdimp.c +++ b/dbdimp.c @@ -1397,6 +1397,412 @@ static void error_no_connection(SV *h, const char *msg) mariadb_dr_do_error(h, CR_CONNECTION_ERROR, msg, "HY000"); } +/************************************************************************** + * + * Name: mariadb_dr_process_warnings_and_info + * + * Purpose: Fetch and process warnings and mysql_info message for the last + * executed SQL query. Set DBI's err, errstr and state according + * to DBI documentation. If there are some existing warnings then + * append new warnings and mysql_info message at the end. + * Call this function after processing of SQL query, after + * mysql_store_result() or mysql_stmt_store_result() call. + * + * Input: h - DBI handle + * imp_dbh - drivers private database handle data + * imp_sth - drivers private statement handle data (may be NULL) + * report_only_warnings - Report to DBI only warnings + * + * Returns: TRUE for success, FALSE otherwise; mariadb_dr_do_error will + * be called in the latter case + * + **************************************************************************/ + +static bool mariadb_dr_process_warnings_and_info(SV *h, imp_dbh_t *imp_dbh, imp_sth_t *imp_sth, bool report_only_warnings) +{ + dTHX; + D_imp_xxh(h); + SV *errstr; + bool append_warnings; + bool append_info; + bool is_warning; + bool is_current_warning; + unsigned int warning_count; + MYSQL_RES *res; + MYSQL_FIELD *fields; + unsigned long *lengths; + MYSQL_ROW row; + my_ulonglong num_rows; + unsigned int show_warnings_count; + unsigned int i; + const char *info; + AV *warnings_av; + AV *warning_row; + SV *sv; + SV **svp; + SV *errno_sv; + SV *error_sv; + SV *diagnostics_sql_sv; + SV *last_warning_state_sv; + SV *last_note_state_sv; + + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t--> mariadb_dr_process_warnings_and_info\n"); + + if (imp_dbh->info) + { + Safefree(imp_dbh->info); + imp_dbh->info = NULL; + } + + info = mysql_info(imp_dbh->pmysql); + if (info) + info = imp_dbh->info = savepv(info); + + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\tmysql_info: %s\n", info ? info : "NULL"); + + imp_dbh->warning_count = warning_count = mysql_warning_count(imp_dbh->pmysql); + if (imp_sth) + imp_sth->warning_count = warning_count; + + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\twarning_count: %u\n", warning_count); + + is_warning = FALSE; + show_warnings_count = 0; + res = NULL; + + warnings_av = newAV(); + sv_2mortal((SV *)warnings_av); + + /* + * Fetch warnings only when mysql_warning_count() indicated warnings. + * If there is pending continuous multi result it is not possible to issue SQL query. + * So for multi result statements it is possible to fetch warnings only for the last statement. + */ + if (warning_count && !mysql_more_results(imp_dbh->pmysql)) + { + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\tIssuing SQL query: SHOW WARNINGS\n"); + + if (mysql_query(imp_dbh->pmysql, "SHOW WARNINGS") != 0) + { + mariadb_dr_do_error(h, mysql_errno(imp_dbh->pmysql), mysql_error(imp_dbh->pmysql), mysql_sqlstate(imp_dbh->pmysql)); + return FALSE; + } + + res = mysql_store_result(imp_dbh->pmysql); + if (!res) + { + mariadb_dr_do_error(h, mysql_errno(imp_dbh->pmysql), mysql_error(imp_dbh->pmysql), mysql_sqlstate(imp_dbh->pmysql)); + return FALSE; + } + + num_rows = mysql_num_rows(res); + + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t\tnum_rows: %" SVf "\n", SVfARG(sv_2mortal(my_ulonglong2sv(num_rows)))); + + if (mysql_num_fields(res) == 3) + { + if (num_rows <= warning_count) + { + if (num_rows != warning_count) + { + /* Server returned less warnings as indicated */ + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t\tServer returned less warnings in SHOW WARNINGS as indicated\n"); + } + show_warnings_count = num_rows; + } + else + { + /* Server returned more warnings as indicated */ + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t\tError: Server returned more warnings in SHOW WARNINGS as indicated, truncating to %u\n", warning_count); + show_warnings_count = warning_count; + } + } + else + { + /* Server returned incompatible result set, so do not use it */ + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t\tError: Server returned incompatible result set for SHOW WARNINGS\n"); + + mysql_free_result(res); + res = NULL; + } + + if (res) + { + /* Process SHOW WARNINGS result, it does not contain sqlstate */ + fields = mysql_fetch_fields(res); + while ((row = mysql_fetch_row(res))) + { + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t\tRow: Level: %s Code: %s Message: %s\n", row[0], row[1], row[2]); + + if (AvFILL(warnings_av)+1 >= show_warnings_count) + { + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t\t\tignoring extra row\n"); + continue; + } + + warning_row = newAV(); + av_push(warnings_av, newRV_noinc((SV *)warning_row)); + + lengths = mysql_fetch_lengths(res); + + for (i = 0; i < 3; ++i) + { + sv = newSVpvn(row[i], lengths[i]); + av_push(warning_row, sv); + if (mysql_charsetnr_is_utf8(fields[i].charsetnr)) + sv_utf8_decode(sv); + } + } + + mysql_free_result(res); + res = NULL; + + /* If server supports GET DIAGNOSTICS then fetch sqlstate for each SHOW WARNING row */ + if (show_warnings_count > 0 && imp_dbh->support_get_diagnostics) + { + errno_sv = sv_newmortal(); + error_sv = sv_newmortal(); + diagnostics_sql_sv = sv_newmortal(); + + for (i = 0; i < show_warnings_count; ++i) + { + sv_setpvf(diagnostics_sql_sv, "GET DIAGNOSTICS CONDITION %u @_dbd_mariadb_sqlstate = RETURNED_SQLSTATE, @_dbd_mariadb_errno = MYSQL_ERRNO, @_dbd_mariadb_error = MESSAGE_TEXT", i+1); + + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\tIssuing SQL query: %" SVf "\n", SVfARG(diagnostics_sql_sv)); + + if (mysql_real_query(imp_dbh->pmysql, SvPVX(diagnostics_sql_sv), SvCUR(diagnostics_sql_sv)) != 0 || + mysql_query(imp_dbh->pmysql, "SELECT @_dbd_mariadb_sqlstate, @_dbd_mariadb_errno, @_dbd_mariadb_error") != 0) + { + mariadb_dr_do_error(h, mysql_errno(imp_dbh->pmysql), mysql_error(imp_dbh->pmysql), mysql_sqlstate(imp_dbh->pmysql)); + return FALSE; + } + + res = mysql_store_result(imp_dbh->pmysql); + if (!res) + { + mariadb_dr_do_error(h, mysql_errno(imp_dbh->pmysql), mysql_error(imp_dbh->pmysql), mysql_sqlstate(imp_dbh->pmysql)); + return FALSE; + } + + if (mysql_num_rows(res) != 1 || mysql_num_fields(res) != 3) + { + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t\tError: Server returned incompatible result set for SELECT after GET DIAGNOSTICS\n"); + mysql_free_result(res); + continue; + } + + fields = mysql_fetch_fields(res); + + row = mysql_fetch_row(res); + if (!row) + { + mysql_free_result(res); + mariadb_dr_do_error(h, mysql_errno(imp_dbh->pmysql), mysql_error(imp_dbh->pmysql), mysql_sqlstate(imp_dbh->pmysql)); + return FALSE; + } + + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t\tRow: sqlstate: %s errno: %s error: %s\n", row[0], row[1], row[2]); + + lengths = mysql_fetch_lengths(res); + + SvUTF8_off(errno_sv); + sv_setpvn(errno_sv, row[1], lengths[1]); + if (mysql_charsetnr_is_utf8(fields[1].charsetnr)) + sv_utf8_decode(errno_sv); + + SvUTF8_off(error_sv); + sv_setpvn(error_sv, row[2], lengths[2]); + if (mysql_charsetnr_is_utf8(fields[2].charsetnr)) + sv_utf8_decode(error_sv); + + warning_row = (AV *)SvRV(*av_fetch(warnings_av, i, FALSE)); + + if (sv_cmp(*av_fetch(warning_row, 1, FALSE), errno_sv) == 0 && + sv_cmp(*av_fetch(warning_row, 2, FALSE), error_sv) == 0) + { + sv = newSVpvn(row[0], lengths[0]); + av_push(warning_row, sv); + if (mysql_charsetnr_is_utf8(fields[0].charsetnr)) + sv_utf8_decode(sv); + } + else + { + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t\tError: GET DIAGNOSTICS row does not match with SHOW WARNINGS row, ignoring sqlstate\n"); + } + } + + /* Clean-up temporary variables */ + if (mysql_query(imp_dbh->pmysql, "SET @_dbd_mariadb_sqlstate = NULL, @_dbd_mariadb_errno = NULL, @_dbd_mariadb_error = NULL") != 0) + { + mariadb_dr_do_error(h, mysql_errno(imp_dbh->pmysql), mysql_error(imp_dbh->pmysql), mysql_sqlstate(imp_dbh->pmysql)); + return FALSE; + } + } + } + } + + /* If there are some existing warnings append new warnings at the end */ + append_warnings = FALSE; + errstr = DBIc_ERRSTR(imp_xxh); + if (SvOK(errstr)) + { + SvPV_force_nolen(errstr); + if (SvCUR(errstr) > 0) + { + append_warnings = TRUE; + errstr = sv_newmortal(); + } + } + + SvUTF8_off(errstr); + sv_setpvs(errstr, ""); + + last_warning_state_sv = NULL; + last_note_state_sv = NULL; + + if (show_warnings_count < warning_count) + { + /* + * Server returned smaller set of warnings as reported in number of total count of warnings or + * multi result statement is in process. In this case just prepend lines with unknown warnings. + * This can happen when session variable max_error_count is set to too low value. + */ + is_warning = TRUE; + av_unshift(warnings_av, warning_count - show_warnings_count); + for (i = 0; i < warning_count - show_warnings_count; ++i) + { + warning_row = newAV(); + av_store(warnings_av, i, newRV_noinc((SV *)warning_row)); + av_push(warning_row, newSVpvs("Warning")); + av_push(warning_row, newSVpvs("0")); + av_push(warning_row, newSVpvs("Unknown warning")); + } + } + + for (i = 0; i <= AvFILL(warnings_av); ++i) + { + /* List of warnings for DBI should be stored in one string, more warnings should be separated by newlines */ + if (SvCUR(errstr) > 0) + sv_catpvs(errstr, "\n"); + + warning_row = (AV *)SvRV(*av_fetch(warnings_av, i, FALSE)); + + /* Use following format for one warning line: " (): " */ + /* If SQLSTATE is not available then omit it. */ + + sv = *av_fetch(warning_row, 0, FALSE); + is_current_warning = !memEQs(SvPVX(sv), SvCUR(sv), "Note"); + if (is_current_warning && report_only_warnings) + continue; + sv_catsv(errstr, sv); + + if (is_current_warning && !is_warning) + is_warning = TRUE; + + sv = *av_fetch(warning_row, 1, FALSE); + sv_catpvs(errstr, " "); + sv_catsv(errstr, sv); + + svp = av_fetch(warning_row, 3, FALSE); + sv = svp ? *svp : NULL; + if (sv) + { + sv_catpvs(errstr, " ("); + sv_catsv(errstr, sv); + sv_catpvs(errstr, ")"); + + if (is_current_warning) + last_warning_state_sv = sv; + else + last_note_state_sv = sv; + } + + sv = *av_fetch(warning_row, 2, FALSE); + sv_catpvs(errstr, ": "); + sv_catsv(errstr, sv); + } + + if (append_warnings && SvCUR(errstr) > 0) + { + sv_catpvs(DBIc_ERRSTR(imp_xxh), "\n"); + sv_catsv(DBIc_ERRSTR(imp_xxh), errstr); + } + + append_info = append_warnings; + if (info && info[0] && !report_only_warnings) + { + /* If there are some existing warnings append info at the end */ + if (!append_info && SvCUR(errstr) > 0) + append_info = TRUE; + + if (!append_warnings && append_info) + errstr = sv_newmortal(); + else + SvUTF8_off(errstr); + + sv_setpv(errstr, info); + sv_utf8_decode(errstr); + + if (append_info) + { + sv_catpvs(DBIc_ERRSTR(imp_xxh), "\n"); + sv_catsv(DBIc_ERRSTR(imp_xxh), errstr); + } + } + + if (SvCUR(errstr) > 0) + { + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\tis_warning: %d\n", is_warning ? 1 : 0); + + if (is_warning && (!append_warnings || (SvPOK(DBIc_ERR(imp_xxh)) && SvCUR(DBIc_ERR(imp_xxh)) == 0))) + { + /* When there is no state use generic warning "01000" */ + if (!last_warning_state_sv) + sv_setpvs(DBIc_STATE(imp_xxh), "01000"); + else + sv_setsv(DBIc_STATE(imp_xxh), last_warning_state_sv); + + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\tsqlstate: %" SVf "\n", SVfARG(DBIc_STATE(imp_xxh))); + + sv_setiv(DBIc_ERR(imp_xxh), 0); /* 0 indicates warning condition */ + } + else if (!append_info || !SvOK(DBIc_ERR(imp_xxh))) + { + /* When there is no state use generic success "00000" */ + if (!last_note_state_sv) + sv_setpvs(DBIc_STATE(imp_xxh), "00000"); + else + sv_setsv(DBIc_STATE(imp_xxh), last_note_state_sv); + + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\tsqlstate: %" SVf "\n", SVfARG(DBIc_STATE(imp_xxh))); + + sv_setpvs(DBIc_ERR(imp_xxh), ""); /* Empty string indicates 'success with information' condition */ + } + } + + if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) + PerlIO_printf(DBIc_LOGPIO(imp_xxh), "\t\t<-- mariadb_dr_process_warnings_and_info\n"); + + return TRUE; +} + static int mariadb_dr_socket_cloexec(my_socket sock_os) { #ifdef _WIN32 @@ -2362,6 +2768,24 @@ static bool mariadb_dr_connect( } } + /* + * Enable warnings and notes. System variable sql_notes does not have to be + * supported which indicates that both warnings and notes are controlled via + * common system variable sql_warnings. + */ + if (mysql_query(sock, "SET sql_warnings = 'ON'") != 0) + { + mariadb_dr_do_error(dbh, mysql_errno(sock), mysql_error(sock), mysql_sqlstate(sock)); + mariadb_db_disconnect(dbh, imp_dbh); + return FALSE; + } + if (mysql_query(sock, "SET sql_notes = 'ON'") != 0 && mysql_errno(sock) != ER_UNKNOWN_SYSTEM_VARIABLE) + { + mariadb_dr_do_error(dbh, mysql_errno(sock), mysql_error(sock), mysql_sqlstate(sock)); + mariadb_db_disconnect(dbh, imp_dbh); + return FALSE; + } + /* we turn off Mysql's auto reconnect and handle re-connecting ourselves so that we can keep track of when this happens. @@ -2439,6 +2863,35 @@ static bool mariadb_dr_connect( imp_dbh->async_query_in_flight = NULL; + imp_dbh->server_version = 0; +#ifndef MARIADB_BASE_VERSION + { + unsigned int major, minor, patch; + const char *serverinfo = mysql_get_server_info(sock); + /* serverinfo for MariaDB server from MySQL client is prefixed by string 5.5.5- */ + if (serverinfo && strBEGINs(serverinfo, "5.5.5-")) + { + /* And in this case mysql_get_server_version() returns just 50505 and not correct + * MariaDB server version. So parse serverversion manually from serverinfo. */ + serverinfo += sizeof("5.5.5-")-1; + if (sscanf(serverinfo, "%u.%u.%u", &major, &minor, &patch) == 3) + imp_dbh->server_version = 10000UL * major + 100UL * minor + patch; + } + } +#endif + if (!imp_dbh->server_version) + imp_dbh->server_version = mysql_get_server_version(sock); + + { + /* The GET DIAGNOSTICS statement was added in MariaDB 10.0.4. */ + /* The GET DIAGNOSTICS statement is available as of MySQL 5.6.4. */ + const char *serverinfo = mysql_get_server_info(sock); + if (strstr(serverinfo, "MariaDB") || strstr(serverinfo, "-maria-")) + imp_dbh->support_get_diagnostics = (imp_dbh->server_version >= 100004); + else + imp_dbh->support_get_diagnostics = (imp_dbh->server_version >= 50604); + } + mariadb_list_add(imp_drh->active_imp_dbhs, imp_dbh->list_entry, imp_dbh); if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) @@ -2705,6 +3158,7 @@ IV mariadb_db_do6(SV *dbh, imp_dbh_t *imp_dbh, SV *statement_sv, SV *attribs, I3 STRLEN blen; unsigned long int num_params; unsigned int error; + unsigned int prev_warning_count; ASYNC_CHECK_RETURN(dbh, -2); @@ -2808,11 +3262,14 @@ IV mariadb_db_do6(SV *dbh, imp_dbh_t *imp_dbh, SV *statement_sv, SV *attribs, I3 } imp_dbh->insertid = mysql_insert_id(imp_dbh->pmysql); } + failed = !mariadb_dr_process_warnings_and_info(dbh, imp_dbh, NULL, TRUE); if (result) { mysql_free_result(result); result = NULL; } + if (failed) + return -2; } if (next_result_rc > 0) @@ -2864,7 +3321,7 @@ IV mariadb_db_do6(SV *dbh, imp_dbh_t *imp_dbh, SV *statement_sv, SV *attribs, I3 /* And also fallback when placeholder is used in unsupported * construction with old server versions (e.g. LIMIT ?) */ (mysql_stmt_errno(stmt) == ER_PARSE_ERROR && - mysql_get_server_version(imp_dbh->pmysql) < 50007 && + imp_dbh->server_version < 50007 && strstr(mysql_stmt_error(stmt), "'?")))) { use_server_side_prepare = FALSE; @@ -2975,6 +3432,9 @@ IV mariadb_db_do6(SV *dbh, imp_dbh_t *imp_dbh, SV *statement_sv, SV *attribs, I3 if (retval != (my_ulonglong)-1 && !async && !result) imp_dbh->insertid = mysql_insert_id(imp_dbh->pmysql); + if (!mariadb_dr_process_warnings_and_info(dbh, imp_dbh, NULL, FALSE)) + retval = (my_ulonglong)-1; + if (result) { mysql_free_result(result); @@ -2994,6 +3454,11 @@ IV mariadb_db_do6(SV *dbh, imp_dbh_t *imp_dbh, SV *statement_sv, SV *attribs, I3 } if (!result) /* Next statement without result set, new insert id */ imp_dbh->insertid = mysql_insert_id(imp_dbh->pmysql); + prev_warning_count = imp_dbh->warning_count; + if (!mariadb_dr_process_warnings_and_info(dbh, imp_dbh, NULL, FALSE)) + retval = (my_ulonglong)-1; + /* Next statement, new warning count, report sum of all */ + imp_dbh->warning_count += prev_warning_count; if (result) mysql_free_result(result); result = NULL; @@ -3342,6 +3807,12 @@ void mariadb_db_destroy(SV* dbh, imp_dbh_t* imp_dbh) { mariadb_db_disconnect(dbh, imp_dbh); } + if (imp_dbh->info) + { + Safefree(imp_dbh->info); + imp_dbh->info = NULL; + } + /* Tell DBI, that dbh->destroy must no longer be called */ DBIc_off(imp_dbh, DBIcf_IMPSET); } @@ -3675,8 +4146,7 @@ SV* mariadb_db_FETCH_attrib(SV *dbh, imp_dbh_t *imp_dbh, SV *keysv) } else if (memEQs(key, kl, "mariadb_info")) { - const char *info = imp_dbh->pmysql ? mysql_info(imp_dbh->pmysql) : NULL; - result = info ? sv_2mortal(newSVpv(info, 0)) : &PL_sv_undef; + result = imp_dbh->info ? sv_2mortal(newSVpv(imp_dbh->info, 0)) : &PL_sv_undef; sv_utf8_decode(result); } else if (memEQs(key, kl, "mariadb_insertid")) @@ -3732,21 +4202,7 @@ SV* mariadb_db_FETCH_attrib(SV *dbh, imp_dbh_t *imp_dbh, SV *keysv) } else if (memEQs(key, kl, "mariadb_serverversion")) { -#ifndef MARIADB_BASE_VERSION - unsigned int major, minor, patch; - const char *serverinfo = imp_dbh->pmysql ? mysql_get_server_info(imp_dbh->pmysql) : NULL; - /* serverinfo for MariaDB server from MySQL client is prefixed by string 5.5.5- */ - if (serverinfo && strBEGINs(serverinfo, "5.5.5-")) - { - /* And in this case mysql_get_server_version() returns just 50505 and not correct - * MariaDB server version. So parse serverversion manually from serverinfo. */ - serverinfo += sizeof("5.5.5-")-1; - if (sscanf(serverinfo, "%u.%u.%u", &major, &minor, &patch) == 3) - result = sv_2mortal(newSVuv(10000UL * major + 100UL * minor + patch)); - } -#endif - if (!result) - result = imp_dbh->pmysql ? sv_2mortal(newSVuv(mysql_get_server_version(imp_dbh->pmysql))) : &PL_sv_undef; + result = sv_2mortal(newSVuv(imp_dbh->server_version)); } else if (memEQs(key, kl, "mariadb_sock")) result = sv_2mortal(newSViv(PTR2IV(imp_dbh->pmysql))); @@ -3765,7 +4221,7 @@ SV* mariadb_db_FETCH_attrib(SV *dbh, imp_dbh_t *imp_dbh, SV *keysv) else if (memEQs(key, kl, "mariadb_thread_id")) result = imp_dbh->pmysql ? sv_2mortal(newSVuv(mysql_thread_id(imp_dbh->pmysql))) : &PL_sv_undef; else if (memEQs(key, kl, "mariadb_warning_count")) - result = imp_dbh->pmysql ? sv_2mortal(newSVuv(mysql_warning_count(imp_dbh->pmysql))) : &PL_sv_undef; + result = sv_2mortal(newSVuv(imp_dbh->warning_count)); else if (memEQs(key, kl, "mariadb_use_result")) result = boolSV(imp_dbh->use_mysql_use_result); else if (memEQs(key, kl, "mariadb_multi_statements")) @@ -3869,7 +4325,7 @@ AV *mariadb_db_data_sources(SV *dbh, imp_dbh_t *imp_dbh, SV *attr) return av; } -static bool mariadb_st_free_result_sets(SV *sth, imp_sth_t *imp_sth, bool free_last); +static bool mariadb_st_free_result_sets(SV *sth, imp_sth_t *imp_sth, bool free_last, bool report_only_warnings); /* ************************************************************************** @@ -4001,7 +4457,7 @@ mariadb_st_prepare_sv( Clean-up previous result set(s) for sth to prevent 'Commands out of sync' error */ - if (!mariadb_st_free_result_sets(sth, imp_sth, TRUE)) + if (!mariadb_st_free_result_sets(sth, imp_sth, TRUE, TRUE)) return 0; if (imp_sth->use_server_side_prepare) @@ -4056,7 +4512,7 @@ mariadb_st_prepare_sv( /* And also fallback when placeholder is used in unsupported * construction with old server versions (e.g. LIMIT ?) */ (mysql_stmt_errno(imp_sth->stmt) == ER_PARSE_ERROR && - mysql_get_server_version(imp_dbh->pmysql) < 50007 && + imp_dbh->server_version < 50007 && strstr(mysql_stmt_error(imp_sth->stmt), "'?")))) { if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) @@ -4146,11 +4602,12 @@ mariadb_st_prepare_sv( * Inputs: sth - Statement handle * imp_sth - driver's private statement handle * free_last - free also the last result set + * report_only_warnings - Report to DBI only warnings * * Returns: TRUE ok * FALSE error; mariadb_dr_do_error will be called *************************************************************************/ -static bool mariadb_st_free_result_sets(SV *sth, imp_sth_t *imp_sth, bool free_last) +static bool mariadb_st_free_result_sets(SV *sth, imp_sth_t *imp_sth, bool free_last, bool report_only_warnings) { dTHX; D_imp_dbh_from_sth; @@ -4191,6 +4648,9 @@ static bool mariadb_st_free_result_sets(SV *sth, imp_sth_t *imp_sth, bool free_l } imp_dbh->insertid = imp_sth->insertid = mysql_insert_id(imp_dbh->pmysql); } + + if (!mariadb_dr_process_warnings_and_info(sth, imp_dbh, imp_sth, report_only_warnings)) + imp_sth->row_num = (my_ulonglong)-1; } if (imp_sth->result && (mysql_more_results(imp_dbh->pmysql) || free_last)) { @@ -4334,8 +4794,6 @@ bool mariadb_st_more_results(SV* sth, imp_sth_t* imp_sth) next_result_return_code= mysql_next_result(imp_dbh->pmysql); - imp_sth->warning_count = mysql_warning_count(imp_dbh->pmysql); - /* mysql_next_result returns 0 if there are more results @@ -4397,6 +4855,9 @@ bool mariadb_st_more_results(SV* sth, imp_sth_t* imp_sth) if (imp_sth->is_async && mysql_more_results(imp_dbh->pmysql)) imp_dbh->async_query_in_flight = imp_sth; + if (!mariadb_dr_process_warnings_and_info(sth, imp_dbh, imp_sth, FALSE)) + return FALSE; + return TRUE; } } @@ -4790,7 +5251,7 @@ IV mariadb_st_execute_iv(SV* sth, imp_sth_t* imp_sth) Clean-up previous result set(s) for sth to prevent 'Commands out of sync' error */ - if (!mariadb_st_free_result_sets(sth, imp_sth, TRUE)) + if (!mariadb_st_free_result_sets(sth, imp_sth, TRUE, TRUE)) return -2; imp_sth->currow = 0; @@ -4873,9 +5334,10 @@ IV mariadb_st_execute_iv(SV* sth, imp_sth_t* imp_sth) if (!use_server_side_prepare) imp_sth->done_desc = FALSE; } - } - imp_sth->warning_count = mysql_warning_count(imp_dbh->pmysql); + if (!mariadb_dr_process_warnings_and_info(sth, imp_dbh, imp_sth, FALSE)) + imp_sth->row_num = (my_ulonglong)-1; + } if (DBIc_TRACE_LEVEL(imp_xxh) >= 2) { @@ -5573,7 +6035,7 @@ int mariadb_st_finish(SV* sth, imp_sth_t* imp_sth) { Clean-up previous result set(s) for sth to prevent 'Commands out of sync' error */ - if (!mariadb_st_free_result_sets(sth, imp_sth, FALSE)) + if (!mariadb_st_free_result_sets(sth, imp_sth, FALSE, FALSE)) return 0; /* @@ -5619,7 +6081,7 @@ void mariadb_st_destroy(SV *sth, imp_sth_t *imp_sth) { /* During global destruction, DBI objects are destroyed in random order * and therefore imp_dbh may be already freed. So do not access it. */ mariadb_st_finish(sth, imp_sth); - mariadb_st_free_result_sets(sth, imp_sth, TRUE); + mariadb_st_free_result_sets(sth, imp_sth, TRUE, FALSE); } DBIc_ACTIVE_off(imp_sth); @@ -6739,9 +7201,13 @@ my_ulonglong mariadb_db_async_result(SV* h, MYSQL_RES** resp) if (!*resp) dbh->insertid = mysql_insert_id(svsock); + if (!mariadb_dr_process_warnings_and_info(h, dbh, NULL, FALSE)) + retval = (my_ulonglong)-1; + if(htype == DBIt_ST) { D_imp_sth(h); - D_imp_dbh_from_sth; + + imp_sth->warning_count = dbh->warning_count; imp_sth->row_num = retval; @@ -6755,7 +7221,6 @@ my_ulonglong mariadb_db_async_result(SV* h, MYSQL_RES** resp) if (imp_sth->row_num) DBIc_ACTIVE_on(imp_sth); } - imp_sth->warning_count = mysql_warning_count(imp_dbh->pmysql); } if (*resp && resp == &_res) diff --git a/dbdimp.h b/dbdimp.h index 8b715810..0f4ec437 100644 --- a/dbdimp.h +++ b/dbdimp.h @@ -540,6 +540,10 @@ struct imp_dbh_st { bool use_multi_statements; void* async_query_in_flight; my_ulonglong insertid; + unsigned int warning_count; + unsigned long server_version; + bool support_get_diagnostics; + char *info; struct { unsigned int auto_reconnects_ok; unsigned int auto_reconnects_failed; diff --git a/t/29warnings.t b/t/29warnings.t index a520b4da..3c1173bb 100644 --- a/t/29warnings.t +++ b/t/29warnings.t @@ -12,19 +12,33 @@ use vars qw($test_dsn $test_user $test_password); my $dbh = DbiTestConnect($test_dsn, $test_user, $test_password, { RaiseError => 1, PrintError => 0, AutoCommit => 0 }); -plan tests => 14; +plan tests => 22; ok(defined $dbh, "Connected to database"); +# test success with information ok(my $sth= $dbh->prepare("DROP TABLE IF EXISTS no_such_table")); ok($sth->execute()); +is($sth->err, '', 'execute finished as success with information'); +my @notes = grep /^Note /, split /\n/, $sth->errstr || ''; +is(scalar @notes, 1, 'only one information message for sth'); +like($notes[0], qr/Unknown table/, 'correct information message text for sth'); +is($sth->{mariadb_warning_count}, 1, 'warning_count is correct for sth'); -is($sth->{mariadb_warning_count}, 1, 'warnings from sth'); - +# test warnings ok($dbh->do("SET sql_mode=''")); ok($dbh->do("CREATE TEMPORARY TABLE dbd_drv_sth_warnings (c CHAR(1))")); +{ +local $dbh->{PrintWarn} = 0; ok($dbh->do("INSERT INTO dbd_drv_sth_warnings (c) VALUES ('perl'), ('dbd'), ('mysql')")); -is($dbh->{mariadb_warning_count}, 3, 'warnings from dbh'); +} +is($sth->err, 0, 'do finished with warnings'); +my @warnings = grep /^Warning /, split /\n/, $sth->errstr || ''; +is(scalar @warnings, 3, 'number of warning messages is correct for dbh'); +like($warnings[0], qr/Data truncated/, 'correct first warning message text for dbh'); +like($warnings[1], qr/Data truncated/, 'correct second warning message text for dbh'); +like($warnings[2], qr/Data truncated/, 'correct third warning message text for dbh'); +is($dbh->{mariadb_warning_count}, 3, 'warning_count is correct for dbh'); # tests to make sure mariadb_warning_count is the same as reported by mysql_info(); @@ -34,7 +48,10 @@ ok($dbh->do("CREATE TEMPORARY TABLE dbd_drv_count_warnings (i TINYINT NOT NULL)" my $q = "INSERT INTO dbd_drv_count_warnings VALUES (333),('as'),(3)"; ok($sth = $dbh->prepare($q)); +{ +local $sth->{PrintWarn} = 0; ok($sth->execute()); +} is($sth->{'mariadb_warning_count'}, 2 ); diff --git a/t/40server_prepare_crash.t b/t/40server_prepare_crash.t index 6e09f5ec..5d0a7bd2 100644 --- a/t/40server_prepare_crash.t +++ b/t/40server_prepare_crash.t @@ -29,8 +29,11 @@ ok $sth->execute(6, "x" x 1000000); ok $sth = $dbh->prepare("SELECT * FROM t WHERE i=? AND n=?"); ok $sth->bind_param(2, "x" x 1000000); -ok $sth->bind_param(1, "abcx", 12); +ok $sth->bind_param(1, "abcx", DBI::SQL_VARCHAR); +{ +local $sth->{PrintWarn} = 0; # Hide warning: Truncated incorrect DOUBLE value: 'abcx' ok $sth->execute(); +} ok $sth->finish(); ok $sth->bind_param(2, "a" x 1000000); diff --git a/t/41int_min_max.t b/t/41int_min_max.t index 4a192265..93440de3 100644 --- a/t/41int_min_max.t +++ b/t/41int_min_max.t @@ -17,7 +17,7 @@ if ($dbh->{mariadb_serverversion} < 50002) { "SKIP TEST: You must have MySQL version 5.0.2 and greater for this test to run"; } # nostrict tests + strict tests + init/tear down commands -plan tests => (19*8 + 19*8 + 3) * 2; +plan tests => (22*8 + 22*8 + 3) * 2; my $table = 'dbd_mysql_t41minmax'; # name of the table we will be using my $mode; # 'strict' or 'nostrict' corresponds to strict SQL mode @@ -71,13 +71,19 @@ sub test_int_type ($$$$) { ok($store->bind_param( 1, ($min-1)->bstr(), $dbh->{mariadb_server_prepare} ? DBI::SQL_VARCHAR : $perl_type ), "binding less than minimal $mariadb_type, mode=$mode"); if ($mode eq 'strict') { ok !defined eval{$store->execute()}; + ok($store->err()); like($store->errstr(), qr/Out of range value (?:adjusted )?for column 'val'/) or note("Error, you stored ".($min-1)." into $mariadb_type, mode=$mode\n". Data::Dumper->Dump([$dbh->selectall_arrayref("SELECT * FROM $table")]). Data::Dumper->Dump([$dbh->selectall_arrayref("describe $table")]) ); } else { - ok($store->execute()); + { + local $store->{PrintWarn} = 0; + ok($store->execute()); + } + is($store->err(), 0); + like($store->errstr(), qr/Out of range value (?:adjusted )?for column 'val'/); ######################################## # Check that it was rounded correctly ######################################## @@ -92,13 +98,19 @@ sub test_int_type ($$$$) { ok($store->bind_param( 1, ($max+1)->bstr(), $dbh->{mariadb_server_prepare} ? DBI::SQL_VARCHAR : $perl_type ), "binding more than maximal $mariadb_type, mode=$mode"); if ($mode eq 'strict') { ok !defined eval{$store->execute()}; + ok($store->err()); like($store->errstr(), qr/Out of range value (?:adjusted )?for column 'val'/) or note("Error, you stored ".($max+1)." into $mariadb_type, mode=$mode\n". Data::Dumper->Dump([$dbh->selectall_arrayref("SELECT * FROM $table")]). Data::Dumper->Dump([$dbh->selectall_arrayref("describe $table")]) ); } else { - ok($store->execute()); + { + local $store->{PrintWarn} = 0; + ok($store->execute()); + } + is($store->err(), 0); + like($store->errstr(), qr/Out of range value (?:adjusted )?for column 'val'/); ######################################## # Check that it was rounded correctly ######################################## diff --git a/t/55utf8.t b/t/55utf8.t index cd08d9af..5b8a4fab 100644 --- a/t/55utf8.t +++ b/t/55utf8.t @@ -16,7 +16,7 @@ binmode $tb->todo_output, ":utf8"; my $dbh = DbiTestConnect($test_dsn, $test_user, $test_password, { RaiseError => 1, PrintError => 0, AutoCommit => 0 }); -plan tests => 40 * 2; +plan tests => 43 * 2; for my $mariadb_server_prepare (0, 1) { $dbh= DBI->connect("$test_dsn;mariadb_server_prepare=$mariadb_server_prepare;mariadb_server_prepare_disable_fallback=1", $test_user, $test_password, @@ -75,11 +75,17 @@ ok $sth->bind_param(4, $unicode_str); ok $sth->bind_param(5, $unicode_str2); ok $sth->bind_param(6, $unicode_str); ok $sth->bind_param(7, $unicode_str2); +{ +local $sth->{PrintWarn} = 0; ok $sth->execute(); +} -cmp_ok($dbh->{mariadb_warning_count}, '==', 1, 'got warning for INSERT') or do { diag("SHOW WARNINGS:"); diag($_->[2]) foreach $dbh->selectall_array("SHOW WARNINGS", { mariadb_server_prepare => 0 }); }; -my (undef, undef, $warning) = $dbh->selectrow_array("SHOW WARNINGS", { mariadb_server_prepare => 0 }); -like($warning, qr/^(?:Incorrect string value: '\\xC4\\x80dam'|Data truncated) for column (?:'ascii'|`.*`\.`.*`\.`ascii`) at row 1$/, 'warning is correct'); +is($sth->err, 0, 'got warning for INSERT'); +my @warnings = grep /^Warning /, split /\n/, $sth->errstr || ''; +is(scalar @warnings, 1, 'number of warning messages is correct'); +like($warnings[0], qr/^Warning 1366(?: \(22007\))?: (?:Incorrect string value: '\\xC4\\x80dam'|Data truncated) for column (?:'ascii'|`.*`\.`.*`\.`ascii`) at row 1$/, 'warning message text is correct'); +cmp_ok($sth->{mariadb_warning_count}, '==', 1, 'sth warning_count is correct'); +cmp_ok($dbh->{mariadb_warning_count}, '==', 1, 'dbh warning_count is correct'); # AsBinary() is deprecated as of MySQL 5.7.6, use ST_AsBinary() instead my $asbinary = $dbh->{mariadb_serverversion} >= 50706 ? 'ST_AsBinary' : 'AsBinary'; diff --git a/t/76multi_statement.t b/t/76multi_statement.t index 2f3fdc7a..6be3e80c 100644 --- a/t/76multi_statement.t +++ b/t/76multi_statement.t @@ -14,7 +14,7 @@ my $dbh = DbiTestConnect($test_dsn, $test_user, $test_password, { RaiseError => 1, PrintError => 0, AutoCommit => 0, mariadb_multi_statements => 1 }); -plan tests => 81; +plan tests => 95; ok (defined $dbh, "Connected to database with multi statement support"); @@ -39,7 +39,10 @@ $dbh->{mariadb_server_prepare}= 0; is($sth->rows, 1, "First update affected 1 row"); is($sth->{mariadb_warning_count}, 0, "First update had no warnings"); ok($sth->{Active}, "Statement handle is Active"); - ok($sth->more_results()); + { + local $sth->{PrintWarn} = 0; + ok($sth->more_results()); + } is($sth->rows, 2, "Second update affected 2 rows"); is($sth->{mariadb_warning_count}, 2, "Second update had 2 warnings"); ok(not $sth->more_results()); @@ -54,6 +57,33 @@ $dbh->{mariadb_server_prepare}= 0; # Test that do() reports errors from all result sets ok(!eval { $dbh->do("INSERT INTO dbd_mysql_t76multi VALUES (1); INSERT INTO bad_dbd_mysql_t76multi VALUES (2);") }, "do() reports errors"); + # Test warnings for do() + { + local $dbh->{PrintWarn} = 0; + ok($dbh->do("INSERT INTO dbd_mysql_t76multi VALUES ('first non integer value'); INSERT INTO dbd_mysql_t76multi VALUES ('second non integer value');"), "do() with warnings"); + } + is($dbh->err, 0, "DBI indicates warnings"); + is(scalar grep(/^Warning /, split(/\n/, $dbh->errstr || '')), 2, "DBI reported correct number of warnings"); + is($dbh->{mariadb_warning_count}, 2, "DBI reported correct number of warnings"); + + # Test warnings for execute() and more_results() + ok($sth = $dbh->prepare("INSERT INTO dbd_mysql_t76multi VALUES ('first non integer value'); INSERT INTO dbd_mysql_t76multi VALUES ('second non integer value');"), "prepare()"); + { + local $sth->{PrintWarn} = 0; + ok($sth->execute, "execute() with warnings"); + } + is($sth->err, 0, "DBI indicates warnings for first statement"); + is(scalar grep(/^Warning /, split(/\n/, $dbh->errstr || '')), 1, "DBI reported correct number of warnings for first statement"); + is($sth->{mariadb_warning_count}, 1, "DBI reported correct number of warnings for first statement"); + { + local $sth->{PrintWarn} = 0; + ok($sth->more_results(), "more_results() with warnings"); + } + is($sth->err, 0, "DBI indicates warnings for second statement"); + is(scalar grep(/^Warning /, split(/\n/, $dbh->errstr || '')), 1, "DBI reported correct number of warnings for second statement"); + is($sth->{mariadb_warning_count}, 1, "DBI reported correct number of warnings for second statement"); + ok(!$sth->more_results(), "no more results"); + # Test that execute() reports errors from only the first result set ok($sth = $dbh->prepare("UPDATE dbd_mysql_t76multi SET a=2; UPDATE bad_dbd_mysql_t76multi SET a=3")); ok($sth->execute(), "Execute updates");