WL#4166: Prepared statements: automatic re-prepare
Affects: Server-5.1
—
Status: Complete
After WL#4165 is implemented, execution of a prepared statement may fail with a new error -- ER_NEED_REPREPARE. This error is returned when any base table, temporary table, or view used in the statement has been altered since statement prepare. The purpose of validating metadata and terminating statement execution upon a validation error is given in WL#4165, but in short the parse tree of the prepared statement may be no longer valid in the context of the new (altered) schema. Indeed, a used table may be replaced with a view, or undergo changes that abrogate some rule-based optimisations performed on the parsed tree. However, in cases when the parsed tree is invalid, but SQL statement text itself is still meaningful in the new schema, the server should not return the error to the user. This is necessary to preserve backward compatibility with old 5.0 applications, which do not expect the new error, and also to provide continuous operation of the server in 24x7 environments. Generally, we should not complicate the job of client application developers by introducing a new error, unless it is strictly necessary. Recovery from an error is implemented by means of re-prepare of the terminated prepared statement. Details of this procedure, as well as new introduced errors, variables and protocol flags are specified in this worklog.
A reprepare happens when execution of the prepared statement failed with ER_NEED_REPREPARE error. A reprepare of a prepared statement is, in fact, a creation of a new prepared statement from the original SQL statement text and in the original environment (current database, SQL mode). Each attempt to reprepare a statement increments new session status variable - Com_stmt_reprepare. The global counterpart of this variable contains the total number of reprepare attempts done in all sessions. If preparation of the copy fails, the whatever error that occurred during preparation is returned to the user, and the copy is destroyed. Otherwise, if the new statement has been created successfully, the parse tree of the original statement is exchanged with the parse tree of the new one. Parameter types and values, which represent the execution state of Item_param objects, are copied in the opposite direction, from the original parsed tree to the new one. This produces a prepared statement that has the new parse tree but the original parameter data. In addition to the status variable, the client is notified about the automatic reprepare by means of a new result set status flag -- SERVER_STATUS_STATEMENT_REPREPARED. In case the statement returns a result set, result set metadata has been previously transferred to the client, and the number of result set columns has changed since then, server status flags also contain SERVER_STATUS_METADATA_CHANGED flag. This flag indicates to the client application that it may need to rebind its output buffers. The flag is necessary, since 4.1 and 5.0 versions of the client library could crash if the number of columns changes between prepare and execute. In future additional checks could be implemented: - for type - character set and collation - width and precision - pad space option - null/not null option All MySQL connectors currently support this type of result set metadata change by performing an automatic data conversion whenever necessary. Therefore, and since there is no request from any Connector to implement the additional checks, they've been made subject of a separate worklog entry, WL#4177 "Prepared statements: additonal metadata checks after reprepare". An attempt to execute a statement after reprepare may fail again with ER_NEED_REPREPARE error: the server does not keep the tables locked between prepare and execute, and thus a new DDL statement may happen in between. In that case a reprepare is executed again, but only until MAX_REPREPARE_ATTEMTS (a constant in sql_prepare.cc) has been reached. Since ER_NEED_REPREPARE is returned by the execution engine, each failed reprepare attempt not only increases Com_stmt_reprepare and Com_stmt_execute statistics counter, but also the respective SQLCOM status counter, such as Com_insert or Com_select. Reprepare has a side effect on the query cache: if at the time of reprepare the query cache is switched off, the new statement will not use it in all subsequent executions, even if later the cache is switched back on again. For performance reasons, the decision to allow a prepared statement work with the query cache is made at prepare time, and thus is triggered automatically at reprepare. This reevaluation could be suppressed, however, the whole issue is considered too minor to justify the work necessary to preserve full backward compatibility. Note, that reprepare never leads to destruction of the original statement, even if it fails. The operation is considered to be fully automatic, and any error it generates -- intermittent. MySQL supports two kinds of prepared statements -- available through extensions of the client/server API, and SQL prepared statements, available inside stored procedures and ordinary SQL. The differences between these two kinds of prepared statements are: - SQL prepared statements do not return metadata to the client at prepare. If prepare succeeded, OK status packet is returned, similarly to INSERT or UPDATE SQL commands. On the contrary, C API prepared statement compute and return result set metadata (if any) at statement prepare. - placeholders in SQL prepared statements ('?', also known as parameter markers) can be assigned from user variables (@variables) only. Value for placeholders of C API parameter markers are retrieved from COM_STMT_EXECUTE protocol packet, or, in case of blob data, assembled from multiple COM_STMT_LONG_DATA packets This difference in working with parameters and metadata resulted in two entry points into statement execution subsystem in the current server: mysql_stmt_execute() (C API prepared statements) mysql_sql_stmt_execute() (SQL prepared statements). The job of the two functions above is to take care of the different way of assignment of parameter values. The rest of the work is delegated to method Prepared_statement::execute(). This structure of execution defines the shape of low level design. After introduction of metadata validation, Prepared_statement::execute() can return with a new error code. Therefore, mysql_stmt_execute() and mysql_sql_stmt_execute() must be changed to handle the new return value. The following new methods are introduced to support reexecution: Prepared_statement::execute_loop() -- try to execute a statement, reprepare in case of validation error, and try again until MAX_REPREPARE_ATTEMPTS has been reached. mysql_{sql_}stmt_execute() are changed to invoke this method instead of plain execute(). Prepared_statement::reprepare() -- reprepare a prepared statement, called from execute_loop() in case of ER_NEED_REPREPARE error. Prepared_statement::validate_metadata() -- called from reprepare() in case of reprepare success to check how much metadata of the result set of the new prepared statement is different from the metadata of the original prepared statement, and update the server status in case of a significant change. As specified earlier, only the number of columns is currently checked. Prepared_statement::swap_prepared_statement() -- called from reprepare() in case of reprepare() success to exchange the parsed trees of the new and the old prepared statements. swap_parameter_array() -- called from reprepare() in case of success to swap the data and preserve types of parameter markers, if any. Use of "swapping", instead of "copying" is employed to efficiently transfer the new parsed tree to the original statement, while preserving the execution parameters. Let's consider it in more detail. Schematically, the parsed tree consists of the LEX and its members, Query_arena instance and the parse memory root. Among LEX members, there is a list of items to represent prepared statement parameter markers (lex->param_list). Parameter markers of the original statement are assigned values, which are stored on the system heap. "Swapping" of the parse trees exchanges the contents of the old and the new prepared statement step by step, pointer by pointer. New and old lex are exchanged, then new and old memory roots, and so on. In the end of this process the original prepared statement contains state of the new one and vice versa. However, the original prepared statement is not ready for execution before its parameters have been assigned. Thus, before destroying the new (now old) prepared statement, a similar "swapping" is performed for parameter data, which ensures that potentially large memory blocks for blob data are not copied. Item_param::set_param_type_and_swap_value() -- called from swap_parameter_array() to initialize/swap data of an individual parameter marker. Pseudocode of some of these methods follows below. mysql_sql_stmt_execute() {invoke Prepared_statement::execute_loop(); } mysql_stmt_execute() { invoke Prepared_statement::execute_loop(); } Prepared_statement::execute_loop() { int reprepare_attemt= 0; reexecute: error= execute(); if (error == ER_NEED_REPREPARE && reprepare_attempt < MAX_REPREPARE_ATTEMPTS) { thd->clear_error(); error= reprepare(); if (! error) /* Success */ goto reexecute; } return error; } Prepared_statement::reprepare() { Prepared_statement *copy= new Prepared_statement; error= copy->prepare(query, query_length); if (! error) { swap_prepared_statement(this, copy); swap_parameter_array(param_array, copy->param_array, param_count); validate_metadata(); } delete copy; return error; } Prepared_statement::validate_metadata(Prepared_statement *copy) { if (this->param_count != copy->param_count) thd->server_status|= SERVER_STATUS_METADATA_CHANGED; } Prepared_statement::swap_prepared_statement() { swap_variables(ulong, id, copy->id); swap_variables(MEM_ROOT *, mem_root, copy->mem_root); swap_variables(Item_param **, param_array, copy->param_array); swap_variables(LEX *, lex, copy->lex); ... } void swap_parameter_array(Item_param **param_array1, Item_param **param_array2) { Item_param *parameter1, *parameter2; for each (parameter1 in param_array1, parameter2 in param_array2) parameter1->set_type_and_swap_value(parameter2); } Annex: comparison to other RDBMS -------------------------------- DB2 --- References: http://publib.boulder.ibm.com/infocenter/db2luw/v8//topic/com.ibm.db2.udb.doc/admin/r0000948.htm DB2 does implement a reprepare internally, and the documentation lists all the statements that can trigger a reprepare.
This task has a moderate impact on server modules and files: - all prepared statement related changes reside in sql_prepare.cc, where class Prepared_statement, mysql_stmt_execute(), mysql_sql_stmt_execute() and all introduced methods are implemented - Item_param class, which resides in item.{h,cc} is extended with method set_param_type_and_swap_value() - the client library (libmysql/libmysql.c) is fixed to not crash upon a changed number of result set columns - various tests are added to new ps_ddl.test in the main test suite and in mysql_client_test.c
Copyright (c) 2000, 2024, Oracle Corporation and/or its affiliates. All rights reserved.