diff options
Diffstat (limited to 'libodb-mysql/odb/mysql/statement.cxx')
-rw-r--r-- | libodb-mysql/odb/mysql/statement.cxx | 814 |
1 files changed, 814 insertions, 0 deletions
diff --git a/libodb-mysql/odb/mysql/statement.cxx b/libodb-mysql/odb/mysql/statement.cxx new file mode 100644 index 0000000..83f294a --- /dev/null +++ b/libodb-mysql/odb/mysql/statement.cxx @@ -0,0 +1,814 @@ +// file : odb/mysql/statement.cxx +// license : GNU GPL v2; see accompanying LICENSE file + +#include <cstring> // std::strlen, std::memmove, std::memset +#include <cassert> + +#include <odb/tracer.hxx> + +#include <odb/mysql/mysql.hxx> +#include <odb/mysql/database.hxx> +#include <odb/mysql/connection.hxx> +#include <odb/mysql/statement.hxx> +#include <odb/mysql/error.hxx> + +using namespace std; + +namespace odb +{ + namespace mysql + { + // statement + // + + statement:: + statement (connection_type& conn, + const string& text, + statement_kind sk, + const binding* process, + bool optimize) + : conn_ (conn) + { + if (process == 0) + { + text_copy_ = text; + text_ = text_copy_.c_str (); + } + else + text_ = text.c_str (); // Temporary, see init(). + + init (text.size (), sk, process, optimize); + } + + statement:: + statement (connection_type& conn, + const char* text, + statement_kind sk, + const binding* process, + bool optimize, + bool copy) + : conn_ (conn) + { + size_t n; + + if (process == 0 && copy) + { + text_copy_ = text; + text_ = text_copy_.c_str (); + n = text_copy_.size (); + } + else + { + text_ = text; // Potentially temporary, see init(). + n = strlen (text_); + } + + init (n, sk, process, optimize); + } + + void statement:: + init (size_t text_size, + statement_kind sk, + const binding* proc, + bool optimize) + { + if (proc != 0) + { + switch (sk) + { + case statement_select: + process_select (text_copy_, + text_, + &proc->bind->buffer, proc->count, sizeof (MYSQL_BIND), + '`', '`', + optimize); + break; + case statement_insert: + process_insert (text_copy_, + text_, + &proc->bind->buffer, proc->count, sizeof (MYSQL_BIND), + '?'); + break; + case statement_update: + process_update (text_copy_, + text_, + &proc->bind->buffer, proc->count, sizeof (MYSQL_BIND), + '?'); + break; + case statement_delete: + assert (false); + } + + text_ = text_copy_.c_str (); + text_size = text_copy_.size (); + } + + // Empty statement. + // + if (*text_ == '\0') + return; + + stmt_.reset (conn_.alloc_stmt_handle ()); + + conn_.clear (); + + { + odb::tracer* t; + if ((t = conn_.transaction_tracer ()) || + (t = conn_.tracer ()) || + (t = conn_.database ().tracer ())) + t->prepare (conn_, *this); + } + + if (mysql_stmt_prepare (stmt_, + text_, + static_cast<unsigned long> (text_size)) != 0) + translate_error (conn_, stmt_); + } + + size_t statement:: + process_bind (MYSQL_BIND* b, size_t n) + { + size_t shifts (0); + for (MYSQL_BIND* e (b + n); b != e;) + { + if (b->buffer == 0) + { + // It is possible that this array has already been processed + // (shared among multiple statements). + // + if (b->length != 0) + { + n -= e - b; + break; + } + + e--; + + // Shift the rest of the entries to the left. + // + memmove (b, b + 1, (e - b) * sizeof (MYSQL_BIND)); + + // Store the original position of the NULL entry at the end. + // + e->buffer = 0; + e->length = reinterpret_cast<unsigned long*> (b + shifts); + + shifts++; + continue; + } + + b++; + } + + return n - shifts; + } + + void statement:: + restore_bind (MYSQL_BIND* b, size_t n) + { + for (MYSQL_BIND* e (b + n - 1); e->buffer == 0 && e->length != 0;) + { + MYSQL_BIND* p (reinterpret_cast<MYSQL_BIND*> (e->length)); + + // Shift the entries from the specified position to the right. + // + memmove (p + 1, p, (e - p) * sizeof (MYSQL_BIND)); + + // Restore the original NULL entry. + // + memset (p, 0, sizeof (MYSQL_BIND)); + } + } + + statement:: + ~statement () + { + if (stmt_ != 0) + { + { + odb::tracer* t; + if ((t = conn_.transaction_tracer ()) || + (t = conn_.tracer ()) || + (t = conn_.database ().tracer ())) + t->deallocate (conn_, *this); + } + + // Let the connection handle the release of the statement (it + // may delay the actual freeing if it will mess up the currently + // active statement). + // + conn_.free_stmt_handle (stmt_); + } + } + + const char* statement:: + text () const + { + return text_; + } + + void statement:: + cancel () + { + } + + // select_statement + // + + select_statement:: + ~select_statement () + { + assert (freed_); + } + + select_statement:: + select_statement (connection_type& conn, + const string& text, + bool process, + bool optimize, + binding& param, + binding& result) + : statement (conn, + text, statement_select, + (process ? &result : 0), optimize), + end_ (false), + cached_ (false), + freed_ (true), + rows_ (0), + param_ (¶m), + param_version_ (0), + result_ (result), + result_version_ (0) + { + } + + select_statement:: + select_statement (connection_type& conn, + const char* text, + bool process, + bool optimize, + binding& param, + binding& result, + bool copy_text) + : statement (conn, + text, statement_select, + (process ? &result : 0), optimize, + copy_text), + end_ (false), + cached_ (false), + freed_ (true), + rows_ (0), + param_ (¶m), + param_version_ (0), + result_ (result), + result_version_ (0) + { + } + + select_statement:: + select_statement (connection_type& conn, + const string& text, + bool process, + bool optimize, + binding& result) + : statement (conn, + text, statement_select, + (process ? &result : 0), optimize), + end_ (false), + cached_ (false), + freed_ (true), + rows_ (0), + param_ (0), + result_ (result), + result_version_ (0) + { + } + + select_statement:: + select_statement (connection_type& conn, + const char* text, + bool process, + bool optimize, + binding& result, + bool copy_text) + : statement (conn, + text, statement_select, + (process ? &result : 0), optimize, + copy_text), + end_ (false), + cached_ (false), + freed_ (true), + rows_ (0), + param_ (0), + result_ (result), + result_version_ (0) + { + } + + void select_statement:: + execute () + { + assert (freed_); + + conn_.clear (); + + end_ = false; + rows_ = 0; + + if (mysql_stmt_reset (stmt_)) + translate_error (conn_, stmt_); + + if (param_ != 0 && param_version_ != param_->version) + { + // For now cannot have NULL entries. + // + if (mysql_stmt_bind_param (stmt_, param_->bind)) + translate_error (conn_, stmt_); + + param_version_ = param_->version; + } + + { + odb::tracer* t; + if ((t = conn_.transaction_tracer ()) || + (t = conn_.tracer ()) || + (t = conn_.database ().tracer ())) + t->execute (conn_, *this); + } + + if (mysql_stmt_execute (stmt_)) + translate_error (conn_, stmt_); + + // This flag appears to be cleared once we start processing the + // result, so we have to cache it for free_result() below. + // +#if MYSQL_VERSION_ID >= 50503 + out_params_ = (conn_.handle ()->server_status & SERVER_PS_OUT_PARAMS); +#endif + + freed_ = false; + conn_.active (this); + } + + void select_statement:: + cache () + { + if (!cached_) + { + if (!end_) + { + if (mysql_stmt_store_result (stmt_)) + translate_error (conn_, stmt_); + + // mysql_stmt_num_rows() returns the number of rows that have been + // fetched by store_result. + // + size_ = rows_ + static_cast<size_t> (mysql_stmt_num_rows (stmt_)); + } + else + size_ = rows_; + + cached_ = true; + } + } + + select_statement::result select_statement:: + fetch (bool next) + { + if (result_version_ != result_.version) + { + size_t count (process_bind (result_.bind, result_.count)); + + // Make sure that the number of columns in the result returned by + // the database matches the number that we expect. A common cause + // of this assertion is a native view with a number of data members + // not matching the number of columns in the SELECT-list. + // + assert (mysql_stmt_field_count (stmt_) == count); + + if (mysql_stmt_bind_result (stmt_, result_.bind)) + translate_error (conn_, stmt_); + + if (count != result_.count) + restore_bind (result_.bind, result_.count); + + result_version_ = result_.version; + } + + if (!next && rows_ != 0) + { + assert (cached_); + mysql_stmt_data_seek (stmt_, static_cast<my_ulonglong> (rows_ - 1)); + } + + int r (mysql_stmt_fetch (stmt_)); + + switch (r) + { + case 0: + { + if (next) + rows_++; + return success; + } + case MYSQL_NO_DATA: + { + end_ = true; + return no_data; + } + case MYSQL_DATA_TRUNCATED: + { + if (next) + rows_++; + return truncated; + } + default: + { + translate_error (conn_, stmt_); + return no_data; // Never reached. + } + } + } + + void select_statement:: + refetch () + { + // Re-fetch columns that were truncated. + // + unsigned int col (0); + for (size_t i (0); i < result_.count; ++i) + { + MYSQL_BIND& b (result_.bind[i]); + + if (b.buffer == 0) // Skip NULL entries. + continue; + + if (*b.error) + { + *b.error = 0; + + if (mysql_stmt_fetch_column (stmt_, &b, col, 0)) + translate_error (conn_, stmt_); + } + + col++; + } + } + + void select_statement:: + free_result () + { + if (!freed_) + { + // If this is a stored procedure call, then we have multiple + // results. The first is the rowset that is the result of the + // procedure (actually, it can be several rowsets if, for + // example, the procedure executes multiple SELECT statements, + // but we don't support this). This result we have just handled. + // Next, there could be another rowset with just one row which + // contains the output variable values (OUT and INOUT; actually, + // if the procedure does not have any SELECT statements, then + // this will be the first result and we have just handled it). + // Finally, the last result is always the stored procedure status + // (not clear how to get this value; all MySQL sample code simply + // ignores it). + // + // So what we need to do here is read and ignore these other + // results since otherwise MySQL won't let us execute any + // subsequent statements. Calling mysql_stmt_next_result() + // until it tells us there is no more results seems to do + // the trick. + // + // mysql_stmt_next_result() is only available since 5.5.3. + // Checking the source code reveals that it does call + // mysql_next_result() (which is available prior to 5.5.3) + // but also does some other house keeping. So it is not + // clear if it is possible to emulate the below logic for + // prior MySQL versions. + // + + // Handling OUT parameters requires a special Voodoo dance: + // we have to fetch the row itself and we have to call fetch + // again and get MYSQL_NO_DATA. Without doing these exact + // steps the server simply drops the connection. Go figure. + // +#if MYSQL_VERSION_ID >= 50503 + if (out_params_) + { + if (mysql_stmt_fetch (stmt_) != MYSQL_NO_DATA) + translate_error (conn_, stmt_); + } +#endif + + if (mysql_stmt_free_result (stmt_)) + translate_error (conn_, stmt_); + +#if MYSQL_VERSION_ID >= 50503 + { + int s; + while ((s = mysql_stmt_next_result (stmt_)) == 0) + { + if (mysql_stmt_field_count (stmt_) != 0) + { + // The same Voodoo dance as above. + // + if (conn_.handle ()->server_status & SERVER_PS_OUT_PARAMS) + { + if (mysql_stmt_fetch (stmt_)) + translate_error (conn_, stmt_); + + if (mysql_stmt_fetch (stmt_) != MYSQL_NO_DATA) + translate_error (conn_, stmt_); + } + + if (mysql_stmt_free_result (stmt_)) + translate_error (conn_, stmt_); + } + } + + if (s != -1) + translate_error (conn_, stmt_); + } +#endif + + if (conn_.active () == this) + conn_.active (0); + + end_ = true; + cached_ = false; + freed_ = true; + rows_ = 0; + } + } + + void select_statement:: + cancel () + { + // If we cached the result, don't free it just yet. + // + if (!cached_ || end_) + free_result (); + else + conn_.active (0); + } + + // insert_statement + // + + insert_statement:: + ~insert_statement () + { + } + + insert_statement:: + insert_statement (connection_type& conn, + const string& text, + bool process, + binding& param, + binding* returning) + : statement (conn, + text, statement_insert, + (process ? ¶m : 0), false), + param_ (param), + param_version_ (0), + returning_ (returning) + { + } + + insert_statement:: + insert_statement (connection_type& conn, + const char* text, + bool process, + binding& param, + binding* returning, + bool copy_text) + : statement (conn, + text, statement_insert, + (process ? ¶m : 0), false, + copy_text), + param_ (param), + param_version_ (0), + returning_ (returning) + { + } + + bool insert_statement:: + execute () + { + conn_.clear (); + + if (mysql_stmt_reset (stmt_)) + translate_error (conn_, stmt_); + + if (param_version_ != param_.version) + { + size_t count (process_bind (param_.bind, param_.count)); + + if (mysql_stmt_bind_param (stmt_, param_.bind)) + translate_error (conn_, stmt_); + + if (count != param_.count) + restore_bind (param_.bind, param_.count); + + param_version_ = param_.version; + } + + { + odb::tracer* t; + if ((t = conn_.transaction_tracer ()) || + (t = conn_.tracer ()) || + (t = conn_.database ().tracer ())) + t->execute (conn_, *this); + } + + if (mysql_stmt_execute (stmt_)) + { + // An auto-assigned object id should never cause a duplicate + // primary key. + // + if (returning_ == 0 && mysql_stmt_errno (stmt_) == ER_DUP_ENTRY) + return false; + else + translate_error (conn_, stmt_); + } + + if (returning_ != 0) + { + unsigned long long i (mysql_stmt_insert_id (stmt_)); + + MYSQL_BIND& b (returning_->bind[0]); + void* v (b.buffer); + + switch (b.buffer_type) + { + case MYSQL_TYPE_TINY: + *static_cast<unsigned char*> (v) = static_cast<unsigned char> (i); + break; + case MYSQL_TYPE_SHORT: + *static_cast<unsigned short*> (v) = static_cast<unsigned short> (i); + break; + case MYSQL_TYPE_LONG: + *static_cast<unsigned int*> (v) = static_cast<unsigned int> (i); + break; + case MYSQL_TYPE_LONGLONG: + *static_cast<unsigned long long*> (v) = i; + break; + default: + assert (false); // Auto id column type is not an integer. + } + + *b.is_null = false; + } + + return true; + } + + // update_statement + // + + update_statement:: + ~update_statement () + { + } + + update_statement:: + update_statement (connection_type& conn, + const string& text, + bool process, + binding& param) + : statement (conn, + text, statement_update, + (process ? ¶m : 0), false), + param_ (param), + param_version_ (0) + { + } + + update_statement:: + update_statement (connection_type& conn, + const char* text, + bool process, + binding& param, + bool copy_text) + : statement (conn, + text, statement_update, + (process ? ¶m : 0), false, + copy_text), + param_ (param), + param_version_ (0) + { + } + + unsigned long long update_statement:: + execute () + { + conn_.clear (); + + if (mysql_stmt_reset (stmt_)) + translate_error (conn_, stmt_); + + if (param_version_ != param_.version) + { + size_t count (process_bind (param_.bind, param_.count)); + + if (mysql_stmt_bind_param (stmt_, param_.bind)) + translate_error (conn_, stmt_); + + if (count != param_.count) + restore_bind (param_.bind, param_.count); + + param_version_ = param_.version; + } + + { + odb::tracer* t; + if ((t = conn_.transaction_tracer ()) || + (t = conn_.tracer ()) || + (t = conn_.database ().tracer ())) + t->execute (conn_, *this); + } + + if (mysql_stmt_execute (stmt_)) + translate_error (conn_, stmt_); + + my_ulonglong r (mysql_stmt_affected_rows (stmt_)); + + if (r == static_cast<my_ulonglong> (-1)) + translate_error (conn_, stmt_); + + return static_cast<unsigned long long> (r); + } + + // delete_statement + // + + delete_statement:: + ~delete_statement () + { + } + + delete_statement:: + delete_statement (connection_type& conn, + const string& text, + binding& param) + : statement (conn, + text, statement_delete, + 0, false), + param_ (param), + param_version_ (0) + { + } + + delete_statement:: + delete_statement (connection_type& conn, + const char* text, + binding& param, + bool copy_text) + : statement (conn, + text, statement_delete, + 0, false, + copy_text), + param_ (param), + param_version_ (0) + { + } + + unsigned long long delete_statement:: + execute () + { + conn_.clear (); + + if (mysql_stmt_reset (stmt_)) + translate_error (conn_, stmt_); + + if (param_version_ != param_.version) + { + // Cannot have NULL entries for now. + // + if (mysql_stmt_bind_param (stmt_, param_.bind)) + translate_error (conn_, stmt_); + + param_version_ = param_.version; + } + + { + odb::tracer* t; + if ((t = conn_.transaction_tracer ()) || + (t = conn_.tracer ()) || + (t = conn_.database ().tracer ())) + t->execute (conn_, *this); + } + + if (mysql_stmt_execute (stmt_)) + translate_error (conn_, stmt_); + + my_ulonglong r (mysql_stmt_affected_rows (stmt_)); + + if (r == static_cast<my_ulonglong> (-1)) + translate_error (conn_, stmt_); + + return static_cast<unsigned long long> (r); + } + } +} |