From 843e19051f10118af974e987b641b41baa0e2c05 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 15 Sep 2022 15:22:19 +0200 Subject: [PATCH 01/78] Postgresql: Implement support for BLOB trimming --- src/backends/postgresql/blob.cpp | 30 ++++++++++++++++++++++++++-- tests/postgresql/test-postgresql.cpp | 15 ++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index 0e46e32f6..482f1d826 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -8,11 +8,13 @@ #define SOCI_POSTGRESQL_SOURCE #include "soci/postgresql/soci-postgresql.h" #include // libpq +#include #include #include #include #include #include +#include #ifdef _MSC_VER #pragma warning(disable:4355) @@ -105,7 +107,31 @@ std::size_t postgresql_blob_backend::append( return static_cast(writen); } -void postgresql_blob_backend::trim(std::size_t /* newLen */) +void postgresql_blob_backend::trim(std::size_t newLen) { - throw soci_error("Trimming BLOBs is not supported."); +#if PG_VERSION_NUM < 80003 + // lo_truncate was introduced in Postgresql v8.3 + (void) newLen; + throw soci_error("Your Postgresql version does not support trimming BLOBs"); +#else + if (newLen > static_cast(std::numeric_limits::max())) { + throw soci_error("Request new BLOB size exceeds INT_MAX, which is not supported"); + } + +# if PG_VERSION_NUM >= 90003 + // lo_truncate64 was introduced in Postgresql v9.3 + int ret_code = lo_truncate64(session_.conn_, fd_, newLen); +# else + int ret_code = -1; +# endif + if (ret_code == -1) { + // If we call lo_truncate64 on a server that is < v9.3, the call will fail and return -1. + // Thus, we'll try again with the slightly older function lo_truncate. + ret_code = lo_truncate(session_.conn_, fd_, newLen); + } + + if (ret_code < 0) { + throw soci_error("Cannot truncate BLOB"); + } +#endif } diff --git a/tests/postgresql/test-postgresql.cpp b/tests/postgresql/test-postgresql.cpp index a69459c49..60a95ac07 100644 --- a/tests/postgresql/test-postgresql.cpp +++ b/tests/postgresql/test-postgresql.cpp @@ -15,6 +15,7 @@ #include #include #include +#include using namespace soci; using namespace soci::tests; @@ -275,6 +276,20 @@ TEST_CASE("PostgreSQL blob", "[postgresql][blob]") b.read_from_start(buf2, 10); CHECK(std::strncmp(buf2, "abcdefghij", 10) == 0); } +#if PG_VERSION_NUM >= 80003 + { + blob b(sql); + sql << "select img from soci_test where id = 7", into(b); + CHECK(b.get_len() == 2 * sizeof(buf)); + b.trim(sizeof(buf)); + CHECK(b.get_len() == sizeof(buf)); + } + { + blob b(sql); + sql << "select img from soci_test where id = 7", into(b); + CHECK(b.get_len() == sizeof(buf)); + } +#endif unsigned long oid; sql << "select img from soci_test where id = 7", into(oid); From 2f330d48c030fe4c2a2806aff1c33d2b6a543591 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 15 Sep 2022 17:07:53 +0200 Subject: [PATCH 02/78] Revamp Postgresql BLOB implementation This commit refactors the existing implementation to factor out common code into dedicated functions. Furthermore, this commit adds support for creating new BLOBs via the soci::blob interface by writing to a default-constructed soci::blob object. The created BLOB will be deleted again, unless it is inserted into a database, in which case the lifetime of the created BLOB gets extended indefinitely (or rather to the point at which the database decides to destroy it). Thus, it is now possible to use the soci::blob type to insert new BLOBs in a Postgresql table. Note: Attempting to insert a default-constructed soci::blob into a database will also create a new BLOB object at the point at which Soci decides to really perform the insert (after checking, e.g. passed indicators). --- include/soci/postgresql/soci-postgresql.h | 34 ++++- src/backends/postgresql/blob.cpp | 132 +++++++++++++----- .../postgresql/standard-into-type.cpp | 8 +- src/backends/postgresql/standard-use-type.cpp | 16 ++- tests/postgresql/test-postgresql.cpp | 35 ++++- 5 files changed, 173 insertions(+), 52 deletions(-) diff --git a/include/soci/postgresql/soci-postgresql.h b/include/soci/postgresql/soci-postgresql.h index 3e86ac299..c4c2a63ac 100644 --- a/include/soci/postgresql/soci-postgresql.h +++ b/include/soci/postgresql/soci-postgresql.h @@ -327,8 +327,20 @@ struct postgresql_rowid_backend : details::rowid_backend unsigned long value_; }; -struct postgresql_blob_backend : details::blob_backend +class postgresql_blob_backend : public details::blob_backend { +public: + + struct blob_details { + // OID of the large object + unsigned long oid; + // File descriptor of the large object + int fd; + + blob_details(); + blob_details(unsigned long oid, int fd); + }; + postgresql_blob_backend(postgresql_session_backend & session); ~postgresql_blob_backend() override; @@ -343,10 +355,24 @@ struct postgresql_blob_backend : details::blob_backend void trim(std::size_t newLen) override; - postgresql_session_backend & session_; + const blob_details &get_blob_details() const; + + void set_blob_details(const blob_details &details); + + bool get_destroy_on_close() const; - unsigned long oid_; // oid of the large object - int fd_; // descriptor of the large object + void set_destroy_on_close(bool destroy); + + std::size_t seek(std::size_t toOffset, int from); + + void init(); + + void reset(); + +private: + postgresql_session_backend & session_; + blob_details details_; + bool destroy_on_close_; }; struct postgresql_session_backend : details::session_backend diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index 482f1d826..a022eb4a0 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -24,9 +24,14 @@ using namespace soci; using namespace soci::details; +postgresql_blob_backend::blob_details::blob_details() : oid(InvalidOid), fd(-1) {} + +postgresql_blob_backend::blob_details::blob_details(unsigned long oid, int fd) : oid(oid), fd(fd) {} + + postgresql_blob_backend::postgresql_blob_backend( postgresql_session_backend & session) - : session_(session), fd_(-1) + : session_(session), details_(), destroy_on_close_(false) { // nothing to do here, the descriptor is open in the postFetch // method of the Into element @@ -34,33 +39,19 @@ postgresql_blob_backend::postgresql_blob_backend( postgresql_blob_backend::~postgresql_blob_backend() { - if (fd_ != -1) - { - lo_close(session_.conn_, fd_); - } + reset(); } std::size_t postgresql_blob_backend::get_len() { - int const pos = lo_lseek(session_.conn_, fd_, 0, SEEK_END); - if (pos == -1) - { - throw soci_error("Cannot retrieve the size of BLOB."); - } - - return static_cast(pos); + return seek(0, SEEK_END); } std::size_t postgresql_blob_backend::read_from_start(char * buf, std::size_t toRead, std::size_t offset) { - int const pos = lo_lseek(session_.conn_, fd_, - static_cast(offset), SEEK_SET); - if (pos == -1) - { - throw soci_error("Cannot seek in BLOB."); - } + seek(offset, SEEK_SET); - int const readn = lo_read(session_.conn_, fd_, buf, toRead); + int const readn = lo_read(session_.conn_, details_.fd, buf, toRead); if (readn < 0) { throw soci_error("Cannot read from BLOB."); @@ -71,33 +62,28 @@ std::size_t postgresql_blob_backend::read_from_start(char * buf, std::size_t toR std::size_t postgresql_blob_backend::write_from_start(char const * buf, std::size_t toWrite, std::size_t offset) { - int const pos = lo_lseek(session_.conn_, fd_, - static_cast(offset), SEEK_SET); - if (pos == -1) - { - throw soci_error("Cannot seek in BLOB."); - } + init(); + + seek(offset, SEEK_SET); - int const writen = lo_write(session_.conn_, fd_, + int const written = lo_write(session_.conn_, details_.fd, const_cast(buf), toWrite); - if (writen < 0) + if (written < 0) { throw soci_error("Cannot write to BLOB."); } - return static_cast(writen); + return static_cast(written); } std::size_t postgresql_blob_backend::append( char const * buf, std::size_t toWrite) { - int const pos = lo_lseek(session_.conn_, fd_, 0, SEEK_END); - if (pos == -1) - { - throw soci_error("Cannot seek in BLOB."); - } + init(); + + seek(0, SEEK_END); - int const writen = lo_write(session_.conn_, fd_, + int const writen = lo_write(session_.conn_, details_.fd, const_cast(buf), toWrite); if (writen < 0) { @@ -120,14 +106,14 @@ void postgresql_blob_backend::trim(std::size_t newLen) # if PG_VERSION_NUM >= 90003 // lo_truncate64 was introduced in Postgresql v9.3 - int ret_code = lo_truncate64(session_.conn_, fd_, newLen); + int ret_code = lo_truncate64(session_.conn_, details_.fd, newLen); # else int ret_code = -1; # endif if (ret_code == -1) { // If we call lo_truncate64 on a server that is < v9.3, the call will fail and return -1. // Thus, we'll try again with the slightly older function lo_truncate. - ret_code = lo_truncate(session_.conn_, fd_, newLen); + ret_code = lo_truncate(session_.conn_, details_.fd, newLen); } if (ret_code < 0) { @@ -135,3 +121,77 @@ void postgresql_blob_backend::trim(std::size_t newLen) } #endif } + +const postgresql_blob_backend::blob_details &postgresql_blob_backend::get_blob_details() const { + return details_; +} + +void postgresql_blob_backend::set_blob_details(const postgresql_blob_backend::blob_details &details) { + reset(); + + details_ = details; +} + +bool postgresql_blob_backend::get_destroy_on_close() const { + return destroy_on_close_; +} + +void postgresql_blob_backend::set_destroy_on_close(bool destroy) { + destroy_on_close_ = destroy; +} + +std::size_t postgresql_blob_backend::seek(std::size_t toOffset, int from) { +#if PG_VERSION_NUM >= 90003 + pg_int64 pos = lo_lseek64(session_.conn_, details_.fd, static_cast(toOffset), from); +#else + int pos = -1; +#endif + if (pos == -1) { + // If we try to use lo_lseek64 on a Postgresql server that is older than 9.3, the function will fail + // and return -1, so we'll try again with the older function lo_lseek. + pos = lo_lseek(session_.conn_, details_.fd, static_cast(toOffset), from); + } + + if (pos < 0) + { + throw soci_error("Cannot retrieve the size of BLOB."); + } + + return static_cast(pos); +} + +void postgresql_blob_backend::init() { + if (details_.fd == -1) { + // Create a new large object + Oid oid = lo_creat(session_.conn_, INV_READ | INV_WRITE); + + if (oid == InvalidOid) { + throw soci_error("Cannot create new BLOB."); + } + + int fd = lo_open(session_.conn_, oid, INV_READ | INV_WRITE); + + if (fd == -1) { + lo_unlink(session_.conn_, oid); + throw soci_error("Cannot open newly created BLOB."); + } + + details_.oid = oid; + details_.fd = fd; + } +} + +void postgresql_blob_backend::reset() { + if (details_.fd != -1) + { + if (destroy_on_close_) { + // Remove the large object from the DB completely + lo_unlink(session_.conn_, details_.fd); + } else { + // Merely close our handle to the large object + lo_close(session_.conn_, details_.fd); + } + } + + destroy_on_close_ = false; +} diff --git a/src/backends/postgresql/standard-into-type.cpp b/src/backends/postgresql/standard-into-type.cpp index 023b1dda1..264b371e5 100644 --- a/src/backends/postgresql/standard-into-type.cpp +++ b/src/backends/postgresql/standard-into-type.cpp @@ -150,13 +150,7 @@ void postgresql_standard_into_type_backend::post_fetch( postgresql_blob_backend * bbe = static_cast(b->get_backend()); - if (bbe->fd_ != -1) - { - lo_close(statement_.session_.conn_, bbe->fd_); - } - - bbe->fd_ = fd; - bbe->oid_ = oid; + bbe->set_blob_details(postgresql_blob_backend::blob_details(oid, fd)); } break; case x_xmltype: diff --git a/src/backends/postgresql/standard-use-type.cpp b/src/backends/postgresql/standard-use-type.cpp index 9cff41eee..b1ce40752 100644 --- a/src/backends/postgresql/standard-use-type.cpp +++ b/src/backends/postgresql/standard-use-type.cpp @@ -170,10 +170,24 @@ void postgresql_standard_use_type_backend::pre_use(indicator const * ind) postgresql_blob_backend * bbe = static_cast(b->get_backend()); + // In case the backend does not reference a proper BLOB yet, this will create a + // new, empty one + bbe->init(); + + unsigned long oid = bbe->get_blob_details().oid; + + if (oid == InvalidOid) { + throw soci_error("Cannot insert invalid BLOB."); + } + + // In case the blob backend has created a new BLOB in the DB, we have to tell it + // to not destroy it again, because we will now actually insert it into the DB. + bbe->set_destroy_on_close(false); + std::size_t const bufSize = std::numeric_limits::digits10 + 2; buf_ = new char[bufSize]; - snprintf(buf_, bufSize, "%lu", bbe->oid_); + snprintf(buf_, bufSize, "%lu", oid); } break; case x_xmltype: diff --git a/tests/postgresql/test-postgresql.cpp b/tests/postgresql/test-postgresql.cpp index 60a95ac07..6c834cda1 100644 --- a/tests/postgresql/test-postgresql.cpp +++ b/tests/postgresql/test-postgresql.cpp @@ -249,23 +249,49 @@ TEST_CASE("PostgreSQL blob", "[postgresql][blob]") blob_table_creator tableCreator(sql); - char buf[] = "abcdefghijklmnopqrstuvwxyz"; - - sql << "insert into soci_test(id, img) values(7, lo_creat(-1))"; + const char buf[] = "abcdefghijklmnopqrstuvwxyz"; // in PostgreSQL, BLOB operations must be within transaction block transaction tr(sql); { + // empty, default-constructed BLOB blob b(sql); + indicator ind; - sql << "select img from soci_test where id = 7", into(b); + sql << "insert into soci_test(id, img) values(1, :img)", use(b); + sql << "select img from soci_test where id = 1", into(b, ind); + + CHECK(ind == i_ok); CHECK(b.get_len() == 0); + sql << "delete from soci_test where id = 1"; + } + { + // Create new BLOB + blob b(sql); + b.write_from_start(buf, sizeof(buf)); + + char substr[20]; + std::size_t i = b.read_from_start(substr, 3); + substr[i] = '\0'; + CHECK(substr[0] == buf[0]); + CHECK(substr[1] == buf[1]); + CHECK(substr[2] == buf[2]); + CHECK(substr[3] == '\0'); + + sql << "insert into soci_test(id, img) values(7, :img)", use(b); + } + { + // Append to BLOB + blob b(sql); + + sql << "select img from soci_test where id = 7", into(b); CHECK(b.get_len() == sizeof(buf)); b.append(buf, sizeof(buf)); + CHECK(b.get_len() == 2 * sizeof(buf)); } { @@ -291,6 +317,7 @@ TEST_CASE("PostgreSQL blob", "[postgresql][blob]") } #endif + // Destroy BLOB unsigned long oid; sql << "select img from soci_test where id = 7", into(oid); sql << "select lo_unlink(" << oid << ")"; From 8f0dbcbcf4b06b3c2577bce156f2307252f353df Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 15 Sep 2022 17:41:29 +0200 Subject: [PATCH 03/78] Simplify SQLite BLOB implementation Make use of std::vector to get rid of all the code related to manual managing of the required data buffer. --- include/soci/sqlite3/soci-sqlite3.h | 5 +- src/backends/sqlite3/blob.cpp | 73 +++++------------------------ 2 files changed, 15 insertions(+), 63 deletions(-) diff --git a/include/soci/sqlite3/soci-sqlite3.h b/include/soci/sqlite3/soci-sqlite3.h index 326d7151f..8f44ecbd1 100644 --- a/include/soci/sqlite3/soci-sqlite3.h +++ b/include/soci/sqlite3/soci-sqlite3.h @@ -279,11 +279,10 @@ struct sqlite3_blob_backend : details::blob_backend sqlite3_session_backend &session_; std::size_t set_data(char const *buf, std::size_t toWrite); - const char *get_buffer() const { return buf_; } + const char *get_buffer() const { return buffer_.data(); } private: - char *buf_; - size_t len_; + std::vector< char > buffer_; }; struct sqlite3_session_backend : details::session_backend diff --git a/src/backends/sqlite3/blob.cpp b/src/backends/sqlite3/blob.cpp index 15dcd371a..716baa0b8 100644 --- a/src/backends/sqlite3/blob.cpp +++ b/src/backends/sqlite3/blob.cpp @@ -14,102 +14,55 @@ using namespace soci; sqlite3_blob_backend::sqlite3_blob_backend(sqlite3_session_backend &session) - : session_(session), buf_(0), len_(0) + : session_(session), buffer_() { } sqlite3_blob_backend::~sqlite3_blob_backend() { - if (buf_) - { - delete [] buf_; - buf_ = 0; - len_ = 0; - } } std::size_t sqlite3_blob_backend::get_len() { - return len_; + return buffer_.size(); } std::size_t sqlite3_blob_backend::read_from_start(char * buf, std::size_t toRead, std::size_t offset) { - size_t r = toRead; + toRead = (std::min)(toRead, buffer_.size() - offset); // make sure that we don't try to read // past the end of the data - if (r > len_ - offset) - { - r = len_ - offset; - } + memcpy(buf, buffer_.data() + offset, toRead); - memcpy(buf, buf_ + offset, r); - - return r; + return toRead; } std::size_t sqlite3_blob_backend::write_from_start(char const * buf, std::size_t toWrite, std::size_t offset) { - const char* oldBuf = buf_; - std::size_t oldLen = len_; - len_ = (std::max)(len_, offset + toWrite); - - buf_ = new char[len_]; - - if (oldBuf) - { - // we need to copy both old and new buffers - // it is possible that the new does not - // completely cover the old - memcpy(buf_, oldBuf, oldLen); - delete [] oldBuf; - } - memcpy(buf_ + offset, buf, toWrite); - - return len_; + buffer_.resize((std::max)(buffer_.size(), offset + toWrite)); + + memcpy(buffer_.data() + offset, buf, toWrite); + + return buffer_.size(); } std::size_t sqlite3_blob_backend::append( char const * buf, std::size_t toWrite) { - const char* oldBuf = buf_; - - buf_ = new char[len_ + toWrite]; - - memcpy(buf_, oldBuf, len_); - - memcpy(buf_ + len_, buf, toWrite); - - delete [] oldBuf; - - len_ += toWrite; - - return len_; + return write_from_start(buf, toWrite, buffer_.size()); } void sqlite3_blob_backend::trim(std::size_t newLen) { - const char* oldBuf = buf_; - len_ = newLen; - - buf_ = new char[len_]; - - memcpy(buf_, oldBuf, len_); - - delete [] oldBuf; + buffer_.resize(newLen); } std::size_t sqlite3_blob_backend::set_data(char const *buf, std::size_t toWrite) { - if (buf_) - { - delete [] buf_; - buf_ = 0; - len_ = 0; - } + buffer_.clear(); return write_from_start(buf, toWrite); } From 642cd19fdb20e69fc78ac4a51186d339684fdf9a Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Fri, 16 Sep 2022 11:14:31 +0200 Subject: [PATCH 04/78] SQLite: Be consistent where to implement methods All methods of sqlite3_blob_backend are implemented in the respective cpp file, except for get_buffer(). This is inconsistent and therefore this commit moves the implementation of this function into the cpp file as well. --- include/soci/sqlite3/soci-sqlite3.h | 2 +- src/backends/sqlite3/blob.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/include/soci/sqlite3/soci-sqlite3.h b/include/soci/sqlite3/soci-sqlite3.h index 8f44ecbd1..1d977087c 100644 --- a/include/soci/sqlite3/soci-sqlite3.h +++ b/include/soci/sqlite3/soci-sqlite3.h @@ -279,7 +279,7 @@ struct sqlite3_blob_backend : details::blob_backend sqlite3_session_backend &session_; std::size_t set_data(char const *buf, std::size_t toWrite); - const char *get_buffer() const { return buffer_.data(); } + const char *get_buffer() const; private: std::vector< char > buffer_; diff --git a/src/backends/sqlite3/blob.cpp b/src/backends/sqlite3/blob.cpp index 716baa0b8..6a0cd394e 100644 --- a/src/backends/sqlite3/blob.cpp +++ b/src/backends/sqlite3/blob.cpp @@ -66,3 +66,8 @@ std::size_t sqlite3_blob_backend::set_data(char const *buf, std::size_t toWrite) buffer_.clear(); return write_from_start(buf, toWrite); } + +const char *sqlite3_blob_backend::get_buffer() const +{ + return buffer_.data(); +} From 02642d0dc1fc88381cf1eb7a61748dc49b00cf2a Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 15 Sep 2022 17:46:59 +0200 Subject: [PATCH 05/78] Streamline SQLite BLOB interface The functions soci::blob::write and soci::blob::append were returning the new total buffer size when using SQLite. However, on other backends, these functions return the amount of bytes written into the BLOB. If the implementation of Postgresql and Firebird can be believed to be correct, then the implementation of SQLite's BLOB actually broke the interface of soci::blob. This discrepancy is removed with this commit. With it, SQLite will now also return the bytes written instead of new total size. --- src/backends/sqlite3/blob.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/sqlite3/blob.cpp b/src/backends/sqlite3/blob.cpp index 6a0cd394e..a1eeb1e9f 100644 --- a/src/backends/sqlite3/blob.cpp +++ b/src/backends/sqlite3/blob.cpp @@ -45,7 +45,7 @@ std::size_t sqlite3_blob_backend::write_from_start(char const * buf, std::size_t memcpy(buffer_.data() + offset, buf, toWrite); - return buffer_.size(); + return toWrite; } From 6cbc98eb21015f267ea64dcc2505778137fda3a4 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Fri, 16 Sep 2022 10:36:04 +0200 Subject: [PATCH 06/78] SQLite: prefer named constant over plain NULL as parameter --- src/backends/sqlite3/statement.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/sqlite3/statement.cpp b/src/backends/sqlite3/statement.cpp index c21909be0..6883cd2ed 100644 --- a/src/backends/sqlite3/statement.cpp +++ b/src/backends/sqlite3/statement.cpp @@ -327,7 +327,7 @@ sqlite3_statement_backend::bind_and_execute(int number) break; case db_blob: - bindRes = sqlite3_bind_blob(stmt_, pos, col.buffer_.constData_, static_cast(col.buffer_.size_), NULL); + bindRes = sqlite3_bind_blob(stmt_, pos, col.buffer_.constData_, static_cast(col.buffer_.size_), SQLITE_STATIC); break; case db_xml: From c8e3de6dbaa2e0a1cb84a07b43fedb4703b18822 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Fri, 16 Sep 2022 10:43:51 +0200 Subject: [PATCH 07/78] Request SQLite to copy BLOB data Before we were using SQLITE_STATIC indicating that we'd make sure that the passed pointer remains valid until the processing is completely done. However, since at this point in the code, we don't own the pointer, we can't make any guarantees about its lifetime. Thus, we now use SQLITE_TRANSIENT, which will cause SQLite to copy the buffer inside the sqlite3_bind_blob call. Note: This makes the handling of BLOBs consistent with what is already done for strings. --- src/backends/sqlite3/statement.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backends/sqlite3/statement.cpp b/src/backends/sqlite3/statement.cpp index 6883cd2ed..05d458ddf 100644 --- a/src/backends/sqlite3/statement.cpp +++ b/src/backends/sqlite3/statement.cpp @@ -327,7 +327,10 @@ sqlite3_statement_backend::bind_and_execute(int number) break; case db_blob: - bindRes = sqlite3_bind_blob(stmt_, pos, col.buffer_.constData_, static_cast(col.buffer_.size_), SQLITE_STATIC); + // Since we don't own the buffer_ pointer we are passing here, we can't make any lifetime guarantees other than + // it is currently valid. Thus, we ask SQLite to make a copy of the underlying buffer to ensure + // the database can always access a valid buffer. + bindRes = sqlite3_bind_blob(stmt_, pos, col.buffer_.constData_, static_cast(col.buffer_.size_), SQLITE_TRANSIENT); break; case db_xml: From 8cc3512ae2eaa19140c8cd8c7a8489ec7b340b4a Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Fri, 16 Sep 2022 11:16:38 +0200 Subject: [PATCH 08/78] SQLite allow insertion of default-constructed BLOBs Previously, attempting to insert a default-constructed soci::blob object into a SQLite database would insert NULL instead of an empty BLOB into the DB. --- include/soci/sqlite3/soci-sqlite3.h | 2 ++ src/backends/sqlite3/blob.cpp | 7 +++++++ src/backends/sqlite3/standard-use-type.cpp | 7 +++++++ 3 files changed, 16 insertions(+) diff --git a/include/soci/sqlite3/soci-sqlite3.h b/include/soci/sqlite3/soci-sqlite3.h index 1d977087c..c14b56160 100644 --- a/include/soci/sqlite3/soci-sqlite3.h +++ b/include/soci/sqlite3/soci-sqlite3.h @@ -281,6 +281,8 @@ struct sqlite3_blob_backend : details::blob_backend std::size_t set_data(char const *buf, std::size_t toWrite); const char *get_buffer() const; + void ensure_buffer_initialized(); + private: std::vector< char > buffer_; }; diff --git a/src/backends/sqlite3/blob.cpp b/src/backends/sqlite3/blob.cpp index a1eeb1e9f..8e7dc9079 100644 --- a/src/backends/sqlite3/blob.cpp +++ b/src/backends/sqlite3/blob.cpp @@ -71,3 +71,10 @@ const char *sqlite3_blob_backend::get_buffer() const { return buffer_.data(); } + +void sqlite3_blob_backend::ensure_buffer_initialized() { + // Ensure that the used buffer is at least large enough to hold one element. + // Thus, in case the vector has not yet allocated a buffer at all, it is forced + // to do so now. + buffer_.reserve(1); +} diff --git a/src/backends/sqlite3/standard-use-type.cpp b/src/backends/sqlite3/standard-use-type.cpp index 016f4da6e..f88f7fe0d 100644 --- a/src/backends/sqlite3/standard-use-type.cpp +++ b/src/backends/sqlite3/standard-use-type.cpp @@ -204,6 +204,13 @@ void sqlite3_standard_use_type_backend::pre_use(indicator const * ind) blob *b = static_cast(data_); sqlite3_blob_backend *bbe = static_cast(b->get_backend()); + // In case the internal buffer has not been initialized yet, get_buffer() will return nullptr. In this case, + // we want to make sure to insert an empty BLOB into the DB. However, when passing a nullptr to the + // sqlite3_bind_blob routine (in statement.cpp), it has the same effect as sqlite3_bind_null and thus + // is inserting NULL instead. + // Therefore, we want to make sure that the buffer is definitely initialized (though it can still be empty). + bbe->ensure_buffer_initialized(); + col.buffer_.constData_ = bbe->get_buffer(); col.buffer_.size_ = bbe->get_len(); break; From e86da4e712d7095f88cbf6ed4007d53148b218fa Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 15 Sep 2022 17:53:11 +0200 Subject: [PATCH 09/78] Extend SQLite BLOB tests --- tests/sqlite3/test-sqlite3.cpp | 60 +++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/tests/sqlite3/test-sqlite3.cpp b/tests/sqlite3/test-sqlite3.cpp index 90b83edcf..7071b50a2 100644 --- a/tests/sqlite3/test-sqlite3.cpp +++ b/tests/sqlite3/test-sqlite3.cpp @@ -278,8 +278,66 @@ TEST_CASE("SQLite blob", "[sqlite][blob]") char buf[] = "abcdefghijklmnopqrstuvwxyz"; - sql << "insert into soci_test(id, img) values(7, '')"; + { + // empty, default-constructed BLOB + blob b(sql); + indicator ind; + + sql << "insert into soci_test(id, img) values(1, :img)", use(b); + sql << "select img from soci_test where id = 1", into(b, ind); + + CHECK(ind == i_ok); + CHECK(b.get_len() == 0); + + sql << "delete from soci_test where id = 1"; + } + { + // Create new BLOB + blob b(sql); + + b.write_from_start(buf, sizeof(buf)); + + char substr[20]; + std::size_t i = b.read_from_start(substr, 3); + substr[i] = '\0'; + CHECK(substr[0] == buf[0]); + CHECK(substr[1] == buf[1]); + CHECK(substr[2] == buf[2]); + CHECK(substr[3] == '\0'); + + sql << "insert into soci_test(id, img) values(7, :img)", use(b); + } + { + // Append to BLOB + blob b(sql); + + sql << "select img from soci_test where id = 7", into(b); + CHECK(b.get_len() == sizeof(buf)); + b.append(buf, sizeof(buf)); + + CHECK(b.get_len() == 2 * sizeof(buf)); + + sql << "update soci_test set img = :img where id = 7", use(b); + } + { + // Read from BLOB as fetched from DB + blob b(sql); + sql << "select img from soci_test where id = 7", into(b); + CHECK(b.get_len() == 2 * sizeof(buf)); + char buf2[100]; + b.read_from_start(buf2, 10); + CHECK(std::strncmp(buf2, "abcdefghij", 10) == 0); + } + { + // Trim BLOB + blob b(sql); + sql << "select img from soci_test where id = 7", into(b); + CHECK(b.get_len() == 2 * sizeof(buf)); + b.trim(0); + CHECK(b.get_len() == 0); + sql << "update soci_test set img = :img where id = 7", use(b); + } { blob b(sql); From 213e99eaad636d6835b68e89783b9a4509c92d07 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 15 Sep 2022 18:06:24 +0200 Subject: [PATCH 10/78] Extend BLOB documentation --- docs/lobs.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/lobs.md b/docs/lobs.md index a6273262c..e02a5297a 100644 --- a/docs/lobs.md +++ b/docs/lobs.md @@ -4,9 +4,17 @@ The SOCI library provides also an interface for basic operations on large objects (BLOBs - Binary Large OBjects). +Selecting a BLOB from a table: + blob b(sql); // sql is a session object sql << "select mp3 from mymusic where id = 123", into(b); +Inserting a BLOB from a table: + + blob b(sql); // sql is a session object + b.write_from_start(data.data(), data.size()); // data is e.g. a std::vector< char > + sql << "insert into mymusic mp3, id VALUES(:mp3, 124)", use(b); + The following functions are provided in the `blob` interface, mimicking the file-like operations: * `std::size_t get_len();` @@ -15,13 +23,18 @@ The following functions are provided in the `blob` interface, mimicking the file * `std::size_t append(char const *buf, std::size_t toWrite);` * `void trim(std::size_t newLen);` -The `offset` parameter is always counted from the beginning of the BLOB's data. +The `offset` parameter is always counted from the beginning of the BLOB's data. `read_from_start` and `write_from_start` and `append` return the amount of read or written bytes. ### Portability notes * The way to define BLOB table columns and create or destroy BLOB objects in the database varies between different database engines. Please see the SQL documentation relevant for the given server to learn how this is actually done. The test programs provided with the SOCI library can be also a simple source of full working examples. -* The `trim` function is not currently available for the PostgreSQL backend. +* BLOBs are currently not implemented for all supported backends. Backends missing an implementation are `ODBC`, `MySQL` and `DB2`. +* The plain `read(...)` and `write(...)` functions use offsets in a backend-specific format (some start at zero, some at one). They are retained only for backwards compatibility. Don't use them in new code! +* Some backends (e.g. PostgreSQL) support BLOBs only while a transaction is active. Using a `soci::blob` object outside of a transaction in these cases is undefined behavior. + In order to write portable code, you should always ensure to start a transaction before working with BLOBs and end it only after you are done with the BLOB object. +* For some backends, writing to the `soci::blob` object immediately updates the values stored in the database, without having to re-insert the value again. + However, for other backends (e.g. SQLite) this is not true and in order for any changes to be reflected in the final DB, you'll have to perform an explicit insert after having modified the blob object. ## Long strings and XML From e80e4949176cd531e7829fe221bd51984acf509f Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 15 Sep 2022 19:32:12 +0200 Subject: [PATCH 11/78] Postgresql: BLOB - Implement append in terms of write --- src/backends/postgresql/blob.cpp | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index a022eb4a0..314d78303 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -79,18 +79,7 @@ std::size_t postgresql_blob_backend::write_from_start(char const * buf, std::siz std::size_t postgresql_blob_backend::append( char const * buf, std::size_t toWrite) { - init(); - - seek(0, SEEK_END); - - int const writen = lo_write(session_.conn_, details_.fd, - const_cast(buf), toWrite); - if (writen < 0) - { - throw soci_error("Cannot append to BLOB."); - } - - return static_cast(writen); + return write(get_len(), buf, toWrite); } void postgresql_blob_backend::trim(std::size_t newLen) From 22af83abf973e8ffaa545e2d6d23514d10fd0822 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 15 Sep 2022 19:33:18 +0200 Subject: [PATCH 12/78] Postgresql: BLOB - make errors more helpful --- src/backends/postgresql/blob.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index 314d78303..1211bfa1a 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -54,7 +54,7 @@ std::size_t postgresql_blob_backend::read_from_start(char * buf, std::size_t toR int const readn = lo_read(session_.conn_, details_.fd, buf, toRead); if (readn < 0) { - throw soci_error("Cannot read from BLOB."); + throw soci_error(std::string("Cannot read from BLOB: ") + PQerrorMessage(session_.conn_)); } return static_cast(readn); @@ -70,7 +70,7 @@ std::size_t postgresql_blob_backend::write_from_start(char const * buf, std::siz const_cast(buf), toWrite); if (written < 0) { - throw soci_error("Cannot write to BLOB."); + throw soci_error(std::string("Cannot write to BLOB: ") + PQerrorMessage(session_.conn_)); } return static_cast(written); @@ -79,7 +79,7 @@ std::size_t postgresql_blob_backend::write_from_start(char const * buf, std::siz std::size_t postgresql_blob_backend::append( char const * buf, std::size_t toWrite) { - return write(get_len(), buf, toWrite); + return write_from_start(buf, toWrite, get_len()); } void postgresql_blob_backend::trim(std::size_t newLen) @@ -106,7 +106,7 @@ void postgresql_blob_backend::trim(std::size_t newLen) } if (ret_code < 0) { - throw soci_error("Cannot truncate BLOB"); + throw soci_error(std::string("Cannot truncate BLOB: ") + PQerrorMessage(session_.conn_)); } #endif } @@ -155,7 +155,7 @@ void postgresql_blob_backend::init() { Oid oid = lo_creat(session_.conn_, INV_READ | INV_WRITE); if (oid == InvalidOid) { - throw soci_error("Cannot create new BLOB."); + throw soci_error(std::string("Cannot create new BLOB: ") + PQerrorMessage(session_.conn_)); } int fd = lo_open(session_.conn_, oid, INV_READ | INV_WRITE); From 52b6862c618c346d37f3fc201cad4d588a9f3b8d Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Fri, 16 Sep 2022 12:02:24 +0200 Subject: [PATCH 13/78] Postgresql: Implement read-ops on uninitialized BLOBs --- src/backends/postgresql/blob.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index 1211bfa1a..3b3d3713c 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -44,11 +44,20 @@ postgresql_blob_backend::~postgresql_blob_backend() std::size_t postgresql_blob_backend::get_len() { - return seek(0, SEEK_END); + return details_.fd == -1 ? 0 : seek(0, SEEK_END); } std::size_t postgresql_blob_backend::read_from_start(char * buf, std::size_t toRead, std::size_t offset) { + std::size_t size = get_len(); + if (offset >= size && !(size == 0 && offset == 0)) { + throw soci_error("Can't read past-the-end of BLOB data."); + } + if (size == 0) { + // Reading from an empty blob, is defined as a no-op + return 0; + } + seek(offset, SEEK_SET); int const readn = lo_read(session_.conn_, details_.fd, buf, toRead); From 7fa93e85869c2807b6654567ecec524773686479 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Fri, 16 Sep 2022 11:49:28 +0200 Subject: [PATCH 14/78] BLOB: Add default-constructed read tests --- tests/firebird/test-firebird.cpp | 10 ++++++++++ tests/postgresql/test-postgresql.cpp | 10 ++++++++++ tests/sqlite3/test-sqlite3.cpp | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/tests/firebird/test-firebird.cpp b/tests/firebird/test-firebird.cpp index 769ae0d8f..9099c01e9 100644 --- a/tests/firebird/test-firebird.cpp +++ b/tests/firebird/test-firebird.cpp @@ -516,6 +516,16 @@ TEST_CASE("Firebird blobs", "[firebird][blob]") sql.commit(); sql.begin(); + { + // Read from default-constructed BLOB + blob b(sql); + + CHECK(b.get_len() == 0); + + char buf[5]; + std::size_t read = b.read_from_start(buf, 5); + CHECK(read == 0); + } { // verify empty blob blob b(sql); diff --git a/tests/postgresql/test-postgresql.cpp b/tests/postgresql/test-postgresql.cpp index 6c834cda1..3e4cb1211 100644 --- a/tests/postgresql/test-postgresql.cpp +++ b/tests/postgresql/test-postgresql.cpp @@ -254,6 +254,16 @@ TEST_CASE("PostgreSQL blob", "[postgresql][blob]") // in PostgreSQL, BLOB operations must be within transaction block transaction tr(sql); + { + // Read from default-constructed BLOB + blob b(sql); + + CHECK(b.get_len() == 0); + + char buf2[5]; + std::size_t read = b.read_from_start(buf2, 5); + CHECK(read == 0); + } { // empty, default-constructed BLOB blob b(sql); diff --git a/tests/sqlite3/test-sqlite3.cpp b/tests/sqlite3/test-sqlite3.cpp index 7071b50a2..93a76f584 100644 --- a/tests/sqlite3/test-sqlite3.cpp +++ b/tests/sqlite3/test-sqlite3.cpp @@ -278,6 +278,16 @@ TEST_CASE("SQLite blob", "[sqlite][blob]") char buf[] = "abcdefghijklmnopqrstuvwxyz"; + { + // Read from default-constructed BLOB + blob b(sql); + + CHECK(b.get_len() == 0); + + char buf2[5]; + std::size_t read = b.read_from_start(buf2, 5); + CHECK(read == 0); + } { // empty, default-constructed BLOB blob b(sql); From 5e2efa4ae6fc12df58edd03740e7f1230755e949 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Fri, 16 Sep 2022 16:03:02 +0200 Subject: [PATCH 15/78] Factor out vector-based blob-backend --- include/soci/mysql/soci-mysql.h | 5 +-- include/soci/soci-backend.h | 67 +++++++++++++++++++++++++++++ include/soci/sqlite3/soci-sqlite3.h | 19 +------- src/backends/mysql/blob.cpp | 4 +- src/backends/sqlite3/blob.cpp | 54 +---------------------- 5 files changed, 74 insertions(+), 75 deletions(-) diff --git a/include/soci/mysql/soci-mysql.h b/include/soci/mysql/soci-mysql.h index 7a45bee8e..4a457b83d 100644 --- a/include/soci/mysql/soci-mysql.h +++ b/include/soci/mysql/soci-mysql.h @@ -241,8 +241,9 @@ struct mysql_rowid_backend : details::rowid_backend ~mysql_rowid_backend() override; }; -struct mysql_blob_backend : details::blob_backend +class mysql_blob_backend : public details::trivial_blob_backend { +public: mysql_blob_backend(mysql_session_backend &session); ~mysql_blob_backend() override; @@ -252,8 +253,6 @@ struct mysql_blob_backend : details::blob_backend std::size_t write_from_start(char const *buf, std::size_t toWrite, std::size_t offset = 0) override; std::size_t append(char const *buf, std::size_t toWrite) override; void trim(std::size_t newLen) override; - - mysql_session_backend &session_; }; struct mysql_session_backend : details::session_backend diff --git a/include/soci/soci-backend.h b/include/soci/soci-backend.h index ffb9099aa..20e82cacd 100644 --- a/include/soci/soci-backend.h +++ b/include/soci/soci-backend.h @@ -15,6 +15,9 @@ #include #include #include +#include +#include +#include namespace soci { @@ -319,6 +322,70 @@ class blob_backend SOCI_NOT_COPYABLE(blob_backend) }; +/** + * This Blob implementation uses an explicit buffer that is read from and written to, instead of + * directly communicating with the underlaying database. + * Thus, it is intended to be used whenever the underlying database does not offer a more efficient + * way of dealing with BLOBs. + */ +class trivial_blob_backend : public blob_backend +{ +public: + std::size_t get_len() override { return buffer_.size(); } + + std::size_t read(std::size_t offset, char* buf, + std::size_t toRead) override + { + toRead = (std::min)(toRead, buffer_.size() - offset); + + // make sure that we don't try to read + // past the end of the data + memcpy(buf, buffer_.data() + offset, toRead); + + return toRead; + } + + std::size_t read_from_start(char* buf, std::size_t toRead, + std::size_t offset = 0) override + { + return read(offset, buf, toRead); + } + + std::size_t write(std::size_t offset, char const* buf, + std::size_t toWrite) override + { + buffer_.resize((std::max)(buffer_.size(), offset + toWrite)); + + memcpy(buffer_.data() + offset, buf, toWrite); + + return toWrite; + } + + std::size_t write_from_start(const char* buf, std::size_t toWrite, + std::size_t offset = 0) override + { + return write(offset, buf, toWrite); + } + + std::size_t append(char const* buf, std::size_t toWrite) override + { + return write(buffer_.size(), buf, toWrite); + } + + void trim(std::size_t newLen) override { buffer_.resize(newLen); } + + std::size_t set_data(char const* buf, std::size_t toWrite) + { + buffer_.clear(); + return write_from_start(buf, toWrite); + } + + const char *get_buffer() const { return buffer_.data(); } + +protected: + std::vector< char > buffer_; +}; + // polymorphic session backend class session_backend diff --git a/include/soci/sqlite3/soci-sqlite3.h b/include/soci/sqlite3/soci-sqlite3.h index c14b56160..7d8e62241 100644 --- a/include/soci/sqlite3/soci-sqlite3.h +++ b/include/soci/sqlite3/soci-sqlite3.h @@ -261,30 +261,13 @@ struct sqlite3_rowid_backend : details::rowid_backend unsigned long value_; }; -struct sqlite3_blob_backend : details::blob_backend +struct sqlite3_blob_backend : details::trivial_blob_backend { sqlite3_blob_backend(sqlite3_session_backend &session); ~sqlite3_blob_backend() override; - std::size_t get_len() override; - - std::size_t read_from_start(char * buf, std::size_t toRead, std::size_t offset = 0) override; - - std::size_t write_from_start(const char * buf, std::size_t toWrite, std::size_t offset = 0) override; - - std::size_t append(char const *buf, std::size_t toWrite) override; - void trim(std::size_t newLen) override; - - sqlite3_session_backend &session_; - - std::size_t set_data(char const *buf, std::size_t toWrite); - const char *get_buffer() const; - void ensure_buffer_initialized(); - -private: - std::vector< char > buffer_; }; struct sqlite3_session_backend : details::session_backend diff --git a/src/backends/mysql/blob.cpp b/src/backends/mysql/blob.cpp index 93d44081a..baa09e99d 100644 --- a/src/backends/mysql/blob.cpp +++ b/src/backends/mysql/blob.cpp @@ -18,8 +18,8 @@ using namespace soci; using namespace soci::details; -mysql_blob_backend::mysql_blob_backend(mysql_session_backend &session) - : session_(session) +mysql_blob_backend::mysql_blob_backend(mysql_session_backend &) + : details::trivial_blob_backend() { throw soci_error("BLOBs are not supported."); } diff --git a/src/backends/sqlite3/blob.cpp b/src/backends/sqlite3/blob.cpp index 8e7dc9079..724dd83d1 100644 --- a/src/backends/sqlite3/blob.cpp +++ b/src/backends/sqlite3/blob.cpp @@ -13,8 +13,8 @@ using namespace soci; -sqlite3_blob_backend::sqlite3_blob_backend(sqlite3_session_backend &session) - : session_(session), buffer_() +sqlite3_blob_backend::sqlite3_blob_backend(sqlite3_session_backend &) + : details::trivial_blob_backend() { } @@ -22,56 +22,6 @@ sqlite3_blob_backend::~sqlite3_blob_backend() { } -std::size_t sqlite3_blob_backend::get_len() -{ - return buffer_.size(); -} - -std::size_t sqlite3_blob_backend::read_from_start(char * buf, std::size_t toRead, std::size_t offset) -{ - toRead = (std::min)(toRead, buffer_.size() - offset); - - // make sure that we don't try to read - // past the end of the data - memcpy(buf, buffer_.data() + offset, toRead); - - return toRead; -} - - -std::size_t sqlite3_blob_backend::write_from_start(char const * buf, std::size_t toWrite, std::size_t offset) -{ - buffer_.resize((std::max)(buffer_.size(), offset + toWrite)); - - memcpy(buffer_.data() + offset, buf, toWrite); - - return toWrite; -} - - -std::size_t sqlite3_blob_backend::append( - char const * buf, std::size_t toWrite) -{ - return write_from_start(buf, toWrite, buffer_.size()); -} - - -void sqlite3_blob_backend::trim(std::size_t newLen) -{ - buffer_.resize(newLen); -} - -std::size_t sqlite3_blob_backend::set_data(char const *buf, std::size_t toWrite) -{ - buffer_.clear(); - return write_from_start(buf, toWrite); -} - -const char *sqlite3_blob_backend::get_buffer() const -{ - return buffer_.data(); -} - void sqlite3_blob_backend::ensure_buffer_initialized() { // Ensure that the used buffer is at least large enough to hold one element. // Thus, in case the vector has not yet allocated a buffer at all, it is forced From 2e27651a7d95ab426d94f91393f3ab6f48e53251 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Fri, 16 Sep 2022 16:03:38 +0200 Subject: [PATCH 16/78] MySQL: Add support for BLOBs --- docs/lobs.md | 2 +- include/private/soci-exchange-cast.h | 7 ++ include/soci/mysql/soci-mysql.h | 10 +-- src/backends/mysql/blob.cpp | 86 +++++++++++++++++--- src/backends/mysql/standard-into-type.cpp | 12 +++ src/backends/mysql/standard-use-type.cpp | 23 ++++++ tests/mysql/test-mysql.cpp | 98 +++++++++++++++++++++++ 7 files changed, 220 insertions(+), 18 deletions(-) diff --git a/docs/lobs.md b/docs/lobs.md index e02a5297a..cf50cb479 100644 --- a/docs/lobs.md +++ b/docs/lobs.md @@ -29,7 +29,7 @@ The `offset` parameter is always counted from the beginning of the BLOB's data. * The way to define BLOB table columns and create or destroy BLOB objects in the database varies between different database engines. Please see the SQL documentation relevant for the given server to learn how this is actually done. The test programs provided with the SOCI library can be also a simple source of full working examples. -* BLOBs are currently not implemented for all supported backends. Backends missing an implementation are `ODBC`, `MySQL` and `DB2`. +* BLOBs are currently not implemented for all supported backends. Backends missing an implementation are `ODBC` and `DB2`. * The plain `read(...)` and `write(...)` functions use offsets in a backend-specific format (some start at zero, some at one). They are retained only for backwards compatibility. Don't use them in new code! * Some backends (e.g. PostgreSQL) support BLOBs only while a transaction is active. Using a `soci::blob` object outside of a transaction in these cases is undefined behavior. In order to write portable code, you should always ensure to start a transaction before working with BLOBs and end it only after you are done with the BLOB object. diff --git a/include/private/soci-exchange-cast.h b/include/private/soci-exchange-cast.h index 596386dc4..755af0ae4 100644 --- a/include/private/soci-exchange-cast.h +++ b/include/private/soci-exchange-cast.h @@ -10,6 +10,7 @@ #include "soci/soci-backend.h" #include "soci/type-wrappers.h" +#include "soci/blob.h" #include #include @@ -107,6 +108,12 @@ struct exchange_type_traits typedef xml_type value_type; }; +template <> +struct exchange_type_traits +{ + typedef blob value_type; +}; + // exchange_type_traits not defined for x_statement, x_rowid and x_blob here. template diff --git a/include/soci/mysql/soci-mysql.h b/include/soci/mysql/soci-mysql.h index 4a457b83d..c43598003 100644 --- a/include/soci/mysql/soci-mysql.h +++ b/include/soci/mysql/soci-mysql.h @@ -248,11 +248,11 @@ class mysql_blob_backend : public details::trivial_blob_backend ~mysql_blob_backend() override; - std::size_t get_len() override; - std::size_t read_from_start(char *buf, std::size_t toRead, std::size_t offset = 0) override; - std::size_t write_from_start(char const *buf, std::size_t toWrite, std::size_t offset = 0) override; - std::size_t append(char const *buf, std::size_t toWrite) override; - void trim(std::size_t newLen) override; + std::size_t hex_str_size() const; + void write_hex_str(char *buf, std::size_t size) const; + std::string as_hex_str() const; + + void load_from_hex_str(const char* str, std::size_t length); }; struct mysql_session_backend : details::session_backend diff --git a/src/backends/mysql/blob.cpp b/src/backends/mysql/blob.cpp index baa09e99d..e3c39ad37 100644 --- a/src/backends/mysql/blob.cpp +++ b/src/backends/mysql/blob.cpp @@ -10,6 +10,9 @@ #include "soci/mysql/soci-mysql.h" #include +#include +#include + #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable:4355 4702) @@ -21,37 +24,96 @@ using namespace soci::details; mysql_blob_backend::mysql_blob_backend(mysql_session_backend &) : details::trivial_blob_backend() { - throw soci_error("BLOBs are not supported."); } mysql_blob_backend::~mysql_blob_backend() { } -std::size_t mysql_blob_backend::get_len() +static unsigned char decode_hex_digit(char c) { - throw soci_error("BLOBs are not supported."); + unsigned char i = static_cast(tolower(c)); + + assert((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f')); + + if (i <= '9') { + return i - '0'; + } else { + return i - 'a'; + } } -std::size_t mysql_blob_backend::read_from_start(char * /* buf */, std::size_t /* toRead */, std::size_t /* offset */) +static char encode_hex_digit(unsigned char d) { + static const char hexMap[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f'}; + + return hexMap[d]; +} + +std::size_t mysql_blob_backend::hex_str_size() const { - throw soci_error("BLOBs are not supported."); + // Every byte is represented by 2 hex digits + // +2 as for non-empty buffers, the resulting hex sequence is prefixed by "0x" + return buffer_.size() * 2 + (buffer_.empty() ? 0 : 2); } -std::size_t mysql_blob_backend::write_from_start(char const * /* buf */, std::size_t /* toWrite */, std::size_t /* offset */) +void mysql_blob_backend::write_hex_str(char *buf, std::size_t size) const { - throw soci_error("BLOBs are not supported."); + assert(size >= hex_str_size()); + + if (size < hex_str_size()) + { + throw soci_error("MySQL BLOB: Provided buffer is too small to hold hex string"); + } + + if (buffer_.empty()) { + return; + } else { + buf[0] = '0'; + buf[1] = 'x'; + } + + // Inspired by https://codereview.stackexchange.com/a/78539 + for (std::size_t i = 0; i < buffer_.size(); ++i) { + // First 4 bits + buf[2 + 2 * i ] = encode_hex_digit(static_cast(buffer_[i]) >> 4); + // Following 4 bits + buf[2 + 2 * i + 1] = encode_hex_digit(static_cast(buffer_[i]) & 0x0F); + } } -std::size_t mysql_blob_backend::append( - char const * /* buf */, std::size_t /* toWrite */) +std::string mysql_blob_backend::as_hex_str() const { - throw soci_error("BLOBs are not supported."); + + std::string hexStr; + hexStr.resize(hex_str_size()); + + write_hex_str(&hexStr[0], hexStr.size()); + + return hexStr; } -void mysql_blob_backend::trim(std::size_t /* newLen */) +void mysql_blob_backend::load_from_hex_str(const char *str, std::size_t length) { - throw soci_error("BLOBs are not supported."); + std::size_t nBytes = length / 2; + + if (nBytes * 2 != length) { + // We expect an even amount of hex digits + throw soci_error("Cannot load BLOB from invalid hex-representation (uneven amount of digits)"); + } + + if (nBytes > 0) { + assert(nBytes > 1); + + // The first "byte" as detected by above calculation is only the prefix "0x" + nBytes -= 1; + } + + buffer_.resize(nBytes); + + for (std::size_t i = 0; i < nBytes; ++i) { + buffer_[i] = (decode_hex_digit(str[2 + 2 * i]) << 4) + decode_hex_digit(str[2 + 2 * i + 1]); + } } #ifdef _MSC_VER diff --git a/src/backends/mysql/standard-into-type.cpp b/src/backends/mysql/standard-into-type.cpp index edb68684f..a07888c1c 100644 --- a/src/backends/mysql/standard-into-type.cpp +++ b/src/backends/mysql/standard-into-type.cpp @@ -13,6 +13,7 @@ #include "common.h" #include "soci-exchange-cast.h" #include "soci-mktime.h" +#include "soci/blob.h" // std #include #include @@ -140,6 +141,17 @@ void mysql_standard_into_type_backend::post_fetch( // attempt to parse the string and convert to std::tm parse_std_tm(buf, exchange_type_cast(data_)); break; + case x_blob: + { + unsigned long * lengths = mysql_fetch_lengths(statement_.result_); + std::size_t size = lengths[pos]; + blob &b = exchange_type_cast(data_); + + mysql_blob_backend *bbe = static_cast(b.get_backend()); + + bbe->set_data(buf, size); + } + break; default: throw soci_error("Into element used with non-supported type."); } diff --git a/src/backends/mysql/standard-use-type.cpp b/src/backends/mysql/standard-use-type.cpp index 17fe5d381..a6d3c19dc 100644 --- a/src/backends/mysql/standard-use-type.cpp +++ b/src/backends/mysql/standard-use-type.cpp @@ -12,6 +12,7 @@ #include "soci/soci-platform.h" #include "soci-dtocstr.h" #include "soci-exchange-cast.h" +#include "soci/blob.h" // std #include #include @@ -159,6 +160,28 @@ void mysql_standard_use_type_backend::pre_use(indicator const *ind) t.tm_hour, t.tm_min, t.tm_sec); } break; + case x_blob: + { + blob * b = static_cast(data_); + mysql_blob_backend * bbe = + static_cast(b->get_backend()); + + std::size_t hex_size = bbe->hex_str_size(); + if (hex_size == 0) { + // We can't represent an empty BLOB as hex (thus hex_str_size returns 0). Instead, we'll + // use '' to initialize and empty BLOB. + buf_ = new char[3]; + buf_[0] = '\''; + buf_[1] = '\''; + buf_[2] = '\0'; + } else { + buf_ = new char[hex_size + 1]; + bbe->write_hex_str(buf_, hex_size); + // Add NULL terminator + buf_[hex_size] = '\0'; + } + } + break; default: throw soci_error("Use element used with non-supported type."); } diff --git a/tests/mysql/test-mysql.cpp b/tests/mysql/test-mysql.cpp index 7e933432e..4fe949b8a 100644 --- a/tests/mysql/test-mysql.cpp +++ b/tests/mysql/test-mysql.cpp @@ -605,6 +605,104 @@ TEST_CASE("MySQL function call", "[mysql][function]") sql << "select concat(@day,' ',@mm,' ',@year)", into(r); } +// BLOB test +struct blob_table_creator : public table_creator_base +{ + blob_table_creator(soci::session & sql) + : table_creator_base(sql) + { + sql << + "create table soci_test (" + " id integer," + " img blob" + ")"; + } +}; + +TEST_CASE("MySQL blob", "[mysql][blob]") +{ + soci::session sql(backEnd, connectString); + + blob_table_creator tableCreator(sql); + + const char buf[] = "abcdefghijklmnopqrstuvwxyz"; + + { + // Read from default-constructed BLOB + blob b(sql); + + CHECK(b.get_len() == 0); + + char buf2[5]; + std::size_t read = b.read_from_start(buf2, 5); + CHECK(read == 0); + } + { + // empty, default-constructed BLOB + blob b(sql); + indicator ind; + + sql << "insert into soci_test(id, img) values(1, :img)", use(b); + sql << "select img from soci_test where id = 1", into(b, ind); + + CHECK(ind == i_ok); + CHECK(b.get_len() == 0); + + sql << "delete from soci_test where id = 1"; + } + { + // Create new BLOB + blob b(sql); + + b.write_from_start(buf, sizeof(buf)); + + char substr[20]; + std::size_t i = b.read_from_start(substr, 3); + substr[i] = '\0'; + CHECK(substr[0] == buf[0]); + CHECK(substr[1] == buf[1]); + CHECK(substr[2] == buf[2]); + CHECK(substr[3] == '\0'); + + sql << "insert into soci_test(id, img) values(7, :img)", use(b); + } + { + // Append to BLOB + blob b(sql); + + sql << "select img from soci_test where id = 7", into(b); + CHECK(b.get_len() == sizeof(buf)); + + b.append(buf, sizeof(buf)); + + CHECK(b.get_len() == 2 * sizeof(buf)); + + sql << "update soci_test set img = :img where id = 7", use(b); + } + { + blob b(sql); + sql << "select img from soci_test where id = 7", into(b); + CHECK(b.get_len() == 2 * sizeof(buf)); + char buf2[100]; + b.read_from_start(buf2, 10); + CHECK(std::strncmp(buf2, "abcdefghij", 10) == 0); + } + { + blob b(sql); + sql << "select img from soci_test where id = 7", into(b); + CHECK(b.get_len() == 2 * sizeof(buf)); + b.trim(sizeof(buf)); + CHECK(b.get_len() == sizeof(buf)); + + sql << "update soci_test set img = :img where id = 7", use(b); + } + { + blob b(sql); + sql << "select img from soci_test where id = 7", into(b); + CHECK(b.get_len() == sizeof(buf)); + } +} + struct double_value_table_creator : table_creator_base { double_value_table_creator(soci::session & sql) From fb492eeb80b6de8f6a953a76f14678b2e7ffd250 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sun, 18 Sep 2022 18:15:27 +0200 Subject: [PATCH 17/78] Oracle: Be conforming to documentation All versions of the documentation of the OCIBindByPos and OCIBindByVal that can be found on the web state that the size parameter should be set to the size of the pointer to the respective descriptor, locator or REF. Passing 0 as the size parameter is never stated as a valid option. --- src/backends/oracle/standard-into-type.cpp | 6 +++--- src/backends/oracle/standard-use-type.cpp | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/backends/oracle/standard-into-type.cpp b/src/backends/oracle/standard-into-type.cpp index 01292313b..242cc0a24 100644 --- a/src/backends/oracle/standard-into-type.cpp +++ b/src/backends/oracle/standard-into-type.cpp @@ -131,7 +131,7 @@ void oracle_standard_into_type_backend::define_by_pos( oracle_statement_backend *stbe = static_cast(st->get_backend()); - size = 0; + size = sizeof(stbe->stmtp_); data = &stbe->stmtp_; } break; @@ -144,7 +144,7 @@ void oracle_standard_into_type_backend::define_by_pos( oracle_rowid_backend *rbe = static_cast(rid->get_backend()); - size = 0; + size = sizeof(rbe->rowidp_); data = &rbe->rowidp_; } break; @@ -157,7 +157,7 @@ void oracle_standard_into_type_backend::define_by_pos( oracle_blob_backend *bbe = static_cast(b->get_backend()); - size = 0; + size = sizeof(bbe->lobp_); data = &bbe->lobp_; } break; diff --git a/src/backends/oracle/standard-use-type.cpp b/src/backends/oracle/standard-use-type.cpp index 0aac00e10..49845b733 100644 --- a/src/backends/oracle/standard-use-type.cpp +++ b/src/backends/oracle/standard-use-type.cpp @@ -149,7 +149,7 @@ void oracle_standard_use_type_backend::prepare_for_bind( oracle_statement_backend *stbe = static_cast(st->get_backend()); - size = 0; + size = sizeof(stbe->stmtp_); data = &stbe->stmtp_; } break; @@ -162,7 +162,7 @@ void oracle_standard_use_type_backend::prepare_for_bind( oracle_rowid_backend *rbe = static_cast(rid->get_backend()); - size = 0; + size = sizeof(rbe->rowidp_); data = &rbe->rowidp_; } break; @@ -175,7 +175,8 @@ void oracle_standard_use_type_backend::prepare_for_bind( oracle_blob_backend *bbe = static_cast(b->get_backend()); - size = 0; + size = sizeof(bbe->lobp_); + data = &bbe->lobp_; } break; From 047b74915bb9e8a259536f6d3596bf4bfefe36d2 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sun, 18 Sep 2022 19:07:46 +0200 Subject: [PATCH 18/78] Oracle: Streamline BLOB interface This commit adapts the oracle BLOB interface such that it now is en par with the implementations for the other backends. Notably this means that it is now possible to perform read and write operations on a default-constructed soci::blob object and then insert this object into the database at a later point (instead of first having to select the BLOB out of the database, before being able to access it). --- include/soci/oracle/soci-oracle.h | 17 +++- src/backends/oracle/blob.cpp | 101 ++++++++++++++++++++- src/backends/oracle/standard-into-type.cpp | 19 +++- src/backends/oracle/standard-use-type.cpp | 24 ++++- tests/oracle/test-oracle.cpp | 10 +- 5 files changed, 154 insertions(+), 17 deletions(-) diff --git a/include/soci/oracle/soci-oracle.h b/include/soci/oracle/soci-oracle.h index 6406b591c..30b4db06c 100644 --- a/include/soci/oracle/soci-oracle.h +++ b/include/soci/oracle/soci-oracle.h @@ -261,6 +261,8 @@ struct oracle_rowid_backend : details::rowid_backend struct oracle_blob_backend : details::blob_backend { + typedef OCILobLocator * locator_t; + oracle_blob_backend(oracle_session_backend &session); ~oracle_blob_backend() override; @@ -289,9 +291,20 @@ struct oracle_blob_backend : details::blob_backend void trim(std::size_t newLen) override; + locator_t get_lob_locator() const; + + void set_lob_locator(locator_t locator, bool initialized = true); + + void reset(); + + void ensure_initialized(); + +private: oracle_session_backend &session_; - OCILobLocator *lobp_; + locator_t lobp_; + + bool initialized_; }; struct oracle_session_backend : details::session_backend @@ -458,7 +471,7 @@ struct oracle_session_backend : details::session_backend struct oracle_backend_factory : backend_factory { - oracle_backend_factory() {} + oracle_backend_factory() {} oracle_session_backend * make_session( connection_parameters const & parameters) const override; }; diff --git a/src/backends/oracle/blob.cpp b/src/backends/oracle/blob.cpp index ee689722f..1e6d9b75d 100644 --- a/src/backends/oracle/blob.cpp +++ b/src/backends/oracle/blob.cpp @@ -23,7 +23,7 @@ using namespace soci::details; using namespace soci::details::oracle; oracle_blob_backend::oracle_blob_backend(oracle_session_backend &session) - : session_(session) + : session_(session), lobp_(NULL), initialized_(false) { sword res = OCIDescriptorAlloc(session.envhp_, reinterpret_cast(&lobp_), OCI_DTYPE_LOB, 0, 0); @@ -35,11 +35,21 @@ oracle_blob_backend::oracle_blob_backend(oracle_session_backend &session) oracle_blob_backend::~oracle_blob_backend() { + try { + reset(); + } catch (const oracle_soci_error &) { + // Ignore, as we shouldn't throw exceptions from the destructor + } + OCIDescriptorFree(lobp_, OCI_DTYPE_LOB); } std::size_t oracle_blob_backend::get_len() { + if (!initialized_) { + return 0; + } + ub4 len; sword res = OCILobGetLength(session_.svchp_, session_.errhp_, @@ -55,6 +65,14 @@ std::size_t oracle_blob_backend::get_len() std::size_t oracle_blob_backend::read_from_start(char *buf, std::size_t toRead, std::size_t offset) { + if (!initialized_) { + if (offset > 1) { + throw soci_error("Can't read past-the-end of BLOB data."); + } + + return 0; + } + ub4 amt = static_cast(toRead); sword res = OCILobRead(session_.svchp_, session_.errhp_, lobp_, &amt, @@ -70,6 +88,8 @@ std::size_t oracle_blob_backend::read_from_start(char *buf, std::size_t toRead, std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t toWrite, std::size_t offset) { + ensure_initialized(); + ub4 amt = static_cast(toWrite); sword res = OCILobWrite(session_.svchp_, session_.errhp_, lobp_, &amt, @@ -86,6 +106,8 @@ std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t t std::size_t oracle_blob_backend::append(char const *buf, std::size_t toWrite) { + ensure_initialized(); + ub4 amt = static_cast(toWrite); sword res = OCILobWriteAppend(session_.svchp_, session_.errhp_, lobp_, @@ -108,3 +130,80 @@ void oracle_blob_backend::trim(std::size_t newLen) throw_oracle_soci_error(res, session_.errhp_); } } + +oracle_blob_backend::locator_t oracle_blob_backend::get_lob_locator() const +{ + return lobp_; +} + +void oracle_blob_backend::set_lob_locator(oracle_blob_backend::locator_t locator, bool initialized) +{ + reset(); + + lobp_ = locator; + + initialized_ = initialized; + + if (initialized) + { + sword res = OCILobOpen(session_.svchp_, session_.errhp_, lobp_, OCI_LOB_READWRITE); + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + } +} + +void oracle_blob_backend::reset() +{ + if (!initialized_) + { + return; + } + + boolean is_temporary = FALSE; + sword res = OCILobIsTemporary(session_.envhp_, session_.errhp_, lobp_, &is_temporary); + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + + if (is_temporary) { + res = OCILobFreeTemporary(session_.svchp_, session_.errhp_, lobp_); + } else { + res = OCILobClose(session_.svchp_, session_.errhp_, lobp_); + } + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + + initialized_ = false; +} + +void oracle_blob_backend::ensure_initialized() +{ + if (!initialized_) + { + // If asked to initialize explicitly, we can only create a temporary LOB + sword res = OCILobCreateTemporary(session_.svchp_, session_.errhp_, lobp_, + OCI_DEFAULT, SQLCS_IMPLICIT, OCI_TEMP_BLOB, FALSE, OCI_DURATION_SESSION); + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + + res = OCILobOpen(session_.svchp_, session_.errhp_, lobp_, OCI_LOB_READWRITE); + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + + initialized_ = true; + } +} diff --git a/src/backends/oracle/standard-into-type.cpp b/src/backends/oracle/standard-into-type.cpp index 242cc0a24..adbf35673 100644 --- a/src/backends/oracle/standard-into-type.cpp +++ b/src/backends/oracle/standard-into-type.cpp @@ -157,8 +157,10 @@ void oracle_standard_into_type_backend::define_by_pos( oracle_blob_backend *bbe = static_cast(b->get_backend()); - size = sizeof(bbe->lobp_); - data = &bbe->lobp_; + ociData_ = bbe->get_lob_locator(); + + size = sizeof(ociData_); + data = &ociData_; } break; @@ -321,6 +323,14 @@ void oracle_standard_into_type_backend::post_fetch( read_from_lob(statement_.session_, lobp, exchange_type_cast(data_).value); } + } else if (type_ == x_blob) + { + blob *b = static_cast(data_); + + oracle_blob_backend *bbe + = static_cast(b->get_backend()); + + bbe->set_lob_locator(reinterpret_cast(ociData_)); } } @@ -367,6 +377,11 @@ void oracle_standard_into_type_backend::clean_up() ociData_ = NULL; } + if (type_ == x_blob) + { + ociData_ = NULL; + } + if (defnp_ != NULL) { OCIHandleFree(defnp_, OCI_HTYPE_DEFINE); diff --git a/src/backends/oracle/standard-use-type.cpp b/src/backends/oracle/standard-use-type.cpp index 49845b733..86d395768 100644 --- a/src/backends/oracle/standard-use-type.cpp +++ b/src/backends/oracle/standard-use-type.cpp @@ -175,9 +175,13 @@ void oracle_standard_use_type_backend::prepare_for_bind( oracle_blob_backend *bbe = static_cast(b->get_backend()); - size = sizeof(bbe->lobp_); + // We have to make sure that our blob backend is actually properly initialized + bbe->ensure_initialized(); - data = &bbe->lobp_; + ociData_ = bbe->get_lob_locator(); + + size = sizeof(ociData_); + data = &ociData_; } break; @@ -676,8 +680,17 @@ void oracle_standard_use_type_backend::post_use(bool gotData, indicator *ind) s->define_and_bind(); } break; - case x_rowid: case x_blob: + { + blob *b = static_cast(data_); + + oracle_blob_backend *bbe + = static_cast(b->get_backend()); + + bbe->reset(); + } + break; + case x_rowid: case x_xmltype: case x_longstring: // nothing to do here @@ -712,6 +725,11 @@ void oracle_standard_use_type_backend::clean_up() free_temp_lob(statement_.session_, static_cast(ociData_)); ociData_ = NULL; } + + if (type_ == x_blob) + { + ociData_ = NULL; + } if (bindp_ != NULL) { diff --git a/tests/oracle/test-oracle.cpp b/tests/oracle/test-oracle.cpp index 88c20251b..5902e66aa 100644 --- a/tests/oracle/test-oracle.cpp +++ b/tests/oracle/test-oracle.cpp @@ -129,18 +129,10 @@ TEST_CASE("Oracle blob", "[oracle][blob]") { blob b(sql); - oracle_session_backend *sessionBackEnd - = static_cast(sql.get_backend()); - - oracle_blob_backend *blobBackEnd - = static_cast(b.get_backend()); - - OCILobDisableBuffering(sessionBackEnd->svchp_, - sessionBackEnd->errhp_, blobBackEnd->lobp_); - sql << "select img from soci_test where id = 7", into(b); CHECK(b.get_len() == 0); + // note: blob offsets start from 1 b.write_from_start(buf, sizeof(buf)); CHECK(b.get_len() == sizeof(buf)); b.trim(10); From 4eec33acf25137f68efd8a3f97e84528a34b2371 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sun, 18 Sep 2022 17:34:37 +0200 Subject: [PATCH 19/78] Oracle: Extend test cases --- tests/oracle/test-oracle.cpp | 49 +++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/tests/oracle/test-oracle.cpp b/tests/oracle/test-oracle.cpp index 5902e66aa..ea1555170 100644 --- a/tests/oracle/test-oracle.cpp +++ b/tests/oracle/test-oracle.cpp @@ -121,6 +121,8 @@ TEST_CASE("Oracle blob", "[oracle][blob]") { soci::session sql(backEnd, connectString); + soci::transaction transaction(sql); + blob_table_creator tableCreator(sql); char buf[] = "abcdefghijklmnopqrstuvwxyz"; @@ -137,13 +139,7 @@ TEST_CASE("Oracle blob", "[oracle][blob]") CHECK(b.get_len() == sizeof(buf)); b.trim(10); CHECK(b.get_len() == 10); - - // append does not work (Oracle bug #886191 ?) - //b.append(buf, sizeof(buf)); - //assert(b.get_len() == sizeof(buf) + 10); - sql.commit(); } - { blob b(sql); sql << "select img from soci_test where id = 7", into(b); @@ -153,6 +149,47 @@ TEST_CASE("Oracle blob", "[oracle][blob]") b.read_from_start(buf2, 10); CHECK(strncmp(buf2, "abcdefghij", 10) == 0); } + { + // Read from default-constructed BLOB + blob b(sql); + + CHECK(b.get_len() == 0); + + char buf[5]; + std::size_t read = b.read_from_start(buf, 5); + CHECK(read == 0); + } + { + // empty, default-constructed BLOB + blob b(sql); + indicator ind; + + sql << "insert into soci_test(id, img) values(1, :img)", use(b); + sql << "select img from soci_test where id = 1", into(b, ind); + + CHECK(ind == i_ok); + CHECK(b.get_len() == 0); + + sql << "delete from soci_test where id = 1"; + } + { + // Create new BLOB + blob b(sql); + + b.write_from_start(buf, sizeof(buf)); + + char substr[20]; + std::size_t i = b.read_from_start(substr, 3); + substr[i] = '\0'; + CHECK(substr[0] == buf[0]); + CHECK(substr[1] == buf[1]); + CHECK(substr[2] == buf[2]); + CHECK(substr[3] == '\0'); + + sql << "insert into soci_test(id, img) values(7, :img)", use(b); + } + + transaction.commit(); } // nested statement test From 62a58ad47ff46feec71d2cdefeb18c2757986e36 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 19 Sep 2022 20:01:03 +0200 Subject: [PATCH 20/78] fix bug in trivial_blob_backend --- include/soci/soci-backend.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/include/soci/soci-backend.h b/include/soci/soci-backend.h index 20e82cacd..5df10b5d2 100644 --- a/include/soci/soci-backend.h +++ b/include/soci/soci-backend.h @@ -336,6 +336,11 @@ class trivial_blob_backend : public blob_backend std::size_t read(std::size_t offset, char* buf, std::size_t toRead) override { + if (offset > buffer_.size() || (offset == buffer_.size() && offset > 0)) + { + throw soci_error("Can't read past-the-end of BLOB data."); + } + toRead = (std::min)(toRead, buffer_.size() - offset); // make sure that we don't try to read @@ -354,6 +359,11 @@ class trivial_blob_backend : public blob_backend std::size_t write(std::size_t offset, char const* buf, std::size_t toWrite) override { + if (offset > buffer_.size()) + { + throw soci_error("Can't start writing far past-the-end of BLOB data."); + } + buffer_.resize((std::max)(buffer_.size(), offset + toWrite)); memcpy(buffer_.data() + offset, buf, toWrite); From 68033e037a4202aaf9582acd1630b2ecdf9499a3 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Mon, 19 Sep 2022 20:15:02 +0200 Subject: [PATCH 21/78] Start implementing shared BLOB tests --- tests/common-tests.h | 131 +++++++++++++++++++++++++++ tests/firebird/test-firebird.cpp | 15 +++ tests/mysql/test-mysql.h | 15 +++ tests/oracle/test-oracle.cpp | 13 +++ tests/postgresql/test-postgresql.cpp | 16 +++- tests/sqlite3/test-sqlite3.cpp | 14 +++ 6 files changed, 203 insertions(+), 1 deletion(-) diff --git a/tests/common-tests.h b/tests/common-tests.h index 9fd7b755c..d02c2412d 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -417,6 +417,12 @@ class test_context_base // Returns null by default to indicate that CLOB is not supported. virtual table_creator_base* table_creator_clob(session&) const { return NULL; } + // Override this to return the table creator for a simple table containing + // an integer "id" column and BLOB "b" one. + // + // Returns null by default to indicate that BLOB is not supported. + virtual table_creator_base* table_creator_blob(session&) const { return NULL; } + // Override this to return the table creator for a simple table containing // an integer "id" column and XML "x" one. // @@ -6364,6 +6370,131 @@ TEST_CASE_METHOD(common_tests, "Into XML vector with several fetches", "[core][x REQUIRE(!st.fetch()); } +TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") +{ + soci::session sql(backEndFactory_, connectString_); + + auto_table_creator tableCreator(tc_.table_creator_blob(sql)); + + if (!tableCreator.get()) + { + try + { + soci::blob blob(sql); + FAIL("BLOB creation should throw, if backend doesn't support BLOBs"); + } catch (const soci_error &) + { + // Throwing is expected if the backend doesn't support BLOBs + } + WARN("BLOB type not supported by the database, skipping the test."); + return; + } + + const char dummy_data[] = "abcdefghijklmnopüqrstuvwxyz"; + + // Cross-DB usage of BLOBs is only possible if the entire lifetime of the blob object + // is covered in an active transaction. + soci::transaction transaction(sql); + { + SECTION("Read-access on default-constructed blob") + { + soci::blob blob(sql); + + CHECK(blob.get_len() == 0); + + char buf[5]; + std::size_t read_bytes = blob.read_from_start(buf, sizeof(buf)); + + // There should be no data that could be read + CHECK(read_bytes == 0); + + // Reading from any offset other than zero is invalid + CHECK_THROWS_AS(blob.read_from_start(buf, sizeof(buf), 1), soci_error); + } + SECTION("Write-access on default-constructed blob") + { + soci::blob blob(sql); + + std::size_t written_bytes = blob.write_from_start(dummy_data, 5); + + CHECK(written_bytes == 5); + CHECK(blob.get_len() == 5); + + char buf[5]; + + std::size_t read_bytes = blob.read_from_start(buf, sizeof(buf)); + + CHECK(read_bytes == 5); + + for (std::size_t i = 0; i < sizeof(buf); ++i) + { + CHECK(buf[i] == dummy_data[i]); + } + + written_bytes = blob.append(dummy_data + 5, 3); + + CHECK(written_bytes == 3); + CHECK(blob.get_len() == 8); + + read_bytes = blob.read_from_start(buf, sizeof(buf), 3); + + CHECK(read_bytes == 5); + + for (std::size_t i = 0; i < sizeof(buf); ++i) + { + CHECK(buf[i] == dummy_data[i + 3]); + } + + blob.trim(2); + + CHECK(blob.get_len() == 2); + + read_bytes = blob.read_from_start(buf, sizeof(buf)); + + CHECK(read_bytes == 2); + + for (std::size_t i = 0; i < read_bytes; ++i) + { + CHECK(buf[i] == dummy_data[i]); + } + + // Reading from an offset >= the current length of the blob is invalid + CHECK_THROWS_AS(blob.read_from_start(buf, sizeof(buf), blob.get_len()), soci_error); + + written_bytes = blob.append("z", 1); + + CHECK(written_bytes == 1); + CHECK(blob.get_len() == 3); + + read_bytes = blob.read_from_start(buf, 1, 2); + + CHECK(read_bytes == 1); + CHECK(buf[0] == 'z'); + + // Writing more than one position beyond the blob is invalid + // (Writing exactly one position beyond is the same as appending) + CHECK_THROWS_AS(blob.write_from_start(dummy_data, 2, blob.get_len() + 1), soci_error); + } + SECTION("Inserting default-constructed blob into DB") + { + soci::blob blob(sql); + + sql << "insert into soci_test (id, b) values(5, :b)", soci::use(blob); + } + SECTION("Fetching default-constructed blob from DB") + { + soci::blob blob(sql); + soci::indicator ind; + + sql << "select b from soci_test where id = 5", soci::into(blob, ind); + + CHECK(ind == soci::i_ok); + CHECK(blob.get_len() == 0); + } + } + transaction.rollback(); +} + TEST_CASE_METHOD(common_tests, "Logger", "[core][log]") { // Logger class used for testing: appends all queries to the provided diff --git a/tests/firebird/test-firebird.cpp b/tests/firebird/test-firebird.cpp index 9099c01e9..4bc4ebf6f 100644 --- a/tests/firebird/test-firebird.cpp +++ b/tests/firebird/test-firebird.cpp @@ -1307,6 +1307,15 @@ struct TableCreatorCLOB : public tests::table_creator_base } }; +struct TableCreatorBLOB : public tests::table_creator_base +{ + TableCreatorBLOB(soci::session & sql) + : tests::table_creator_base(sql) + { + sql << "create table soci_test(id integer, b blob)"; + } +}; + struct TableCreatorXML : public tests::table_creator_base { TableCreatorXML(soci::session & sql) @@ -1351,6 +1360,12 @@ class test_context : public tests::test_context_base return new TableCreatorCLOB(s); } + + tests::table_creator_base* table_creator_blob(soci::session& s) const override + { + return new TableCreatorBLOB(s); + } + tests::table_creator_base* table_creator_xml(soci::session& s) const override { return new TableCreatorXML(s); diff --git a/tests/mysql/test-mysql.h b/tests/mysql/test-mysql.h index 8d120c3f8..fceff8878 100644 --- a/tests/mysql/test-mysql.h +++ b/tests/mysql/test-mysql.h @@ -49,6 +49,16 @@ struct table_creator_for_get_affected_rows : table_creator_base } }; +struct table_creator_for_blob : public tests::table_creator_base +{ + table_creator_for_blob(soci::session & sql) + : tests::table_creator_base(sql) + { + sql << "create table soci_test(id integer, b blob)"; + } +}; + + // // Support for SOCI Common Tests // @@ -85,6 +95,11 @@ class test_context : public test_context_base return "\'" + datdt_string + "\'"; } + table_creator_base* table_creator_blob(soci::session& s) const override + { + return new table_creator_for_blob(s); + } + bool has_fp_bug() const override { // MySQL fails in the common test3() with "1.8000000000000000 != diff --git a/tests/oracle/test-oracle.cpp b/tests/oracle/test-oracle.cpp index ea1555170..48c89272a 100644 --- a/tests/oracle/test-oracle.cpp +++ b/tests/oracle/test-oracle.cpp @@ -1480,6 +1480,14 @@ struct table_creator_for_clob : table_creator_base } }; +struct table_creator_for_blob : public tests::table_creator_base +{ + table_creator_for_blob(soci::session &sql) : tests::table_creator_base(sql) + { + sql << "create table soci_test(id integer, b blob)"; + } +}; + class test_context :public test_context_base { public: @@ -1512,6 +1520,11 @@ class test_context :public test_context_base return new table_creator_for_clob(s); } + table_creator_base* table_creator_blob(soci::session& s) const override + { + return new table_creator_for_blob(s); + } + table_creator_base* table_creator_xml(soci::session& s) const override { return new table_creator_for_xml(s); diff --git a/tests/postgresql/test-postgresql.cpp b/tests/postgresql/test-postgresql.cpp index 3e4cb1211..9dd20c8c5 100644 --- a/tests/postgresql/test-postgresql.cpp +++ b/tests/postgresql/test-postgresql.cpp @@ -1433,7 +1433,16 @@ struct table_creator_for_clob : table_creator_base } }; -// Common tests context +struct table_creator_for_blob : public tests::table_creator_base +{ + table_creator_for_blob(soci::session & sql) + : tests::table_creator_base(sql) + { + sql << "create table soci_test(id integer, b oid)"; + } +}; + + class test_context : public test_context_base { public: @@ -1471,6 +1480,11 @@ class test_context : public test_context_base return new table_creator_for_clob(s); } + table_creator_base* table_creator_blob(soci::session& s) const override + { + return new table_creator_for_blob(s); + } + bool has_real_xml_support() const override { return true; diff --git a/tests/sqlite3/test-sqlite3.cpp b/tests/sqlite3/test-sqlite3.cpp index 93a76f584..75bfb87ba 100644 --- a/tests/sqlite3/test-sqlite3.cpp +++ b/tests/sqlite3/test-sqlite3.cpp @@ -898,6 +898,15 @@ struct table_creator_from_str : table_creator_base } }; +struct table_creator_for_blob : public tests::table_creator_base +{ + table_creator_for_blob(soci::session & sql) + : tests::table_creator_base(sql) + { + sql << "create table soci_test(id integer, b blob)"; + } +}; + class test_context : public test_context_base { public: @@ -931,6 +940,11 @@ class test_context : public test_context_base "create table soci_test (id integer primary key, val integer)"); } + table_creator_base* table_creator_blob(soci::session& s) const override + { + return new table_creator_for_blob(s); + } + table_creator_base* table_creator_xml(soci::session& s) const override { return new table_creator_from_str(s, From a075da6529a0fc84736fe00c53b950895d455920 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 09:41:14 +0100 Subject: [PATCH 22/78] Fix Postgresql out-of-bound write not erroring --- src/backends/postgresql/blob.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index 3b3d3713c..607e299fe 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -71,6 +71,12 @@ std::size_t postgresql_blob_backend::read_from_start(char * buf, std::size_t toR std::size_t postgresql_blob_backend::write_from_start(char const * buf, std::size_t toWrite, std::size_t offset) { + if (offset > get_len()) + { + // If offset == length, the operation is to be understood as appending (and is therefore allowed) + throw soci_error("Can't start writing far past-the-end of BLOB data."); + } + init(); seek(offset, SEEK_SET); From 3ed2c88a945987fb072d440d8c2cc4a16f2e699b Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 09:57:47 +0100 Subject: [PATCH 23/78] Oracle: Make conforming to common standards --- src/backends/oracle/blob.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/backends/oracle/blob.cpp b/src/backends/oracle/blob.cpp index 1e6d9b75d..fc400a243 100644 --- a/src/backends/oracle/blob.cpp +++ b/src/backends/oracle/blob.cpp @@ -65,12 +65,15 @@ std::size_t oracle_blob_backend::get_len() std::size_t oracle_blob_backend::read_from_start(char *buf, std::size_t toRead, std::size_t offset) { - if (!initialized_) { - if (offset > 1) { - throw soci_error("Can't read past-the-end of BLOB data."); + if (offset >= get_len()) + { + if (!initialized_ && offset == 0) + { + // Read-attempts (from the beginning) on uninitialized BLOBs is defined to be a no-op + return 0; } - return 0; + throw soci_error("Can't read past-the-end of BLOB data."); } ub4 amt = static_cast(toRead); @@ -88,6 +91,12 @@ std::size_t oracle_blob_backend::read_from_start(char *buf, std::size_t toRead, std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t toWrite, std::size_t offset) { + if (offset > get_len()) + { + // If offset == length, the operation is to be understood as appending (and is therefore allowed) + throw soci_error("Can't start writing far past-the-end of BLOB data."); + } + ensure_initialized(); ub4 amt = static_cast(toWrite); From 761e35d28694ae27b7cac5ae4d600272fb2d4871 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 10:05:20 +0100 Subject: [PATCH 24/78] Firebird: Fix failing common tests --- src/backends/firebird/blob.cpp | 12 +++++++++--- tests/firebird/test-firebird.cpp | 2 ++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/backends/firebird/blob.cpp b/src/backends/firebird/blob.cpp index 5c3bb8078..ee4b78b4b 100644 --- a/src/backends/firebird/blob.cpp +++ b/src/backends/firebird/blob.cpp @@ -13,8 +13,8 @@ using namespace soci; using namespace soci::details::firebird; firebird_blob_backend::firebird_blob_backend(firebird_session_backend &session) - : session_(session), bid_(), from_db_(false), bhp_(0), data_(), - loaded_(false), max_seg_size_(0) + : session_(session), bid_(), from_db_(false), bhp_(0), data_(), + loaded_(false), max_seg_size_(0) {} firebird_blob_backend::~firebird_blob_backend() @@ -42,8 +42,14 @@ std::size_t firebird_blob_backend::read_from_start(char * buf, std::size_t toRea std::size_t size = data_.size(); - if (offset > size) + if (offset >= size) { + if (offset == 0) + { + // Read (from beginning) on empty (default-initialized) BLOB is defined as no-op + return 0; + } + throw soci_error("Can't read past-the-end of BLOB data"); } diff --git a/tests/firebird/test-firebird.cpp b/tests/firebird/test-firebird.cpp index 4bc4ebf6f..ba0dc4277 100644 --- a/tests/firebird/test-firebird.cpp +++ b/tests/firebird/test-firebird.cpp @@ -1313,6 +1313,8 @@ struct TableCreatorBLOB : public tests::table_creator_base : tests::table_creator_base(sql) { sql << "create table soci_test(id integer, b blob)"; + sql.commit(); + sql.begin(); } }; From 25b5b2779102eb9ee98924d3d67cb65276be235a Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 14:46:11 +0100 Subject: [PATCH 25/78] Fix default-constructed blob test --- tests/common-tests.h | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/common-tests.h b/tests/common-tests.h index d02c2412d..bd7c0a2da 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -6475,21 +6475,19 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") // (Writing exactly one position beyond is the same as appending) CHECK_THROWS_AS(blob.write_from_start(dummy_data, 2, blob.get_len() + 1), soci_error); } - SECTION("Inserting default-constructed blob into DB") + SECTION("Inserting/Reading default-constructed blob") { - soci::blob blob(sql); + soci::blob input_blob(sql); - sql << "insert into soci_test (id, b) values(5, :b)", soci::use(blob); - } - SECTION("Fetching default-constructed blob from DB") - { - soci::blob blob(sql); + sql << "insert into soci_test (id, b) values(5, :b)", soci::use(input_blob); + + soci::blob output_blob(sql); soci::indicator ind; - sql << "select b from soci_test where id = 5", soci::into(blob, ind); + sql << "select b from soci_test where id = 5", soci::into(output_blob, ind); CHECK(ind == soci::i_ok); - CHECK(blob.get_len() == 0); + CHECK(output_blob.get_len() == 0); } } transaction.rollback(); From 3d3f456eb78132676a76447755ca442bc31ecfb9 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 17:19:31 +0100 Subject: [PATCH 26/78] trivial_backend: don't implement depracated functions --- include/soci/soci-backend.h | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/include/soci/soci-backend.h b/include/soci/soci-backend.h index 5df10b5d2..751810f3d 100644 --- a/include/soci/soci-backend.h +++ b/include/soci/soci-backend.h @@ -333,8 +333,8 @@ class trivial_blob_backend : public blob_backend public: std::size_t get_len() override { return buffer_.size(); } - std::size_t read(std::size_t offset, char* buf, - std::size_t toRead) override + std::size_t read_from_start(char* buf, std::size_t toRead, + std::size_t offset = 0) override { if (offset > buffer_.size() || (offset == buffer_.size() && offset > 0)) { @@ -350,14 +350,8 @@ class trivial_blob_backend : public blob_backend return toRead; } - std::size_t read_from_start(char* buf, std::size_t toRead, + std::size_t write_from_start(const char* buf, std::size_t toWrite, std::size_t offset = 0) override - { - return read(offset, buf, toRead); - } - - std::size_t write(std::size_t offset, char const* buf, - std::size_t toWrite) override { if (offset > buffer_.size()) { @@ -371,15 +365,9 @@ class trivial_blob_backend : public blob_backend return toWrite; } - std::size_t write_from_start(const char* buf, std::size_t toWrite, - std::size_t offset = 0) override - { - return write(offset, buf, toWrite); - } - std::size_t append(char const* buf, std::size_t toWrite) override { - return write(buffer_.size(), buf, toWrite); + return write_from_start(buf, toWrite, buffer_.size()); } void trim(std::size_t newLen) override { buffer_.resize(newLen); } From de996e9298cc534cd7bf157a63689ebeeabf0a3e Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 18:59:20 +0100 Subject: [PATCH 27/78] Disable BLOB tests for ODBC::MySQL --- tests/mysql/test-mysql.h | 3 +++ tests/odbc/test-odbc-mysql.cpp | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/mysql/test-mysql.h b/tests/mysql/test-mysql.h index fceff8878..cc12a17bb 100644 --- a/tests/mysql/test-mysql.h +++ b/tests/mysql/test-mysql.h @@ -95,10 +95,13 @@ class test_context : public test_context_base return "\'" + datdt_string + "\'"; } +#ifndef SOCI_INCLUDED_FROM_ODBC_TEST + // ODBC backend doesn't support BLOBs yet table_creator_base* table_creator_blob(soci::session& s) const override { return new table_creator_for_blob(s); } +#endif bool has_fp_bug() const override { diff --git a/tests/odbc/test-odbc-mysql.cpp b/tests/odbc/test-odbc-mysql.cpp index 5b14e7ea4..f66001898 100644 --- a/tests/odbc/test-odbc-mysql.cpp +++ b/tests/odbc/test-odbc-mysql.cpp @@ -7,12 +7,15 @@ #include "soci/soci.h" #include "soci/odbc/soci-odbc.h" -#include "mysql/test-mysql.h" #include #include #include #include +#define SOCI_INCLUDED_FROM_ODBC_TEST +#include "mysql/test-mysql.h" +#undef SOCI_INCLUDED_FROM_ODBC_TEST + std::string connectString; backend_factory const &backEnd = *soci::factory_odbc(); From ae75990cb2310787e2872ba5ff4f8426c966cc0a Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 18:40:42 +0100 Subject: [PATCH 28/78] finalize common blob test & remove backend-specific ones --- tests/common-tests.h | 256 ++++++++++++++++++--------- tests/firebird/test-firebird.cpp | 146 --------------- tests/mysql/test-mysql.cpp | 98 ---------- tests/mysql/test-mysql.h | 2 +- tests/oracle/test-oracle.cpp | 90 +--------- tests/postgresql/test-postgresql.cpp | 104 ----------- tests/sqlite3/test-sqlite3.cpp | 120 ------------- 7 files changed, 179 insertions(+), 637 deletions(-) diff --git a/tests/common-tests.h b/tests/common-tests.h index bd7c0a2da..7f0f7d505 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -6378,119 +6378,217 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") if (!tableCreator.get()) { - try - { - soci::blob blob(sql); - FAIL("BLOB creation should throw, if backend doesn't support BLOBs"); - } catch (const soci_error &) - { - // Throwing is expected if the backend doesn't support BLOBs - } + try + { + soci::blob blob(sql); + FAIL("BLOB creation should throw, if backend doesn't support BLOBs"); + } catch (const soci_error &) + { + // Throwing is expected if the backend doesn't support BLOBs + } WARN("BLOB type not supported by the database, skipping the test."); return; } - const char dummy_data[] = "abcdefghijklmnopüqrstuvwxyz"; + const char dummy_data[] = "abcdefghijklmnopüqrstuvwxyz"; - // Cross-DB usage of BLOBs is only possible if the entire lifetime of the blob object - // is covered in an active transaction. - soci::transaction transaction(sql); - { - SECTION("Read-access on default-constructed blob") - { - soci::blob blob(sql); + // Cross-DB usage of BLOBs is only possible if the entire lifetime of the blob object + // is covered in an active transaction. + soci::transaction transaction(sql); + { + SECTION("Read-access on default-constructed blob") + { + soci::blob blob(sql); - CHECK(blob.get_len() == 0); + CHECK(blob.get_len() == 0); - char buf[5]; - std::size_t read_bytes = blob.read_from_start(buf, sizeof(buf)); + char buf[5]; + std::size_t read_bytes = blob.read_from_start(buf, sizeof(buf)); - // There should be no data that could be read - CHECK(read_bytes == 0); + // There should be no data that could be read + CHECK(read_bytes == 0); - // Reading from any offset other than zero is invalid - CHECK_THROWS_AS(blob.read_from_start(buf, sizeof(buf), 1), soci_error); - } - SECTION("Write-access on default-constructed blob") - { - soci::blob blob(sql); + // Reading from any offset other than zero is invalid + CHECK_THROWS_AS(blob.read_from_start(buf, sizeof(buf), 1), soci_error); + } + SECTION("BLOB I/O") + { + soci::blob blob(sql); - std::size_t written_bytes = blob.write_from_start(dummy_data, 5); + std::size_t written_bytes = blob.write_from_start(dummy_data, 5); - CHECK(written_bytes == 5); - CHECK(blob.get_len() == 5); + CHECK(written_bytes == 5); + CHECK(blob.get_len() == 5); - char buf[5]; + char buf[5]; + static_assert(sizeof(buf) <= sizeof(dummy_data), "Underlying assumption violated"); - std::size_t read_bytes = blob.read_from_start(buf, sizeof(buf)); + std::size_t read_bytes = blob.read_from_start(buf, sizeof(buf)); - CHECK(read_bytes == 5); + CHECK(read_bytes == sizeof(buf)); - for (std::size_t i = 0; i < sizeof(buf); ++i) - { - CHECK(buf[i] == dummy_data[i]); - } + for (std::size_t i = 0; i < sizeof(buf); ++i) + { + CHECK(buf[i] == dummy_data[i]); + } - written_bytes = blob.append(dummy_data + 5, 3); + written_bytes = blob.append(dummy_data + 5, 3); - CHECK(written_bytes == 3); - CHECK(blob.get_len() == 8); + CHECK(written_bytes == 3); + CHECK(blob.get_len() == 8); - read_bytes = blob.read_from_start(buf, sizeof(buf), 3); + read_bytes = blob.read_from_start(buf, sizeof(buf), 3); - CHECK(read_bytes == 5); + CHECK(read_bytes == 5); - for (std::size_t i = 0; i < sizeof(buf); ++i) - { - CHECK(buf[i] == dummy_data[i + 3]); - } + for (std::size_t i = 0; i < sizeof(buf); ++i) + { + CHECK(buf[i] == dummy_data[i + 3]); + } - blob.trim(2); + blob.trim(2); - CHECK(blob.get_len() == 2); + CHECK(blob.get_len() == 2); - read_bytes = blob.read_from_start(buf, sizeof(buf)); + read_bytes = blob.read_from_start(buf, sizeof(buf)); - CHECK(read_bytes == 2); + CHECK(read_bytes == 2); - for (std::size_t i = 0; i < read_bytes; ++i) - { - CHECK(buf[i] == dummy_data[i]); - } + for (std::size_t i = 0; i < read_bytes; ++i) + { + CHECK(buf[i] == dummy_data[i]); + } - // Reading from an offset >= the current length of the blob is invalid - CHECK_THROWS_AS(blob.read_from_start(buf, sizeof(buf), blob.get_len()), soci_error); + // Reading from an offset >= the current length of the blob is invalid + CHECK_THROWS_AS(blob.read_from_start(buf, sizeof(buf), blob.get_len()), soci_error); - written_bytes = blob.append("z", 1); - - CHECK(written_bytes == 1); - CHECK(blob.get_len() == 3); + written_bytes = blob.append("z", 1); + + CHECK(written_bytes == 1); + CHECK(blob.get_len() == 3); - read_bytes = blob.read_from_start(buf, 1, 2); + read_bytes = blob.read_from_start(buf, 1, 2); - CHECK(read_bytes == 1); - CHECK(buf[0] == 'z'); + CHECK(read_bytes == 1); + CHECK(buf[0] == 'z'); - // Writing more than one position beyond the blob is invalid - // (Writing exactly one position beyond is the same as appending) - CHECK_THROWS_AS(blob.write_from_start(dummy_data, 2, blob.get_len() + 1), soci_error); - } - SECTION("Inserting/Reading default-constructed blob") - { - soci::blob input_blob(sql); + // Writing more than one position beyond the blob is invalid + // (Writing exactly one position beyond is the same as appending) + CHECK_THROWS_AS(blob.write_from_start(dummy_data, 2, blob.get_len() + 1), soci_error); + } + SECTION("Inserting/Reading default-constructed blob") + { + soci::blob input_blob(sql); + + sql << "insert into soci_test (id, b) values(5, :b)", soci::use(input_blob); + + soci::blob output_blob(sql); + soci::indicator ind; - sql << "insert into soci_test (id, b) values(5, :b)", soci::use(input_blob); + sql << "select b from soci_test where id = 5", soci::into(output_blob, ind); + + CHECK(ind == soci::i_ok); + CHECK(output_blob.get_len() == 0); + } + SECTION("Ensure reading into blob overwrites previous contents") + { + soci::blob blob(sql); + blob.write_from_start("hello kitty", 10); - soci::blob output_blob(sql); - soci::indicator ind; + CHECK(blob.get_len() == 10); - sql << "select b from soci_test where id = 5", soci::into(output_blob, ind); + soci::blob write_blob(sql); + write_blob.write_from_start("test", 4); + sql << "insert into soci_test (id, b) values (5, :b)", soci::use(write_blob); + + sql << "select b from soci_test where id = 5", soci::into(blob); + + CHECK(blob.get_len() == 4); + char buf[5]; + + std::size_t read_bytes = blob.read_from_start(buf, sizeof(buf)); + CHECK(read_bytes == 4); + + CHECK(buf[0] == 't'); + CHECK(buf[1] == 'e'); + CHECK(buf[2] == 's'); + CHECK(buf[3] == 't'); + } + SECTION("Blob-DB interaction") + { + soci::blob write_blob(sql); - CHECK(ind == soci::i_ok); - CHECK(output_blob.get_len() == 0); - } - } - transaction.rollback(); + static_assert(sizeof(dummy_data) >= 10, "Underlying assumption violated"); + write_blob.write_from_start(dummy_data, 10); + + const int first_id = 42; + + // Write and retrieve blob from/into database + sql << "insert into soci_test (id, b) values(:id, :b)", soci::use(first_id), soci::use(write_blob); + + soci::blob read_blob(sql); + sql << "select b from soci_test where id = :id", soci::use(first_id), soci::into(read_blob); + CHECK(sql.got_data()); + + CHECK(read_blob.get_len() == write_blob.get_len()); + + char buf[15]; + std::size_t bytes_read = read_blob.read_from_start(buf, sizeof(buf)); + CHECK(bytes_read == read_blob.get_len()); + CHECK(bytes_read == 10); + for (std::size_t i = 0; i < bytes_read; ++i) { + CHECK(buf[i] == dummy_data[i]); + } + + // Update original blob and insert new db-entry (must not change previous entry) + const int second_id = first_id + 1; + write_blob.trim(0); + static_assert(sizeof(dummy_data) >= 15 + 5, "Underlying assumption violated"); + write_blob.write_from_start(dummy_data + 15, 5); + + sql << "insert into soci_test (id, b) values (:id, :b)", soci::use(second_id), soci::use(write_blob); + + // First, check that the original entry has not been changed + sql << "select b from soci_test where id = :id", soci::use(first_id), soci::into(read_blob); + CHECK(read_blob.get_len() == 10); + + // Then check new entry can be read + sql << "select b from soci_test where id = :id", soci::use(second_id), soci::into(read_blob); + + bytes_read = read_blob.read_from_start(buf, sizeof(buf)); + CHECK(bytes_read == read_blob.get_len()); + CHECK(bytes_read == 5); + for (std::size_t i = 0; i < bytes_read; ++i) { + CHECK(buf[i] == dummy_data[i + 15]); + } + } + SECTION("Binary data") + { + const std::uint8_t binary_data[12] = {0, 1, 2, 3, 4, 5, 6, 7, 22, 255, 250 }; + + soci::blob write_blob(sql); + + std::size_t bytes_written = write_blob.write_from_start(reinterpret_cast(binary_data), sizeof(binary_data)); + CHECK(bytes_written == sizeof(binary_data)); + + sql << "insert into soci_test (id, b) values (1, :b)", soci::use(write_blob); + + soci::blob read_blob(sql); + + sql << "select b from soci_test where id = 1", soci::into(read_blob); + + CHECK(read_blob.get_len() == sizeof(binary_data)); + + std::uint8_t buf[20]; + std::size_t bytes_read = read_blob.read_from_start(reinterpret_cast(buf), sizeof(buf)); + + CHECK(bytes_read == sizeof(binary_data)); + for (std::size_t i = 0; i < sizeof(binary_data); ++i) { + CHECK(buf[i] == binary_data[i]); + } + } + } + transaction.rollback(); } TEST_CASE_METHOD(common_tests, "Logger", "[core][log]") diff --git a/tests/firebird/test-firebird.cpp b/tests/firebird/test-firebird.cpp index ba0dc4277..cfe688957 100644 --- a/tests/firebird/test-firebird.cpp +++ b/tests/firebird/test-firebird.cpp @@ -500,152 +500,6 @@ TEST_CASE("Firebird bulk operations", "[firebird][bulk]") sql << "drop table test6"; } -// blob test -TEST_CASE("Firebird blobs", "[firebird][blob]") -{ - soci::session sql(backEnd, connectString); - - try - { - sql << "drop table test7"; - } - catch (std::runtime_error &) - {} // ignore if error - - sql << "create table test7(id integer, img blob)"; - sql.commit(); - - sql.begin(); - { - // Read from default-constructed BLOB - blob b(sql); - - CHECK(b.get_len() == 0); - - char buf[5]; - std::size_t read = b.read_from_start(buf, 5); - CHECK(read == 0); - } - { - // verify empty blob - blob b(sql); - indicator ind; - - sql << "insert into test7(id, img) values(1,?)", use(b); - sql << "select img from test7 where id = 1", into(b, ind); - - CHECK(ind == i_ok); - CHECK(b.get_len() == 0); - - sql << "delete from test7"; - } - - { - // create a new blob - blob b(sql); - - char str1[] = "Hello"; - b.write_from_start(str1, strlen(str1)); - - char str2[20]; - std::size_t i = b.read_from_start(str2, 2, 3); - str2[i] = '\0'; - CHECK(str2[0] == 'l'); - CHECK(str2[1] == 'o'); - CHECK(str2[2] == '\0'); - - char str3[] = ", Firebird!"; - b.append(str3, strlen(str3)); - - sql << "insert into test7(id, img) values(1,?)", use(b); - } - - { - // read & update blob - blob b(sql); - - sql << "select img from test7 where id = 1", into(b); - - std::vector text(b.get_len()); - b.read_from_start(&text[0], b.get_len()); - CHECK(strncmp(&text[0], "Hello, Firebird!", b.get_len()) == 0); - - char str1[] = "FIREBIRD"; - b.write_from_start(str1, strlen(str1), 7); - - // after modification blob must be written to database - sql << "update test7 set img=? where id=1", use(b); - } - - { - // read blob from database, modify and write to another record - blob b(sql); - - sql << "select img from test7 where id = 1", into(b); - - std::vector text(b.get_len()); - b.read_from_start(&text[0], b.get_len()); - - char str1[] = "HELLO"; - b.write_from_start(str1, strlen(str1)); - - b.read_from_start(&text[0], b.get_len()); - CHECK(strncmp(&text[0], "HELLO, FIREBIRD!", b.get_len()) == 0); - - b.trim(5); - sql << "insert into test7(id, img) values(2,?)", use(b); - } - - { - blob b(sql); - statement st = (sql.prepare << "select img from test7", into(b)); - - st.execute(); - - st.fetch(); - std::vector text(b.get_len()); - b.read_from_start(&text[0], b.get_len()); - CHECK(strncmp(&text[0], "Hello, FIREBIRD!", b.get_len()) == 0); - - st.fetch(); - text.resize(b.get_len()); - b.read_from_start(&text[0], b.get_len()); - CHECK(strncmp(&text[0], "HELLO", b.get_len()) == 0); - } - - { - // delete blob - blob b(sql); - indicator ind=i_null; - sql << "update test7 set img=? where id = 1", use(b, ind); - - sql << "select img from test7 where id = 2", into(b, ind); - CHECK(ind==i_ok); - - sql << "select img from test7 where id = 1", into(b, ind); - CHECK(ind==i_null); - } - - { - //create large blob - const int blobSize = 65536; //max segment size is 65535(unsigned short) - std::vector data(blobSize); - blob b(sql); - b.write_from_start(data.data(), blobSize); - sql << "insert into test7(id, img) values(3,?)", use(b); - - //now read blob back from database and make sure it has correct content and size - blob br(sql); - sql << "select img from test7 where id = 3", into(br); - std::vector data2(br.get_len()); - if(br.get_len()>0) - br.read_from_start(data2.data(), br.get_len()); - CHECK(data == data2); - } - - sql << "drop table test7"; -} - // named parameters TEST_CASE("Firebird named parameters", "[firebird][named-params]") { diff --git a/tests/mysql/test-mysql.cpp b/tests/mysql/test-mysql.cpp index 4fe949b8a..7e933432e 100644 --- a/tests/mysql/test-mysql.cpp +++ b/tests/mysql/test-mysql.cpp @@ -605,104 +605,6 @@ TEST_CASE("MySQL function call", "[mysql][function]") sql << "select concat(@day,' ',@mm,' ',@year)", into(r); } -// BLOB test -struct blob_table_creator : public table_creator_base -{ - blob_table_creator(soci::session & sql) - : table_creator_base(sql) - { - sql << - "create table soci_test (" - " id integer," - " img blob" - ")"; - } -}; - -TEST_CASE("MySQL blob", "[mysql][blob]") -{ - soci::session sql(backEnd, connectString); - - blob_table_creator tableCreator(sql); - - const char buf[] = "abcdefghijklmnopqrstuvwxyz"; - - { - // Read from default-constructed BLOB - blob b(sql); - - CHECK(b.get_len() == 0); - - char buf2[5]; - std::size_t read = b.read_from_start(buf2, 5); - CHECK(read == 0); - } - { - // empty, default-constructed BLOB - blob b(sql); - indicator ind; - - sql << "insert into soci_test(id, img) values(1, :img)", use(b); - sql << "select img from soci_test where id = 1", into(b, ind); - - CHECK(ind == i_ok); - CHECK(b.get_len() == 0); - - sql << "delete from soci_test where id = 1"; - } - { - // Create new BLOB - blob b(sql); - - b.write_from_start(buf, sizeof(buf)); - - char substr[20]; - std::size_t i = b.read_from_start(substr, 3); - substr[i] = '\0'; - CHECK(substr[0] == buf[0]); - CHECK(substr[1] == buf[1]); - CHECK(substr[2] == buf[2]); - CHECK(substr[3] == '\0'); - - sql << "insert into soci_test(id, img) values(7, :img)", use(b); - } - { - // Append to BLOB - blob b(sql); - - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == sizeof(buf)); - - b.append(buf, sizeof(buf)); - - CHECK(b.get_len() == 2 * sizeof(buf)); - - sql << "update soci_test set img = :img where id = 7", use(b); - } - { - blob b(sql); - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == 2 * sizeof(buf)); - char buf2[100]; - b.read_from_start(buf2, 10); - CHECK(std::strncmp(buf2, "abcdefghij", 10) == 0); - } - { - blob b(sql); - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == 2 * sizeof(buf)); - b.trim(sizeof(buf)); - CHECK(b.get_len() == sizeof(buf)); - - sql << "update soci_test set img = :img where id = 7", use(b); - } - { - blob b(sql); - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == sizeof(buf)); - } -} - struct double_value_table_creator : table_creator_base { double_value_table_creator(soci::session & sql) diff --git a/tests/mysql/test-mysql.h b/tests/mysql/test-mysql.h index cc12a17bb..40e17298d 100644 --- a/tests/mysql/test-mysql.h +++ b/tests/mysql/test-mysql.h @@ -52,7 +52,7 @@ struct table_creator_for_get_affected_rows : table_creator_base struct table_creator_for_blob : public tests::table_creator_base { table_creator_for_blob(soci::session & sql) - : tests::table_creator_base(sql) + : tests::table_creator_base(sql) { sql << "create table soci_test(id integer, b blob)"; } diff --git a/tests/oracle/test-oracle.cpp b/tests/oracle/test-oracle.cpp index 48c89272a..d3531fd13 100644 --- a/tests/oracle/test-oracle.cpp +++ b/tests/oracle/test-oracle.cpp @@ -102,95 +102,7 @@ TEST_CASE("Oracle explicit calls", "[oracle]") CHECK(i == 7); } -// DDL + blob test - -struct blob_table_creator : public table_creator_base -{ - blob_table_creator(soci::session & sql) - : table_creator_base(sql) - { - sql << - "create table soci_test (" - " id number(10) not null," - " img blob" - ")"; - } -}; - -TEST_CASE("Oracle blob", "[oracle][blob]") -{ - soci::session sql(backEnd, connectString); - - soci::transaction transaction(sql); - - blob_table_creator tableCreator(sql); - - char buf[] = "abcdefghijklmnopqrstuvwxyz"; - sql << "insert into soci_test (id, img) values (7, empty_blob())"; - - { - blob b(sql); - - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == 0); - - // note: blob offsets start from 1 - b.write_from_start(buf, sizeof(buf)); - CHECK(b.get_len() == sizeof(buf)); - b.trim(10); - CHECK(b.get_len() == 10); - } - { - blob b(sql); - sql << "select img from soci_test where id = 7", into(b); - //assert(b.get_len() == sizeof(buf) + 10); - CHECK(b.get_len() == 10); - char buf2[100]; - b.read_from_start(buf2, 10); - CHECK(strncmp(buf2, "abcdefghij", 10) == 0); - } - { - // Read from default-constructed BLOB - blob b(sql); - - CHECK(b.get_len() == 0); - - char buf[5]; - std::size_t read = b.read_from_start(buf, 5); - CHECK(read == 0); - } - { - // empty, default-constructed BLOB - blob b(sql); - indicator ind; - - sql << "insert into soci_test(id, img) values(1, :img)", use(b); - sql << "select img from soci_test where id = 1", into(b, ind); - - CHECK(ind == i_ok); - CHECK(b.get_len() == 0); - - sql << "delete from soci_test where id = 1"; - } - { - // Create new BLOB - blob b(sql); - - b.write_from_start(buf, sizeof(buf)); - - char substr[20]; - std::size_t i = b.read_from_start(substr, 3); - substr[i] = '\0'; - CHECK(substr[0] == buf[0]); - CHECK(substr[1] == buf[1]); - CHECK(substr[2] == buf[2]); - CHECK(substr[3] == '\0'); - - sql << "insert into soci_test(id, img) values(7, :img)", use(b); - } - - transaction.commit(); -} +// DDL test // nested statement test // (the same syntax is used for output cursors in PL/SQL) diff --git a/tests/postgresql/test-postgresql.cpp b/tests/postgresql/test-postgresql.cpp index 9dd20c8c5..4bd85b611 100644 --- a/tests/postgresql/test-postgresql.cpp +++ b/tests/postgresql/test-postgresql.cpp @@ -229,110 +229,6 @@ TEST_CASE("PostgreSQL function call", "[postgresql][function]") } } -// BLOB test -struct blob_table_creator : public table_creator_base -{ - blob_table_creator(soci::session & sql) - : table_creator_base(sql) - { - sql << - "create table soci_test (" - " id integer," - " img oid" - ")"; - } -}; - -TEST_CASE("PostgreSQL blob", "[postgresql][blob]") -{ - soci::session sql(backEnd, connectString); - - blob_table_creator tableCreator(sql); - - const char buf[] = "abcdefghijklmnopqrstuvwxyz"; - - // in PostgreSQL, BLOB operations must be within transaction block - transaction tr(sql); - - { - // Read from default-constructed BLOB - blob b(sql); - - CHECK(b.get_len() == 0); - - char buf2[5]; - std::size_t read = b.read_from_start(buf2, 5); - CHECK(read == 0); - } - { - // empty, default-constructed BLOB - blob b(sql); - indicator ind; - - sql << "insert into soci_test(id, img) values(1, :img)", use(b); - sql << "select img from soci_test where id = 1", into(b, ind); - - CHECK(ind == i_ok); - CHECK(b.get_len() == 0); - - sql << "delete from soci_test where id = 1"; - } - { - // Create new BLOB - blob b(sql); - - b.write_from_start(buf, sizeof(buf)); - - char substr[20]; - std::size_t i = b.read_from_start(substr, 3); - substr[i] = '\0'; - CHECK(substr[0] == buf[0]); - CHECK(substr[1] == buf[1]); - CHECK(substr[2] == buf[2]); - CHECK(substr[3] == '\0'); - - sql << "insert into soci_test(id, img) values(7, :img)", use(b); - } - { - // Append to BLOB - blob b(sql); - - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == sizeof(buf)); - - b.append(buf, sizeof(buf)); - - CHECK(b.get_len() == 2 * sizeof(buf)); - } - { - blob b(sql); - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == 2 * sizeof(buf)); - char buf2[100]; - b.read_from_start(buf2, 10); - CHECK(std::strncmp(buf2, "abcdefghij", 10) == 0); - } -#if PG_VERSION_NUM >= 80003 - { - blob b(sql); - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == 2 * sizeof(buf)); - b.trim(sizeof(buf)); - CHECK(b.get_len() == sizeof(buf)); - } - { - blob b(sql); - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == sizeof(buf)); - } -#endif - - // Destroy BLOB - unsigned long oid; - sql << "select img from soci_test where id = 7", into(oid); - sql << "select lo_unlink(" << oid << ")"; -} - struct longlong_table_creator : table_creator_base { longlong_table_creator(soci::session & sql) diff --git a/tests/sqlite3/test-sqlite3.cpp b/tests/sqlite3/test-sqlite3.cpp index 75bfb87ba..0aa91f76d 100644 --- a/tests/sqlite3/test-sqlite3.cpp +++ b/tests/sqlite3/test-sqlite3.cpp @@ -256,126 +256,6 @@ TEST_CASE("SQLite get_last_insert_id escapes table name", CHECK(val == 0); } -// BLOB test -struct blob_table_creator : public table_creator_base -{ - blob_table_creator(soci::session & sql) - : table_creator_base(sql) - { - sql << - "create table soci_test (" - " id integer," - " img blob" - ")"; - } -}; - -TEST_CASE("SQLite blob", "[sqlite][blob]") -{ - soci::session sql(backEnd, connectString); - - blob_table_creator tableCreator(sql); - - char buf[] = "abcdefghijklmnopqrstuvwxyz"; - - { - // Read from default-constructed BLOB - blob b(sql); - - CHECK(b.get_len() == 0); - - char buf2[5]; - std::size_t read = b.read_from_start(buf2, 5); - CHECK(read == 0); - } - { - // empty, default-constructed BLOB - blob b(sql); - indicator ind; - - sql << "insert into soci_test(id, img) values(1, :img)", use(b); - sql << "select img from soci_test where id = 1", into(b, ind); - - CHECK(ind == i_ok); - CHECK(b.get_len() == 0); - - sql << "delete from soci_test where id = 1"; - } - { - // Create new BLOB - blob b(sql); - - b.write_from_start(buf, sizeof(buf)); - - char substr[20]; - std::size_t i = b.read_from_start(substr, 3); - substr[i] = '\0'; - CHECK(substr[0] == buf[0]); - CHECK(substr[1] == buf[1]); - CHECK(substr[2] == buf[2]); - CHECK(substr[3] == '\0'); - - sql << "insert into soci_test(id, img) values(7, :img)", use(b); - } - { - // Append to BLOB - blob b(sql); - - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == sizeof(buf)); - - b.append(buf, sizeof(buf)); - - CHECK(b.get_len() == 2 * sizeof(buf)); - - sql << "update soci_test set img = :img where id = 7", use(b); - } - { - // Read from BLOB as fetched from DB - blob b(sql); - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == 2 * sizeof(buf)); - char buf2[100]; - b.read_from_start(buf2, 10); - CHECK(std::strncmp(buf2, "abcdefghij", 10) == 0); - } - { - // Trim BLOB - blob b(sql); - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == 2 * sizeof(buf)); - b.trim(0); - CHECK(b.get_len() == 0); - sql << "update soci_test set img = :img where id = 7", use(b); - } - { - blob b(sql); - - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == 0); - - b.write_from_start(buf, sizeof(buf)); - CHECK(b.get_len() == sizeof(buf)); - sql << "update soci_test set img=? where id = 7", use(b); - - b.append(buf, sizeof(buf)); - CHECK(b.get_len() == 2 * sizeof(buf)); - sql << "insert into soci_test(id, img) values(8, ?)", use(b); - } - { - blob b(sql); - sql << "select img from soci_test where id = 8", into(b); - CHECK(b.get_len() == 2 * sizeof(buf)); - char buf2[100]; - b.read_from_start(buf2, 10); - CHECK(std::strncmp(buf2, "abcdefghij", 10) == 0); - - sql << "select img from soci_test where id = 7", into(b); - CHECK(b.get_len() == sizeof(buf)); - - } -} - // This test was put in to fix a problem that occurs when there are both // into and use elements in the same query and one of them (into) binds // to a vector object. From 53969cc23cdc14932f582f4f911a1016160e6a7f Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 19:42:37 +0100 Subject: [PATCH 29/78] postgres: require halfway recent Postgresql version (9+) --- src/backends/postgresql/blob.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index 607e299fe..d3c802d5d 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -99,21 +99,12 @@ std::size_t postgresql_blob_backend::append( void postgresql_blob_backend::trim(std::size_t newLen) { -#if PG_VERSION_NUM < 80003 - // lo_truncate was introduced in Postgresql v8.3 - (void) newLen; - throw soci_error("Your Postgresql version does not support trimming BLOBs"); -#else if (newLen > static_cast(std::numeric_limits::max())) { throw soci_error("Request new BLOB size exceeds INT_MAX, which is not supported"); } -# if PG_VERSION_NUM >= 90003 // lo_truncate64 was introduced in Postgresql v9.3 int ret_code = lo_truncate64(session_.conn_, details_.fd, newLen); -# else - int ret_code = -1; -# endif if (ret_code == -1) { // If we call lo_truncate64 on a server that is < v9.3, the call will fail and return -1. // Thus, we'll try again with the slightly older function lo_truncate. @@ -123,7 +114,6 @@ void postgresql_blob_backend::trim(std::size_t newLen) if (ret_code < 0) { throw soci_error(std::string("Cannot truncate BLOB: ") + PQerrorMessage(session_.conn_)); } -#endif } const postgresql_blob_backend::blob_details &postgresql_blob_backend::get_blob_details() const { @@ -145,11 +135,7 @@ void postgresql_blob_backend::set_destroy_on_close(bool destroy) { } std::size_t postgresql_blob_backend::seek(std::size_t toOffset, int from) { -#if PG_VERSION_NUM >= 90003 pg_int64 pos = lo_lseek64(session_.conn_, details_.fd, static_cast(toOffset), from); -#else - int pos = -1; -#endif if (pos == -1) { // If we try to use lo_lseek64 on a Postgresql server that is older than 9.3, the function will fail // and return -1, so we'll try again with the older function lo_lseek. From 50c713073fc8d85a375ce7bd2c7415475f87f275 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 19:49:04 +0100 Subject: [PATCH 30/78] postgresql: properly reset blob --- src/backends/postgresql/blob.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index d3c802d5d..f42f9ed62 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -181,6 +181,8 @@ void postgresql_blob_backend::reset() { // Merely close our handle to the large object lo_close(session_.conn_, details_.fd); } + + details_ = {}; } destroy_on_close_ = false; From b00f9e1574d9e9d928cc66ca47277c56e9468e68 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 20:35:11 +0100 Subject: [PATCH 31/78] postgresql: clone before modify after having inserted BLOB into DB --- include/soci/postgresql/soci-postgresql.h | 6 +- src/backends/postgresql/blob.cpp | 99 ++++++++++++++++--- src/backends/postgresql/standard-use-type.cpp | 4 + 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/include/soci/postgresql/soci-postgresql.h b/include/soci/postgresql/soci-postgresql.h index c4c2a63ac..0bdf3376b 100644 --- a/include/soci/postgresql/soci-postgresql.h +++ b/include/soci/postgresql/soci-postgresql.h @@ -363,7 +363,7 @@ class postgresql_blob_backend : public details::blob_backend void set_destroy_on_close(bool destroy); - std::size_t seek(std::size_t toOffset, int from); + void set_clone_before_modify(bool clone); void init(); @@ -373,6 +373,10 @@ class postgresql_blob_backend : public details::blob_backend postgresql_session_backend & session_; blob_details details_; bool destroy_on_close_; + bool clone_before_modify_; + + std::size_t seek(std::size_t toOffset, int from); + void clone(); }; struct postgresql_session_backend : details::session_backend diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index f42f9ed62..eb15d09fe 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -31,7 +31,7 @@ postgresql_blob_backend::blob_details::blob_details(unsigned long oid, int fd) : postgresql_blob_backend::postgresql_blob_backend( postgresql_session_backend & session) - : session_(session), details_(), destroy_on_close_(false) + : session_(session), details_(), destroy_on_close_(false), clone_before_modify_(false) { // nothing to do here, the descriptor is open in the postFetch // method of the Into element @@ -77,6 +77,10 @@ std::size_t postgresql_blob_backend::write_from_start(char const * buf, std::siz throw soci_error("Can't start writing far past-the-end of BLOB data."); } + if (clone_before_modify_) { + clone(); + } + init(); seek(offset, SEEK_SET); @@ -103,6 +107,17 @@ void postgresql_blob_backend::trim(std::size_t newLen) throw soci_error("Request new BLOB size exceeds INT_MAX, which is not supported"); } + if (clone_before_modify_) { + if (newLen == 0) { + // In case we're clearing the BLOB anyway, there is no point in copying the old data over first + reset(); + } else { + clone(); + } + } + + init(); + // lo_truncate64 was introduced in Postgresql v9.3 int ret_code = lo_truncate64(session_.conn_, details_.fd, newLen); if (ret_code == -1) { @@ -134,20 +149,8 @@ void postgresql_blob_backend::set_destroy_on_close(bool destroy) { destroy_on_close_ = destroy; } -std::size_t postgresql_blob_backend::seek(std::size_t toOffset, int from) { - pg_int64 pos = lo_lseek64(session_.conn_, details_.fd, static_cast(toOffset), from); - if (pos == -1) { - // If we try to use lo_lseek64 on a Postgresql server that is older than 9.3, the function will fail - // and return -1, so we'll try again with the older function lo_lseek. - pos = lo_lseek(session_.conn_, details_.fd, static_cast(toOffset), from); - } - - if (pos < 0) - { - throw soci_error("Cannot retrieve the size of BLOB."); - } - - return static_cast(pos); +void postgresql_blob_backend::set_clone_before_modify(bool clone) { + clone_before_modify_ = clone; } void postgresql_blob_backend::init() { @@ -186,4 +189,70 @@ void postgresql_blob_backend::reset() { } destroy_on_close_ = false; + clone_before_modify_ = false; +} + +std::size_t do_seek(std::size_t toOffset, int from, + soci::postgresql_session_backend &session, soci::postgresql_blob_backend::blob_details &details) +{ + pg_int64 pos = lo_lseek64(session.conn_, details.fd, static_cast(toOffset), from); + if (pos == -1) { + // If we try to use lo_lseek64 on a Postgresql server that is older than 9.3, the function will fail + // and return -1, so we'll try again with the older function lo_lseek. + pos = lo_lseek(session.conn_, details.fd, static_cast(toOffset), from); + } + + if (pos < 0) + { + throw soci_error("Cannot retrieve the seek in BLOB."); + } + + return static_cast(pos); +} + +std::size_t postgresql_blob_backend::seek(std::size_t toOffset, int from) { + return do_seek(toOffset, from, session_, details_); +} + +void postgresql_blob_backend::clone() { + clone_before_modify_ = false; + if (details_.fd == -1) { + return; + } + + blob_details old_details = details_; + details_ = {}; + reset(); + init(); + + char buf[1024]; + std::size_t offset = 0; + int read_bytes = 0; + do { + do_seek(offset, SEEK_SET, session_, old_details); + read_bytes = lo_read(session_.conn_, old_details.fd, buf, sizeof(buf)); + + if (read_bytes < 0) + { + throw soci::soci_error("Can't read from original BLOB during clone"); + } + + int bytes_written = lo_write(session_.conn_, details_.fd, buf, read_bytes); + + if (bytes_written != read_bytes) + { + throw soci::soci_error("Can't write (all) data from old BLOB to new one during clone"); + } + + offset += sizeof(buf); + } while (read_bytes == sizeof(buf)); + + // Dispose old BLOB object + if (destroy_on_close_) { + // Remove the large object from the DB completely + lo_unlink(session_.conn_, old_details.fd); + } else { + // Merely close our handle to the large object + lo_close(session_.conn_, old_details.fd); + } } diff --git a/src/backends/postgresql/standard-use-type.cpp b/src/backends/postgresql/standard-use-type.cpp index b1ce40752..4487950c7 100644 --- a/src/backends/postgresql/standard-use-type.cpp +++ b/src/backends/postgresql/standard-use-type.cpp @@ -184,6 +184,10 @@ void postgresql_standard_use_type_backend::pre_use(indicator const * ind) // to not destroy it again, because we will now actually insert it into the DB. bbe->set_destroy_on_close(false); + // We don't want any subsequent changes made to the blob object to leak to the + // object that we are currently writing into the DB + bbe->set_clone_before_modify(true); + std::size_t const bufSize = std::numeric_limits::digits10 + 2; buf_ = new char[bufSize]; From d3b722a8ade8dcc9f7c1793f40059fc830f5972b Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sat, 7 Jan 2023 20:56:53 +0100 Subject: [PATCH 32/78] extended BLOB test-cases --- tests/common-tests.h | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/common-tests.h b/tests/common-tests.h index 7f0f7d505..a368da51d 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -6526,13 +6526,25 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") // Write and retrieve blob from/into database sql << "insert into soci_test (id, b) values(:id, :b)", soci::use(first_id), soci::use(write_blob); + // Append to write_blob - these changes must not reflect in the BLOB stored in the DB + write_blob.append("ay", 2); + CHECK(write_blob.get_len() == 12); + char buf[15]; + std::size_t read_bytes = write_blob.read_from_start(buf, sizeof(buf)); + CHECK(read_bytes == 12); + for (std::size_t i = 0; i < 10; ++i) { + CHECK(buf[i] == dummy_data[i]); + } + CHECK(buf[10] == 'a'); + CHECK(buf[11] == 'y'); + + soci::blob read_blob(sql); sql << "select b from soci_test where id = :id", soci::use(first_id), soci::into(read_blob); CHECK(sql.got_data()); - CHECK(read_blob.get_len() == write_blob.get_len()); + CHECK(read_blob.get_len() == 10); - char buf[15]; std::size_t bytes_read = read_blob.read_from_start(buf, sizeof(buf)); CHECK(bytes_read == read_blob.get_len()); CHECK(bytes_read == 10); From be816d2de5efb7bb8a9efadac6893123c4f5e3bd Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sun, 8 Jan 2023 14:20:17 +0100 Subject: [PATCH 33/78] oracle: don't reset after use --- src/backends/oracle/standard-use-type.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/backends/oracle/standard-use-type.cpp b/src/backends/oracle/standard-use-type.cpp index 86d395768..78ac109e1 100644 --- a/src/backends/oracle/standard-use-type.cpp +++ b/src/backends/oracle/standard-use-type.cpp @@ -681,15 +681,6 @@ void oracle_standard_use_type_backend::post_use(bool gotData, indicator *ind) } break; case x_blob: - { - blob *b = static_cast(data_); - - oracle_blob_backend *bbe - = static_cast(b->get_backend()); - - bbe->reset(); - } - break; case x_rowid: case x_xmltype: case x_longstring: From ad611658967e3cc54550d515d507060f0395c5b0 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sun, 8 Jan 2023 14:15:27 +0100 Subject: [PATCH 34/78] postgresql: remove unnecessary include --- src/backends/postgresql/blob.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backends/postgresql/blob.cpp b/src/backends/postgresql/blob.cpp index eb15d09fe..2718c8d6a 100644 --- a/src/backends/postgresql/blob.cpp +++ b/src/backends/postgresql/blob.cpp @@ -8,7 +8,6 @@ #define SOCI_POSTGRESQL_SOURCE #include "soci/postgresql/soci-postgresql.h" #include // libpq -#include #include #include #include From 3b5a0fe1c7c1c17e8b8afcee07ced26bab07aca3 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sun, 15 Oct 2023 19:11:06 +0200 Subject: [PATCH 35/78] Make describe_column properly identify BLOB cols --- src/backends/firebird/statement.cpp | 6 ++++-- src/backends/mysql/statement.cpp | 13 ++++++++++--- src/backends/oracle/statement.cpp | 3 +++ src/backends/postgresql/statement.cpp | 10 ++++++++-- src/backends/sqlite3/statement.cpp | 2 ++ 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/backends/firebird/statement.cpp b/src/backends/firebird/statement.cpp index aa58ca46c..648c24dcf 100644 --- a/src/backends/firebird/statement.cpp +++ b/src/backends/firebird/statement.cpp @@ -731,8 +731,10 @@ void firebird_statement_backend::describe_column(int colNum, dbtype = db_int64; } break; - /* case SQL_BLOB: - case SQL_ARRAY:*/ + case SQL_BLOB: + dbtype = db_blob; + break; + /* case SQL_ARRAY:*/ default: std::ostringstream msg; msg << "Type of column ["<< colNum << "] \"" << columnName diff --git a/src/backends/mysql/statement.cpp b/src/backends/mysql/statement.cpp index f38db1f29..75c78dcc0 100644 --- a/src/backends/mysql/statement.cpp +++ b/src/backends/mysql/statement.cpp @@ -463,12 +463,19 @@ void mysql_statement_backend::describe_column(int colNum, case 245: //MYSQL_TYPE_JSON: case FIELD_TYPE_VAR_STRING: //MYSQL_TYPE_VAR_STRING: case FIELD_TYPE_STRING: //MYSQL_TYPE_STRING: - case FIELD_TYPE_BLOB: // TEXT OR BLOB + dbtype = db_string; + break; + case FIELD_TYPE_BLOB: // BLOB case FIELD_TYPE_TINY_BLOB: case FIELD_TYPE_MEDIUM_BLOB: - case FIELD_TYPE_LONG_BLOB: - dbtype = db_string; + case FIELD_TYPE_LONG_BLOB: { + // Quoted from the docs: + // To distinguish between binary and nonbinary data for string data types, check whether the + // charsetnr value is 63. If so, the character set is binary, which indicates binary rather + // than nonbinary data. + dbtype = field->charsetnr == 63 ? db_blob : db_string; break; + } default: //std::cerr << "field->type: " << field->type << std::endl; throw soci_error("Unknown data type."); diff --git a/src/backends/oracle/statement.cpp b/src/backends/oracle/statement.cpp index 0db66132a..0622db91c 100644 --- a/src/backends/oracle/statement.cpp +++ b/src/backends/oracle/statement.cpp @@ -315,6 +315,9 @@ void oracle_statement_backend::describe_column(int colNum, case SQLT_DAT: xdbtype = db_date; break; + case SQLT_BLOB: + xdbtype = db_blob; + break; default: // Unknown oracle types will just be represented by a string xdbtype = db_string; diff --git a/src/backends/postgresql/statement.cpp b/src/backends/postgresql/statement.cpp index 5995b45b7..f65934753 100644 --- a/src/backends/postgresql/statement.cpp +++ b/src/backends/postgresql/statement.cpp @@ -750,7 +750,7 @@ void postgresql_statement_backend::describe_column(int colNum, switch (typeOid) { // Note: the following list of OIDs was taken from the pg_type table - // we do not claim that this list is exchaustive or even correct. + // we do not claim that this list is exhaustive or even correct. // from pg_type: @@ -796,7 +796,6 @@ void postgresql_statement_backend::describe_column(int colNum, break; case 23: // int4 - case 26: // oid dbtype = db_int32; break; @@ -804,6 +803,13 @@ void postgresql_statement_backend::describe_column(int colNum, dbtype = db_int64; break; + case 26: // oid + // Note that in theory OIDs can refer to all sorts of things, but their use + // for anything but BLOBs seems to be deprecated since PostreSQL 8, so we simply + // assume any OID refers to a BLOB. + dbtype = db_blob; + break; + default: { auto typeCategoryIt = categoryByColumnOID_.find(typeOid); diff --git a/src/backends/sqlite3/statement.cpp b/src/backends/sqlite3/statement.cpp index 05d458ddf..35c426d69 100644 --- a/src/backends/sqlite3/statement.cpp +++ b/src/backends/sqlite3/statement.cpp @@ -568,6 +568,8 @@ void sqlite3_statement_backend::describe_column(int colNum, dbtype = db_double; break; case SQLITE_BLOB: + dbtype = db_blob; + break; case SQLITE_TEXT: dbtype = db_string; break; From 9f05ee4c3c9686c5d526c51ec1ac690f90228db2 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sun, 15 Oct 2023 19:11:46 +0200 Subject: [PATCH 36/78] Add rowset type detection test for BLOBs --- tests/common-tests.h | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/common-tests.h b/tests/common-tests.h index a368da51d..018107fdf 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -6463,7 +6463,7 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") CHECK_THROWS_AS(blob.read_from_start(buf, sizeof(buf), blob.get_len()), soci_error); written_bytes = blob.append("z", 1); - + CHECK(written_bytes == 1); CHECK(blob.get_len() == 3); @@ -6599,6 +6599,25 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") CHECK(buf[i] == binary_data[i]); } } + SECTION("Rowset type detection") { + soci::blob blob(sql); + static_assert(sizeof(dummy_data) >= 10, "Underlying assumption violated"); + blob.write_from_start(dummy_data, 10); + + // Write and retrieve blob from/into database + int id = 1; + sql << "insert into soci_test (id, b) values(:id, :b)", soci::use(id), soci::use(blob); + + soci::rowset< soci::row > rowSet = sql.prepare << "select id, b from soci_test"; + bool containedData = false; + for (auto it = rowSet.begin(); it != rowSet.end(); ++it) { + containedData = true; + const soci::row ¤tRow = *it; + CHECK(currentRow.get_properties(0).get_data_type() == soci::dt_integer); + CHECK(currentRow.get_properties(1).get_data_type() == soci::dt_blob); + } + CHECK(containedData); + } } transaction.rollback(); } From f48b1ac3f78374a3d5db1b3730c42df4a76ca90e Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Tue, 17 Oct 2023 19:56:06 +0200 Subject: [PATCH 37/78] Remove MySQL-specific (and now wrong) BLOB tests --- tests/mysql/test-mysql.cpp | 118 ++++++++----------------------------- 1 file changed, 25 insertions(+), 93 deletions(-) diff --git a/tests/mysql/test-mysql.cpp b/tests/mysql/test-mysql.cpp index 7e933432e..7d548d217 100644 --- a/tests/mysql/test-mysql.cpp +++ b/tests/mysql/test-mysql.cpp @@ -371,124 +371,59 @@ TEST_CASE("MySQL datetime", "[mysql][datetime]") CHECK(t.tm_sec == 52); } -// TEXT and BLOB types support test. -TEST_CASE("MySQL text and blob", "[mysql][text][blob]") +// TEXT type support test. +TEST_CASE("MySQL text", "[mysql][text]") { soci::session sql(backEnd, connectString); std::string a("asdfg\0hjkl", 10); - std::string b("lkjhg\0fd\0\0sa\0", 13); - std::string c("\\0aa\\0bb\\0cc\\0", 10); // The maximum length for TEXT and BLOB is 65536. std::string x(60000, 'X'); - std::string y(60000, 'Y'); - // The default max_allowed_packet value for a MySQL server is 1M, - // so let's limit ourselves to 800k, even though the maximum length - // for LONGBLOB is 4G. - std::string z(800000, 'Z'); - - sql << "create table soci_test (id int, text_value text, " - "blob_value blob, longblob_value longblob)"; - sql << "insert into soci_test values (1, \'foo\', \'bar\', \'baz\')"; + + sql << "create table soci_test (id int, text_value text)"; + sql << "insert into soci_test values (1, \'foo\')"; sql << "insert into soci_test " - << "values (2, \'qwerty\\0uiop\', \'zxcv\\0bnm\', " - << "\'qwerty\\0uiop\\0zxcvbnm\\0\')"; - sql << "insert into soci_test values (3, :a, :b, :c)", - use(a), use(b), use(c); - sql << "insert into soci_test values (4, :x, :y, :z)", - use(x), use(y), use(z); + << "values (2, \'qwerty\\0uiop\')"; + sql << "insert into soci_test values (3, :a)", + use(a); + sql << "insert into soci_test values (4, :x)", + use(x); std::vector text_vec(100); - std::vector blob_vec(100); - std::vector longblob_vec(100); - sql << "select text_value, blob_value, longblob_value " - << "from soci_test order by id", - into(text_vec), into(blob_vec), into(longblob_vec); + sql << "select text_value from soci_test order by id", into(text_vec); REQUIRE(text_vec.size() == 4); - REQUIRE(blob_vec.size() == 4); - REQUIRE(longblob_vec.size() == 4); CHECK(text_vec[0] == "foo"); - CHECK(blob_vec[0] == "bar"); - CHECK(longblob_vec[0] == "baz"); CHECK(text_vec[1] == std::string("qwerty\0uiop", 11)); - CHECK(blob_vec[1] == std::string("zxcv\0bnm", 8)); - CHECK(longblob_vec[1] == std::string("qwerty\0uiop\0zxcvbnm\0", 20)); CHECK(text_vec[2] == a); - CHECK(blob_vec[2] == b); - CHECK(longblob_vec[2] == c); CHECK(text_vec[3] == x); - CHECK(blob_vec[3] == y); - CHECK(longblob_vec[3] == z); - std::string text, blob, longblob; - sql << "select text_value, blob_value, longblob_value " - << "from soci_test where id = 1", - into(text), into(blob), into(longblob); + std::string text; + sql << "select text_value from soci_test where id = 1", into(text); CHECK(text == "foo"); - CHECK(blob == "bar"); - CHECK(longblob == "baz"); - sql << "select text_value, blob_value, longblob_value " - << "from soci_test where id = 2", - into(text), into(blob), into(longblob); + sql << "select text_value from soci_test where id = 2", into(text); CHECK(text == std::string("qwerty\0uiop", 11)); - CHECK(blob == std::string("zxcv\0bnm", 8)); - CHECK(longblob == std::string("qwerty\0uiop\0zxcvbnm\0", 20)); - sql << "select text_value, blob_value, longblob_value " - << "from soci_test where id = 3", - into(text), into(blob), into(longblob); + sql << "select text_value from soci_test where id = 3", into(text); CHECK(text == a); - CHECK(blob == b); - CHECK(longblob == c); - sql << "select text_value, blob_value, longblob_value " - << "from soci_test where id = 4", - into(text), into(blob), into(longblob); + sql << "select text_value from soci_test where id = 4", into(text); CHECK(text == x); - CHECK(blob == y); - CHECK(longblob == z); rowset rs = - (sql.prepare << "select text_value, blob_value, longblob_value " - "from soci_test order by id"); + (sql.prepare << "select text_value from soci_test order by id"); rowset::const_iterator r = rs.begin(); CHECK(r->get_properties(0).get_data_type() == dt_string); CHECK(r->get_properties(0).get_db_type() == db_string); CHECK(r->get(0) == "foo"); - CHECK(r->get_properties(1).get_data_type() == dt_string); - CHECK(r->get_properties(1).get_db_type() == db_string); - CHECK(r->get(1) == "bar"); - CHECK(r->get_properties(2).get_data_type() == dt_string); - CHECK(r->get_properties(2).get_db_type() == db_string); - CHECK(r->get(2) == "baz"); ++r; CHECK(r->get_properties(0).get_data_type() == dt_string); CHECK(r->get_properties(0).get_db_type() == db_string); CHECK(r->get(0) == std::string("qwerty\0uiop", 11)); - CHECK(r->get_properties(1).get_data_type() == dt_string); - CHECK(r->get_properties(1).get_db_type() == db_string); - CHECK(r->get(1) == std::string("zxcv\0bnm", 8)); - CHECK(r->get_properties(2).get_data_type() == dt_string); - CHECK(r->get_properties(2).get_db_type() == db_string); - CHECK(r->get(2) == - std::string("qwerty\0uiop\0zxcvbnm\0", 20)); ++r; CHECK(r->get_properties(0).get_data_type() == dt_string); CHECK(r->get_properties(0).get_db_type() == db_string); CHECK(r->get(0) == a); - CHECK(r->get_properties(1).get_data_type() == dt_string); - CHECK(r->get_properties(1).get_db_type() == db_string); - CHECK(r->get(1) == b); - CHECK(r->get_properties(2).get_data_type() == dt_string); - CHECK(r->get_properties(2).get_db_type() == db_string); - CHECK(r->get(2) == c); ++r; CHECK(r->get_properties(0).get_data_type() == dt_string); CHECK(r->get_properties(0).get_db_type() == db_string); CHECK(r->get(0) == x); - CHECK(r->get_properties(1).get_data_type() == dt_string); - CHECK(r->get_properties(1).get_db_type() == db_string); - CHECK(r->get(1) == y); - CHECK(r->get_properties(2).get_data_type() == dt_string); - CHECK(r->get_properties(2).get_db_type() == db_string); - CHECK(r->get(2) == z); ++r; CHECK(r == rs.end()); @@ -760,8 +695,7 @@ struct strings_table_creator : table_creator_base { sql << "create table soci_test(s1 char(20), s2 varchar(20), " "s3 tinytext, s4 mediumtext, s5 text, s6 longtext, " - "b1 binary(20), b2 varbinary(20), b3 tinyblob, b4 mediumblob, " - "b5 blob, b6 longblob, e1 enum ('foo', 'bar', 'baz'))"; + "b1 binary(20), b2 varbinary(20), e1 enum ('foo', 'bar', 'baz'))"; } }; @@ -772,22 +706,20 @@ TEST_CASE("MySQL strings", "[mysql][string]") std::string text = "Ala ma kota."; std::string binary("Ala\0ma\0kota.........", 20); sql << "insert into soci_test " - "(s1, s2, s3, s4, s5, s6, b1, b2, b3, b4, b5, b6, e1) values " - "(:s1, :s2, :s3, :s4, :d5, :s6, :b1, :b2, :b3, :b4, :b5, :b6, " - "\'foo\')", + "(s1, s2, s3, s4, s5, s6, b1, b2, e1) values " + "(:s1, :s2, :s3, :s4, :d5, :s6, :b1, :b2, 'foo')", use(text), use(text), use(text), use(text), use(text), use(text), - use(binary), use(binary), use(binary), use(binary), use(binary), - use(binary); + use(binary), use(binary); row r; - sql << "select s1, s2, s3, s4, s5, s6, b1, b2, b3, b4, b5, b6, e1 " + sql << "select s1, s2, s3, s4, s5, s6, b1, b2, e1 " "from soci_test", into(r); - REQUIRE(r.size() == 13); - for (int i = 0; i < 13; i++) { + REQUIRE(r.size() == 9); + for (int i = 0; i < static_cast(r.size()); i++) { CHECK(r.get_properties(i).get_data_type() == dt_string); CHECK(r.get_properties(i).get_db_type() == db_string); if (i < 6) { CHECK(r.get(i) == text); - } else if (i < 12) { + } else if (i < 8) { CHECK(r.get(i) == binary); } else { CHECK(r.get(i) == "foo"); From d6986a3cbb8bd807f66a9f0b6531cdd14cf739c5 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Sun, 8 Jan 2023 10:00:35 +0100 Subject: [PATCH 38/78] Debug output --- src/backends/oracle/blob.cpp | 16 ++++++++++++++++ tests/common-tests.h | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/backends/oracle/blob.cpp b/src/backends/oracle/blob.cpp index fc400a243..7659b5f8a 100644 --- a/src/backends/oracle/blob.cpp +++ b/src/backends/oracle/blob.cpp @@ -14,6 +14,8 @@ #include #include +#include + #ifdef _MSC_VER #pragma warning(disable:4355) #endif @@ -31,6 +33,7 @@ oracle_blob_backend::oracle_blob_backend(oracle_session_backend &session) { throw soci_error("Cannot allocate the LOB locator"); } + std::cout << "Created Oracle Blob obj\n"; } oracle_blob_backend::~oracle_blob_backend() @@ -42,10 +45,12 @@ oracle_blob_backend::~oracle_blob_backend() } OCIDescriptorFree(lobp_, OCI_DTYPE_LOB); + std::cout << "Destroyed Oracle Blob obj\n"; } std::size_t oracle_blob_backend::get_len() { + std::cout << "Getting blob length\n"; if (!initialized_) { return 0; } @@ -65,6 +70,7 @@ std::size_t oracle_blob_backend::get_len() std::size_t oracle_blob_backend::read_from_start(char *buf, std::size_t toRead, std::size_t offset) { + std::cout << "Reading from blob (" << toRead << ", " << offset << ")\n"; if (offset >= get_len()) { if (!initialized_ && offset == 0) @@ -91,6 +97,7 @@ std::size_t oracle_blob_backend::read_from_start(char *buf, std::size_t toRead, std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t toWrite, std::size_t offset) { + std::cout << "Writing to blob (" << toWrite << ", " << offset << ")\n"; if (offset > get_len()) { // If offset == length, the operation is to be understood as appending (and is therefore allowed) @@ -115,6 +122,7 @@ std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t t std::size_t oracle_blob_backend::append(char const *buf, std::size_t toWrite) { + std::cout << "Appending to blob (" << toWrite << ")\n"; ensure_initialized(); ub4 amt = static_cast(toWrite); @@ -132,6 +140,7 @@ std::size_t oracle_blob_backend::append(char const *buf, std::size_t toWrite) void oracle_blob_backend::trim(std::size_t newLen) { + std::cout << "Trimming blob (" << newLen << ")\n"; sword res = OCILobTrim(session_.svchp_, session_.errhp_, lobp_, static_cast(newLen)); if (res != OCI_SUCCESS) @@ -147,6 +156,7 @@ oracle_blob_backend::locator_t oracle_blob_backend::get_lob_locator() const void oracle_blob_backend::set_lob_locator(oracle_blob_backend::locator_t locator, bool initialized) { + std::cout << "Setting locator\n"; reset(); lobp_ = locator; @@ -166,6 +176,7 @@ void oracle_blob_backend::set_lob_locator(oracle_blob_backend::locator_t locator void oracle_blob_backend::reset() { + std::cout << "Resetting blob\n"; if (!initialized_) { return; @@ -176,6 +187,7 @@ void oracle_blob_backend::reset() if (res != OCI_SUCCESS) { + std::cout << "Can't check if temporary LOB\n"; throw_oracle_soci_error(res, session_.errhp_); } @@ -187,16 +199,20 @@ void oracle_blob_backend::reset() if (res != OCI_SUCCESS) { + std::cout << "Can't free/close LOB (is temporary: " << is_temporary << ")\n"; throw_oracle_soci_error(res, session_.errhp_); } initialized_ = false; + + std::cout << "Reset complete\n"; } void oracle_blob_backend::ensure_initialized() { if (!initialized_) { + std::cout << "Initializing blob\n"; // If asked to initialize explicitly, we can only create a temporary LOB sword res = OCILobCreateTemporary(session_.svchp_, session_.errhp_, lobp_, OCI_DEFAULT, SQLCS_IMPLICIT, OCI_TEMP_BLOB, FALSE, OCI_DURATION_SESSION); diff --git a/tests/common-tests.h b/tests/common-tests.h index 018107fdf..7db561788 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -6492,6 +6492,7 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") } SECTION("Ensure reading into blob overwrites previous contents") { + std::cout << "Relevant part begin\n"; soci::blob blob(sql); blob.write_from_start("hello kitty", 10); @@ -6516,6 +6517,7 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") } SECTION("Blob-DB interaction") { + std::cout << "Next part begin\n"; soci::blob write_blob(sql); static_assert(sizeof(dummy_data) >= 10, "Underlying assumption violated"); From 552f321c05c5677e8f4f74bdd7a8891ffe8ec0c9 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Tue, 24 Oct 2023 19:24:53 +0200 Subject: [PATCH 39/78] Bind blobs to actual blob objects This ensures that when binding blobs dynamically (e.g. in a row), they are properly represented as a soci::blob object and not by a std::string. --- include/soci/statement.h | 5 +++++ src/core/statement.cpp | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/include/soci/statement.h b/include/soci/statement.h index a1397d106..b1dbc4269 100644 --- a/include/soci/statement.h +++ b/include/soci/statement.h @@ -16,6 +16,7 @@ #include "soci/use.h" #include "soci/soci-backend.h" #include "soci/row.h" +#include "soci/blob.h" // std #include #include @@ -164,6 +165,10 @@ class SOCI_DECL statement_impl SOCI_NOT_COPYABLE(statement_impl) }; +template<> +void statement_impl::into_row(); + + } // namespace details // Statement is a handle class for statement_impl diff --git a/src/core/statement.cpp b/src/core/statement.cpp index 495cca58e..7bf5e777c 100644 --- a/src/core/statement.cpp +++ b/src/core/statement.cpp @@ -6,6 +6,8 @@ // #define SOCI_SOURCE +#include "soci/blob.h" +#include "soci/blob-exchange.h" #include "soci/statement.h" #include "soci/session.h" #include "soci/into-type.h" @@ -695,6 +697,12 @@ void statement_impl::bind_into() into_row(); } +template<> +void statement_impl::bind_into() +{ + into_row(); +} + void statement_impl::describe() { row_->clean_up(); @@ -715,10 +723,12 @@ void statement_impl::describe() switch (dbtype) { case db_string: - case db_blob: case db_xml: bind_into(); break; + case db_blob: + bind_into(); + break; case db_double: bind_into(); break; @@ -868,3 +878,12 @@ statement_impl::rethrow_current_exception_with_context(char const* operation) throw; } } + +template<> +void statement_impl::into_row() +{ + blob * b = new blob(session_); + indicator * ind = new indicator(i_ok); + row_->add_holder(b, ind); + exchange_for_row(into(*b, *ind)); +} From 48e460964c2402c7479c9ea012df3137926dca92 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Tue, 24 Oct 2023 19:57:19 +0200 Subject: [PATCH 40/78] More debug output --- src/backends/oracle/blob.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/oracle/blob.cpp b/src/backends/oracle/blob.cpp index 7659b5f8a..877062965 100644 --- a/src/backends/oracle/blob.cpp +++ b/src/backends/oracle/blob.cpp @@ -199,7 +199,7 @@ void oracle_blob_backend::reset() if (res != OCI_SUCCESS) { - std::cout << "Can't free/close LOB (is temporary: " << is_temporary << ")\n"; + std::cout << "Can't free/close LOB (is temporary: " << std::boolalpha << is_temporary << ") res: " << res << "\n"; throw_oracle_soci_error(res, session_.errhp_); } From 6f35bf1f53199a1e4f4dd50e0f785144d0c17272 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 26 Oct 2023 18:44:00 +0200 Subject: [PATCH 41/78] test --- src/backends/oracle/blob.cpp | 49 ++++++++++++++++++++++++++++++++++-- tests/common-tests.h | 2 ++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/backends/oracle/blob.cpp b/src/backends/oracle/blob.cpp index 877062965..e5e35b10e 100644 --- a/src/backends/oracle/blob.cpp +++ b/src/backends/oracle/blob.cpp @@ -34,6 +34,7 @@ oracle_blob_backend::oracle_blob_backend(oracle_session_backend &session) throw soci_error("Cannot allocate the LOB locator"); } std::cout << "Created Oracle Blob obj\n"; + std::cout << lobp_ << std::endl; } oracle_blob_backend::~oracle_blob_backend() @@ -108,7 +109,18 @@ std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t t ub4 amt = static_cast(toWrite); - sword res = OCILobWrite(session_.svchp_, session_.errhp_, lobp_, &amt, + boolean is_temporary = FALSE; + sword res = OCILobIsTemporary(session_.envhp_, session_.errhp_, lobp_, &is_temporary); + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + + std::cout << "Is temporary before writing: " << std::boolalpha << (is_temporary == TRUE) << std::endl; + std::cout << lobp_ << std::endl; + + res = OCILobWrite(session_.svchp_, session_.errhp_, lobp_, &amt, static_cast(offset + 1), reinterpret_cast(const_cast(buf)), amt, OCI_ONE_PIECE, 0, 0, 0, 0); @@ -117,6 +129,16 @@ std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t t throw_oracle_soci_error(res, session_.errhp_); } + res = OCILobIsTemporary(session_.envhp_, session_.errhp_, lobp_, &is_temporary); + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + + std::cout << "Is temporary after writing: " << std::boolalpha << (is_temporary == TRUE) << std::endl; + std::cout << lobp_ << std::endl; + return static_cast(amt); } @@ -191,6 +213,9 @@ void oracle_blob_backend::reset() throw_oracle_soci_error(res, session_.errhp_); } + std::cout << "Is temporary when resetting: " << std::boolalpha << (is_temporary == TRUE) << std::endl; + std::cout << lobp_ << std::endl; + if (is_temporary) { res = OCILobFreeTemporary(session_.svchp_, session_.errhp_, lobp_); } else { @@ -199,7 +224,7 @@ void oracle_blob_backend::reset() if (res != OCI_SUCCESS) { - std::cout << "Can't free/close LOB (is temporary: " << std::boolalpha << is_temporary << ") res: " << res << "\n"; + std::cout << "Can't free/close LOB (is temporary: " << is_temporary << ") res: " << res << "\n"; throw_oracle_soci_error(res, session_.errhp_); } @@ -222,6 +247,16 @@ void oracle_blob_backend::ensure_initialized() throw_oracle_soci_error(res, session_.errhp_); } + boolean is_temporary = FALSE; + res = OCILobIsTemporary(session_.envhp_, session_.errhp_, lobp_, &is_temporary); + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + + std::cout << "Is temporary immediately after creation as temporary: " << std::boolalpha << (is_temporary == TRUE) << std::endl; + res = OCILobOpen(session_.svchp_, session_.errhp_, lobp_, OCI_LOB_READWRITE); if (res != OCI_SUCCESS) @@ -229,6 +264,16 @@ void oracle_blob_backend::ensure_initialized() throw_oracle_soci_error(res, session_.errhp_); } + res = OCILobIsTemporary(session_.envhp_, session_.errhp_, lobp_, &is_temporary); + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + + std::cout << "Is temporary immediately after opening: " << std::boolalpha << (is_temporary == TRUE) << std::endl; + std::cout << lobp_ << std::endl; + initialized_ = true; } } diff --git a/tests/common-tests.h b/tests/common-tests.h index 7db561788..6193d0fc3 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -6502,6 +6502,8 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") write_blob.write_from_start("test", 4); sql << "insert into soci_test (id, b) values (5, :b)", soci::use(write_blob); + std::cout << "Finished writing into DB, now reading back from it" << std::endl; + sql << "select b from soci_test where id = 5", soci::into(blob); CHECK(blob.get_len() == 4); From 8b5b312d48960144351935945ef99c1b7c3c0b61 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Wed, 1 Nov 2023 19:41:33 +0100 Subject: [PATCH 42/78] Fix Oracle bug that prevented selecting into initialized BLOB --- src/backends/oracle/blob.cpp | 86 ++++++---------------- src/backends/oracle/standard-into-type.cpp | 4 + tests/common-tests.h | 27 +++++-- 3 files changed, 46 insertions(+), 71 deletions(-) diff --git a/src/backends/oracle/blob.cpp b/src/backends/oracle/blob.cpp index e5e35b10e..d7b610d5e 100644 --- a/src/backends/oracle/blob.cpp +++ b/src/backends/oracle/blob.cpp @@ -33,8 +33,6 @@ oracle_blob_backend::oracle_blob_backend(oracle_session_backend &session) { throw soci_error("Cannot allocate the LOB locator"); } - std::cout << "Created Oracle Blob obj\n"; - std::cout << lobp_ << std::endl; } oracle_blob_backend::~oracle_blob_backend() @@ -46,12 +44,10 @@ oracle_blob_backend::~oracle_blob_backend() } OCIDescriptorFree(lobp_, OCI_DTYPE_LOB); - std::cout << "Destroyed Oracle Blob obj\n"; } std::size_t oracle_blob_backend::get_len() { - std::cout << "Getting blob length\n"; if (!initialized_) { return 0; } @@ -71,7 +67,6 @@ std::size_t oracle_blob_backend::get_len() std::size_t oracle_blob_backend::read_from_start(char *buf, std::size_t toRead, std::size_t offset) { - std::cout << "Reading from blob (" << toRead << ", " << offset << ")\n"; if (offset >= get_len()) { if (!initialized_ && offset == 0) @@ -98,7 +93,6 @@ std::size_t oracle_blob_backend::read_from_start(char *buf, std::size_t toRead, std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t toWrite, std::size_t offset) { - std::cout << "Writing to blob (" << toWrite << ", " << offset << ")\n"; if (offset > get_len()) { // If offset == length, the operation is to be understood as appending (and is therefore allowed) @@ -109,18 +103,7 @@ std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t t ub4 amt = static_cast(toWrite); - boolean is_temporary = FALSE; - sword res = OCILobIsTemporary(session_.envhp_, session_.errhp_, lobp_, &is_temporary); - - if (res != OCI_SUCCESS) - { - throw_oracle_soci_error(res, session_.errhp_); - } - - std::cout << "Is temporary before writing: " << std::boolalpha << (is_temporary == TRUE) << std::endl; - std::cout << lobp_ << std::endl; - - res = OCILobWrite(session_.svchp_, session_.errhp_, lobp_, &amt, + sword res = OCILobWrite(session_.svchp_, session_.errhp_, lobp_, &amt, static_cast(offset + 1), reinterpret_cast(const_cast(buf)), amt, OCI_ONE_PIECE, 0, 0, 0, 0); @@ -129,22 +112,11 @@ std::size_t oracle_blob_backend::write_from_start(char const *buf, std::size_t t throw_oracle_soci_error(res, session_.errhp_); } - res = OCILobIsTemporary(session_.envhp_, session_.errhp_, lobp_, &is_temporary); - - if (res != OCI_SUCCESS) - { - throw_oracle_soci_error(res, session_.errhp_); - } - - std::cout << "Is temporary after writing: " << std::boolalpha << (is_temporary == TRUE) << std::endl; - std::cout << lobp_ << std::endl; - return static_cast(amt); } std::size_t oracle_blob_backend::append(char const *buf, std::size_t toWrite) { - std::cout << "Appending to blob (" << toWrite << ")\n"; ensure_initialized(); ub4 amt = static_cast(toWrite); @@ -162,7 +134,6 @@ std::size_t oracle_blob_backend::append(char const *buf, std::size_t toWrite) void oracle_blob_backend::trim(std::size_t newLen) { - std::cout << "Trimming blob (" << newLen << ")\n"; sword res = OCILobTrim(session_.svchp_, session_.errhp_, lobp_, static_cast(newLen)); if (res != OCI_SUCCESS) @@ -178,27 +149,44 @@ oracle_blob_backend::locator_t oracle_blob_backend::get_lob_locator() const void oracle_blob_backend::set_lob_locator(oracle_blob_backend::locator_t locator, bool initialized) { - std::cout << "Setting locator\n"; - reset(); + // If we select a BLOB value into a BLOB object, then the post_fetch code in + // the standard_into_type_backend will set this object's locator to the one it is + // already holding. + // In this case, the locator now already points to the desired BLOB object and thus we + // must not reset it. + if (lobp_ != locator) + { + reset(); - lobp_ = locator; + lobp_ = locator; + } initialized_ = initialized; if (initialized) { - sword res = OCILobOpen(session_.svchp_, session_.errhp_, lobp_, OCI_LOB_READWRITE); + boolean already_open = FALSE; + sword res = OCILobIsOpen(session_.svchp_, session_.errhp_, lobp_, &already_open); if (res != OCI_SUCCESS) { throw_oracle_soci_error(res, session_.errhp_); } + + if (!already_open) + { + res = OCILobOpen(session_.svchp_, session_.errhp_, lobp_, OCI_LOB_READWRITE); + + if (res != OCI_SUCCESS) + { + throw_oracle_soci_error(res, session_.errhp_); + } + } } } void oracle_blob_backend::reset() { - std::cout << "Resetting blob\n"; if (!initialized_) { return; @@ -209,13 +197,9 @@ void oracle_blob_backend::reset() if (res != OCI_SUCCESS) { - std::cout << "Can't check if temporary LOB\n"; throw_oracle_soci_error(res, session_.errhp_); } - std::cout << "Is temporary when resetting: " << std::boolalpha << (is_temporary == TRUE) << std::endl; - std::cout << lobp_ << std::endl; - if (is_temporary) { res = OCILobFreeTemporary(session_.svchp_, session_.errhp_, lobp_); } else { @@ -224,20 +208,16 @@ void oracle_blob_backend::reset() if (res != OCI_SUCCESS) { - std::cout << "Can't free/close LOB (is temporary: " << is_temporary << ") res: " << res << "\n"; throw_oracle_soci_error(res, session_.errhp_); } initialized_ = false; - - std::cout << "Reset complete\n"; } void oracle_blob_backend::ensure_initialized() { if (!initialized_) { - std::cout << "Initializing blob\n"; // If asked to initialize explicitly, we can only create a temporary LOB sword res = OCILobCreateTemporary(session_.svchp_, session_.errhp_, lobp_, OCI_DEFAULT, SQLCS_IMPLICIT, OCI_TEMP_BLOB, FALSE, OCI_DURATION_SESSION); @@ -247,16 +227,6 @@ void oracle_blob_backend::ensure_initialized() throw_oracle_soci_error(res, session_.errhp_); } - boolean is_temporary = FALSE; - res = OCILobIsTemporary(session_.envhp_, session_.errhp_, lobp_, &is_temporary); - - if (res != OCI_SUCCESS) - { - throw_oracle_soci_error(res, session_.errhp_); - } - - std::cout << "Is temporary immediately after creation as temporary: " << std::boolalpha << (is_temporary == TRUE) << std::endl; - res = OCILobOpen(session_.svchp_, session_.errhp_, lobp_, OCI_LOB_READWRITE); if (res != OCI_SUCCESS) @@ -264,16 +234,6 @@ void oracle_blob_backend::ensure_initialized() throw_oracle_soci_error(res, session_.errhp_); } - res = OCILobIsTemporary(session_.envhp_, session_.errhp_, lobp_, &is_temporary); - - if (res != OCI_SUCCESS) - { - throw_oracle_soci_error(res, session_.errhp_); - } - - std::cout << "Is temporary immediately after opening: " << std::boolalpha << (is_temporary == TRUE) << std::endl; - std::cout << lobp_ << std::endl; - initialized_ = true; } } diff --git a/src/backends/oracle/standard-into-type.cpp b/src/backends/oracle/standard-into-type.cpp index adbf35673..5d969e0fc 100644 --- a/src/backends/oracle/standard-into-type.cpp +++ b/src/backends/oracle/standard-into-type.cpp @@ -157,6 +157,10 @@ void oracle_standard_into_type_backend::define_by_pos( oracle_blob_backend *bbe = static_cast(b->get_backend()); + // Reset the blob to ensure that a potentially open temporary BLOB gets + // freed before the locator is changed to point to a different BLOB by the + // to-be-executed statement (which would leave us with a dangling temporary BLOB) + bbe->reset(); ociData_ = bbe->get_lob_locator(); size = sizeof(ociData_); diff --git a/tests/common-tests.h b/tests/common-tests.h index 6193d0fc3..39f0e8511 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -6478,9 +6478,11 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") } SECTION("Inserting/Reading default-constructed blob") { - soci::blob input_blob(sql); + { + soci::blob input_blob(sql); - sql << "insert into soci_test (id, b) values(5, :b)", soci::use(input_blob); + sql << "insert into soci_test (id, b) values(5, :b)", soci::use(input_blob); + } soci::blob output_blob(sql); soci::indicator ind; @@ -6492,15 +6494,16 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") } SECTION("Ensure reading into blob overwrites previous contents") { - std::cout << "Relevant part begin\n"; soci::blob blob(sql); blob.write_from_start("hello kitty", 10); CHECK(blob.get_len() == 10); - soci::blob write_blob(sql); - write_blob.write_from_start("test", 4); - sql << "insert into soci_test (id, b) values (5, :b)", soci::use(write_blob); + { + soci::blob write_blob(sql); + write_blob.write_from_start("test", 4); + sql << "insert into soci_test (id, b) values (5, :b)", soci::use(write_blob); + } std::cout << "Finished writing into DB, now reading back from it" << std::endl; @@ -6519,7 +6522,6 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") } SECTION("Blob-DB interaction") { - std::cout << "Next part begin\n"; soci::blob write_blob(sql); static_assert(sizeof(dummy_data) >= 10, "Underlying assumption violated"); @@ -6603,7 +6605,8 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") CHECK(buf[i] == binary_data[i]); } } - SECTION("Rowset type detection") { + SECTION("Rowset") + { soci::blob blob(sql); static_assert(sizeof(dummy_data) >= 10, "Underlying assumption violated"); blob.write_from_start(dummy_data, 10); @@ -6619,6 +6622,14 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") const soci::row ¤tRow = *it; CHECK(currentRow.get_properties(0).get_data_type() == soci::dt_integer); CHECK(currentRow.get_properties(1).get_data_type() == soci::dt_blob); + //soci::blob retrieved = currentRow.get(1); + //CHECK(retrieved.get_len() == 10); + //std::uint8_t buffer[20]; + //std::size_t bytes_read = blob.read_from_start(reinterpret_cast(buffer), sizeof(buffer)); + //CHECK(bytes_read == 10); + //for (std::size_t i = 0; i < 10; ++i) { + // CHECK(buffer[i] == dummy_data[i]); + //} } CHECK(containedData); } From 47d9068366ccada3106a7c53c64cdfb64b8783f2 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Wed, 1 Nov 2023 19:49:52 +0100 Subject: [PATCH 43/78] Special case data type for Oracle backend --- tests/common-tests.h | 19 ++++++++++++++++++- tests/db2/test-db2.cpp | 2 ++ tests/firebird/test-firebird.cpp | 2 ++ tests/mysql/test-mysql.h | 2 ++ tests/odbc/test-odbc-access.cpp | 2 ++ tests/odbc/test-odbc-db2.cpp | 2 ++ tests/odbc/test-odbc-mssql.cpp | 2 ++ tests/odbc/test-odbc-postgresql.cpp | 2 ++ tests/oracle/test-oracle.cpp | 2 ++ tests/postgresql/test-postgresql.cpp | 2 ++ tests/sqlite3/test-sqlite3.cpp | 2 ++ 11 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/common-tests.h b/tests/common-tests.h index 39f0e8511..2d5f7e651 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -363,6 +363,17 @@ class function_creator_base SOCI_NOT_COPYABLE(function_creator_base) }; +enum class Backend { + Empty, + SQLite, + MySQL, + PostgreSQL, + ODBC, + Oracle, + Firebird, + DB2, +}; + // This is a singleton class, at any given time there is at most one test // context alive and common_tests fixture class uses it. class test_context_base @@ -404,6 +415,8 @@ class test_context_base return connectString_; } + virtual Backend get_backend() const = 0; + virtual std::string to_date_time(std::string const &dateTime) const = 0; virtual table_creator_base* table_creator_1(session&) const = 0; @@ -6620,7 +6633,11 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") for (auto it = rowSet.begin(); it != rowSet.end(); ++it) { containedData = true; const soci::row ¤tRow = *it; - CHECK(currentRow.get_properties(0).get_data_type() == soci::dt_integer); + + soci::data_type type = currentRow.get_properties(0).get_data_type(); + soci::data_type expectedType = tc_.get_backend() != Backend::Oracle ? soci::dt_integer : soci::dt_long_long; + CHECK(type == expectedType); + CHECK(currentRow.get_properties(1).get_data_type() == soci::dt_blob); //soci::blob retrieved = currentRow.get(1); //CHECK(retrieved.get_len() == 10); diff --git a/tests/db2/test-db2.cpp b/tests/db2/test-db2.cpp index 5501f1da1..4feefd3ab 100644 --- a/tests/db2/test-db2.cpp +++ b/tests/db2/test-db2.cpp @@ -68,6 +68,8 @@ class test_context :public test_context_base test_context(backend_factory const & pi_back_end, std::string const & pi_connect_string) : test_context_base(pi_back_end, pi_connect_string) {} + Backend get_backend() const override { return Backend::DB2; } + table_creator_base* table_creator_1(soci::session & pr_s) const override { pr_s << "SET CURRENT SCHEMA = 'DB2INST1'"; diff --git a/tests/firebird/test-firebird.cpp b/tests/firebird/test-firebird.cpp index cfe688957..4a8ac8552 100644 --- a/tests/firebird/test-firebird.cpp +++ b/tests/firebird/test-firebird.cpp @@ -1191,6 +1191,8 @@ class test_context : public tests::test_context_base : test_context_base(backEnd, connectString) {} + tests::Backend get_backend() const override { return tests::Backend::Firebird; } + tests::table_creator_base* table_creator_1(soci::session& s) const override { return new TableCreator1(s); diff --git a/tests/mysql/test-mysql.h b/tests/mysql/test-mysql.h index 40e17298d..b47d488f9 100644 --- a/tests/mysql/test-mysql.h +++ b/tests/mysql/test-mysql.h @@ -70,6 +70,8 @@ class test_context : public test_context_base std::string const &connectString) : test_context_base(backEnd, connectString) {} + Backend get_backend() const override { return Backend::MySQL; } + table_creator_base* table_creator_1(soci::session& s) const override { return new table_creator_one(s); diff --git a/tests/odbc/test-odbc-access.cpp b/tests/odbc/test-odbc-access.cpp index 2783646f4..af266900c 100644 --- a/tests/odbc/test-odbc-access.cpp +++ b/tests/odbc/test-odbc-access.cpp @@ -73,6 +73,8 @@ class test_context : public test_context_base test_context(backend_factory const &backend, std::string const &connstr) : test_context_base(backend, connstr) {} + Backend get_backend() const override { return Backend::ODBC; } + table_creator_base * table_creator_1(soci::session& s) const { return new table_creator_one(s); diff --git a/tests/odbc/test-odbc-db2.cpp b/tests/odbc/test-odbc-db2.cpp index 91788949e..85b5b6cf0 100644 --- a/tests/odbc/test-odbc-db2.cpp +++ b/tests/odbc/test-odbc-db2.cpp @@ -69,6 +69,8 @@ class test_context : public test_context_base std::string const &connectString) : test_context_base(backEnd, connectString) {} + Backend get_backend() const override { return Backend::ODBC; } + table_creator_base * table_creator_1(soci::session& s) const { return new table_creator_one(s); diff --git a/tests/odbc/test-odbc-mssql.cpp b/tests/odbc/test-odbc-mssql.cpp index 8b3ecd740..fd20a6067 100644 --- a/tests/odbc/test-odbc-mssql.cpp +++ b/tests/odbc/test-odbc-mssql.cpp @@ -156,6 +156,8 @@ class test_context : public test_context_base std::string const &connstr) : test_context_base(backend, connstr) {} + Backend get_backend() const override { return Backend::ODBC; } + table_creator_base* table_creator_1(soci::session& s) const override { return new table_creator_one(s); diff --git a/tests/odbc/test-odbc-postgresql.cpp b/tests/odbc/test-odbc-postgresql.cpp index 4dbfa3c46..fb2f66f34 100644 --- a/tests/odbc/test-odbc-postgresql.cpp +++ b/tests/odbc/test-odbc-postgresql.cpp @@ -161,6 +161,8 @@ class test_context : public test_context_base std::cout << "Using ODBC driver version " << m_verDriver << "\n"; } + Backend get_backend() const override { return Backend::ODBC; } + table_creator_base * table_creator_1(soci::session& s) const override { return new table_creator_one(s); diff --git a/tests/oracle/test-oracle.cpp b/tests/oracle/test-oracle.cpp index d3531fd13..8fcffbe82 100644 --- a/tests/oracle/test-oracle.cpp +++ b/tests/oracle/test-oracle.cpp @@ -1407,6 +1407,8 @@ class test_context :public test_context_base std::string const &connectString) : test_context_base(backEnd, connectString) {} + Backend get_backend() const override { return Backend::Oracle; } + table_creator_base* table_creator_1(soci::session& s) const override { return new table_creator_one(s); diff --git a/tests/postgresql/test-postgresql.cpp b/tests/postgresql/test-postgresql.cpp index 4bd85b611..f5cb94633 100644 --- a/tests/postgresql/test-postgresql.cpp +++ b/tests/postgresql/test-postgresql.cpp @@ -1346,6 +1346,8 @@ class test_context : public test_context_base : test_context_base(backend, connstr) {} + Backend get_backend() const override { return Backend::PostgreSQL; } + table_creator_base* table_creator_1(soci::session& s) const override { return new table_creator_one(s); diff --git a/tests/sqlite3/test-sqlite3.cpp b/tests/sqlite3/test-sqlite3.cpp index 0aa91f76d..b015267ca 100644 --- a/tests/sqlite3/test-sqlite3.cpp +++ b/tests/sqlite3/test-sqlite3.cpp @@ -794,6 +794,8 @@ class test_context : public test_context_base std::string const &connstr) : test_context_base(backend, connstr) {} + Backend get_backend() const override { return Backend::SQLite; } + table_creator_base* table_creator_1(soci::session& s) const override { return new table_creator_one(s); From 9040094e9de49f698ba998060db12bd75f5e42ab Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Wed, 1 Nov 2023 20:08:08 +0100 Subject: [PATCH 44/78] Remove superfluous test block --- tests/common-tests.h | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/common-tests.h b/tests/common-tests.h index 2d5f7e651..1d52485c8 100644 --- a/tests/common-tests.h +++ b/tests/common-tests.h @@ -6621,8 +6621,6 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") SECTION("Rowset") { soci::blob blob(sql); - static_assert(sizeof(dummy_data) >= 10, "Underlying assumption violated"); - blob.write_from_start(dummy_data, 10); // Write and retrieve blob from/into database int id = 1; @@ -6639,14 +6637,6 @@ TEST_CASE_METHOD(common_tests, "BLOB", "[core][blob]") CHECK(type == expectedType); CHECK(currentRow.get_properties(1).get_data_type() == soci::dt_blob); - //soci::blob retrieved = currentRow.get(1); - //CHECK(retrieved.get_len() == 10); - //std::uint8_t buffer[20]; - //std::size_t bytes_read = blob.read_from_start(reinterpret_cast(buffer), sizeof(buffer)); - //CHECK(bytes_read == 10); - //for (std::size_t i = 0; i < 10; ++i) { - // CHECK(buffer[i] == dummy_data[i]); - //} } CHECK(containedData); } From b198321151dbc578b5e110fe9c39e71c9a326ed4 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Fri, 3 Nov 2023 19:29:31 +0100 Subject: [PATCH 45/78] Add private default ctor to blob class --- include/soci/blob.h | 3 +++ src/core/blob.cpp | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/include/soci/blob.h b/include/soci/blob.h index 868ea8163..e6672a651 100644 --- a/include/soci/blob.h +++ b/include/soci/blob.h @@ -26,6 +26,9 @@ class blob_backend; class SOCI_DECL blob { +private: + blob() = default; + public: explicit blob(session & s); ~blob(); diff --git a/src/core/blob.cpp b/src/core/blob.cpp index f42930ed7..b5ba6197f 100644 --- a/src/core/blob.cpp +++ b/src/core/blob.cpp @@ -11,6 +11,7 @@ #include "soci/soci-platform.h" #include +#include using namespace soci; @@ -23,11 +24,13 @@ blob::~blob() = default; std::size_t blob::get_len() { + assert(backEnd_); return backEnd_->get_len(); } std::size_t blob::read(std::size_t offset, char *buf, std::size_t toRead) { + assert(backEnd_); SOCI_ALLOW_DEPRECATED_BEGIN return backEnd_->read(offset, buf, toRead); SOCI_ALLOW_DEPRECATED_END @@ -36,12 +39,14 @@ std::size_t blob::read(std::size_t offset, char *buf, std::size_t toRead) std::size_t blob::read_from_start(char * buf, std::size_t toRead, std::size_t offset) { + assert(backEnd_); return backEnd_->read_from_start(buf, toRead, offset); } std::size_t blob::write( std::size_t offset, char const * buf, std::size_t toWrite) { + assert(backEnd_); SOCI_ALLOW_DEPRECATED_BEGIN return backEnd_->write(offset, buf, toWrite); SOCI_ALLOW_DEPRECATED_END @@ -50,15 +55,18 @@ std::size_t blob::write( std::size_t blob::write_from_start(const char * buf, std::size_t toWrite, std::size_t offset) { + assert(backEnd_); return backEnd_->write_from_start(buf, toWrite, offset); } std::size_t blob::append(char const * buf, std::size_t toWrite) { + assert(backEnd_); return backEnd_->append(buf, toWrite); } void blob::trim(std::size_t newLen) { + assert(backEnd_); backEnd_->trim(newLen); } From 5f5f5f28b044634cfaf5e5e7cbfce7991572334c Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 9 Nov 2023 19:29:52 +0100 Subject: [PATCH 46/78] Add creator surrogate for default-constructed BLOBs --- include/soci/blob.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/include/soci/blob.h b/include/soci/blob.h index e6672a651..ec7caa455 100644 --- a/include/soci/blob.h +++ b/include/soci/blob.h @@ -22,11 +22,13 @@ class session; namespace details { class blob_backend; +struct blob_placeholder; } // namespace details class SOCI_DECL blob { private: + friend struct details::blob_placeholder; blob() = default; public: @@ -67,6 +69,17 @@ class SOCI_DECL blob std::unique_ptr backEnd_; }; +namespace details +{ + struct blob_placeholder + { + static blob create() + { + return blob(); + } + }; +} + } // namespace soci #endif From dc4c58413489c73c3b4b6313bdcd746325bf2af2 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Thu, 9 Nov 2023 19:32:05 +0100 Subject: [PATCH 47/78] Introduce row::move_as as an alternative to row::get() The new move_as behaves just like the get() function, except that it'll move the value from the row object. This can be leveraged to deal with non-copyable types and/or as an optimization to avoid copying large objects around. --- include/soci/row.h | 47 ++++++++++++++ include/soci/type-conversion-traits.h | 89 +++++++++++++++++++++++++-- include/soci/type-holder.h | 7 ++- include/soci/type-traits.h | 46 ++++++++++++++ 4 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 include/soci/type-traits.h diff --git a/include/soci/row.h b/include/soci/row.h index 3279dae25..c58342d15 100644 --- a/include/soci/row.h +++ b/include/soci/row.h @@ -9,6 +9,7 @@ #define SOCI_ROW_H_INCLUDED #include "soci/type-holder.h" +#include "soci/type-conversion-traits.h" #include "soci/soci-backend.h" #include "soci/type-conversion.h" // std @@ -74,6 +75,8 @@ class SOCI_DECL row T get(std::size_t pos) const { typedef typename type_conversion::base_type base_type; + static_assert(details::can_use_from_base>(), + "Can't use row::get() with this type (not convertible/copy-assignable from base_type) - did you mean to use move_as?"); base_type const& baseVal = holders_.at(pos)->get(); T ret; @@ -81,6 +84,19 @@ class SOCI_DECL row return ret; } + template + T move_as(std::size_t pos) const + { + typedef typename type_conversion::base_type base_type; + static_assert(details::can_use_move_from_base(), + "row::move_as() can only be called with types that can be instantiated from a base type rvalue reference"); + base_type & baseVal = holders_.at(pos)->get(); + + T ret; + type_conversion::move_from_base(baseVal, *indicators_.at(pos), ret); + return ret; + } + template T get(std::size_t pos, T const &nullValue) const { @@ -92,6 +108,17 @@ class SOCI_DECL row return get(pos); } + template + T move_as(std::size_t pos, T const &nullValue) const + { + if (i_null == *indicators_.at(pos)) + { + return nullValue; + } + + return move_as(pos); + } + template T get(std::string const &name) const { @@ -99,6 +126,13 @@ class SOCI_DECL row return get(pos); } + template + T move_as(std::string const &name) const + { + std::size_t const pos = find_column(name); + return move_as(pos); + } + template T get(std::string const &name, T const &nullValue) const { @@ -112,6 +146,19 @@ class SOCI_DECL row return get(pos); } + template + T move_as(std::string const &name, T const &nullValue) const + { + std::size_t const pos = find_column(name); + + if (i_null == *indicators_[pos]) + { + return nullValue; + } + + return move_as(pos); + } + template row const& operator>>(T& value) const { diff --git a/include/soci/type-conversion-traits.h b/include/soci/type-conversion-traits.h index e334071ea..d7e91a5ec 100644 --- a/include/soci/type-conversion-traits.h +++ b/include/soci/type-conversion-traits.h @@ -9,33 +9,110 @@ #define SOCI_TYPE_CONVERSION_TRAITS_H_INCLUDED #include "soci/soci-backend.h" +#include "soci/type-traits.h" + +#include namespace soci { -// default traits class type_conversion, acts as pass through for row::get() +// default traits class type_conversion, acts as pass through for row::get() or row::move_as() // when no actual conversion is needed. template struct type_conversion { typedef T base_type; - static void from_base(base_type const & in, indicator ind, T & out) + struct from_base_check : std::integral_constant::value> {}; + + static void from_base(base_type const& in, indicator ind, T & out) { - if (ind == i_null) - { - throw soci_error("Null value not allowed for this type"); - } + static_assert(from_base_check::value, + "move_to_base can only be used if the target type can be constructed from an lvalue base reference"); + assert_non_null(ind); out = in; } + struct move_from_base_check : + std::integral_constant::value + && std::is_constructible::type>::value + > {}; + + + static void move_from_base(base_type & in, indicator ind, T & out) + { + static_assert(move_from_base_check::value, + "move_to_base can only be used if the target type can be constructed from an rvalue base reference"); + assert_non_null(ind); + out = std::move(in); + } + static void to_base(T const & in, base_type & out, indicator & ind) { out = in; ind = i_ok; } + + static void move_to_base(T & in, base_type & out, indicator & ind) + { + out = std::move(in); + ind = i_ok; + } + +private: + static void assert_non_null(indicator ind) + { + if (ind == i_null) + { + throw soci_error("Null value not allowed for this type"); + } + } }; +namespace details +{ + +template +using from_base_check_t = decltype(T::from_base_check::value); +template +using move_from_base_check_t = decltype(T::move_from_base_check::value); + +template +using supports_from_base_check = is_detected; +template +using supports_move_from_base_check = is_detected; + +template +constexpr auto can_use_from_base() + -> typename std::enable_if::value, bool>::type +{ + return Trait::from_base_check::value; +} + +template +constexpr auto can_use_from_base() + -> typename std::enable_if::value, bool>::type +{ + return true; +} + +template +constexpr auto can_use_move_from_base() + -> typename std::enable_if::value, bool>::type +{ + return Trait::from_base_check::value; +} + +template +constexpr auto can_use_move_from_base() + -> typename std::enable_if::value, bool>::type +{ + return true; +} + +} + } // namespace soci #endif // SOCI_TYPE_CONVERSION_TRAITS_H_INCLUDED diff --git a/include/soci/type-holder.h b/include/soci/type-holder.h index f103edb58..a9e189d23 100644 --- a/include/soci/type-holder.h +++ b/include/soci/type-holder.h @@ -56,7 +56,7 @@ class holder virtual ~holder() {} template - T get() + T &get() { type_holder* p = checked_ptr_cast >(this); if (p) @@ -83,7 +83,10 @@ class type_holder : public holder ~type_holder() override { delete t_; } template - TypeValue value() const { return *t_; } + const TypeValue &value() const { return *t_; } + + template + TypeValue &value() { return *t_; } private: T * t_; diff --git a/include/soci/type-traits.h b/include/soci/type-traits.h new file mode 100644 index 000000000..c46e37722 --- /dev/null +++ b/include/soci/type-traits.h @@ -0,0 +1,46 @@ +// +// Copyright (C) 2023 Robert Adam +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef SOCI_PRIVATE_SOCI_TYPE_TRAITS_H_INCLUDED +#define SOCI_PRIVATE_SOCI_TYPE_TRAITS_H_INCLUDED + +#include + +namespace soci +{ + +namespace details +{ + +template +using void_t = void; + +using false_type = std::integral_constant; +using true_type = std::integral_constant; + +// Implementation from https://blog.tartanllama.xyz/detection-idiom/ + +namespace detector_detail +{ + + template