From 239c24da194d98e0823642d408d35fc8fe3e7ae9 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 14 Aug 2014 09:37:06 +0200 Subject: Implement bulk database operation support for Oracle and SQL Server --- odb/mssql/statement.cxx | 944 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 691 insertions(+), 253 deletions(-) (limited to 'odb/mssql/statement.cxx') diff --git a/odb/mssql/statement.cxx b/odb/mssql/statement.cxx index e87374b..1d4b204 100644 --- a/odb/mssql/statement.cxx +++ b/odb/mssql/statement.cxx @@ -2,7 +2,7 @@ // copyright : Copyright (c) 2005-2013 Code Synthesis Tools CC // license : ODB NCUEL; see accompanying LICENSE file -#include // std::strlen, std::strstr +#include // std::strlen, std::strstr, std::memset, std::memcpy #include #include @@ -27,7 +27,7 @@ namespace odb SQL_BIT, // bind::bit SQL_TINYINT, // bind::tinyint SQL_SMALLINT, // bind::smallint - SQL_INTEGER, // bind::integer + SQL_INTEGER, // bind::int_ SQL_BIGINT, // bind::bigint SQL_DECIMAL, // bind::decimal @@ -62,7 +62,7 @@ namespace odb SQL_C_BIT, // bind::bit SQL_C_UTINYINT, // bind::tinyint SQL_C_SSHORT, // bind::smallint - SQL_C_SLONG, // bind::integer + SQL_C_SLONG, // bind::int_ SQL_C_SBIGINT, // bind::bigint SQL_C_NUMERIC, // bind::decimal @@ -90,6 +90,41 @@ namespace odb SQL_C_BINARY // bind::rowversion }; + // Mapping of bind::buffer_type to fixed buffer capacity values. + // + static const SQLLEN capacity_lookup [bind::last] = + { + 1, // bind::bit + 1, // bind::tinyint + 2, // bind::smallint + 4, // bind::int_ + 8, // bind::bigint + + sizeof (decimal), // bind::decimal + 4, // bind::smallmoney + 8, // bind::money + + 4, // bind::float4 + 8, // bind::float8 + + 0, // bind::string + 0, // bind::long_string + + 0, // bind::nstring + 0, // bind::long_nstring + + 0, // bind::binary + 0, // bind::long_binary + + sizeof (date), // bind::date + sizeof (time), // bind::time + sizeof (datetime), // bind::datetime + sizeof (datetimeoffset), // bind::datetimeoffset + + 16, // bind::uniqueidentifier + 8 // bind::rowversion + }; + // // statement // @@ -289,7 +324,7 @@ namespace odb case bind::long_string: case bind::long_binary: { - buf = (SQLPOINTER) b; + buf = (SQLPOINTER) b->buffer; col_size = b->capacity != 0 ? (SQLULEN) b->capacity : SQL_SS_LENGTH_UNLIMITED; @@ -297,7 +332,7 @@ namespace odb } case bind::long_nstring: { - buf = (SQLPOINTER) b; + buf = (SQLPOINTER) b->buffer; col_size = b->capacity != 0 ? (SQLULEN) b->capacity / 2 // In characters, not bytes. : SQL_SS_LENGTH_UNLIMITED; @@ -429,56 +464,10 @@ namespace odb if (b->buffer == 0) // Skip NULL entries. continue; - SQLLEN cap (0); + SQLLEN cap (capacity_lookup[b->type]); switch (b->type) { - case bind::bit: - case bind::tinyint: - { - cap = 1; - break; - } - case bind::smallint: - { - cap = 2; - break; - } - case bind::int_: - { - cap = 4; - break; - } - case bind::bigint: - { - cap = 8; - break; - } - case bind::decimal: - { - cap = (SQLLEN) sizeof (decimal); - break; - } - case bind::smallmoney: - { - cap = 4; - break; - } - case bind::money: - { - cap = 8; - break; - } - case bind::float4: - { - cap = 4; - break; - } - case bind::float8: - { - cap = 8; - break; - } case bind::string: case bind::nstring: case bind::binary: @@ -495,41 +484,13 @@ namespace odb long_count++; continue; } - case bind::date: - { - cap = (SQLLEN) sizeof (date); - break; - } - case bind::time: - { - cap = (SQLLEN) sizeof (time); - break; - } - case bind::datetime: - { - cap = (SQLLEN) sizeof (datetime); - break; - } - case bind::datetimeoffset: - { - cap = (SQLLEN) sizeof (datetimeoffset); - break; - } - case bind::uniqueidentifier: - { - cap = 16; - break; - } - case bind::rowversion: - { - cap = 8; - break; - } case bind::last: { assert (false); break; } + default: + break; } r = SQLBindCol (stmt_, @@ -566,10 +527,13 @@ namespace odb if (tmp_buf.capacity () == 0) tmp_buf.capacity (4096); - bind* b; + long_callback* pcb; for (;;) { - r = SQLParamData (stmt_, (SQLPOINTER*) &b); + // ODBC seems to already offset the returned pointer for us + // in case of a batch. + // + r = SQLParamData (stmt_, (SQLPOINTER*) &pcb); // If we get anything other than SQL_NEED_DATA, then this is // the return code of SQLExecute(). @@ -577,12 +541,11 @@ namespace odb if (r != SQL_NEED_DATA) break; - long_callback& cb (*static_cast (b->buffer)); - // Store the pointer to the long_callback struct in buf on the // first call to the callback. This allows the callback to // redirect further calls to some other callback. // + long_callback cb (*pcb); const void* buf (&cb); size_t position (0); @@ -670,7 +633,7 @@ namespace odb cbp = nb + (p - ob); } - long_callback& cb (*static_cast (cbp)); + long_callback cb (*static_cast (cbp)); // First determine if the value is NULL as well as try to // get the total data size. @@ -755,6 +718,175 @@ namespace odb } // + // bulk_statement + // + bulk_statement:: + ~bulk_statement () {} + + void bulk_statement:: + init (size_t skip) + { + // Setup row-wise batch operation. We set the actual number of + // parameter sets in the batch in execute(). + // + SQLRETURN r; + + r = SQLSetStmtAttr (stmt_, + SQL_ATTR_PARAM_BIND_TYPE, + (SQLPOINTER) skip, + 0); + + if (!SQL_SUCCEEDED (r)) + translate_error (r, conn_, stmt_); + + r = SQLSetStmtAttr (stmt_, + SQL_ATTR_PARAMS_PROCESSED_PTR, + (SQLPOINTER) &processed_, + 0); + + if (!SQL_SUCCEEDED (r)) + translate_error (r, conn_, stmt_); + + r = SQLSetStmtAttr (stmt_, + SQL_ATTR_PARAM_STATUS_PTR, + (SQLPOINTER) status_, + 0); + + if (!SQL_SUCCEEDED (r)) + translate_error (r, conn_, stmt_); + } + + SQLRETURN bulk_statement:: + execute (size_t n, multiple_exceptions* mex) + { + mex_ = mex; + + if (status_ != 0) + { + SQLRETURN r (SQLSetStmtAttr (stmt_, + SQL_ATTR_PARAMSET_SIZE, + (SQLPOINTER) n, + 0)); + + if (!SQL_SUCCEEDED (r)) + translate_error (r, conn_, stmt_); + + // Some SQL* functions would only update the status in case of + // an error. + // + memset (status_, 0, sizeof (status_[0]) * n); + } + + processed_ = 0; + SQLRETURN r (statement::execute ()); + bool ok (SQL_SUCCEEDED (r) || r == SQL_NO_DATA); + + // If we have a batch of 1 parameter set, SQL Server ODBC driver + // returns the error via SQLExecute() rather than via the status + // array even if we set all the attributes necessary for row-wise + // binding. So what we are going to do here is convert this case + // to the batch way of reporting errors (not that we also check + // processed_ so that we only do this is the parameter set was + // actually attempted). + // + if (!ok && status_ != 0 && n == 1 && processed_ == 1) + { + status_[0] = SQL_PARAM_ERROR; + r = SQL_SUCCESS; + ok = true; + } + + // If the statement failed as a whole, assume no parameter sets + // were attempted in case of a batch. Otherwise, the documentation + // says that the native client driver keeps processing remaining + // sets even in case of an error. + // + i_ = 0; + n_ = (ok ? n : (status_ == 0 ? 1 : 0)); + + if (mex_ != 0) + { + mex_->current (i_); + mex_->attempted (processed_); + } + + if (!ok) + { + if (mex_ != 0) + mex_->fatal (true); // An incomplete batch is always fatal. + + return r; + } + + return r; + } + + size_t bulk_statement:: + extract_errors () + { + size_t e (0); + + for (size_t i (0); i != n_; ++i) + { + if (status_[i] != SQL_PARAM_SUCCESS && + status_[i] != SQL_PARAM_SUCCESS_WITH_INFO) + { + translate_error (SQL_ERROR, conn_, stmt_, i, mex_); + e++; + } + } + + return e; + } + + unsigned long long bulk_statement:: + affected (SQLRETURN r, size_t errors, bool unique) + { + unsigned long long rows (0); + + // SQL_NO_DATA indicates that the statement hasn't affected any rows. + // + if (r != SQL_NO_DATA) + { + SQLLEN n; + r = SQLRowCount (stmt_, &n); + + if (!SQL_SUCCEEDED (r)) + translate_error (r, conn_, stmt_); + + // If all the parameter sets failed, then the returned count is -1, + // which means "not available" according to the documentation. + // + rows = (n != -1 ? static_cast (n) : 0); + } + + if (n_ > 1) // Batch. + { + if (rows != 0) // Some rows did get affected. + { + // Subtract the parameter sets that failed since they haven't + // affected any rows. + // + size_t p (n_ - errors); + + if (p > 1) // True batch. + { + if (unique) // Each can affect 0 or 1 row. + { + rows = (p == static_cast (rows) + ? 1 + : result_unknown); + } + else + rows = result_unknown; + } + } + } + + return rows; + } + + // // select_statement // select_statement:: @@ -949,16 +1081,19 @@ namespace odb bool process, binding& param, bool returning_id, - bool returning_version) - : statement (conn, - text, statement_insert, - (process ? ¶m : 0), false), + bool returning_version, + binding* returning) + : bulk_statement (conn, + text, statement_insert, + (process ? ¶m : 0), false, + param.batch, param.skip, param.status), returning_id_ (returning_id), - returning_version_ (returning_version) + returning_version_ (returning_version), + ret_ (returning) { bind_param (param.bind, param.count); - if (returning_id_ || returning_version_) + if (ret_ != 0) init_result (); } @@ -969,20 +1104,31 @@ namespace odb binding& param, bool returning_id, bool returning_version, + binding* returning, bool copy_text) - : statement (conn, - text, statement_insert, - (process ? ¶m : 0), false, - copy_text), + : bulk_statement (conn, + text, statement_insert, + (process ? ¶m : 0), false, + param.batch, param.skip, param.status, + copy_text), returning_id_ (returning_id), - returning_version_ (returning_version) + returning_version_ (returning_version), + ret_ (returning) { bind_param (param.bind, param.count); - if (returning_id_ || returning_version_) + if (ret_ != 0) init_result (); } + template + inline T* + offset (T* base, size_t count, size_t size) + { + return reinterpret_cast ( + reinterpret_cast (base) + count * size); + } + void insert_statement:: init_result () { @@ -993,20 +1139,39 @@ namespace odb // for one of the inserted columns is supplied at execution // (long data). // - batch_ = strstr (text_, "OUTPUT INSERTED.") == 0 && - strstr (text_, "output inserted.") == 0; - + text_batch_ = (strstr (text_, "OUTPUT INSERTED.") == 0 && + strstr (text_, "output inserted.") == 0); + + // It might seem logical to set up the array of results if this is a + // batch (i.e., the SQL_ATTR_ROW_BIND_TYPE, SQL_ATTR_ROW_ARRAY_SIZE). + // This won't work because what we are getting is multiple result + // sets (each containing a single row) and not multiple rows. As a + // result, the SQL Server ODBC driver will always store the data in + // the first element of our array. A bit counter-intuitive. + // + // At the same time it would be conceptually cleaner to have the + // returned data extracted into the batch array instead of always + // the first element. This is also how other database runtimes (e.g., + // Oracle) behave. So what we are going to do here is emulate this + // by making the ODBC driver store the data into the last element + // of the batch array and then copying it into the right place + // after processing each result set (see fetch() below). + // + SQLRETURN r; SQLUSMALLINT col (1); + size_t last (ret_->batch - 1); + if (returning_id_) { - SQLRETURN r ( - SQLBindCol (stmt_, - col++, - SQL_C_SBIGINT, - (SQLPOINTER) &id_, - sizeof (id_), - &id_size_ind_)); + bind& b (ret_->bind[0]); // Auto id is the first element. + + r = SQLBindCol (stmt_, + col++, + c_type_lookup[b.type], + (SQLPOINTER) offset (b.buffer, last, ret_->skip), + capacity_lookup[b.type], + offset (b.size_ind, last, ret_->skip)); if (!SQL_SUCCEEDED (r)) translate_error (r, conn_, stmt_); @@ -1014,84 +1179,173 @@ namespace odb if (returning_version_) { - SQLRETURN r ( - SQLBindCol (stmt_, - col++, - SQL_C_BINARY, - (SQLPOINTER) &version_, - sizeof (version_), - &version_size_ind_)); + bind& b (ret_->bind[ret_->count - 1]); // Version is the last element. + + r = SQLBindCol (stmt_, + col++, + c_type_lookup[b.type], + (SQLPOINTER) offset (b.buffer, last, ret_->skip), + capacity_lookup[b.type], + offset (b.size_ind, last, ret_->skip)); if (!SQL_SUCCEEDED (r)) translate_error (r, conn_, stmt_); } } - bool insert_statement:: - execute () + size_t insert_statement:: + execute (size_t n, multiple_exceptions* mex) { - SQLRETURN r (statement::execute ()); + // The batch INSERT works in two different ways, depending on + // whether we have the OUTPUT clause. If there is no OUTPUT, then + // all the parameter sets are processed inside the SQLExecute() + // call. If, however, there is OUTPUT, then the sets are + // processed one at a time as we consume the results with + // the SQLMoreResults() call below. Thus we in effect have + // two counts: the "processed so far" as set by the API + // (SQL_ATTR_PARAMS_PROCESSED_PTR) and the "to be processed" + // (value in n_). Note that in the OUTPUT case if there is an + // error, the processed count seems to jump by 2 for some reason. + // + // The OUTPUT case can be handled in two different ways: we can + // "execute" (with SQLMoreResults()) each set as the user moves + // from one result to the next (result() call). The advantage of + // this approach is that the returned data ends up in the right + // place automatically. The potential drawback is that the total + // affected row count will only be available at the end. As a + // result, this approach probably won't work if we need to handle, + // say, UPDATE with OUTPUT (SQLRowCount() does not return an + // intermediate total, at least not for INSERT). + // + // The alternative implementation would call SQLMoreResults() + // inside execute() until all the parameter sets are executed. + // In this case we will have to copy the extracted data into + // the right place in the bindings (or update the binding before + // each call to SQLMoreResults()). It is also not clear whether + // the diagnostic records for the failed sets would accumulate. + // If not, those will have to be stashed into mex on each + // iteration. + // + SQLRETURN r (bulk_statement::execute (n, mex)); + // Statement failed as a whole, assume no parameter sets were + // attempted in case of a batch. + // if (!SQL_SUCCEEDED (r)) { - // Translate the integrity contraint violation (SQLSTATE 23000) - // to the flase return value. This code is similar to that found - // in translate_error(). - // - char sqlstate[SQL_SQLSTATE_SIZE + 1]; - SQLINTEGER native_code; - SQLSMALLINT msg_size; + fetch (r); + return n_; + } - bool cv (false); + if (status_ == 0) // Non-batch case. + fetch (SQL_SUCCESS); + else + fetch (status_[i_] == SQL_PARAM_SUCCESS || + status_[i_] == SQL_PARAM_SUCCESS_WITH_INFO + ? SQL_SUCCESS : SQL_ERROR); + + return n_; + } - for (SQLSMALLINT i (1);; ++i) + void insert_statement:: + fetch (SQLRETURN r) + { + result_ = true; + + if (!SQL_SUCCEEDED (r)) + { + // An auto-assigned object id should never cause a duplicate primary + // key. + // + if (!returning_id_) { - SQLRETURN r (SQLGetDiagRecA (SQL_HANDLE_STMT, - stmt_, - i, - (SQLCHAR*) sqlstate, - &native_code, - 0, - 0, - &msg_size)); + // Translate the integrity contraint violation (SQLSTATE 23000) + // to the false result value. This code is similar to that found + // in translate_error(). + // + char sqlstate[SQL_SQLSTATE_SIZE + 1]; + SQLINTEGER native_code; + SQLSMALLINT msg_size; - if (r == SQL_NO_DATA) - break; - else if (SQL_SUCCEEDED (r)) + bool cv (false); + + for (SQLSMALLINT i (1);; ++i) { - string s (sqlstate); + SQLRETURN r; - if (s == "23000") // Integrity contraint violation. - cv = true; - else if (s != "01000") // General warning. + // Filter based on row association. + // + if (mex_ != 0) + { + SQLLEN n; + r = SQLGetDiagField (SQL_HANDLE_STMT, + stmt_, + i, + SQL_DIAG_ROW_NUMBER, + &n, + 0, + 0); + + if (r == SQL_NO_DATA) + break; + else if (!SQL_SUCCEEDED (r)) + continue; + + if (n == SQL_NO_ROW_NUMBER || + n == SQL_ROW_NUMBER_UNKNOWN || + n != static_cast (i_ + 1)) // 1-based + continue; + } + + r= SQLGetDiagRecA (SQL_HANDLE_STMT, + stmt_, + i, + (SQLCHAR*) sqlstate, + &native_code, + 0, + 0, + &msg_size); + + if (r == SQL_NO_DATA) + break; + else if (SQL_SUCCEEDED (r)) + { + string s (sqlstate); + + if (s == "23000") // Integrity contraint violation. + cv = true; + else if (s != "01000") // General warning. + { + // Some other code. + // + cv = false; + break; + } + } + else // SQLGetDiagRec() failure. { - // Some other code. - // cv = false; break; } } - else - { - // SQLGetDiagRec() failure. - // - cv = false; - break; - } + + if (cv) + result_ = false; } - if (cv) - return false; - else - translate_error (r, conn_, stmt_); + if (result_) + { + translate_error (r, conn_, stmt_, i_, mex_); // Can return. + result_ = false; // Prevent id/version extraction below or. + } } // Fetch the row containing the id/version if this statement is // returning. // - if (returning_id_ || returning_version_) + if (result_ && ret_ != 0) { - if (batch_) + if (text_batch_) { r = SQLMoreResults (stmt_); @@ -1111,21 +1365,99 @@ namespace odb if (r != SQL_NO_DATA && !SQL_SUCCEEDED (r)) translate_error (r, conn_, stmt_); - { - SQLRETURN r (SQLCloseCursor (stmt_)); // Don't overwrite r. - - if (!SQL_SUCCEEDED (r)) - translate_error (r, conn_, stmt_); - } - if (r == SQL_NO_DATA) throw database_exception ( 0, "?????", "result set expected from a statement with the OUTPUT clause"); + + // See init_result() for details on what's going here. + // + size_t last (ret_->batch - 1); + if (i_ != last) + { + if (returning_id_) + { + bind& b (ret_->bind[0]); // Auto id is the first element. + + memcpy (offset (b.buffer, i_, ret_->skip), + offset (b.buffer, last, ret_->skip), + capacity_lookup[b.type]); + + memcpy (offset (b.size_ind, i_, ret_->skip), + offset (b.size_ind, last, ret_->skip), + sizeof (*b.size_ind)); + } + + if (returning_version_) + { + bind& b (ret_->bind[ret_->count - 1]); // Version is the last. + + memcpy (offset (b.buffer, i_, ret_->skip), + offset (b.buffer, last, ret_->skip), + capacity_lookup[b.type]); + + memcpy (offset (b.size_ind, i_, ret_->skip), + offset (b.size_ind, last, ret_->skip), + sizeof (*b.size_ind)); + } + } } + } - return true; + bool insert_statement:: + result (size_t i) + { + assert ((i_ == i || i_ + 1 == i) && i < n_); + + SQLRETURN r; + + // Get to the next result set if necessary. + // + if (i != i_) + { + mex_->current (++i_); // mex cannot be NULL since this is a batch. + + // Only in case of the OUTPUT clause do we have multiple result sets. + // + if (ret_ != 0) + { + r = SQLMoreResults (stmt_); + + // The actually processed count could have changed (see execute()). + // + mex_->attempted (processed_); + + if (r == SQL_NO_DATA) + { + throw database_exception ( + 0, + "?????", + "multiple result sets expected from an array of parameters"); + } + } + + fetch (status_[i_] == SQL_PARAM_SUCCESS || + status_[i_] == SQL_PARAM_SUCCESS_WITH_INFO + ? SQL_SUCCESS : SQL_ERROR); + } + + // Close the cursor if we are done. + // + if (ret_ != 0 && i_ + 1 == n_) + { + // Use SQLFreeStmt(SQL_CLOSE) instead of SQLCloseCursor() to avoid + // an error if a cursor is not open. This seem to happen if the + // statement failure was translated to a parameter set failure in + // bulk_statement for batches of one. + // + r = SQLFreeStmt (stmt_, SQL_CLOSE); + + if (!SQL_SUCCEEDED (r)) + translate_error (r, conn_, stmt_); + } + + return result_; } // @@ -1142,19 +1474,33 @@ namespace odb const string& text, bool process, binding& param, - bool returning_version) - : statement (conn, - text, statement_update, - (process ? ¶m : 0), false), - returning_version_ (returning_version) + binding* returning) + : bulk_statement (conn, + text, statement_update, + (process ? ¶m : 0), false, + param.batch, param.skip, param.status), + unique_ (false), + returning_ (returning != 0) { - if (!empty ()) - { - bind_param (param.bind, param.count); + assert (param.batch == 1); // Specify unique_hint explicitly. + init (param, returning); + } - if (returning_version_) - init_result (); - } + update_statement:: + update_statement (connection_type& conn, + const string& text, + bool unique, + bool process, + binding& param, + binding* returning) + : bulk_statement (conn, + text, statement_update, + (process ? ¶m : 0), false, + param.batch, param.skip, param.status), + unique_ (unique), + returning_ (returning != 0) + { + init (param, returning); } update_statement:: @@ -1162,85 +1508,132 @@ namespace odb const char* text, bool process, binding& param, - bool returning_version, + binding* returning, bool copy_text) - : statement (conn, - text, statement_update, - (process ? ¶m : 0), false, - copy_text), - returning_version_ (returning_version) + : bulk_statement (conn, + text, statement_update, + (process ? ¶m : 0), false, + param.batch, param.skip, param.status, + copy_text), + unique_ (false), + returning_ (returning != 0) { - if (!empty ()) - { - bind_param (param.bind, param.count); - - if (returning_version_) - init_result (); - } + assert (param.batch == 1); // Specify unique_hint explicitly. + init (param, returning); } - void update_statement:: - init_result () + update_statement:: + update_statement (connection_type& conn, + const char* text, + bool unique, + bool process, + binding& param, + binding* returning, + bool copy_text) + : bulk_statement (conn, + text, statement_update, + (process ? ¶m : 0), false, + param.batch, param.skip, param.status, + copy_text), + unique_ (unique), + returning_ (returning != 0) { - SQLRETURN r ( - SQLBindCol (stmt_, - 1, - SQL_C_BINARY, - (SQLPOINTER) &version_, - sizeof (version_), - &version_size_ind_)); - - if (!SQL_SUCCEEDED (r)) - translate_error (r, conn_, stmt_); + init (param, returning); } - unsigned long long update_statement:: - execute () + void update_statement:: + init (binding& param, binding* ret) { - SQLRETURN r (statement::execute ()); + if (!empty ()) + { + bind_param (param.bind, param.count); - // SQL_NO_DATA indicates that the statement hasn't affected any rows. - // - if (r == SQL_NO_DATA) - return 0; + if (ret != 0) + { + bind& b (ret->bind[ret->count - 1]); // Version is the last element. - if (!SQL_SUCCEEDED (r)) - translate_error (r, conn_, stmt_); + SQLRETURN r (SQLBindCol (stmt_, + 1, + c_type_lookup[b.type], + (SQLPOINTER) b.buffer, + capacity_lookup[b.type], + b.size_ind)); + + if (!SQL_SUCCEEDED (r)) + translate_error (r, conn_, stmt_); + } + } + } - // Get the number of affected rows. + size_t update_statement:: + execute (size_t n, multiple_exceptions* mex) + { + // In batch UPDATE without the OUTPUT clause (which is the + // only kind we currently support) all the parameter sets + // are processed inside SQLExecute() and the total count of + // affected rows is available after it returns. // - SQLLEN rows; - r = SQLRowCount (stmt_, &rows); + assert (!returning_ || status_ == 0); - if (!SQL_SUCCEEDED (r)) - translate_error (r, conn_, stmt_); + SQLRETURN r (bulk_statement::execute (n, mex)); - // Fetch the row containing the version if this statement is - // returning. We still need to close the cursor even if we - // haven't updated any rows. + // Statement failed as a whole, assume no parameter sets were + // attempted in case of a batch. // - if (returning_version_) + if (!(SQL_SUCCEEDED (r) || r == SQL_NO_DATA)) { - r = SQLFetch (stmt_); - - if (r != SQL_NO_DATA && !SQL_SUCCEEDED (r)) - translate_error (r, conn_, stmt_); + translate_error (r, conn_, stmt_, 0, mex_); + return n_; + } + if (status_ == 0) // Non-batch case. + { + // Fetch the row containing the data if this statement is + // returning. We still need to close the cursor even if we + // haven't updated any rows. + // + if (returning_) { - SQLRETURN r (SQLCloseCursor (stmt_)); // Don't overwrite r. + r = SQLFetch (stmt_); - if (!SQL_SUCCEEDED (r)) + if (r != SQL_NO_DATA && !SQL_SUCCEEDED (r)) translate_error (r, conn_, stmt_); + + // We have to get the result after fetching the OUTPUT data + // but before closing the cursor. + // + result_ = affected (SQL_SUCCESS, 0, unique_); + + { + SQLRETURN r (SQLCloseCursor (stmt_)); // Don't overwrite r. + + if (!SQL_SUCCEEDED (r)) + translate_error (r, conn_, stmt_); + } + + if (result_ != 0 && r == SQL_NO_DATA) + throw database_exception ( + 0, + "?????", + "result set expected from a statement with the OUTPUT clause"); } + else + result_ = affected (r, 0, unique_); + } + else + { + // Extract error information for failed parameter sets. If we do + // this after calling SQLRowCount(), all the diagnostics records + // that we need will be gone. + // + size_t errors (extract_errors ()); - if (rows != 0 && r == SQL_NO_DATA) - throw database_exception ( - 0, - "?????", - "result set expected from a statement with the OUTPUT clause"); + // Figure out the affected row count. + // + result_ = affected (r, errors, unique_); } - return static_cast (rows); + return n_; } // @@ -1256,9 +1649,26 @@ namespace odb delete_statement (connection_type& conn, const string& text, binding& param) - : statement (conn, - text, statement_delete, - 0, false) + : bulk_statement (conn, + text, statement_delete, + 0, false, + param.batch, param.skip, param.status), + unique_ (false) + { + assert (param.batch == 1); // Specify unique_hint explicitly. + bind_param (param.bind, param.count); + } + + delete_statement:: + delete_statement (connection_type& conn, + const string& text, + bool unique, + binding& param) + : bulk_statement (conn, + text, statement_delete, + 0, false, + param.batch, param.skip, param.status), + unique_ (unique) { bind_param (param.bind, param.count); } @@ -1268,36 +1678,64 @@ namespace odb const char* text, binding& param, bool copy_text) - : statement (conn, - text, statement_delete, - 0, false, - copy_text) + : bulk_statement (conn, + text, statement_delete, + 0, false, + param.batch, param.skip, param.status, + copy_text), + unique_ (false) { + assert (param.batch == 1); // Specify unique_hint explicitly. bind_param (param.bind, param.count); } - unsigned long long delete_statement:: - execute () + delete_statement:: + delete_statement (connection_type& conn, + const char* text, + bool unique, + binding& param, + bool copy_text) + : bulk_statement (conn, + text, statement_delete, + 0, false, + param.batch, param.skip, param.status, + copy_text), + unique_ (unique) { - SQLRETURN r (statement::execute ()); + bind_param (param.bind, param.count); + } - // SQL_NO_DATA indicates that the statement hasn't affected any rows. + size_t delete_statement:: + execute (size_t n, multiple_exceptions* mex) + { + // In batch DELETE without the OUTPUT clause (which is the + // only kind we currently support) all the parameter sets + // are processed inside SQLExecute() and the total count of + // affected rows is available after it returns. // - if (r == SQL_NO_DATA) - return 0; - if (!SQL_SUCCEEDED (r)) - translate_error (r, conn_, stmt_); + SQLRETURN r (bulk_statement::execute (n, mex)); - // Get the number of affected rows. + // Statement failed as a whole, assume no parameter sets were + // attempted in case of a batch. // - SQLLEN rows; - r = SQLRowCount (stmt_, &rows); + if (!(SQL_SUCCEEDED (r) || r == SQL_NO_DATA)) + { + translate_error (r, conn_, stmt_, 0, mex_); + return n_; + } - if (!SQL_SUCCEEDED (r)) - translate_error (r, conn_, stmt_); + // Extract error information for failed parameter sets. If we do + // this after calling SQLRowCount(), all the diagnostics records + // that we need will be gone. + // + size_t errors (status_ != 0 ? extract_errors () : 0); + + // Figure out the affected row count. + // + result_ = affected (r, errors, unique_); - return static_cast (rows); + return n_; } } } -- cgit v1.1