diff --git a/include/sqlpp11/postgresql/bind_result.h b/include/sqlpp11/postgresql/bind_result.h index eaa52b61..db8d3b0d 100644 --- a/include/sqlpp11/postgresql/bind_result.h +++ b/include/sqlpp11/postgresql/bind_result.h @@ -103,6 +103,7 @@ namespace sqlpp void _bind_text_result(size_t index, const char** value, size_t* len); void _bind_date_result(size_t index, ::sqlpp::chrono::day_point* value, bool* is_null); void _bind_date_time_result(size_t index, ::sqlpp::chrono::microsecond_point* value, bool* is_null); + void _bind_blob_result(size_t index, const uint8_t** value, size_t* len); int size() const; }; @@ -387,6 +388,27 @@ namespace sqlpp } } + inline void bind_result_t::_bind_blob_result(size_t _index, const uint8_t** value, size_t* len) + { + + auto index = static_cast(_index); + if (_handle->debug()) + { + std::cerr << "PostgreSQL debug: binding blob result at index: " << index << std::endl; + } + + if (_handle->result.isNull(_handle->count, index)) + { + *value = nullptr; + *len = 0; + } + else + { + *value = _handle->result.getValue(_handle->count, index); + *len = _handle->result.length(_handle->count, index); + } + } + inline int bind_result_t::size() const { return _handle->result.records_size(); diff --git a/include/sqlpp11/postgresql/connection.h b/include/sqlpp11/postgresql/connection.h index 003c904b..760c6a88 100644 --- a/include/sqlpp11/postgresql/connection.h +++ b/include/sqlpp11/postgresql/connection.h @@ -34,6 +34,7 @@ #include #include +#include #include #include #include diff --git a/include/sqlpp11/postgresql/prepared_statement.h b/include/sqlpp11/postgresql/prepared_statement.h index 1681554d..4282487d 100644 --- a/include/sqlpp11/postgresql/prepared_statement.h +++ b/include/sqlpp11/postgresql/prepared_statement.h @@ -1,5 +1,6 @@ /** * Copyright © 2014-2015, Matthijs Möhlmann + * Copyright © 2021-2021, Roland Bock * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -78,6 +79,7 @@ namespace sqlpp void _bind_text_parameter(size_t index, const std::string* value, bool is_null); void _bind_date_parameter(size_t index, const ::sqlpp::chrono::day_point* value, bool is_null); void _bind_date_time_parameter(size_t index, const ::sqlpp::chrono::microsecond_point* value, bool is_null); + void _bind_blob_parameter(size_t index, const std::vector* value, bool is_null); }; // ctor @@ -210,6 +212,34 @@ namespace sqlpp } } } + + inline void prepared_statement_t::_bind_blob_parameter(size_t index, const std::vector* value, bool is_null) + { + if (_handle->debug()) + { + std::cerr << "PostgreSQL debug: binding blob parameter at index " + << index << ", being " << (is_null ? "" : "not ") << "null" << std::endl; + } + _handle->nullValues[index] = is_null; + if (not is_null) + { + constexpr char hexChars[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + auto param = std::string(value->size() * 2 + 2, '\0'); + param[0] = '\\'; + param[1] = 'x'; + auto i = size_t{1}; + for (const auto c : *value) + { + param[++i] = hexChars[c >> 4]; + param[++i] = hexChars[c & 0x0F]; + } + _handle->paramValues[index] = std::move(param); + if (_handle->debug()) + { + std::cerr << "PostgreSQL debug: binding blob parameter string (up to 100 chars): " << _handle->paramValues[index].substr(0, 100) << std::endl; + } + } + } } } diff --git a/include/sqlpp11/postgresql/result.h b/include/sqlpp11/postgresql/result.h index be86bcba..78e8fbe4 100644 --- a/include/sqlpp11/postgresql/result.h +++ b/include/sqlpp11/postgresql/result.h @@ -132,6 +132,13 @@ namespace sqlpp return const_cast(val); } + + template <> + inline const uint8_t* Result::getValue(int record, int field) const + { + return reinterpret_cast(getValue(record, field)); + } + inline Result::Result() : m_result(nullptr) { } diff --git a/include/sqlpp11/postgresql/result_field.h b/include/sqlpp11/postgresql/result_field.h new file mode 100644 index 00000000..cd95f2b7 --- /dev/null +++ b/include/sqlpp11/postgresql/result_field.h @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2021-2021, Roland Bock + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef SQLPP_POSTGRESQL_BLOB_RESULT_FIELD_H +#define SQLPP_POSTGRESQL_BLOB_RESULT_FIELD_H + +#include +#include +#include +#include +#include +#include +#include + +namespace sqlpp +{ + namespace postgresql + { + struct connection; + } + + namespace detail + { + inline unsigned char unhex(unsigned char c) + { + switch (c) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return c - '0'; + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + return c + 10 - 'a'; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + return c + 10 - 'A'; + } + throw sqlpp::exception(std::string("Unexpected hex char: ") += c); + } + + inline void hex_assign(std::vector& value, const uint8_t* blob, size_t len) + { + value.resize(len / 2 - 1); // unhex - leading chars + size_t val_index = 0; + size_t blob_index = 2; + while (blob_index < len) + { + value[val_index] = (unhex(blob[blob_index]) << 4) + unhex(blob[blob_index + 1]); + ++val_index; + blob_index += 2; + } + } + } // namespace detail + + template + struct result_field_t> + : public result_field_base> + { + private: + const uint8_t* _blob{nullptr}; // Non-owning + + public: + size_t len{}; + + template + void _bind(Target& target, size_t index) + { + target._bind_blob_result(index, &_blob, &len); + if (_blob) + { + detail::hex_assign(this->_value, _blob, len); + len = this->_value.size(); + } + else + this->_value.clear(); + this->_is_null = (_blob == nullptr); + } + + }; +} // namespace sqlpp +#endif diff --git a/include/sqlpp11/postgresql/serializer.h b/include/sqlpp11/postgresql/serializer.h index ea8eb7b7..50c37cfd 100644 --- a/include/sqlpp11/postgresql/serializer.h +++ b/include/sqlpp11/postgresql/serializer.h @@ -41,6 +41,19 @@ namespace sqlpp context.pop_count(); return context; } + + inline postgresql::context_t& serialize(const blob_operand& t, postgresql::context_t& context) + { + constexpr char hexChars[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + context << "'\\x"; + for (const auto c : t._t) + { + context << hexChars[c >> 4] << hexChars[c & 0x0F]; + } + context << '\''; + + return context; + } } #endif diff --git a/scripts/ddl2cpp b/scripts/ddl2cpp index b6cd0b2f..b8a4eb4e 100755 --- a/scripts/ddl2cpp +++ b/scripts/ddl2cpp @@ -259,6 +259,7 @@ types = { 'character varying': 'varchar', #PostgreSQL 'text': 'text', 'clob': 'text', + 'bytea': 'blob', 'tinyblob': 'blob', 'blob': 'blob', 'mediumblob': 'blob', diff --git a/tests/postgresql/usage/Blob.cpp b/tests/postgresql/usage/Blob.cpp new file mode 100644 index 00000000..0d3fece7 --- /dev/null +++ b/tests/postgresql/usage/Blob.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019-2019, Jaroslav Bisikirski + * Copyright (c) 2021-2021, Roland Bock + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include +#include +#include + +#include "BlobSample.h" +#include "make_test_connection.h" + +namespace sql = sqlpp::postgresql; +const auto blob = model::BlobSample{}; + +// This would be a great fuzzing target :-) +constexpr size_t blob_size = 1000 * 1000ul; +constexpr size_t blob_small_size = 999; + +void verify_blob(sql::connection& db, const std::vector& data, uint64_t id) +{ + auto result = db(select(blob.data).from(blob).where(blob.id == id)); + const auto& result_row = result.front(); + std::cerr << "id: " << id << std::endl; + std::cerr << "Insert size: " << data.size() << std::endl; + std::cerr << "Select size: " << result_row.data.len << std::endl; + if (data.size() != result_row.data.len) + { + std::cerr << "Size mismatch" << std::endl; + + throw std::runtime_error("Size mismatch " + std::to_string(data.size()) + + " != " + std::to_string(result_row.data.len)); + } + std::cerr << "Verifying content" << std::endl; + const auto result_blob = result_row.data.value(); + if (data != result_blob) + { + std::cout << "Content mismatch ([row] original -> received)" << std::endl; + + for (size_t i = 0; i < data.size(); i++) + { + if (data[i] != result_row.data.value()[i]) + { + std::cerr << "[" << i << "] " << static_cast(data.at(i)) << " -> " << static_cast(result_blob.at(i)) + << std::endl; + } + } + throw std::runtime_error("Content mismatch"); + } +} + +int Blob(int, char*[]) +{ + sql::connection db = sql::make_test_connection(); + + db.execute(R"(DROP TABLE IF EXISTS blob_sample;)"); + db.execute(R"(CREATE TABLE blob_sample ( + id bigserial PRIMARY KEY, + data bytea + ))"); + std::cerr << "Generating data " << blob_size << std::endl; + std::vector data(blob_size); + std::uniform_int_distribution distribution(0, 255); + std::mt19937 engine; + auto generator = std::bind(distribution, engine); + std::generate_n(data.begin(), blob_size, generator); + + std::vector data_smaller(blob_small_size); + std::generate_n(data_smaller.begin(), blob_small_size, generator); + + db(insert_into(blob).set(blob.data = data_smaller)); + const auto id = db.last_insert_id("blob_sample", "id"); + verify_blob(db, data_smaller, id); + + auto prepared_insert = db.prepare(insert_into(blob).set(blob.data = parameter(blob.data))); + prepared_insert.params.data = data; + db(prepared_insert); + const auto prep_id = db.last_insert_id("blob_sample", "id"); + prepared_insert.params.data.set_null(); + db(prepared_insert); + const auto null_id = db.last_insert_id("blob_sample", "id"); + + verify_blob(db, data, prep_id); + { + auto result = db(select(blob.data).from(blob).where(blob.id == null_id)); + const auto& result_row = result.front(); + std::cerr << "Null blob is_null:\t" << std::boolalpha << result_row.data.is_null() << std::endl; + std::cerr << "Null blob len == 0:\t" << std::boolalpha << (result_row.data.len == 0) << std::endl; + std::cerr << "Null blob blob == nullptr:\t" << std::boolalpha << (result_row.data.is_null()) << std::endl; + if (!result_row.data.is_null() || result_row.data.len != 0) + { + throw std::runtime_error("Null blob has incorrect values"); + } + } + + return 0; +} diff --git a/tests/postgresql/usage/BlobSample.h b/tests/postgresql/usage/BlobSample.h new file mode 100644 index 00000000..b71f14c0 --- /dev/null +++ b/tests/postgresql/usage/BlobSample.h @@ -0,0 +1,65 @@ +// generated by scripts/ddl2cpp tests/postgresql/usage/BlobSample.sql tests/postgresql/usage/BlobSample model +#ifndef MODEL_BLOBSAMPLE_H +#define MODEL_BLOBSAMPLE_H + +#include +#include +#include + +namespace model +{ + namespace BlobSample_ + { + struct Id + { + struct _alias_t + { + static constexpr const char _literal[] = "id"; + using _name_t = sqlpp::make_char_sequence; + template + struct _member_t + { + T id; + T& operator()() { return id; } + const T& operator()() const { return id; } + }; + }; + using _traits = sqlpp::make_traits; + }; + struct Data + { + struct _alias_t + { + static constexpr const char _literal[] = "data"; + using _name_t = sqlpp::make_char_sequence; + template + struct _member_t + { + T data; + T& operator()() { return data; } + const T& operator()() const { return data; } + }; + }; + using _traits = sqlpp::make_traits; + }; + } // namespace BlobSample_ + + struct BlobSample: sqlpp::table_t + { + struct _alias_t + { + static constexpr const char _literal[] = "blob_sample"; + using _name_t = sqlpp::make_char_sequence; + template + struct _member_t + { + T blobSample; + T& operator()() { return blobSample; } + const T& operator()() const { return blobSample; } + }; + }; + }; +} // namespace model +#endif diff --git a/tests/postgresql/usage/BlobSample.sql b/tests/postgresql/usage/BlobSample.sql new file mode 100644 index 00000000..b6a74a93 --- /dev/null +++ b/tests/postgresql/usage/BlobSample.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS blob_sample; +CREATE TABLE blob_sample ( + id bigserial PRIMARY KEY, + data bytea +); diff --git a/tests/postgresql/usage/CMakeLists.txt b/tests/postgresql/usage/CMakeLists.txt index 6abf1009..58b41456 100644 --- a/tests/postgresql/usage/CMakeLists.txt +++ b/tests/postgresql/usage/CMakeLists.txt @@ -28,6 +28,7 @@ target_include_directories(sqlpp11_postgresql_testing INTERFACE ${CMAKE_CURRENT_ set(test_names Basic + Blob Constructor Date DateTime