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/binding.hxx | 21 +- odb/mssql/container-statements.hxx | 3 +- odb/mssql/database.hxx | 31 + odb/mssql/database.ixx | 35 ++ odb/mssql/error.cxx | 66 +- odb/mssql/error.hxx | 9 +- odb/mssql/exceptions.cxx | 18 + odb/mssql/exceptions.hxx | 9 + odb/mssql/no-id-object-statements.hxx | 10 +- odb/mssql/no-id-object-statements.txx | 10 +- odb/mssql/polymorphic-object-statements.hxx | 10 +- odb/mssql/section-statements.hxx | 9 +- odb/mssql/section-statements.txx | 3 +- odb/mssql/simple-object-statements.hxx | 77 ++- odb/mssql/simple-object-statements.txx | 36 +- odb/mssql/statement.cxx | 944 ++++++++++++++++++++-------- odb/mssql/statement.hxx | 236 ++++++- odb/mssql/statement.ixx | 75 ++- 18 files changed, 1223 insertions(+), 379 deletions(-) diff --git a/odb/mssql/binding.hxx b/odb/mssql/binding.hxx index 76aa2b2..1ff6f50 100644 --- a/odb/mssql/binding.hxx +++ b/odb/mssql/binding.hxx @@ -24,10 +24,23 @@ namespace odb typedef mssql::bind bind_type; typedef mssql::change_callback change_callback_type; - binding (): bind (0), count (0), version (0), change_callback (0) {} + binding () + : bind (0), count (0), version (0), + batch (0), skip (0), status (0), + change_callback (0) {} binding (bind_type* b, std::size_t n) - : bind (b), count (n), version (0), change_callback (0) + : bind (b), count (n), version (0), + batch (1), skip (0), status (0), + change_callback (0) + { + } + + binding (bind_type* b, std::size_t n, + std::size_t bt, std::size_t s, SQLUSMALLINT* st) + : bind (b), count (n), version (0), + batch (bt), skip (s), status (st), + change_callback (0) { } @@ -35,6 +48,10 @@ namespace odb std::size_t count; std::size_t version; + std::size_t batch; + std::size_t skip; + SQLUSMALLINT* status; // Batch status array. + change_callback_type* change_callback; private: diff --git a/odb/mssql/container-statements.hxx b/odb/mssql/container-statements.hxx index 5a7e52c..adc09ac 100644 --- a/odb/mssql/container-statements.hxx +++ b/odb/mssql/container-statements.hxx @@ -124,6 +124,7 @@ namespace odb insert_image_binding_, false, false, + 0, false)); return *insert_; @@ -287,7 +288,7 @@ namespace odb update_text_, this->versioned_, // Process if versioned. update_image_binding_, - false, + 0, false)); return *update_; diff --git a/odb/mssql/database.hxx b/odb/mssql/database.hxx index 4ac85d2..fc13974 100644 --- a/odb/mssql/database.hxx +++ b/odb/mssql/database.hxx @@ -153,6 +153,10 @@ namespace odb template typename object_traits::id_type + persist (const T& object); + + template + typename object_traits::id_type persist (T* obj_ptr); template class P> @@ -175,6 +179,13 @@ namespace odb typename object_traits::id_type persist (const typename object_traits::pointer_type& obj_ptr); + // Bulk persist. Can be a range of references or pointers (including + // smart pointers) to objects. + // + template + void + persist (I begin, I end, bool continue_failed = true); + // Load an object. Throw object_not_persistent if not found. // template @@ -261,6 +272,13 @@ namespace odb void update (const typename object_traits::pointer_type& obj_ptr); + // Bulk update. Can be a range of references or pointers (including + // smart pointers) to objects. + // + template + void + update (I begin, I end, bool continue_failed = true); + // Update a section of an object. Throws the section_not_loaded // exception if the section is not loaded. Note also that this // function does not clear the changed flag if it is set. @@ -304,6 +322,19 @@ namespace odb void erase (const typename object_traits::pointer_type& obj_ptr); + // Bulk erase. + // + template + void + erase (I id_begin, I id_end, bool continue_failed = true); + + // Can be a range of references or pointers (including smart pointers) + // to objects. + // + template + void + erase (I obj_begin, I obj_end, bool continue_failed = true); + // Erase multiple objects matching a query predicate. // template diff --git a/odb/mssql/database.ixx b/odb/mssql/database.ixx index 93919f6..fc43934 100644 --- a/odb/mssql/database.ixx +++ b/odb/mssql/database.ixx @@ -27,6 +27,13 @@ namespace odb template inline typename object_traits::id_type database:: + persist (const T& obj) + { + return persist_ (obj); + } + + template + inline typename object_traits::id_type database:: persist (T* p) { typedef typename object_traits::pointer_type object_pointer; @@ -93,6 +100,13 @@ namespace odb return persist_ (pobj); } + template + inline void database:: + persist (I b, I e, bool cont) + { + persist_ (b, e, cont); + } + template inline typename object_traits::pointer_type database:: load (const typename object_traits::id_type& id) @@ -254,6 +268,13 @@ namespace odb update_ (pobj); } + template + inline void database:: + update (I b, I e, bool cont) + { + update_ (b, e, cont); + } + template inline void database:: update (const T& obj, const section& s) @@ -343,6 +364,20 @@ namespace odb erase_ (pobj); } + template + inline void database:: + erase (I idb, I ide, bool cont) + { + erase_id_ (idb, ide, cont); + } + + template + inline void database:: + erase (I ob, I oe, bool cont) + { + erase_object_ (ob, oe, cont); + } + template inline unsigned long long database:: erase_query () diff --git a/odb/mssql/error.cxx b/odb/mssql/error.cxx index f6be6ea..0c41d32 100644 --- a/odb/mssql/error.cxx +++ b/odb/mssql/error.cxx @@ -20,7 +20,9 @@ namespace odb SQLHANDLE h, SQLSMALLINT htype, connection* conn, - bool end_tran) + bool end_tran, + size_t pos, + multiple_exceptions* mex) { // First see if we have one of the errors indicated via the // return error code. @@ -166,14 +168,41 @@ namespace odb for (SQLSMALLINT i (1);; ++i) { - r = SQLGetDiagRecA (htype, - h, - i, - (SQLCHAR*) sqlstate, - &native_code, - (SQLCHAR*) msg, - sizeof (msg), - &msg_size); + // If this is for a batch, filter out based on row association. + // Here we only ignore records that have the associated row + // number and this number doesn't match ours. In particular, + // this means that all the un-associated records which will be + // duplicated for all the failed rows, which seems like the + // correct thing to do. + // + if (mex != 0) + { + SQLLEN n; + r = SQLGetDiagField (htype, + h, + i, + SQL_DIAG_ROW_NUMBER, + &n, + 0, + 0); + + if (r == SQL_NO_DATA) + break; + else if (SQL_SUCCEEDED (r) && + n != SQL_NO_ROW_NUMBER && + n != SQL_ROW_NUMBER_UNKNOWN && + n != static_cast (pos + 1)) // 1-based + continue; + } + + r = SQLGetDiagRecA (htype, + h, + i, + (SQLCHAR*) sqlstate, + &native_code, + (SQLCHAR*) msg, + sizeof (msg), + &msg_size); if (r == SQL_NO_DATA) break; @@ -203,27 +232,36 @@ namespace odb if (e.size () == 0) e.append (0, "?????", "no diagnostic record (using wrong handle?)"); - throw e; + if (mex == 0) + throw e; + else + // It could be that some of these errors are fatal. I guess we + // will just have to learn from experience which ones are. The + // client code can always treat specific error codes as fatal. + // + mex->insert (pos, e); } void translate_error (SQLRETURN r, connection& c, bool end_tran) { - translate_error (r, c.handle (), SQL_HANDLE_DBC, &c, end_tran); + translate_error (r, c.handle (), SQL_HANDLE_DBC, &c, end_tran, 0, 0); } void translate_error (SQLRETURN r, connection& c, - const auto_handle& h) + const auto_handle& h, + size_t pos, + multiple_exceptions* mex) { - translate_error (r, h, SQL_HANDLE_STMT, &c, false); + translate_error (r, h, SQL_HANDLE_STMT, &c, false, pos, mex); } void translate_error (SQLRETURN r, SQLHANDLE h, SQLSMALLINT htype) { - translate_error (r, h, htype, 0, false); + translate_error (r, h, htype, 0, false, 0, 0); } } } diff --git a/odb/mssql/error.hxx b/odb/mssql/error.hxx index 69ff6d4..59ba3bf 100644 --- a/odb/mssql/error.hxx +++ b/odb/mssql/error.hxx @@ -6,10 +6,11 @@ #define ODB_MSSQL_ERROR_HXX #include +#include // std::size_t #include #include -#include // connection +#include // connection, multiple_exceptions #include #include @@ -18,13 +19,17 @@ namespace odb { namespace mssql { + // Translate ODBC error given a handle and throw (or return, in case + // multiple_exceptions is not NULL) an appropriate exception. + // LIBODB_MSSQL_EXPORT void translate_error (SQLRETURN, connection&, bool end_tran = false); LIBODB_MSSQL_EXPORT void translate_error (SQLRETURN, connection&, - const auto_handle&); + const auto_handle&, + std::size_t pos = 0, multiple_exceptions* = 0); LIBODB_MSSQL_EXPORT void translate_error (SQLRETURN, SQLHANDLE, SQLSMALLINT htype); diff --git a/odb/mssql/exceptions.cxx b/odb/mssql/exceptions.cxx index 33aaba4..44cdf2e 100644 --- a/odb/mssql/exceptions.cxx +++ b/odb/mssql/exceptions.cxx @@ -57,6 +57,12 @@ namespace odb return what_.c_str (); } + database_exception* database_exception:: + clone () const + { + return new database_exception (*this); + } + // // cli_exception // @@ -78,6 +84,12 @@ namespace odb return what_.c_str (); } + cli_exception* cli_exception:: + clone () const + { + return new cli_exception (*this); + } + // // long_data_reload // @@ -88,5 +100,11 @@ namespace odb return "attempt to re-load object or view with long data " "from query result"; } + + long_data_reload* long_data_reload:: + clone () const + { + return new long_data_reload (*this); + } } } diff --git a/odb/mssql/exceptions.hxx b/odb/mssql/exceptions.hxx index c503070..7d44a7b 100644 --- a/odb/mssql/exceptions.hxx +++ b/odb/mssql/exceptions.hxx @@ -79,6 +79,9 @@ namespace odb virtual const char* what () const throw (); + virtual database_exception* + clone () const; + public: ~database_exception () throw (); @@ -105,6 +108,9 @@ namespace odb virtual const char* what () const throw (); + virtual cli_exception* + clone () const; + private: std::string what_; }; @@ -113,6 +119,9 @@ namespace odb { virtual const char* what () const throw (); + + virtual long_data_reload* + clone () const; }; namespace core diff --git a/odb/mssql/no-id-object-statements.hxx b/odb/mssql/no-id-object-statements.hxx index 981bc9f..4ead693 100644 --- a/odb/mssql/no-id-object-statements.hxx +++ b/odb/mssql/no-id-object-statements.hxx @@ -49,9 +49,9 @@ namespace odb // Object image. // image_type& - image () + image (std::size_t i = 0) { - return image_; + return image_[i]; } // Insert binding. @@ -89,7 +89,8 @@ namespace odb object_traits::versioned, // Process if versioned. insert_image_binding_, false, - object_traits::rowversion, + false, + 0, false)); return *persist_; @@ -110,7 +111,8 @@ namespace odb no_id_object_statements& operator= (const no_id_object_statements&); private: - image_type image_; + image_type image_[object_traits::batch]; + SQLUSMALLINT status_[object_traits::batch]; // Select binding. // diff --git a/odb/mssql/no-id-object-statements.txx b/odb/mssql/no-id-object-statements.txx index 0d92686..65dc8e9 100644 --- a/odb/mssql/no-id-object-statements.txx +++ b/odb/mssql/no-id-object-statements.txx @@ -19,13 +19,17 @@ namespace odb no_id_object_statements (connection_type& conn) : statements_base (conn), select_image_binding_ (select_image_bind_, select_column_count), - insert_image_binding_ (insert_image_bind_, insert_column_count) + insert_image_binding_ (insert_image_bind_, + insert_column_count, + object_traits::batch, + sizeof (image_type), + status_) { - image_.version = 0; + image_[0].version = 0; select_image_version_ = 0; insert_image_version_ = 0; - select_image_binding_.change_callback = image_.change_callback (); + select_image_binding_.change_callback = image_[0].change_callback (); std::memset (insert_image_bind_, 0, sizeof (insert_image_bind_)); std::memset (select_image_bind_, 0, sizeof (select_image_bind_)); diff --git a/odb/mssql/polymorphic-object-statements.hxx b/odb/mssql/polymorphic-object-statements.hxx index 381c607..49b5438 100644 --- a/odb/mssql/polymorphic-object-statements.hxx +++ b/odb/mssql/polymorphic-object-statements.hxx @@ -310,7 +310,7 @@ namespace odb insert_image_binding_, false, false, - false)); + 0)); return *persist_; } @@ -345,7 +345,7 @@ namespace odb object_traits::update_statement, object_traits::versioned, // Process if versioned. update_image_binding_, - false, + 0, false)); return *update_; @@ -373,6 +373,7 @@ namespace odb return extra_statement_cache_.get ( conn_, image_, + id_image (), id_image_binding (), &id_image_binding ()); // Note, not id+version. } @@ -411,8 +412,9 @@ namespace odb root_statements_type& root_statements_; base_statements_type& base_statements_; - extra_statement_cache_ptr - extra_statement_cache_; + extra_statement_cache_ptr extra_statement_cache_; image_type image_; diff --git a/odb/mssql/section-statements.hxx b/odb/mssql/section-statements.hxx index 43919d5..8bb1650 100644 --- a/odb/mssql/section-statements.hxx +++ b/odb/mssql/section-statements.hxx @@ -36,6 +36,7 @@ namespace odb typedef ST traits; typedef typename traits::image_type image_type; + typedef typename traits::id_image_type id_image_type; typedef mssql::select_statement select_statement_type; typedef mssql::update_statement update_statement_type; @@ -43,7 +44,7 @@ namespace odb typedef mssql::connection connection_type; section_statements (connection_type&, - image_type&, + image_type&, id_image_type&, binding& id, binding& idv); connection_type& @@ -61,6 +62,9 @@ namespace odb image_type& image () {return image_;} + id_image_type& + id_image () {return id_image_;} + const binding& id_binding () {return id_binding_;} @@ -128,7 +132,7 @@ namespace odb traits::update_statement, traits::versioned, // Process if versioned. update_image_binding_, - traits::rowversion, + (traits::rowversion ? &idv_binding_ : 0), false)); return *update_; @@ -155,6 +159,7 @@ namespace odb // These come from object_statements. // image_type& image_; + id_image_type& id_image_; binding& id_binding_; binding& idv_binding_; diff --git a/odb/mssql/section-statements.txx b/odb/mssql/section-statements.txx index 358c216..319b0b4 100644 --- a/odb/mssql/section-statements.txx +++ b/odb/mssql/section-statements.txx @@ -11,11 +11,12 @@ namespace odb template section_statements:: section_statements (connection_type& conn, - image_type& im, + image_type& im, id_image_type& idim, binding& id, binding& idv) : conn_ (conn), svm_ (0), image_ (im), + id_image_ (idim), id_binding_ (id), idv_binding_ (idv), select_image_binding_ (select_image_bind_, diff --git a/odb/mssql/simple-object-statements.hxx b/odb/mssql/simple-object-statements.hxx index ab43439..77b1c34 100644 --- a/odb/mssql/simple-object-statements.hxx +++ b/odb/mssql/simple-object-statements.hxx @@ -39,49 +39,56 @@ namespace odb // deleter function which will be initialized during allocation // (at that point we know that the cache class is defined). // - template + template struct extra_statement_cache_ptr { typedef I image_type; + typedef ID id_image_type; typedef mssql::connection connection_type; extra_statement_cache_ptr (): p_ (0) {} ~extra_statement_cache_ptr () { if (p_ != 0) - (this->*deleter_) (0, 0, 0, 0); + (this->*deleter_) (0, 0, 0, 0, 0); } T& - get (connection_type& c, image_type& im, binding& id, binding* idv) + get (connection_type& c, + image_type& im, id_image_type& idim, + binding& id, binding* idv) { if (p_ == 0) - allocate (&c, &im, &id, (idv != 0 ? idv : &id)); + allocate (&c, &im, &idim, &id, (idv != 0 ? idv : &id)); return *p_; } private: void - allocate (connection_type*, image_type*, binding*, binding*); + allocate (connection_type*, + image_type*, id_image_type*, + binding*, binding*); private: T* p_; void (extra_statement_cache_ptr::*deleter_) ( - connection_type*, image_type*, binding*, binding*); + connection_type*, image_type*, id_image_type*, binding*, binding*); }; - template - void extra_statement_cache_ptr:: - allocate (connection_type* c, image_type* im, binding* id, binding* idv) + template + void extra_statement_cache_ptr:: + allocate (connection_type* c, + image_type* im, id_image_type* idim, + binding* id, binding* idv) { // To reduce object code size, this function acts as both allocator // and deleter. // if (p_ == 0) { - p_ = new T (*c, *im, *id, *idv); - deleter_ = &extra_statement_cache_ptr::allocate; + p_ = new T (*c, *im, *idim, *id, *idv); + deleter_ = &extra_statement_cache_ptr::allocate; } else delete p_; @@ -155,7 +162,7 @@ namespace odb typedef T object_type; typedef object_traits_impl object_traits; - optimistic_data (bind*); + optimistic_data (bind*, std::size_t skip, SQLUSMALLINT* status); binding* id_image_binding () {return &id_image_binding_;} @@ -170,7 +177,7 @@ namespace odb template struct optimistic_data { - optimistic_data (bind*) {} + optimistic_data (bind*, std::size_t, SQLUSMALLINT*) {} binding* id_image_binding () {return 0;} @@ -276,9 +283,9 @@ namespace odb // Object image. // image_type& - image () + image (std::size_t i = 0) { - return image_; + return images_[i].obj; } // Insert binding. @@ -323,7 +330,7 @@ namespace odb // Object id image and binding. // id_image_type& - id_image () {return id_image_;} + id_image (std::size_t i = 0) {return images_[i].id;} std::size_t id_image_version () const {return id_image_version_;} @@ -339,7 +346,7 @@ namespace odb // at the same time. // binding& - optimistic_id_image_binding () {return od_.id_image_binding_;} + optimistic_id_image_binding () {return *od_.id_image_binding ();} // Statements. // @@ -355,6 +362,9 @@ namespace odb insert_image_binding_, object_traits::auto_id, object_traits::rowversion, + (object_traits::rowversion + ? &optimistic_id_image_binding () + : (object_traits::auto_id ? &id_image_binding () : 0)), false)); return *persist_; @@ -385,9 +395,10 @@ namespace odb new (details::shared) update_statement_type ( conn_, object_traits::update_statement, - object_traits::versioned, // Process if versioned. + true, // Unique (0 or 1). + object_traits::versioned, // Process if versioned. update_image_binding_, - object_traits::rowversion, + object_traits::rowversion ? &optimistic_id_image_binding () : 0, false)); return *update_; @@ -401,6 +412,7 @@ namespace odb new (details::shared) delete_statement_type ( conn_, object_traits::erase_statement, + true, // Unique (0 or 1 affected rows). id_image_binding_, false)); @@ -429,7 +441,9 @@ namespace odb extra_statement_cache () { return extra_statement_cache_.get ( - conn_, image_, id_image_binding_, od_.id_image_binding ()); + conn_, + images_[0].obj, images_[0].id, + id_image_binding_, od_.id_image_binding ()); } public: @@ -476,10 +490,22 @@ namespace odb template friend class polymorphic_derived_object_statements; - extra_statement_cache_ptr - extra_statement_cache_; + extra_statement_cache_ptr extra_statement_cache_; + + // The UPDATE statement uses both the object and id image. Keep + // them next to each other so that the same skip distance can + // be used in batch binding. + // + struct images + { + image_type obj; + id_image_type id; + }; - image_type image_; + images images_[object_traits::batch]; + SQLUSMALLINT status_[object_traits::batch]; // Select binding. // @@ -508,10 +534,9 @@ namespace odb bind update_image_bind_[update_column_count + id_column_count + managed_optimistic_column_count]; - // Id image binding (only used as a parameter). Uses the suffix in - // the update bind. + // Id image binding (only used as a parameter or in OUTPUT for + // auto id and version). Uses the suffix in the update bind. // - id_image_type id_image_; std::size_t id_image_version_; binding id_image_binding_; diff --git a/odb/mssql/simple-object-statements.txx b/odb/mssql/simple-object-statements.txx index 84ed5db..1cdffb2 100644 --- a/odb/mssql/simple-object-statements.txx +++ b/odb/mssql/simple-object-statements.txx @@ -20,11 +20,14 @@ namespace odb template optimistic_data:: - optimistic_data (bind* b) + optimistic_data (bind* b, std::size_t skip, SQLUSMALLINT* status) : id_image_binding_ ( b, object_traits::id_column_count + - object_traits::managed_optimistic_column_count) + object_traits::managed_optimistic_column_count, + object_traits::batch, + skip, + status) { } @@ -43,24 +46,37 @@ namespace odb object_statements (connection_type& conn) : object_statements_base (conn), select_image_binding_ (select_image_bind_, select_column_count), - insert_image_binding_ (insert_image_bind_, insert_column_count), + insert_image_binding_ (insert_image_bind_, + insert_column_count, + object_traits::batch, + sizeof (images), + status_), update_image_binding_ (update_image_bind_, update_column_count + id_column_count + - managed_optimistic_column_count), + managed_optimistic_column_count, + object_traits::batch, + sizeof (images), + status_), id_image_binding_ (update_image_bind_ + update_column_count, - id_column_count), - od_ (update_image_bind_ + update_column_count) + id_column_count, + object_traits::batch, + sizeof (images), + status_), + od_ (update_image_bind_ + update_column_count, + sizeof (images), + status_) { - image_.version = 0; + images_[0].obj.version = 0; // @@ TODO [0] + images_[0].id.version = 0; // @@ TODO + select_image_version_ = 0; insert_image_version_ = 0; update_image_version_ = 0; update_id_image_version_ = 0; - - id_image_.version = 0; id_image_version_ = 0; - select_image_binding_.change_callback = image_.change_callback (); + select_image_binding_.change_callback = + images_[0].obj.change_callback (); std::memset (insert_image_bind_, 0, sizeof (insert_image_bind_)); std::memset (update_image_bind_, 0, sizeof (update_image_bind_)); 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_; } } } diff --git a/odb/mssql/statement.hxx b/odb/mssql/statement.hxx index 36c87f1..4ed279d 100644 --- a/odb/mssql/statement.hxx +++ b/odb/mssql/statement.hxx @@ -11,6 +11,7 @@ #include // std::size_t #include +#include #include #include @@ -126,6 +127,62 @@ namespace odb auto_handle stmt_; }; + class LIBODB_MSSQL_EXPORT bulk_statement: public statement + { + public: + virtual + ~bulk_statement () = 0; + + protected: + bulk_statement (connection_type&, + const std::string& text, + statement_kind, + const binding* process, + bool optimize, + std::size_t batch, + std::size_t skip, + SQLUSMALLINT* status); + + bulk_statement (connection_type&, + const char* text, + statement_kind, + const binding* process, + bool optimize, + std::size_t batch, + std::size_t skip, + SQLUSMALLINT* status, + bool copy_text); + + // Call SQLExecute() and set up the batch tracking variables (see + // below). Note that this function does not treat SQL_NO_DATA as + // an error since for DELETE and UPDATE statements this is a + // shortcut notation for zero rows affected. + // + SQLRETURN + execute (std::size_t n, multiple_exceptions*); + + // Return the number of failed parameter sets. + // + std::size_t + extract_errors (); + + static const unsigned long long result_unknown = ~0ULL; + + unsigned long long + affected (SQLRETURN, std::size_t errors, bool unique); + + private: + void + init (std::size_t skip); + + protected: + SQLULEN processed_; // Number of parameter sets processed so far. + SQLUSMALLINT* status_; // Parameter sets status array. + std::size_t n_; // Actual batch size. + std::size_t i_; // Position in result. + multiple_exceptions* mex_; + }; + class LIBODB_MSSQL_EXPORT select_statement: public statement { public: @@ -237,7 +294,7 @@ namespace odb select_statement* s_; }; - class LIBODB_MSSQL_EXPORT insert_statement: public statement + class LIBODB_MSSQL_EXPORT insert_statement: public bulk_statement { public: virtual @@ -248,7 +305,8 @@ namespace odb bool process_text, binding& param, bool returning_id, - bool returning_version); + bool returning_version, + binding* returning); insert_statement (connection_type& conn, const char* text, @@ -256,23 +314,30 @@ namespace odb binding& param, bool returning_id, bool returning_version, + binding* returning, bool copy_text = true); - // Return true if successful and false if the row is a duplicate. + // Return the number of parameter sets (out of n) that were attempted. + // + std::size_t + execute (std::size_t n, multiple_exceptions& mex) + { + return execute (n, &mex); + } + + // Return true if successful and false if this row is a duplicate. // All other errors are reported by throwing exceptions. // bool - execute (); + result (std::size_t i); - unsigned long long - id () + bool + execute () { - return id_; + execute (1, 0); + return result (0); } - unsigned long long - version (); - private: insert_statement (const insert_statement&); insert_statement& operator= (const insert_statement&); @@ -281,42 +346,97 @@ namespace odb void init_result (); + std::size_t + execute (std::size_t, multiple_exceptions*); + + void + fetch (SQLRETURN); + private: bool returning_id_; bool returning_version_; - bool batch_; + binding* ret_; + bool text_batch_; - unsigned long long id_; - SQLLEN id_size_ind_; - - unsigned char version_[8]; - SQLLEN version_size_ind_; + bool result_; }; - class LIBODB_MSSQL_EXPORT update_statement: public statement + class LIBODB_MSSQL_EXPORT update_statement: public bulk_statement { public: virtual ~update_statement (); + // SQL Server native client ODBC driver does not expose individual + // affected row counts for batch operations, even though it says it + // does (SQLGetInfo(SQL_PARAM_ARRAY_ROW_COUNTS) returns SQL_PARC_BATCH). + // Instead, it adds them all up and returns a single count. This is + // bad news for us. + // + // In case of updating by primary key (the affected row count is + // either 1 or 0), we can recognize the presumably successful case + // where the total affected row count is equal to the batch size + // (we can also recognize the "all unsuccessful" case where the + // total affected row count is 0). The unique_hint argument in the + // constructors below indicates whether this is a "0 or 1" UPDATE + // statement. + // + // In all other situations (provided this is a batch), the result() + // function below returns the special result_unknown value. + // update_statement (connection_type& conn, const std::string& text, bool process, binding& param, - bool returning_version); + binding* returning); + + update_statement (connection_type& conn, + const std::string& text, + bool unique_hint, + bool process, + binding& param, + binding* returning); update_statement (connection_type& conn, const char* text, bool process, binding& param, - bool returning_version, + binding* returning, bool copy_text = true); + update_statement (connection_type& conn, + const char* text, + bool unique_hint, + bool process, + binding& param, + binding* returning, + bool copy_text = true); + + // Return the number of parameter sets (out of n) that were attempted. + // + std::size_t + execute (std::size_t n, multiple_exceptions& mex) + { + return execute (n, &mex); + } + + // Return the number of rows affected (deleted) by the parameter + // set. If this is a batch (n > 1 in execute() call above) and it + // is impossible to determine the affected row count for each + // parameter set, then this function returns result_unknown. All + // other errors are reported by throwing exceptions. + // + using bulk_statement::result_unknown; + unsigned long long - execute (); + result (std::size_t i); unsigned long long - version (); + execute () + { + execute (1, 0); + return result (0); + } private: update_statement (const update_statement&); @@ -324,23 +444,48 @@ namespace odb private: void - init_result (); + init (binding& param, binding* ret); + + std::size_t + execute (std::size_t, multiple_exceptions*); private: - bool returning_version_; + bool unique_; + bool returning_; - unsigned char version_[8]; - SQLLEN version_size_ind_; + unsigned long long result_; }; - class LIBODB_MSSQL_EXPORT delete_statement: public statement + class LIBODB_MSSQL_EXPORT delete_statement: public bulk_statement { public: virtual ~delete_statement (); + // SQL Server native client ODBC driver does not expose individual + // affected row counts for batch operations, even though it says it + // does (SQLGetInfo(SQL_PARAM_ARRAY_ROW_COUNTS) returns SQL_PARC_BATCH). + // Instead, it adds them all up and returns a single count. This is + // bad news for us. + // + // In case of deleting by primary key (the affected row count is + // either 1 or 0), we can recognize the presumably successful case + // where the total affected row count is equal to the batch size + // (we can also recognize the "all unsuccessful" case where the + // total affected row count is 0). The unique_hint argument in the + // constructors below indicates whether this is a "0 or 1" DELETE + // statement. + // + // In all other situations (provided this is a batch), the result() + // function below returns the special result_unknown value. + // + delete_statement (connection_type& conn, + const std::string& text, + binding& param); + delete_statement (connection_type& conn, const std::string& text, + bool unique_hint, binding& param); delete_statement (connection_type& conn, @@ -348,12 +493,49 @@ namespace odb binding& param, bool copy_text = true); + delete_statement (connection_type& conn, + const char* text, + bool unique_hint, + binding& param, + bool copy_text = true); + + // Return the number of parameter sets (out of n) that were attempted. + // + std::size_t + execute (std::size_t n, multiple_exceptions& mex) + { + return execute (n, &mex); + } + + // Return the number of rows affected (deleted) by the parameter + // set. If this is a batch (n > 1 in execute() call above) and it + // is impossible to determine the affected row count for each + // parameter set, then this function returns result_unknown. All + // other errors are reported by throwing exceptions. + // + using bulk_statement::result_unknown; + unsigned long long - execute (); + result (std::size_t i); + + unsigned long long + execute () + { + execute (1, 0); + return result (0); + } private: delete_statement (const delete_statement&); delete_statement& operator= (const delete_statement&); + + private: + std::size_t + execute (std::size_t, multiple_exceptions*); + + private: + bool unique_; + unsigned long long result_; }; } } diff --git a/odb/mssql/statement.ixx b/odb/mssql/statement.ixx index 89be46f..56e0ca4 100644 --- a/odb/mssql/statement.ixx +++ b/odb/mssql/statement.ixx @@ -6,44 +6,59 @@ namespace odb { namespace mssql { - inline unsigned long long insert_statement:: - version () + inline bulk_statement:: + bulk_statement (connection_type& c, + const std::string& text, + statement_kind k, + const binding* process, + bool optimize, + std::size_t batch, + std::size_t skip, + SQLUSMALLINT* status) + : statement (c, text, k, process, optimize), + status_ (batch == 1 ? 0 : status) { - unsigned long long r; - - // The value is in the big-endian format. - // - unsigned char* p (reinterpret_cast (&r)); - p[0] = version_[7]; - p[1] = version_[6]; - p[2] = version_[5]; - p[3] = version_[4]; - p[4] = version_[3]; - p[5] = version_[2]; - p[6] = version_[1]; - p[7] = version_[0]; + if (status_ != 0 && !empty ()) + init (skip); + } - return r; + inline bulk_statement:: + bulk_statement (connection_type& c, + const char* text, + statement_kind k, + const binding* process, + bool optimize, + std::size_t batch, + std::size_t skip, + SQLUSMALLINT* status, + bool copy_text) + : statement (c, text, k, process, optimize, copy_text), + status_ (batch == 1 ? 0 : status) + { + if (status_ != 0 && !empty ()) + init (skip); } + // update_statement + // inline unsigned long long update_statement:: - version () + result (std::size_t i) { - unsigned long long r; + if (i != i_) + mex_->current (++i_); // mex cannot be NULL since this is a batch. - // The value is in the big-endian format. - // - unsigned char* p (reinterpret_cast (&r)); - p[0] = version_[7]; - p[1] = version_[6]; - p[2] = version_[5]; - p[3] = version_[4]; - p[4] = version_[3]; - p[5] = version_[2]; - p[6] = version_[1]; - p[7] = version_[0]; + return result_; + } + + // delete_statement + // + inline unsigned long long delete_statement:: + result (std::size_t i) + { + if (i != i_) + mex_->current (++i_); // mex cannot be NULL since this is a batch. - return r; + return result_; } } } -- cgit v1.1