First Commit
Some checks failed
/ build_macos (push) Has been cancelled
/ build_windows (push) Has been cancelled
/ build_ubuntu (push) Has been cancelled

This commit is contained in:
2025-11-19 16:23:45 +07:00
commit dbdc5bcc4a
1791 changed files with 489451 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
if(NOT PostgreSQL_FOUND)
if(POLICY CMP0074)
cmake_policy(PUSH)
# CMP0074 is `OLD` by `cmake_minimum_required(VERSION 3.7)`, sets `NEW`
# to enable support CMake variable `PostgreSQL_ROOT`.
cmake_policy(SET CMP0074 NEW)
endif()
find_package(PostgreSQL REQUIRED)
if(POLICY CMP0074)
cmake_policy(POP)
endif()
endif()
file(GLOB UNIT_TEST_SOURCES *.cxx)
add_executable(unit_runner ${UNIT_TEST_SOURCES})
target_link_libraries(unit_runner PUBLIC pqxx)
target_include_directories(unit_runner PRIVATE ${PostgreSQL_INCLUDE_DIRS})
add_test(
NAME unit_runner
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
COMMAND unit_runner
)

View File

@@ -0,0 +1,548 @@
#include <pqxx/transaction>
#include "../test_helpers.hxx"
// Test program for libpqxx array parsing.
namespace pqxx
{
template<>
struct nullness<array_parser::juncture> : no_null<array_parser::juncture>
{};
inline std::string to_string(pqxx::array_parser::juncture const &j)
{
using junc = pqxx::array_parser::juncture;
switch (j)
{
case junc::row_start: return "row_start";
case junc::row_end: return "row_end";
case junc::null_value: return "null_value";
case junc::string_value: return "string_value";
case junc::done: return "done";
default: return "UNKNOWN JUNCTURE: " + to_string(static_cast<int>(j));
}
}
} // namespace pqxx
namespace
{
void test_empty_arrays()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
// Parsing a null pointer just immediately returns "done".
output = pqxx::array_parser(std::string_view()).get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"get_next on null array did not return done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
// Parsing an empty array string immediately returns "done".
output = pqxx::array_parser("").get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"get_next on an empty array string did not return done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
// Parsing an empty array returns "row_start", "row_end", "done".
pqxx::array_parser empty_parser("{}");
output = empty_parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Empty array did not start with row_start.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = empty_parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Empty array did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = empty_parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Empty array did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
void test_null_value()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser containing_null("{NULL}");
output = containing_null.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array containing null did not start with row_start.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = containing_null.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::null_value,
"Array containing null did not return null_value.");
PQXX_CHECK_EQUAL(output.second, "", "Null value was not empty.");
output = containing_null.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Array containing null did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = containing_null.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Array containing null did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
void test_single_quoted_string()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser parser("{'item'}");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "item", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Array did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Array did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
void test_single_quoted_escaping()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser parser("{'don''t\\\\ care'}");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "don't\\ care", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Array did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Array did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
void test_double_quoted_string()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser parser("{\"item\"}");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "item", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Array did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Array did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
void test_double_quoted_escaping()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser parser(R"--({"don''t\\ care"})--");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "don''t\\ care", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Array did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Array did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
// A pair of double quotes in a double-quoted string is an escaped quote.
void test_double_double_quoted_string()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser parser{R"--({"3"" steel"})--"};
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "3\" steel", "Unexpected string value.");
}
void test_unquoted_string()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser parser("{item}");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "item", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Array did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Array did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
void test_multiple_values()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser parser("{1,2}");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "1", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "2", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Array did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Array did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
void test_nested_array()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser parser("{{item}}");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Nested array did not start 2nd dimension with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "item", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Nested array did not end 2nd dimension with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Array did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Array did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
void test_nested_array_with_multiple_entries()
{
std::pair<pqxx::array_parser::juncture, std::string> output;
pqxx::array_parser parser("{{1,2},{3,4}}");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Nested array did not start 2nd dimension with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "1", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "2", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Nested array did not end 2nd dimension with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_start,
"Nested array did not descend to 2nd dimension with row_start.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "3", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::string_value,
"Array did not return string_value.");
PQXX_CHECK_EQUAL(output.second, "4", "Unexpected string value.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Nested array did not leave 2nd dimension with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::row_end,
"Array did not end with row_end.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
output = parser.get_next();
PQXX_CHECK_EQUAL(
output.first, pqxx::array_parser::juncture::done,
"Array did not conclude with done.");
PQXX_CHECK_EQUAL(output.second, "", "Unexpected nonempty output.");
}
void test_array_parse()
{
test_empty_arrays();
test_null_value();
test_single_quoted_string();
test_single_quoted_escaping();
test_double_quoted_string();
test_double_quoted_escaping();
test_double_double_quoted_string();
test_unquoted_string();
test_multiple_values();
test_nested_array();
test_nested_array_with_multiple_entries();
}
void test_generate_empty_array()
{
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<int>{}), "{}",
"Basic array output is not as expected.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<std::string>{}), "{}",
"String array comes out different.");
}
void test_generate_null_value()
{
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<char const *>{nullptr}), "{NULL}",
"Null array value did not come out as expected.");
}
void test_generate_single_item()
{
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<int>{42}), "{42}",
"Numeric conversion came out wrong.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<char const *>{"foo"}), "{\"foo\"}",
"String array conversion came out wrong.");
}
void test_generate_multiple_items()
{
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<int>{5, 4, 3, 2}), "{5,4,3,2}",
"Array with multiple values is not correct.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<std::string>{"foo", "bar"}),
"{\"foo\",\"bar\"}", "Array with multiple strings came out wrong.");
}
void test_generate_nested_array()
{
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<std::vector<int>>{{1, 2}, {3, 4}}),
"{{1,2},{3,4}}", "Nested arrays don't work right.");
}
void test_generate_escaped_strings()
{
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<std::string>{"a\\b"}), "{\"a\\\\b\"}",
"Backslashes are not escaped properly.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::vector<std::string>{"x\"y\""}), "{\"x\\\"y\\\"\"}",
"Double quotes are not escaped properly.");
}
void test_array_generate()
{
test_generate_empty_array();
test_generate_null_value();
test_generate_single_item();
test_generate_multiple_items();
test_generate_nested_array();
test_generate_escaped_strings();
}
void test_array_roundtrip()
{
pqxx::connection c;
pqxx::work w{c};
std::vector<int> const in{0, 1, 2, 3, 5};
auto const r1{w.exec1("SELECT " + c.quote(in) + "::integer[]")};
pqxx::array_parser parser{r1[0].view()};
auto item{parser.get_next()};
PQXX_CHECK_EQUAL(
item.first, pqxx::array_parser::juncture::row_start,
"Array did not start with row_start.");
std::vector<int> out;
for (item = parser.get_next();
item.first == pqxx::array_parser::juncture::string_value;
item = parser.get_next())
{
out.push_back(pqxx::from_string<int>(item.second));
}
PQXX_CHECK_EQUAL(
item.first, pqxx::array_parser::juncture::row_end,
"Array values did not end in row_end.");
PQXX_CHECK_EQUAL(
std::size(out), std::size(in), "Array came back with different length.");
for (std::size_t i{0}; i < std::size(in); ++i)
PQXX_CHECK_EQUAL(out[i], in[i], "Array element has changed.");
item = parser.get_next();
PQXX_CHECK_EQUAL(
item.first, pqxx::array_parser::juncture::done,
"Array did not end in done.");
}
PQXX_REGISTER_TEST(test_array_parse);
PQXX_REGISTER_TEST(test_array_generate);
PQXX_REGISTER_TEST(test_array_roundtrip);
} // namespace

View File

@@ -0,0 +1,211 @@
#include <pqxx/binarystring>
#include <pqxx/stream_to>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
#include "../test_types.hxx"
namespace
{
pqxx::binarystring
make_binarystring(pqxx::transaction_base &T, std::string content)
{
#include "pqxx/internal/ignore-deprecated-pre.hxx"
return pqxx::binarystring(T.exec1("SELECT " + T.quote_raw(content))[0]);
#include "pqxx/internal/ignore-deprecated-post.hxx"
}
void test_binarystring()
{
pqxx::connection conn;
pqxx::work tx{conn};
auto b{make_binarystring(tx, "")};
PQXX_CHECK(std::empty(b), "Empty binarystring is not empty.");
PQXX_CHECK_EQUAL(b.str(), "", "Empty binarystring doesn't work.");
PQXX_CHECK_EQUAL(std::size(b), 0u, "Empty binarystring has nonzero size.");
PQXX_CHECK_EQUAL(b.length(), 0u, "Length/size mismatch.");
PQXX_CHECK(std::begin(b) == std::end(b), "Empty binarystring iterates.");
PQXX_CHECK(
std::cbegin(b) == std::begin(b), "Wrong cbegin for empty binarystring.");
PQXX_CHECK(
std::rbegin(b) == std::rend(b), "Empty binarystring reverse-iterates.");
PQXX_CHECK(
std::crbegin(b) == std::rbegin(b),
"Wrong crbegin for empty binarystring.");
PQXX_CHECK_THROWS(
b.at(0), std::out_of_range, "Empty binarystring accepts at().");
b = make_binarystring(tx, "z");
PQXX_CHECK_EQUAL(b.str(), "z", "Basic nonempty binarystring is broken.");
PQXX_CHECK(not std::empty(b), "Nonempty binarystring is empty.");
PQXX_CHECK_EQUAL(std::size(b), 1u, "Bad binarystring size.");
PQXX_CHECK_EQUAL(b.length(), 1u, "Length/size mismatch.");
PQXX_CHECK(
std::begin(b) != std::end(b), "Nonempty binarystring does not iterate.");
PQXX_CHECK(
std::rbegin(b) != std::rend(b),
"Nonempty binarystring does not reverse-iterate.");
PQXX_CHECK(std::begin(b) + 1 == std::end(b), "Bad iteration.");
PQXX_CHECK(std::rbegin(b) + 1 == std::rend(b), "Bad reverse iteration.");
PQXX_CHECK(std::cbegin(b) == std::begin(b), "Wrong cbegin.");
PQXX_CHECK(std::cend(b) == std::end(b), "Wrong cend.");
PQXX_CHECK(std::crbegin(b) == std::rbegin(b), "Wrong crbegin.");
PQXX_CHECK(std::crend(b) == std::rend(b), "Wrong crend.");
PQXX_CHECK(b.front() == 'z', "Unexpected front().");
PQXX_CHECK(b.back() == 'z', "Unexpected back().");
PQXX_CHECK(b.at(0) == 'z', "Unexpected data at index 0.");
PQXX_CHECK_THROWS(
b.at(1), std::out_of_range, "Failed to catch range error.");
std::string const simple{"ab"};
b = make_binarystring(tx, simple);
PQXX_CHECK_EQUAL(
b.str(), simple, "Binary (un)escaping went wrong somewhere.");
PQXX_CHECK_EQUAL(
std::size(b), std::size(simple), "Escaping confuses length.");
std::string const simple_escaped{
tx.esc_raw(std::basic_string_view<std::byte>{
reinterpret_cast<std::byte const *>(std::data(simple)),
std::size(simple)})};
for (auto c : simple_escaped)
{
auto const uc{static_cast<unsigned char>(c)};
PQXX_CHECK(uc <= 127, "Non-ASCII byte in escaped string.");
}
#include "pqxx/internal/ignore-deprecated-pre.hxx"
PQXX_CHECK_EQUAL(
tx.quote_raw(
reinterpret_cast<unsigned char const *>(simple.c_str()),
std::size(simple)),
tx.quote(b), "quote_raw is broken");
PQXX_CHECK_EQUAL(
tx.quote(b), tx.quote_raw(simple), "Binary quoting is broken.");
PQXX_CHECK_EQUAL(
pqxx::binarystring(tx.exec1("SELECT " + tx.quote(b))[0]).str(), simple,
"Binary string is not idempotent.");
#include "pqxx/internal/ignore-deprecated-post.hxx"
std::string const bytes("\x01\x23\x23\xa1\x2b\x0c\xff");
b = make_binarystring(tx, bytes);
PQXX_CHECK_EQUAL(b.str(), bytes, "Binary data breaks (un)escaping.");
std::string const nully("a\0b", 3);
b = make_binarystring(tx, nully);
PQXX_CHECK_EQUAL(b.str(), nully, "Nul byte broke binary (un)escaping.");
PQXX_CHECK_EQUAL(std::size(b), 3u, "Nul byte broke binarystring size.");
b = make_binarystring(tx, "foo");
PQXX_CHECK_EQUAL(std::string(b.get(), 3), "foo", "get() appears broken.");
auto b1{make_binarystring(tx, "1")}, b2{make_binarystring(tx, "2")};
PQXX_CHECK_NOT_EQUAL(b1.get(), b2.get(), "Madness rules.");
PQXX_CHECK_NOT_EQUAL(b1.str(), b2.str(), "Logic has no more meaning.");
b1.swap(b2);
PQXX_CHECK_NOT_EQUAL(b1.str(), b2.str(), "swap() equalized binarystrings.");
PQXX_CHECK_NOT_EQUAL(b1.str(), "1", "swap() did not happen.");
PQXX_CHECK_EQUAL(b1.str(), "2", "swap() is broken.");
PQXX_CHECK_EQUAL(b2.str(), "1", "swap() went insane.");
b = make_binarystring(tx, "bar");
b.swap(b);
PQXX_CHECK_EQUAL(b.str(), "bar", "Self-swap confuses binarystring.");
b = make_binarystring(tx, "\\x");
PQXX_CHECK_EQUAL(b.str(), "\\x", "Hex-escape header confused (un)escaping.");
}
void test_binarystring_conversion()
{
constexpr char bytes[]{"f\to\0o\n\0"};
std::string_view const data{bytes, std::size(bytes) - 1};
#include "pqxx/internal/ignore-deprecated-pre.hxx"
pqxx::binarystring bin{data};
#include "pqxx/internal/ignore-deprecated-post.hxx"
auto const escaped{pqxx::to_string(bin)};
PQXX_CHECK_EQUAL(
escaped, std::string_view{"\\x66096f006f0a00"}, "Unexpected hex escape.");
auto const restored{pqxx::from_string<pqxx::binarystring>(escaped)};
PQXX_CHECK_EQUAL(
std::size(restored), std::size(data), "Unescaping produced wrong length.");
}
void test_binarystring_stream()
{
constexpr char bytes[]{"a\tb\0c"};
std::string_view const data{bytes, std::size(bytes) - 1};
#include "pqxx/internal/ignore-deprecated-pre.hxx"
pqxx::binarystring bin{data};
#include "pqxx/internal/ignore-deprecated-post.hxx"
pqxx::connection conn;
pqxx::transaction tx{conn};
tx.exec0("CREATE TEMP TABLE pqxxbinstream(id integer, bin bytea)");
auto to{pqxx::stream_to::table(tx, {"pqxxbinstream"})};
to.write_values(0, bin);
to.complete();
auto ptr{reinterpret_cast<std::byte const *>(std::data(data))};
auto expect{
tx.quote(std::basic_string_view<std::byte>{ptr, std::size(data)})};
PQXX_CHECK(
tx.query_value<bool>("SELECT bin = " + expect + " FROM pqxxbinstream"),
"binarystring did not stream_to properly.");
PQXX_CHECK_EQUAL(
tx.query_value<std::size_t>("SELECT octet_length(bin) FROM pqxxbinstream"),
std::size(data), "Did the terminating zero break the bytea?");
}
void test_binarystring_array_stream()
{
pqxx::connection conn;
pqxx::transaction tx{conn};
tx.exec0("CREATE TEMP TABLE pqxxbinstream(id integer, vec bytea[])");
constexpr char bytes1[]{"a\tb\0c"}, bytes2[]{"1\0.2"};
std::string_view const data1{bytes1}, data2{bytes2};
#include "pqxx/internal/ignore-deprecated-pre.hxx"
pqxx::binarystring bin1{data1}, bin2{data2};
std::vector<pqxx::binarystring> const vec{bin1, bin2};
#include "pqxx/internal/ignore-deprecated-post.hxx"
auto to{pqxx::stream_to::table(tx, {"pqxxbinstream"})};
to.write_values(0, vec);
to.complete();
PQXX_CHECK_EQUAL(
tx.query_value<std::size_t>(
"SELECT array_length(vec, 1) FROM pqxxbinstream"),
std::size(vec), "Array came out with wrong length.");
auto ptr1{reinterpret_cast<std::byte const *>(std::data(data1))},
ptr2{reinterpret_cast<std::byte const *>(std::data(data2))};
auto expect1{
tx.quote(std::basic_string_view<std::byte>{ptr1, std::size(data1)})},
expect2{
tx.quote(std::basic_string_view<std::byte>{ptr2, std::size(data2)})};
PQXX_CHECK(
tx.query_value<bool>("SELECT vec[1] = " + expect1 + " FROM pqxxbinstream"),
"Bytea in array came out wrong.");
PQXX_CHECK(
tx.query_value<bool>("SELECT vec[2] = " + expect2 + " FROM pqxxbinstream"),
"First bytea in array worked, but second did not.");
PQXX_CHECK_EQUAL(
tx.query_value<std::size_t>(
"SELECT octet_length(vec[1]) FROM pqxxbinstream"),
std::size(data1), "Bytea length broke inside array.");
}
PQXX_REGISTER_TEST(test_binarystring);
PQXX_REGISTER_TEST(test_binarystring_conversion);
PQXX_REGISTER_TEST(test_binarystring_stream);
PQXX_REGISTER_TEST(test_binarystring_array_stream);
} // namespace

View File

@@ -0,0 +1,644 @@
#include <cstdio>
#include <pqxx/blob>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
#include "../test_types.hxx"
namespace
{
void test_blob_is_useless_by_default()
{
pqxx::blob b{};
std::basic_string<std::byte> buf;
PQXX_CHECK_THROWS(
b.read(buf, 1), pqxx::usage_error,
"Read on default-constructed blob did not throw failure.");
PQXX_CHECK_THROWS(
b.write(buf), pqxx::usage_error,
"Write on default-constructed blob did not throw failure.");
}
void test_blob_create_makes_empty_blob()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::oid id{pqxx::blob::create(tx)};
auto b{pqxx::blob::open_r(tx, id)};
b.seek_end(0);
PQXX_CHECK_EQUAL(b.tell(), 0, "New blob is not empty.");
}
void test_blob_create_with_oid_requires_oid_be_free()
{
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::create(tx)};
PQXX_CHECK_THROWS(
pqxx::ignore_unused(pqxx::blob::create(tx, id)), pqxx::failure,
"Not getting expected error when oid not free.");
}
void test_blob_create_with_oid_obeys_oid()
{
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::create(tx)};
pqxx::blob::remove(tx, id);
auto actual_id{pqxx::blob::create(tx, id)};
PQXX_CHECK_EQUAL(actual_id, id, "Create with oid returned different oid.");
}
void test_blobs_are_transactional()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::oid id{pqxx::blob::create(tx)};
tx.abort();
pqxx::work tx2{conn};
PQXX_CHECK_THROWS(
pqxx::ignore_unused(pqxx::blob::open_r(tx2, id)), pqxx::failure,
"Blob from aborted transaction still exists.");
}
void test_blob_remove_removes_blob()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::oid id{pqxx::blob::create(tx)};
pqxx::blob::remove(tx, id);
PQXX_CHECK_THROWS(
pqxx::ignore_unused(pqxx::blob::open_r(tx, id)), pqxx::failure,
"Attempt to open blob after removing should have failed.");
}
void test_blob_remove_is_not_idempotent()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::oid id{pqxx::blob::create(tx)};
pqxx::blob::remove(tx, id);
PQXX_CHECK_THROWS(
pqxx::blob::remove(tx, id), pqxx::failure,
"Redundant remove() did not throw failure.");
}
void test_blob_checks_open_mode()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::oid id{pqxx::blob::create(tx)};
pqxx::blob b_r{pqxx::blob::open_r(tx, id)};
pqxx::blob b_w{pqxx::blob::open_w(tx, id)};
pqxx::blob b_rw{pqxx::blob::open_rw(tx, id)};
std::basic_string<std::byte> buf{std::byte{3}, std::byte{2}, std::byte{1}};
// These are all allowed:
b_w.write(buf);
b_r.read(buf, 3);
b_rw.seek_end(0);
b_rw.write(buf);
b_rw.seek_abs(0);
b_rw.read(buf, 6);
// These are not:
PQXX_CHECK_THROWS(
b_r.write(buf), pqxx::failure, "Read-only blob did not stop write.");
PQXX_CHECK_THROWS(
b_w.read(buf, 10), pqxx::failure, "Write-only blob did not stop read.");
}
void test_blob_supports_move()
{
std::basic_string<std::byte> buf;
buf.push_back(std::byte{'x'});
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::oid id{pqxx::blob::create(tx)};
pqxx::blob b1{pqxx::blob::open_rw(tx, id)};
b1.write(buf);
pqxx::blob b2{std::move(b1)};
b2.seek_abs(0);
b2.read(buf, 1u);
PQXX_CHECK_THROWS(
b1.read(buf, 1u), pqxx::usage_error,
"Blob still works after move construction.");
b1 = std::move(b2);
b1.read(buf, 1u);
PQXX_CHECK_THROWS(
b2.read(buf, 1u), pqxx::usage_error,
"Blob still works after move assignment.");
}
void test_blob_read_reads_data()
{
std::basic_string<std::byte> const data{
std::byte{'a'}, std::byte{'b'}, std::byte{'c'}};
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::oid id{pqxx::blob::from_buf(tx, data)};
std::basic_string<std::byte> buf;
auto b{pqxx::blob::open_rw(tx, id)};
PQXX_CHECK_EQUAL(
b.read(buf, 2), 2u, "Full read() returned an unexpected value.");
PQXX_CHECK_EQUAL(
buf, (std::basic_string<std::byte>{std::byte{'a'}, std::byte{'b'}}),
"Read back the wrong data.");
PQXX_CHECK_EQUAL(
b.read(buf, 2), 1u, "Partial read() returned an unexpected value.");
PQXX_CHECK_EQUAL(
buf, (std::basic_string<std::byte>{std::byte{'c'}}),
"Continued read produced wrong data.");
PQXX_CHECK_EQUAL(
b.read(buf, 2), 0u, "read at end returned an unexpected value.");
PQXX_CHECK_EQUAL(
buf, (std::basic_string<std::byte>{}), "Read past end produced data.");
}
void test_blob_read_span()
{
#if defined(PQXX_HAVE_SPAN)
std::basic_string<std::byte> const data{std::byte{'u'}, std::byte{'v'},
std::byte{'w'}, std::byte{'x'},
std::byte{'y'}, std::byte{'z'}};
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::oid id{pqxx::blob::from_buf(tx, data)};
auto b{pqxx::blob::open_r(tx, id)};
std::basic_string<std::byte> string_buf;
string_buf.resize(2);
std::span<std::byte> output;
output = b.read(std::span<std::byte>{});
PQXX_CHECK_EQUAL(
std::size(output), 0u, "Empty read produced nonempty buffer.");
output = b.read(string_buf);
PQXX_CHECK_EQUAL(
std::size(output), 2u, "Got unexpected buf size from blob::read().");
PQXX_CHECK_EQUAL(
output[0], std::byte{'u'}, "Unexpected byte from blob::read().");
PQXX_CHECK_EQUAL(
output[1], std::byte{'v'}, "Unexpected byte from blob::read().");
string_buf.resize(100);
output = b.read(std::span<std::byte>{string_buf.data(), 1});
PQXX_CHECK_EQUAL(
std::size(output), 1u,
"Did blob::read() follow string size instead of span size?");
PQXX_CHECK_EQUAL(
output[0], std::byte{'w'}, "Unexpected byte from blob::read().");
std::vector<std::byte> vec_buf;
vec_buf.resize(2);
auto output2{b.read(vec_buf)};
PQXX_CHECK_EQUAL(
std::size(output2), 2u, "Got unexpected buf size from blob::read().");
PQXX_CHECK_EQUAL(
output2[0], std::byte{'x'}, "Unexpected byte from blob::read().");
PQXX_CHECK_EQUAL(
output2[1], std::byte{'y'}, "Unexpected byte from blob::read().");
vec_buf.resize(100);
output2 = b.read(vec_buf);
PQXX_CHECK_EQUAL(std::size(output2), 1u, "Weird things happened at EOF.");
PQXX_CHECK_EQUAL(output2[0], std::byte{'z'}, "Bad data at EOF.");
#endif // PQXX_HAVE_SPAN
}
void test_blob_reads_vector()
{
char const content[]{"abcd"};
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::from_buf(
tx, std::basic_string_view<std::byte>{
reinterpret_cast<std::byte const *>(content), std::size(content)})};
std::vector<std::byte> buf;
buf.resize(10);
auto out{pqxx::blob::open_r(tx, id).read(buf)};
PQXX_CHECK_EQUAL(
std::size(out), std::size(content),
"Got wrong length back when reading as vector.");
PQXX_CHECK_EQUAL(
out[0], std::byte{'a'}, "Got bad data when reading as vector.");
}
void test_blob_write_appends_at_insertion_point()
{
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::create(tx)};
auto b{pqxx::blob::open_rw(tx, id)};
b.write(std::basic_string<std::byte>{std::byte{'z'}});
b.write(std::basic_string<std::byte>{std::byte{'a'}});
std::basic_string<std::byte> buf;
b.read(buf, 5);
PQXX_CHECK_EQUAL(
buf, (std::basic_string<std::byte>{}), "Found data at the end.");
b.seek_abs(0);
b.read(buf, 5);
PQXX_CHECK_EQUAL(
buf, (std::basic_string<std::byte>{std::byte{'z'}, std::byte{'a'}}),
"Consecutive writes did not append correctly.");
b.write(std::basic_string<std::byte>{std::byte{'x'}});
// Blob now contains "zax". That's not we wanted... Rewind and rewrite.
b.seek_abs(1);
b.write(std::basic_string<std::byte>{std::byte{'y'}});
b.seek_abs(0);
b.read(buf, 5);
PQXX_CHECK_EQUAL(
buf,
(std::basic_string<std::byte>{
std::byte{'z'}, std::byte{'y'}, std::byte{'x'}}),
"Rewriting in the middle did not work right.");
}
void test_blob_writes_span()
{
#if defined(PQXX_HAVE_SPAN)
pqxx::connection conn;
pqxx::work tx{conn};
constexpr char content[]{"gfbltk"};
std::basic_string<std::byte> data{
reinterpret_cast<std::byte const *>(content), std::size(content)};
auto id{pqxx::blob::create(tx)};
auto b{pqxx::blob::open_rw(tx, id)};
b.write(std::span<std::byte>{data.data() + 1, 3u});
b.seek_abs(0);
std::vector<std::byte> buf;
buf.resize(4);
auto out{b.read(std::span<std::byte>{buf.data(), 4u})};
PQXX_CHECK_EQUAL(
std::size(out), 3u, "Did not get expected number of bytes back.");
PQXX_CHECK_EQUAL(out[0], std::byte{'f'}, "Data did not come back right.");
PQXX_CHECK_EQUAL(out[2], std::byte{'l'}, "Data started right, ended wrong!");
#endif // PQXX_HAVE_SPAN
}
void test_blob_resize_shortens_to_desired_length()
{
std::basic_string<std::byte> const data{
std::byte{'w'}, std::byte{'o'}, std::byte{'r'}, std::byte{'k'}};
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::from_buf(tx, data)};
pqxx::blob::open_w(tx, id).resize(2);
std::basic_string<std::byte> buf;
pqxx::blob::to_buf(tx, id, buf, 10);
PQXX_CHECK_EQUAL(
buf, (std::basic_string<std::byte>{std::byte{'w'}, std::byte{'o'}}),
"Truncate did not shorten correctly.");
}
void test_blob_resize_extends_to_desired_length()
{
pqxx::connection conn;
pqxx::work tx{conn};
auto id{
pqxx::blob::from_buf(tx, std::basic_string<std::byte>{std::byte{100}})};
pqxx::blob::open_w(tx, id).resize(3);
std::basic_string<std::byte> buf;
pqxx::blob::to_buf(tx, id, buf, 10);
PQXX_CHECK_EQUAL(
buf,
(std::basic_string<std::byte>{std::byte{100}, std::byte{0}, std::byte{0}}),
"Resize did not zero-extend correctly.");
}
void test_blob_tell_tracks_position()
{
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::create(tx)};
auto b{pqxx::blob::open_rw(tx, id)};
PQXX_CHECK_EQUAL(
b.tell(), 0, "Empty blob started out in non-zero position.");
b.write(std::basic_string<std::byte>{std::byte{'e'}, std::byte{'f'}});
PQXX_CHECK_EQUAL(
b.tell(), 2, "Empty blob started out in non-zero position.");
b.seek_abs(1);
PQXX_CHECK_EQUAL(b.tell(), 1, "tell() did not track seek.");
}
void test_blob_seek_sets_positions()
{
std::basic_string<std::byte> data{
std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4},
std::byte{5}, std::byte{6}, std::byte{7}, std::byte{8}, std::byte{9}};
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::from_buf(tx, data)};
auto b{pqxx::blob::open_r(tx, id)};
std::basic_string<std::byte> buf;
b.seek_rel(3);
b.read(buf, 1u);
PQXX_CHECK_EQUAL(
buf[0], std::byte{3},
"seek_rel() from beginning did not take us to the right position.");
b.seek_abs(2);
b.read(buf, 1u);
PQXX_CHECK_EQUAL(
buf[0], std::byte{2}, "seek_abs() did not take us to the right position.");
b.seek_end(-2);
b.read(buf, 1u);
PQXX_CHECK_EQUAL(
buf[0], std::byte{8}, "seek_end() did not take us to the right position.");
}
void test_blob_from_buf_interoperates_with_to_buf()
{
std::basic_string<std::byte> const data{std::byte{'h'}, std::byte{'i'}};
std::basic_string<std::byte> buf;
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::blob::to_buf(tx, pqxx::blob::from_buf(tx, data), buf, 10);
PQXX_CHECK_EQUAL(buf, data, "from_buf()/to_buf() roundtrip did not work.");
}
void test_blob_append_from_buf_appends()
{
std::basic_string<std::byte> const data{std::byte{'h'}, std::byte{'o'}};
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::create(tx)};
pqxx::blob::append_from_buf(tx, data, id);
pqxx::blob::append_from_buf(tx, data, id);
std::basic_string<std::byte> buf;
pqxx::blob::to_buf(tx, id, buf, 10);
PQXX_CHECK_EQUAL(buf, data + data, "append_from_buf() wrote wrong data?");
}
namespace
{
/// Wrap `std::fopen`.
/** This is just here to stop Visual Studio from advertising its own
* alternative.
*/
std::unique_ptr<FILE, std::function<int(FILE *)>>
my_fopen(char const *path, char const *mode)
{
#if defined(_MSC_VER)
# pragma warning(push)
# pragma warning(disable : 4996)
#endif
return {std::fopen(path, mode), std::fclose};
#if defined(_MSC_VER)
# pragma warning(pop)
#endif
}
void read_file(
char const path[], std::size_t len, std::basic_string<std::byte> &buf)
{
buf.resize(len);
auto f{my_fopen(path, "rb")};
auto bytes{
std::fread(reinterpret_cast<char *>(buf.data()), 1, len, f.get())};
if (bytes == 0)
throw std::runtime_error{"Error reading test file."};
buf.resize(bytes);
}
void write_file(char const path[], std::basic_string_view<std::byte> data)
{
try
{
auto f{my_fopen(path, "wb")};
if (
std::fwrite(
reinterpret_cast<char const *>(data.data()), 1, std::size(data),
f.get()) < std::size(data))
std::runtime_error{"File write failed."};
}
catch (const std::exception &)
{
std::remove(path);
throw;
}
}
/// Temporary file.
class TempFile
{
public:
/// Create (and later clean up) a file at path containing data.
TempFile(char const path[], std::basic_string_view<std::byte> data) :
m_path(path)
{
write_file(path, data);
}
~TempFile() { std::remove(m_path.c_str()); }
private:
std::string m_path;
};
} // namespace
void test_blob_from_file_creates_blob_from_file_contents()
{
char const temp_file[] = "blob-test-from_file.tmp";
std::basic_string<std::byte> const data{std::byte{'4'}, std::byte{'2'}};
pqxx::connection conn;
pqxx::work tx{conn};
std::basic_string<std::byte> buf;
pqxx::oid id;
{
TempFile f{temp_file, data};
id = pqxx::blob::from_file(tx, temp_file);
}
pqxx::blob::to_buf(tx, id, buf, 10);
PQXX_CHECK_EQUAL(buf, data, "Wrong data from blob::from_file().");
}
void test_blob_from_file_with_oid_writes_blob()
{
std::basic_string<std::byte> const data{std::byte{'6'}, std::byte{'9'}};
char const temp_file[] = "blob-test-from_file-oid.tmp";
std::basic_string<std::byte> buf;
pqxx::connection conn;
pqxx::work tx{conn};
// Guarantee (more or less) that id is not in use.
auto id{pqxx::blob::create(tx)};
pqxx::blob::remove(tx, id);
{
TempFile f{temp_file, data};
pqxx::blob::from_file(tx, temp_file, id);
}
pqxx::blob::to_buf(tx, id, buf, 10);
PQXX_CHECK_EQUAL(buf, data, "Wrong data from blob::from_file().");
}
void test_blob_append_to_buf_appends()
{
std::basic_string<std::byte> const data{
std::byte{'b'}, std::byte{'l'}, std::byte{'u'}, std::byte{'b'}};
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::from_buf(tx, data)};
std::basic_string<std::byte> buf;
PQXX_CHECK_EQUAL(
pqxx::blob::append_to_buf(tx, id, 0u, buf, 1u), 1u,
"append_to_buf() returned unexpected value.");
PQXX_CHECK_EQUAL(std::size(buf), 1u, "Appended the wrong number of bytes.");
PQXX_CHECK_EQUAL(
pqxx::blob::append_to_buf(tx, id, 1u, buf, 5u), 3u,
"append_to_buf() returned unexpected value.");
PQXX_CHECK_EQUAL(std::size(buf), 4u, "Appended the wrong number of bytes.");
PQXX_CHECK_EQUAL(
buf, data, "Reading using append_to_buf gave us wrong data.");
}
void test_blob_to_file_writes_file()
{
std::basic_string<std::byte> const data{
std::byte{'C'}, std::byte{'+'}, std::byte{'+'}};
char const temp_file[] = "blob-test-to_file.tmp";
pqxx::connection conn;
pqxx::work tx{conn};
auto id{pqxx::blob::from_buf(tx, data)};
std::basic_string<std::byte> buf;
try
{
pqxx::blob::to_file(tx, id, temp_file);
read_file(temp_file, 10u, buf);
std::remove(temp_file);
}
catch (std::exception const &)
{
std::remove(temp_file);
throw;
}
PQXX_CHECK_EQUAL(buf, data, "Got wrong data from to_file().");
}
void test_blob_close_leaves_blob_unusable()
{
pqxx::connection conn;
pqxx::work tx{conn};
auto id{
pqxx::blob::from_buf(tx, std::basic_string<std::byte>{std::byte{1}})};
auto b{pqxx::blob::open_rw(tx, id)};
b.close();
std::basic_string<std::byte> buf;
PQXX_CHECK_THROWS(
b.read(buf, 1), pqxx::usage_error,
"Reading from closed blob did not fail right.");
}
void test_blob_accepts_std_filesystem_path()
{
#if defined(PQXX_HAVE_PATH) && !defined(_WIN32)
// A bug in gcc 8's ~std::filesystem::path() causes a run-time crash.
# if !defined(__GNUC__) || (__GNUC__ > 8)
char const temp_file[] = "blob-test-filesystem-path.tmp";
std::basic_string<std::byte> const data{std::byte{'4'}, std::byte{'2'}};
pqxx::connection conn;
pqxx::work tx{conn};
std::basic_string<std::byte> buf;
TempFile f{temp_file, data};
std::filesystem::path const path{temp_file};
auto id{pqxx::blob::from_file(tx, path)};
pqxx::blob::to_buf(tx, id, buf, 10);
PQXX_CHECK_EQUAL(buf, data, "Wrong data from blob::from_file().");
# endif
#endif
}
PQXX_REGISTER_TEST(test_blob_is_useless_by_default);
PQXX_REGISTER_TEST(test_blob_create_makes_empty_blob);
PQXX_REGISTER_TEST(test_blob_create_with_oid_requires_oid_be_free);
PQXX_REGISTER_TEST(test_blob_create_with_oid_obeys_oid);
PQXX_REGISTER_TEST(test_blobs_are_transactional);
PQXX_REGISTER_TEST(test_blob_remove_removes_blob);
PQXX_REGISTER_TEST(test_blob_remove_is_not_idempotent);
PQXX_REGISTER_TEST(test_blob_checks_open_mode);
PQXX_REGISTER_TEST(test_blob_supports_move);
PQXX_REGISTER_TEST(test_blob_read_reads_data);
PQXX_REGISTER_TEST(test_blob_reads_vector);
PQXX_REGISTER_TEST(test_blob_read_span);
PQXX_REGISTER_TEST(test_blob_write_appends_at_insertion_point);
PQXX_REGISTER_TEST(test_blob_writes_span);
PQXX_REGISTER_TEST(test_blob_resize_shortens_to_desired_length);
PQXX_REGISTER_TEST(test_blob_resize_extends_to_desired_length);
PQXX_REGISTER_TEST(test_blob_tell_tracks_position);
PQXX_REGISTER_TEST(test_blob_seek_sets_positions);
PQXX_REGISTER_TEST(test_blob_from_buf_interoperates_with_to_buf);
PQXX_REGISTER_TEST(test_blob_append_from_buf_appends);
PQXX_REGISTER_TEST(test_blob_from_file_creates_blob_from_file_contents);
PQXX_REGISTER_TEST(test_blob_from_file_with_oid_writes_blob);
PQXX_REGISTER_TEST(test_blob_append_to_buf_appends);
PQXX_REGISTER_TEST(test_blob_to_file_writes_file);
PQXX_REGISTER_TEST(test_blob_close_leaves_blob_unusable);
PQXX_REGISTER_TEST(test_blob_accepts_std_filesystem_path);
} // namespace

View File

@@ -0,0 +1,25 @@
#include <pqxx/pipeline>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_cancel_query()
{
pqxx::connection conn;
pqxx::work tx{conn};
// Calling cancel_query() while none is in progress has no effect.
conn.cancel_query();
// Nothing much is guaranteed about cancel_query, except that it doesn't make
// the process die in flames.
pqxx::pipeline p{tx, "test_cancel_query"};
p.retain(0);
p.insert("SELECT pg_sleep(1)");
conn.cancel_query();
}
PQXX_REGISTER_TEST(test_cancel_query);
} // namespace

View File

@@ -0,0 +1,61 @@
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_table_column()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE pqxxfoo (x varchar, y integer, z integer)");
tx.exec0("INSERT INTO pqxxfoo VALUES ('xx', 1, 2)");
auto R{tx.exec("SELECT z,y,x FROM pqxxfoo")};
auto X{tx.exec("SELECT x,y,z,99 FROM pqxxfoo")};
pqxx::row::size_type x{R.table_column(2)}, y{R.table_column(1)},
z{R.table_column(static_cast<int>(0))};
PQXX_CHECK_EQUAL(x, 0, "Wrong column number.");
PQXX_CHECK_EQUAL(y, 1, "Wrong column number.");
PQXX_CHECK_EQUAL(z, 2, "Wrong column number.");
x = R.table_column("x");
y = R.table_column("y");
z = R.table_column("z");
PQXX_CHECK_EQUAL(x, 0, "Wrong number for named column.");
PQXX_CHECK_EQUAL(y, 1, "Wrong number for named column.");
PQXX_CHECK_EQUAL(z, 2, "Wrong number for named column.");
pqxx::row::size_type xx{X[0].table_column(static_cast<int>(0))},
yx{X[0].table_column(pqxx::row::size_type(1))}, zx{X[0].table_column("z")};
PQXX_CHECK_EQUAL(xx, 0, "Bad result from table_column(int).");
PQXX_CHECK_EQUAL(yx, 1, "Bad result from table_column(size_type).");
PQXX_CHECK_EQUAL(zx, 2, "Bad result from table_column(string).");
for (pqxx::row::size_type i{0}; i < std::size(R[0]); ++i)
PQXX_CHECK_EQUAL(
R[0][i].table_column(), R.table_column(i),
"Bad result from column_table().");
int col;
PQXX_CHECK_THROWS_EXCEPTION(
col = R.table_column(3), "table_column() with invalid index didn't fail.");
pqxx::ignore_unused(col);
PQXX_CHECK_THROWS_EXCEPTION(
col = R.table_column("nonexistent"),
"table_column() with invalid column name didn't fail.");
pqxx::ignore_unused(col);
PQXX_CHECK_THROWS_EXCEPTION(
col = X.table_column(3), "table_column() on non-table didn't fail.");
pqxx::ignore_unused(col);
}
} // namespace
PQXX_REGISTER_TEST(test_table_column);

View File

@@ -0,0 +1,98 @@
#include "../test_helpers.hxx"
#include "pqxx/composite"
#include "pqxx/transaction"
namespace
{
void test_composite()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TYPE pqxxfoo AS (a integer, b text)");
auto const r{tx.exec1("SELECT '(5,hello)'::pqxxfoo")};
int a;
std::string b;
pqxx::parse_composite(r[0].view(), a, b);
PQXX_CHECK_EQUAL(a, 5, "Integer composite field came back wrong.");
PQXX_CHECK_EQUAL(b, "hello", "String composite field came back wrong.");
}
void test_composite_escapes()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::row r;
tx.exec0("CREATE TYPE pqxxsingle AS (x text)");
std::string s;
r = tx.exec1(R"--(SELECT '("a""b")'::pqxxsingle)--");
pqxx::parse_composite(r[0].view(), s);
PQXX_CHECK_EQUAL(
s, "a\"b", "Double-double-quotes escaping did not parse correctly.");
r = tx.exec1(R"--(SELECT '("a\"b")'::pqxxsingle)--");
pqxx::parse_composite(r[0].view(), s);
PQXX_CHECK_EQUAL(s, "a\"b", "Backslash escaping did not parse correctly.");
}
void test_composite_handles_nulls()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::row r;
tx.exec0("CREATE TYPE pqxxnull AS (a integer)");
int nonnull;
r = tx.exec1("SELECT '()'::pqxxnull");
PQXX_CHECK_THROWS(
pqxx::parse_composite(r[0].view(), nonnull), pqxx::conversion_error,
"No conversion error when reading a null into a nulless variable.");
std::optional<int> nullable{5};
pqxx::parse_composite(r[0].view(), nullable);
PQXX_CHECK(
not nullable.has_value(), "Null integer came out as having a value.");
tx.exec0("CREATE TYPE pqxxnulls AS (a integer, b integer)");
std::optional<int> a{2}, b{4};
r = tx.exec1("SELECT '(,)'::pqxxnulls");
pqxx::parse_composite(r[0].view(), a, b);
PQXX_CHECK(not a.has_value(), "Null first integer stored as value.");
PQXX_CHECK(not b.has_value(), "Null second integer stored as value.");
}
void test_composite_renders_to_string()
{
pqxx::connection conn;
pqxx::work tx{conn};
char buf[1000];
pqxx::composite_into_buf(
std::begin(buf), std::end(buf), 355, "foo", "b\na\\r");
PQXX_CHECK_EQUAL(
std::string{buf}, "(355,\"foo\",\"b\na\\\\r\")",
"Composite was not rendered as expected.");
tx.exec0("CREATE TYPE pqxxcomp AS (a integer, b text, c text)");
auto const r{tx.exec1("SELECT '" + std::string{buf} + "'::pqxxcomp")};
int a;
std::string b, c;
bool const nonnull{r[0].composite_to(a, b, c)};
PQXX_CHECK(nonnull, "Mistaken nullness.");
PQXX_CHECK_EQUAL(a, 355, "Int came back wrong.");
PQXX_CHECK_EQUAL(b, "foo", "Simple string came back wrong.");
PQXX_CHECK_EQUAL(c, "b\na\\r", "Escaping went wrong.");
}
PQXX_REGISTER_TEST(test_composite);
PQXX_REGISTER_TEST(test_composite_escapes);
PQXX_REGISTER_TEST(test_composite_handles_nulls);
PQXX_REGISTER_TEST(test_composite_renders_to_string);
} // namespace

View File

@@ -0,0 +1,212 @@
#include <numeric>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_connection_string_constructor()
{
pqxx::connection c1{""};
pqxx::connection c2{std::string{}};
}
void test_move_constructor()
{
pqxx::connection c1;
PQXX_CHECK(c1.is_open(), "New connection is not open.");
pqxx::connection c2{std::move(c1)};
PQXX_CHECK(not c1.is_open(), "Moving did not close original connection.");
PQXX_CHECK(c2.is_open(), "Moved constructor is not open.");
pqxx::work tx{c2};
PQXX_CHECK_EQUAL(tx.query_value<int>("SELECT 5"), 5, "Weird result!");
PQXX_CHECK_THROWS(
pqxx::connection c3{std::move(c2)}, pqxx::usage_error,
"Moving a connection with a transaction open should be an error.");
}
void test_move_assign()
{
pqxx::connection c1;
pqxx::connection c2;
c2.close();
c2 = std::move(c1);
PQXX_CHECK(not c1.is_open(), "Connection still open after being moved out.");
PQXX_CHECK(c2.is_open(), "Moved constructor is not open.");
{
pqxx::work tx1{c2};
PQXX_CHECK_EQUAL(tx1.query_value<int>("SELECT 8"), 8, "What!?");
pqxx::connection c3;
PQXX_CHECK_THROWS(
c3 = std::move(c2), pqxx::usage_error,
"Moving a connection with a transaction open should be an error.");
PQXX_CHECK_THROWS(
c2 = std::move(c3), pqxx::usage_error,
"Moving a connection onto one with a transaction open should be "
"an error.");
}
// After failed move attempts, the connection is still usable.
pqxx::work tx2{c2};
PQXX_CHECK_EQUAL(tx2.query_value<int>("SELECT 6"), 6, "Huh!?");
}
void test_encrypt_password()
{
pqxx::connection c;
auto pw{c.encrypt_password("user", "password")};
PQXX_CHECK(not std::empty(pw), "Encrypted password was empty.");
PQXX_CHECK_EQUAL(
std::strlen(pw.c_str()), std::size(pw),
"Encrypted password contains a null byte.");
}
void test_connection_string()
{
pqxx::connection c;
std::string const connstr{c.connection_string()};
#if defined(_MSC_VER)
# pragma warning(push)
# pragma warning(disable : 4996)
#endif
if (std::getenv("PGUSER") == nullptr)
#if defined(_MSC_VER)
# pragma warning(pop)
#endif
{
PQXX_CHECK(
connstr.find("user=" + std::string{c.username()}) != std::string::npos,
"Connection string did not specify user name: " + connstr);
}
else
{
PQXX_CHECK(
connstr.find("user=" + std::string{c.username()}) == std::string::npos,
"Connection string specified user name, even when using default: " +
connstr);
}
}
#if defined(PQXX_HAVE_CONCEPTS)
template<typename STR> std::size_t length(STR const &str)
{
return std::size(str);
}
std::size_t length(char const str[])
{
return std::strlen(str);
}
#endif // PQXX_HAVE_CONCEPTS
template<typename MAP> void test_params_type()
{
#if defined(PQXX_HAVE_CONCEPTS)
using item_t = std::remove_reference_t<
decltype(*std::declval<std::ranges::iterator_t<MAP>>())>;
using key_t = decltype(std::get<0>(std::declval<item_t>()));
using value_t = decltype(std::get<1>(std::declval<item_t>()));
// Set some parameters that are relatively safe to change arbitrarily.
MAP const params{{
{key_t{"application_name"}, value_t{"pqxx-test"}},
{key_t{"connect_timeout"}, value_t{"96"}},
{key_t{"keepalives_idle"}, value_t{"771"}},
}};
// Can we create a connection from these parameters?
pqxx::connection c{params};
// Check that the parameters came through in the connection string.
// We don't know the exact format, but the parameters have to be in there.
auto const min_size{std::accumulate(
std::cbegin(params), std::cend(params), std::size(params) - 1,
[](auto count, auto item) {
return count + length(std::get<0>(item)) + 1 + length(std::get<1>(item));
})};
auto const connstr{c.connection_string()};
PQXX_CHECK_GREATER_EQUAL(
std::size(connstr), min_size,
"Connection string can't possibly contain the options we gave.");
for (auto const &[key, value] : params)
{
PQXX_CHECK_NOT_EQUAL(
connstr.find(key), std::string::npos,
"Could not find param name '" + std::string{key} +
"' in connection string: " + connstr);
PQXX_CHECK_NOT_EQUAL(
connstr.find(value), std::string::npos,
"Could not find value for '" + std::string{value} +
"' in connection string: " + connstr);
}
#endif // PQXX_HAVE_CONCEPTS
}
void test_connection_params()
{
// Connecting in this way supports a wide variety of formats for the
// parameters.
test_params_type<std::map<char const *, char const *>>();
test_params_type<std::map<pqxx::zview, pqxx::zview>>();
test_params_type<std::map<std::string, std::string>>();
test_params_type<std::map<std::string, pqxx::zview>>();
test_params_type<std::map<pqxx::zview, char const *>>();
test_params_type<std::vector<std::tuple<char const *, char const *>>>();
test_params_type<std::vector<std::tuple<pqxx::zview, std::string>>>();
test_params_type<std::vector<std::pair<std::string, char const *>>>();
test_params_type<std::vector<std::array<std::string, 2>>>();
}
void test_raw_connection()
{
pqxx::connection conn1;
PQXX_CHECK(conn1.is_open(), "Fresh connection is not open!");
pqxx::work tx1{conn1};
PQXX_CHECK_EQUAL(
tx1.query_value<int>("SELECT 8"), 8, "Something weird happened.");
pqxx::internal::pq::PGconn *raw{std::move(conn1).release_raw_connection()};
PQXX_CHECK(raw != nullptr, "Raw connection is null.");
PQXX_CHECK(
not conn1.is_open(),
"Releasing raw connection did not close pqxx::connection.");
pqxx::connection conn2{pqxx::connection::seize_raw_connection(raw)};
PQXX_CHECK(
conn2.is_open(), "Can't produce open connection from raw connection.");
pqxx::work tx2{conn2};
PQXX_CHECK_EQUAL(
tx2.query_value<int>("SELECT 9"), 9,
"Raw connection did not produce a working new connection.");
}
PQXX_REGISTER_TEST(test_connection_string_constructor);
PQXX_REGISTER_TEST(test_move_constructor);
PQXX_REGISTER_TEST(test_move_assign);
PQXX_REGISTER_TEST(test_encrypt_password);
PQXX_REGISTER_TEST(test_connection_string);
PQXX_REGISTER_TEST(test_connection_params);
PQXX_REGISTER_TEST(test_raw_connection);
} // namespace

View File

@@ -0,0 +1,50 @@
#include <pqxx/cursor>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_stateless_cursor_provides_random_access(pqxx::connection &conn)
{
pqxx::work tx{conn};
pqxx::stateless_cursor<
pqxx::cursor_base::read_only, pqxx::cursor_base::owned>
c{tx, "SELECT * FROM generate_series(0, 3)", "count", false};
auto r{c.retrieve(1, 2)};
PQXX_CHECK_EQUAL(std::size(r), 1, "Wrong number of rows from retrieve().");
PQXX_CHECK_EQUAL(r[0][0].as<int>(), 1, "Cursor retrieved wrong data.");
r = c.retrieve(3, 10);
PQXX_CHECK_EQUAL(std::size(r), 1, "Expected 1 row retrieving past end.");
PQXX_CHECK_EQUAL(r[0][0].as<int>(), 3, "Wrong data retrieved at end.");
r = c.retrieve(0, 1);
PQXX_CHECK_EQUAL(std::size(r), 1, "Wrong number of rows back at beginning.");
PQXX_CHECK_EQUAL(r[0][0].as<int>(), 0, "Wrong data back at beginning.");
}
void test_stateless_cursor_ignores_trailing_semicolon(pqxx::connection &conn)
{
pqxx::work tx{conn};
pqxx::stateless_cursor<
pqxx::cursor_base::read_only, pqxx::cursor_base::owned>
c{tx, "SELECT * FROM generate_series(0, 3) ;; ; \n \t ", "count", false};
auto r{c.retrieve(1, 2)};
PQXX_CHECK_EQUAL(std::size(r), 1, "Trailing semicolon confused retrieve().");
}
void test_cursor()
{
pqxx::connection conn;
test_stateless_cursor_provides_random_access(conn);
test_stateless_cursor_ignores_trailing_semicolon(conn);
}
PQXX_REGISTER_TEST(test_cursor);
} // namespace

View File

@@ -0,0 +1,114 @@
#include "../test_helpers.hxx"
#include "pqxx/internal/encodings.hxx"
namespace
{
void test_scan_ascii()
{
auto const scan{pqxx::internal::get_glyph_scanner(
pqxx::internal::encoding_group::MONOBYTE)};
std::string const text{"hello"};
PQXX_CHECK_EQUAL(
scan(text.c_str(), std::size(text), 0), 1ul,
"Monobyte scanner acting up.");
PQXX_CHECK_EQUAL(
scan(text.c_str(), std::size(text), 1), 2ul,
"Monobyte scanner is inconsistent.");
}
void test_scan_utf8()
{
auto const scan{
pqxx::internal::get_glyph_scanner(pqxx::internal::encoding_group::UTF8)};
// Thai: "Khrab".
std::string const text{"\xe0\xb8\x95\xe0\xb8\xa3\xe0\xb8\xb1\xe0\xb8\x9a"};
PQXX_CHECK_EQUAL(
scan(text.c_str(), std::size(text), 0), 3ul,
"UTF-8 scanner mis-scanned Thai khor khwai.");
PQXX_CHECK_EQUAL(
scan(text.c_str(), std::size(text), 3), 6ul,
"UTF-8 scanner mis-scanned Thai ror reua.");
}
void test_for_glyphs_empty()
{
bool iterated{false};
pqxx::internal::for_glyphs(
pqxx::internal::encoding_group::MONOBYTE,
[&iterated](char const *, char const *) { iterated = true; }, "", 0);
PQXX_CHECK(!iterated, "Empty string went through an iteration.");
}
void test_for_glyphs_ascii()
{
std::string const text{"hi"};
std::vector<std::ptrdiff_t> points;
pqxx::internal::for_glyphs(
pqxx::internal::encoding_group::UTF8,
[&points](char const *gbegin, char const *gend) {
points.push_back(gend - gbegin);
},
text.c_str(), std::size(text));
PQXX_CHECK_EQUAL(std::size(points), 2u, "Wrong number of ASCII iterations.");
PQXX_CHECK_EQUAL(points[0], 1u, "ASCII iteration started off wrong.");
PQXX_CHECK_EQUAL(points[1], 1u, "ASCII iteration was inconsistent.");
}
void test_for_glyphs_utf8()
{
// Greek: alpha omega.
std::string const text{"\xce\x91\xce\xa9"};
std::vector<std::ptrdiff_t> points;
pqxx::internal::for_glyphs(
pqxx::internal::encoding_group::UTF8,
[&points](char const *gbegin, char const *gend) {
points.push_back(gend - gbegin);
},
text.c_str(), std::size(text));
PQXX_CHECK_EQUAL(std::size(points), 2u, "Wrong number of UTF-8 iterations.");
PQXX_CHECK_EQUAL(points[0], 2u, "UTF-8 iteration started off wrong.");
PQXX_CHECK_EQUAL(points[1], 2u, "ASCII iteration was inconsistent.");
// Greek lambda, ASCII plus sign, Old Persian Gu.
std::string const mix{"\xce\xbb+\xf0\x90\x8e\xa6"};
points.clear();
pqxx::internal::for_glyphs(
pqxx::internal::encoding_group::UTF8,
[&points](char const *gbegin, char const *gend) {
points.push_back(gend - gbegin);
},
mix.c_str(), std::size(mix));
PQXX_CHECK_EQUAL(std::size(points), 3u, "Mixed UTF-8 iteration is broken.");
PQXX_CHECK_EQUAL(points[0], 2u, "Mixed UTF-8 iteration started off wrong.");
PQXX_CHECK_EQUAL(points[1], 1u, "Mixed UTF-8 iteration got ASCII wrong.");
PQXX_CHECK_EQUAL(
points[2], 4u, "Mixed UTF-8 iteration got long char wrong.");
}
void test_encodings()
{
test_scan_ascii();
test_scan_utf8();
test_for_glyphs_empty();
test_for_glyphs_ascii();
test_for_glyphs_utf8();
}
PQXX_REGISTER_TEST(test_encodings);
} // namespace

View File

@@ -0,0 +1,37 @@
#include <pqxx/transaction>
#include "../test_helpers.hxx"
extern "C"
{
#include <libpq-fe.h>
}
namespace
{
void test_error_verbosity()
{
PQXX_CHECK_EQUAL(
static_cast<int>(pqxx::error_verbosity::terse),
static_cast<int>(PQERRORS_TERSE),
"error_verbosity enum should match PGVerbosity.");
PQXX_CHECK_EQUAL(
static_cast<int>(pqxx::error_verbosity::normal),
static_cast<int>(PQERRORS_DEFAULT),
"error_verbosity enum should match PGVerbosity.");
PQXX_CHECK_EQUAL(
static_cast<int>(pqxx::error_verbosity::verbose),
static_cast<int>(PQERRORS_VERBOSE),
"error_verbosity enum should match PGVerbosity.");
pqxx::connection conn;
pqxx::work tx{conn};
conn.set_verbosity(pqxx::error_verbosity::terse);
tx.exec1("SELECT 1");
conn.set_verbosity(pqxx::error_verbosity::verbose);
tx.exec1("SELECT 2");
}
PQXX_REGISTER_TEST(test_error_verbosity);
} // namespace

View File

@@ -0,0 +1,223 @@
#include <vector>
#include <pqxx/connection>
#include <pqxx/errorhandler>
#include "../test_helpers.hxx"
namespace
{
class TestErrorHandler final : public pqxx::errorhandler
{
public:
TestErrorHandler(
pqxx::connection &c, std::vector<TestErrorHandler *> &activated_handlers,
bool retval = true) :
pqxx::errorhandler(c),
return_value(retval),
message(),
handler_list(activated_handlers)
{}
bool operator()(char const msg[]) noexcept override
{
message = std::string{msg};
handler_list.push_back(this);
return return_value;
}
bool return_value;
std::string message;
std::vector<TestErrorHandler *> &handler_list;
};
} // namespace
namespace pqxx
{
template<> struct nullness<TestErrorHandler *>
{
// clang warns about these being unused. And clang 6 won't accept a
// [[maybe_unused]] attribute on them either!
// static inline constexpr bool has_null{true};
// static inline constexpr bool always_null{false};
static constexpr bool is_null(TestErrorHandler *e) noexcept
{
return e == nullptr;
}
static constexpr TestErrorHandler *null() noexcept { return nullptr; }
};
template<> struct string_traits<TestErrorHandler *>
{
static constexpr std::size_t size_buffer(TestErrorHandler *const &) noexcept
{
return 100;
}
static char *into_buf(char *begin, char *end, TestErrorHandler *const &value)
{
std::string text{"TestErrorHandler at " + pqxx::to_string(value)};
if (pqxx::internal::cmp_greater_equal(std::size(text), end - begin))
throw conversion_overrun{"Not enough buffer for TestErrorHandler."};
std::memcpy(begin, text.c_str(), std::size(text) + 1);
return begin + std::size(text) + 1;
}
};
} // namespace pqxx
namespace
{
void test_process_notice_calls_errorhandler(pqxx::connection &c)
{
std::vector<TestErrorHandler *> dummy;
TestErrorHandler handler(c, dummy);
c.process_notice("Error!\n");
PQXX_CHECK_EQUAL(handler.message, "Error!\n", "Error not handled.");
}
void test_error_handlers_get_called_newest_to_oldest(pqxx::connection &c)
{
std::vector<TestErrorHandler *> handlers;
TestErrorHandler h1(c, handlers);
TestErrorHandler h2(c, handlers);
TestErrorHandler h3(c, handlers);
c.process_notice("Warning.\n");
PQXX_CHECK_EQUAL(h3.message, "Warning.\n", "Message not handled.");
PQXX_CHECK_EQUAL(h2.message, "Warning.\n", "Broken handling chain.");
PQXX_CHECK_EQUAL(h1.message, "Warning.\n", "Insane handling chain.");
PQXX_CHECK_EQUAL(std::size(handlers), 3u, "Wrong number of handler calls.");
PQXX_CHECK_EQUAL(&h3, handlers[0], "Unexpected handling order.");
PQXX_CHECK_EQUAL(&h2, handlers[1], "Insane handling order.");
PQXX_CHECK_EQUAL(&h1, handlers[2], "Impossible handling order.");
}
void test_returning_false_stops_error_handling(pqxx::connection &c)
{
std::vector<TestErrorHandler *> handlers;
TestErrorHandler starved(c, handlers);
TestErrorHandler blocker(c, handlers, false);
c.process_notice("Error output.\n");
PQXX_CHECK_EQUAL(std::size(handlers), 1u, "Handling chain was not stopped.");
PQXX_CHECK_EQUAL(handlers[0], &blocker, "Wrong handler got message.");
PQXX_CHECK_EQUAL(blocker.message, "Error output.\n", "Didn't get message.");
PQXX_CHECK_EQUAL(starved.message, "", "Message received; it shouldn't be.");
}
void test_destroyed_error_handlers_are_not_called(pqxx::connection &c)
{
std::vector<TestErrorHandler *> handlers;
{
TestErrorHandler doomed(c, handlers);
}
c.process_notice("Unheard output.");
PQXX_CHECK(
std::empty(handlers), "Message was received on dead errorhandler.");
}
void test_destroying_connection_unregisters_handlers()
{
TestErrorHandler *survivor;
std::vector<TestErrorHandler *> handlers;
{
pqxx::connection c;
survivor = new TestErrorHandler(c, handlers);
}
// Make some pointless use of survivor just to prove that this doesn't crash.
(*survivor)("Hi");
PQXX_CHECK_EQUAL(
std::size(handlers), 1u, "Ghost of dead ex-connection haunts handler.");
delete survivor;
}
class MinimalErrorHandler final : public pqxx::errorhandler
{
public:
explicit MinimalErrorHandler(pqxx::connection &c) : pqxx::errorhandler(c) {}
bool operator()(char const[]) noexcept override { return true; }
};
void test_get_errorhandlers(pqxx::connection &c)
{
std::unique_ptr<MinimalErrorHandler> eh3;
auto const handlers_before{c.get_errorhandlers()};
std::size_t const base_handlers{std::size(handlers_before)};
{
MinimalErrorHandler eh1(c);
auto const handlers_with_eh1{c.get_errorhandlers()};
PQXX_CHECK_EQUAL(
std::size(handlers_with_eh1), base_handlers + 1,
"Registering a handler didn't create exactly one handler.");
PQXX_CHECK_EQUAL(
std::size_t(*std::rbegin(handlers_with_eh1)), std::size_t(&eh1),
"Wrong handler or wrong order.");
{
MinimalErrorHandler eh2(c);
auto const handlers_with_eh2{c.get_errorhandlers()};
PQXX_CHECK_EQUAL(
std::size(handlers_with_eh2), base_handlers + 2,
"Adding second handler didn't work.");
PQXX_CHECK_EQUAL(
std::size_t(*(std::rbegin(handlers_with_eh2) + 1)), std::size_t(&eh1),
"Second handler upset order.");
PQXX_CHECK_EQUAL(
std::size_t(*std::rbegin(handlers_with_eh2)), std::size_t(&eh2),
"Second handler isn't right.");
}
auto const handlers_without_eh2{c.get_errorhandlers()};
PQXX_CHECK_EQUAL(
std::size(handlers_without_eh2), base_handlers + 1,
"Handler destruction produced wrong-sized handlers list.");
PQXX_CHECK_EQUAL(
std::size_t(*std::rbegin(handlers_without_eh2)), std::size_t(&eh1),
"Destroyed wrong handler.");
eh3 = std::make_unique<MinimalErrorHandler>(c);
auto const handlers_with_eh3{c.get_errorhandlers()};
PQXX_CHECK_EQUAL(
std::size(handlers_with_eh3), base_handlers + 2,
"Remove-and-add breaks.");
PQXX_CHECK_EQUAL(
std::size_t(*std::rbegin(handlers_with_eh3)), std::size_t(eh3.get()),
"Added wrong third handler.");
}
auto const handlers_without_eh1{c.get_errorhandlers()};
PQXX_CHECK_EQUAL(
std::size(handlers_without_eh1), base_handlers + 1,
"Destroying oldest handler didn't work as expected.");
PQXX_CHECK_EQUAL(
std::size_t(*std::rbegin(handlers_without_eh1)), std::size_t(eh3.get()),
"Destroyed wrong handler.");
eh3.reset();
auto const handlers_without_all{c.get_errorhandlers()};
PQXX_CHECK_EQUAL(
std::size(handlers_without_all), base_handlers,
"Destroying all custom handlers didn't work as expected.");
}
void test_errorhandler()
{
pqxx::connection conn;
test_process_notice_calls_errorhandler(conn);
test_error_handlers_get_called_newest_to_oldest(conn);
test_returning_false_stops_error_handling(conn);
test_destroyed_error_handlers_are_not_called(conn);
test_destroying_connection_unregisters_handlers();
test_get_errorhandlers(conn);
}
PQXX_REGISTER_TEST(test_errorhandler);
} // namespace

View File

@@ -0,0 +1,228 @@
#include <iostream>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
using namespace std::literals;
void compare_esc(
pqxx::connection &c, pqxx::transaction_base &t, char const text[])
{
std::size_t const len{std::size(std::string{text})};
PQXX_CHECK_EQUAL(
c.esc(std::string_view{text, len}), t.esc(std::string_view{text, len}),
"Connection & transaction escape differently.");
PQXX_CHECK_EQUAL(
t.esc(std::string_view{text, len}), t.esc(text),
"Length argument to esc() changes result.");
PQXX_CHECK_EQUAL(
t.esc(std::string{text}), t.esc(text),
"esc(std::string()) differs from esc(char const[]).");
PQXX_CHECK_EQUAL(
text,
t.query_value<std::string>(
"SELECT '" + t.esc(std::string_view{text, len}) + "'"),
"esc() is not idempotent.");
PQXX_CHECK_EQUAL(
t.esc(std::string_view{text, len}), t.esc(text),
"Oversized buffer affects esc().");
}
void test_esc(pqxx::connection &c, pqxx::transaction_base &t)
{
PQXX_CHECK_EQUAL(
t.esc(std::string_view{"", 0}), "",
"Empty string doesn't escape properly.");
PQXX_CHECK_EQUAL(
t.esc(std::string_view{"'", 1}), "''",
"Single quote escaped incorrectly.");
PQXX_CHECK_EQUAL(
t.esc(std::string_view{"hello"}), "hello", "Trivial escape went wrong.");
char const *const escstrings[]{"x", " ", "", nullptr};
for (std::size_t i{0}; escstrings[i] != nullptr; ++i)
compare_esc(c, t, escstrings[i]);
}
void test_quote(pqxx::connection &c, pqxx::transaction_base &t)
{
PQXX_CHECK_EQUAL(t.quote("x"), "'x'", "Basic quote() fails.");
PQXX_CHECK_EQUAL(
t.quote(1), "'1'", "quote() not dealing with int properly.");
PQXX_CHECK_EQUAL(t.quote(0), "'0'", "Quoting zero is a problem.");
char const *const null_ptr{nullptr};
PQXX_CHECK_EQUAL(t.quote(null_ptr), "NULL", "Not quoting NULL correctly.");
PQXX_CHECK_EQUAL(
t.quote(std::string{"'"}), "''''", "Escaping quotes goes wrong.");
PQXX_CHECK_EQUAL(
t.quote("x"), c.quote("x"),
"Connection and transaction quote differently.");
char const *test_strings[]{"", "x", "\\", "\\\\", "'",
"''", "\\'", "\t", "\n", nullptr};
for (std::size_t i{0}; test_strings[i] != nullptr; ++i)
{
auto r{t.query_value<std::string>("SELECT " + t.quote(test_strings[i]))};
PQXX_CHECK_EQUAL(
r, test_strings[i], "Selecting quoted string does not come back equal.");
}
}
void test_quote_name(pqxx::transaction_base &t)
{
PQXX_CHECK_EQUAL(
"\"A b\"", t.quote_name("A b"), "Escaped identifier is not as expected.");
PQXX_CHECK_EQUAL(
std::string{"A b"},
t.exec("SELECT 1 AS " + t.quote_name("A b")).column_name(0),
"Escaped identifier does not work in SQL.");
}
void test_esc_raw_unesc_raw(pqxx::transaction_base &t)
{
constexpr char binary[]{"1\0023\\4x5"};
std::basic_string<std::byte> const data(
reinterpret_cast<std::byte const *>(binary), std::size(binary));
std::string const escaped{t.esc_raw(
std::basic_string_view<std::byte>{std::data(data), std::size(binary)})};
for (auto const i : escaped)
{
PQXX_CHECK_GREATER(
static_cast<unsigned>(static_cast<unsigned char>(i)), 7u,
"Non-ASCII character in escaped data: " + escaped);
PQXX_CHECK_LESS(
static_cast<unsigned>(static_cast<unsigned char>(i)), 127u,
"Non-ASCII character in escaped data: " + escaped);
}
for (auto const i : escaped)
PQXX_CHECK(
isprint(i), "Unprintable character in escaped data: " + escaped);
PQXX_CHECK_EQUAL(
escaped, "\\x3102335c34783500", "Binary data escaped wrong.");
PQXX_CHECK_EQUAL(
std::size(t.unesc_bin(escaped)), std::size(data),
"Wrong size after unescaping.");
auto unescaped{t.unesc_bin(escaped)};
PQXX_CHECK_EQUAL(
std::size(unescaped), std::size(data),
"Unescaping did not restore original size.");
for (std::size_t i{0}; i < std::size(unescaped); ++i)
PQXX_CHECK_EQUAL(
int(unescaped[i]), int(data[i]),
"Unescaping binary data did not restore byte #" + pqxx::to_string(i) +
".");
}
void test_esc_like(pqxx::transaction_base &tx)
{
PQXX_CHECK_EQUAL(tx.esc_like(""), "", "esc_like breaks on empty string.");
PQXX_CHECK_EQUAL(tx.esc_like("abc"), "abc", "esc_like is broken.");
PQXX_CHECK_EQUAL(tx.esc_like("_"), "\\_", "esc_like fails on underscore.");
PQXX_CHECK_EQUAL(tx.esc_like("%"), "\\%", "esc_like fails on %.");
PQXX_CHECK_EQUAL(
tx.esc_like("a%b_c"), "a\\%b\\_c", "esc_like breaks on mix.");
PQXX_CHECK_EQUAL(
tx.esc_like("_", '+'), "+_", "esc_like ignores escape character.");
}
void test_escaping()
{
pqxx::connection conn;
pqxx::work tx{conn};
test_esc(conn, tx);
test_quote(conn, tx);
test_quote_name(tx);
test_esc_raw_unesc_raw(tx);
test_esc_like(tx);
}
void test_esc_escapes_into_buffer()
{
#if defined(PQXX_HAVE_CONCEPTS)
pqxx::connection conn;
pqxx::work tx{conn};
std::string buffer;
buffer.resize(20);
auto const text{"Ain't"sv};
auto escaped_text{tx.esc(text, buffer)};
PQXX_CHECK_EQUAL(escaped_text, "Ain''t", "Escaping into buffer went wrong.");
std::basic_string<std::byte> const data{std::byte{0x22}, std::byte{0x43}};
auto escaped_data(tx.esc(data, buffer));
PQXX_CHECK_EQUAL(escaped_data, "\\x2243", "Binary data escaped wrong.");
#endif
}
void test_esc_accepts_various_types()
{
#if defined(PQXX_HAVE_CONCEPTS) && defined(PQXX_HAVE_SPAN)
pqxx::connection conn;
pqxx::work tx{conn};
std::string buffer;
buffer.resize(20);
std::string const text{"it's"};
auto escaped_text{tx.esc(text, buffer)};
PQXX_CHECK_EQUAL(escaped_text, "it''s", "Escaping into buffer went wrong.");
std::vector<std::byte> const data{std::byte{0x23}, std::byte{0x44}};
auto escaped_data(tx.esc(data, buffer));
PQXX_CHECK_EQUAL(escaped_data, "\\x2344", "Binary data escaped wrong.");
#endif
}
void test_binary_esc_checks_buffer_length()
{
#if defined(PQXX_HAVE_CONCEPTS) && defined(PQXX_HAVE_SPAN)
pqxx::connection conn;
pqxx::work tx{conn};
std::string buf;
std::basic_string<std::byte> bin{
std::byte{'b'}, std::byte{'o'}, std::byte{'o'}};
buf.resize(2 * std::size(bin) + 3);
pqxx::ignore_unused(tx.esc(bin, buf));
PQXX_CHECK_EQUAL(int{buf[0]}, int{'\\'}, "Unexpected binary escape format.");
PQXX_CHECK_NOT_EQUAL(
int(buf[std::size(buf) - 2]), int('\0'), "Escaped binary ends too soon.");
PQXX_CHECK_EQUAL(
int(buf[std::size(buf) - 1]), int('\0'), "Terminating zero is missing.");
buf.resize(2 * std::size(bin) + 2);
PQXX_CHECK_THROWS(
pqxx::ignore_unused(tx.esc(bin, buf)), pqxx::range_error,
"Didn't get expected exception from escape overrun.");
#endif
}
PQXX_REGISTER_TEST(test_escaping);
PQXX_REGISTER_TEST(test_esc_escapes_into_buffer);
PQXX_REGISTER_TEST(test_esc_accepts_various_types);
PQXX_REGISTER_TEST(test_binary_esc_checks_buffer_length);
} // namespace

View File

@@ -0,0 +1,45 @@
#include <pqxx/except>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_exceptions()
{
std::string const broken_query{"SELECT HORRIBLE ERROR"},
err{"Error message"};
try
{
throw pqxx::sql_error{err, broken_query};
}
catch (std::exception const &e)
{
PQXX_CHECK_EQUAL(e.what(), err, "Exception contains wrong message.");
auto downcast{dynamic_cast<pqxx::sql_error const *>(&e)};
PQXX_CHECK(
downcast != nullptr, "exception-to-sql_error downcast is broken.");
PQXX_CHECK_EQUAL(
downcast->query(), broken_query,
"Getting query from pqxx exception is broken.");
}
pqxx::connection conn;
pqxx::work tx{conn};
try
{
tx.exec("INVALID QUERY HERE");
}
catch (pqxx::syntax_error const &e)
{
// SQL syntax error has sqlstate error 42601.
PQXX_CHECK_EQUAL(
e.sqlstate(), "42601", "Unexpected sqlstate on syntax error.");
}
}
PQXX_REGISTER_TEST(test_exceptions);
} // namespace

View File

@@ -0,0 +1,57 @@
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_field()
{
pqxx::connection c;
pqxx::work tx{c};
auto const r1{tx.exec1("SELECT 9")};
auto const &f1{r1[0]};
PQXX_CHECK_EQUAL(f1.as<std::string>(), "9", "as<string>() is broken.");
PQXX_CHECK_EQUAL(
f1.as<std::string>("z"), "9", "as<string>(string) is broken.");
PQXX_CHECK_EQUAL(f1.as<int>(), 9, "as<int>() is broken.");
PQXX_CHECK_EQUAL(f1.as<int>(10), 9, "as<int>(int) is broken.");
std::string s;
PQXX_CHECK(f1.to(s), "to(string) failed.");
PQXX_CHECK_EQUAL(s, "9", "to(string) is broken.");
s = "x";
PQXX_CHECK(f1.to(s, std::string{"7"}), "to(string, string) failed.");
PQXX_CHECK_EQUAL(s, "9", "to(string, string) is broken.");
int i{};
PQXX_CHECK(f1.to(i), "to(int) failed.");
PQXX_CHECK_EQUAL(i, 9, "to(int) is broken.");
i = 8;
PQXX_CHECK(f1.to(i, 12), "to(int, int) failed.");
PQXX_CHECK_EQUAL(i, 9, "to(int, int) is broken.");
auto const r2{tx.exec1("SELECT NULL")};
auto const f2{r2[0]};
i = 100;
PQXX_CHECK_THROWS(
f2.as<int>(), pqxx::conversion_error, "Null conversion failed to throw.");
PQXX_CHECK_EQUAL(i, 100, "Null conversion touched its output.");
PQXX_CHECK_EQUAL(f2.as<int>(66), 66, "as<int> default is broken.");
PQXX_CHECK(!(f2.to(i)), "to(int) failed to report a null.");
PQXX_CHECK(!(f2.to(i, 54)), "to(int, int) failed to report a null.");
PQXX_CHECK_EQUAL(i, 54, "to(int, int) failed to default.");
auto const r3{tx.exec("SELECT generate_series(1, 5)")};
PQXX_CHECK_EQUAL(r3.at(3, 0).as<int>(), 4, "Two-argument at() went wrong.");
#if defined(PQXX_HAVE_MULTIDIMENSIONAL_SUBSCRIPT)
PQXX_CHECK_EQUAL((r3[3, 0].as<int>()), 4, "Two-argument [] went wrong.");
#endif
}
PQXX_REGISTER_TEST(test_field);
} // namespace

View File

@@ -0,0 +1,175 @@
#include <cmath>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
/// Test conversions for some floating-point type.
template<typename T> void infinity_test()
{
T inf{std::numeric_limits<T>::infinity()};
std::string inf_string;
T back_conversion;
inf_string = pqxx::to_string(inf);
pqxx::from_string(inf_string, back_conversion);
PQXX_CHECK_LESS(
T(999999999), back_conversion,
"Infinity doesn't convert back to something huge.");
inf_string = pqxx::to_string(-inf);
pqxx::from_string(inf_string, back_conversion);
PQXX_CHECK_LESS(
back_conversion, -T(999999999), "Negative infinity is broken");
}
void test_infinities()
{
infinity_test<float>();
infinity_test<double>();
infinity_test<long double>();
}
/// Reproduce bug #262: repeated float conversions break without charconv.
template<typename T> void bug_262()
{
pqxx::connection conn;
conn.prepare("stmt", "select cast($1 as float)");
pqxx::work tr{conn};
// We must use the same float type both for passing the value to the
// statement and for retrieving result of the statement execution. This is
// due to an internal stringstream being instantiated as a a parameterized
// thread-local singleton. So, there are separate stream<float>,
// stream<double>, stream<long double>, but every such instance is a
// singleton. We should use only one of them for this test.
pqxx::row row;
// Nothing bad here, select a float value.
// The stream is clear, so just fill it with the value and extract str().
row = tr.exec1("SELECT 1.0");
// This works properly, but as we parse the value from the stream, the
// seeking cursor moves towards the EOF. When the inevitable EOF happens
// 'eof' flag is set in the stream and 'good' flag is unset.
row[0].as<T>();
// The second try. Select a float value again. The stream is not clean, so
// we need to put an empty string into its buffer {stream.str("");}. This
// resets the seeking cursor to 0. Then we will put the value using
// operator<<().
// ...
// ...
// OOPS. stream.str("") does not reset 'eof' flag and 'good' flag! We are
// trying to read from EOF! This is no good.
// Throws on unpatched pqxx v6.4.5
row = tr.exec1("SELECT 2.0");
// We won't get here without patch. The following statements are just for
// demonstration of how are intended to work. If we
// simply just reset the stream flags properly, this would work fine.
// The most obvious patch is just explicitly stream.seekg(0).
row[0].as<T>();
row = tr.exec1("SELECT 3.0");
row[0].as<T>();
}
/// Test for bug #262.
void test_bug_262()
{
bug_262<float>();
bug_262<double>();
bug_262<long double>();
}
/// Test conversion of malformed floating-point values.
void test_bad_float()
{
float x [[maybe_unused]];
PQXX_CHECK_THROWS(
x = pqxx::from_string<float>(""), pqxx::conversion_error,
"Conversion of empty string to float was not caught.");
PQXX_CHECK_THROWS(
x = pqxx::from_string<float>("Infancy"), pqxx::conversion_error,
"Misleading infinity was not caught.");
PQXX_CHECK_THROWS(
x = pqxx::from_string<float>("-Infighting"), pqxx::conversion_error,
"Misleading negative infinity was not caught.");
PQXX_CHECK_THROWS(
x = pqxx::from_string<float>("Nanny"), pqxx::conversion_error,
"Conversion of misleading NaN was not caught.");
}
template<typename T> void test_float_length(T value)
{
auto const text{pqxx::to_string(value)};
PQXX_CHECK_GREATER_EQUAL(
pqxx::size_buffer(value), std::size(text) + 1,
"Not enough buffer space for " + text + ".");
}
/// Test conversion of long float values to strings.
void test_long_float()
{
test_float_length(0.1f);
test_float_length(0.1);
test_float_length(std::numeric_limits<float>::denorm_min());
test_float_length(-std::numeric_limits<float>::denorm_min());
test_float_length(std::numeric_limits<float>::min());
test_float_length(-std::numeric_limits<float>::min());
test_float_length(std::numeric_limits<float>::max());
test_float_length(-std::numeric_limits<float>::max());
test_float_length(-std::nextafter(1.0f, 2.0f));
test_float_length(std::numeric_limits<double>::denorm_min());
test_float_length(-std::numeric_limits<double>::denorm_min());
test_float_length(std::numeric_limits<double>::min());
test_float_length(-std::numeric_limits<double>::min());
test_float_length(std::numeric_limits<double>::max());
test_float_length(-std::numeric_limits<double>::max());
test_float_length(-std::nextafter(1.0, 2.0));
test_float_length(std::numeric_limits<long double>::denorm_min());
test_float_length(-std::numeric_limits<long double>::denorm_min());
test_float_length(std::numeric_limits<long double>::min());
test_float_length(-std::numeric_limits<long double>::min());
test_float_length(std::numeric_limits<long double>::max());
test_float_length(-std::numeric_limits<long double>::max());
test_float_length(-std::nextafter(1.0L, 2.0L));
// Ahem. I'm not proud of this. We really can't assume much about the
// floating-point types, but I'd really like to try a few things to see that
// buffer sizes are in the right ballpark. So, if "double" is at least 64
// bits, check for some examples of long conversions.
if constexpr (sizeof(double) >= 8)
{
auto constexpr awkward{-2.2250738585072014e-308};
auto const text{pqxx::to_string(awkward)};
PQXX_CHECK_LESS_EQUAL(
std::size(text), 25u, text + " converted to too long a string.");
}
if constexpr (sizeof(double) <= 8)
{
auto const text{pqxx::to_string(0.99)};
PQXX_CHECK_LESS_EQUAL(
pqxx::size_buffer(0.99), 25u, text + " converted to too long a string.");
}
}
PQXX_REGISTER_TEST(test_infinities);
PQXX_REGISTER_TEST(test_bug_262);
PQXX_REGISTER_TEST(test_bad_float);
PQXX_REGISTER_TEST(test_long_float);
} // namespace

View File

@@ -0,0 +1,58 @@
#include <iostream>
#include <sstream>
#include <pqxx/largeobject>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_stream_large_object()
{
pqxx::connection conn;
// Construct a really nasty string. (Don't just construct a std::string from
// a char[] constant, because it'll terminate at the embedded zero.)
//
// The crucial thing is the "ff" byte at the beginning. It tests for
// possible conflation between "eof" (-1) and a char which just happens to
// have the same bit pattern as an 8-bit value of -1. This conflation can be
// a problem when it occurs at buffer boundaries.
constexpr char bytes[]{"\xff\0end"};
std::string const contents{bytes, std::size(bytes)};
pqxx::work tx{conn};
#include "pqxx/internal/ignore-deprecated-pre.hxx"
pqxx::largeobject new_obj{tx};
pqxx::olostream write{tx, new_obj};
write << contents;
write.flush();
pqxx::largeobjectaccess check{tx, new_obj, std::ios::in | std::ios::binary};
std::array<char, 50> buf;
std::size_t const len{
static_cast<std::size_t>(check.read(std::data(buf), std::size(buf)))};
PQXX_CHECK_EQUAL(len, std::size(contents), "olostream truncated data.");
std::string const check_str{std::data(buf), len};
PQXX_CHECK_EQUAL(check_str, contents, "olostream mangled data.");
pqxx::ilostream read{tx, new_obj};
std::string read_back;
std::string chunk;
while (read >> chunk) read_back += chunk;
new_obj.remove(tx);
PQXX_CHECK_EQUAL(read_back, contents, "Got wrong data from ilostream.");
PQXX_CHECK_EQUAL(
std::size(read_back), std::size(contents), "ilostream truncated data.");
PQXX_CHECK_EQUAL(
std::size(read_back), std::size(bytes), "ilostream truncated data.");
#include "pqxx/internal/ignore-deprecated-post.hxx"
}
PQXX_REGISTER_TEST(test_stream_large_object);
} // namespace

View File

@@ -0,0 +1,27 @@
#include <pqxx/transaction>
#include <pqxx/internal/wait.hxx>
#include "../test_helpers.hxx"
namespace
{
void test_nonblocking_connect()
{
pqxx::connecting nbc;
while (not nbc.done())
{
pqxx::internal::wait_fd(
nbc.sock(), nbc.wait_to_read(), nbc.wait_to_write());
nbc.process();
}
pqxx::connection conn{std::move(nbc).produce()};
pqxx::work tx{conn};
PQXX_CHECK_EQUAL(tx.query_value<int>("SELECT 10"), 10, "Bad value!?");
}
PQXX_REGISTER_TEST(test_nonblocking_connect);
} // namespace

View File

@@ -0,0 +1,86 @@
#include <chrono>
#include <pqxx/internal/header-pre.hxx>
#include <pqxx/internal/wait.hxx>
#include <pqxx/internal/header-post.hxx>
#include <pqxx/nontransaction>
#include <pqxx/notification>
#include "../test_helpers.hxx"
namespace
{
class TestReceiver final : public pqxx::notification_receiver
{
public:
std::string payload;
int backend_pid;
TestReceiver(pqxx::connection &c, std::string const &channel_name) :
pqxx::notification_receiver(c, channel_name),
payload(),
backend_pid(0)
{}
virtual void
operator()(std::string const &payload_string, int backend) override
{
this->payload = payload_string;
this->backend_pid = backend;
}
};
void test_receive(
pqxx::transaction_base &t, std::string const &channel,
char const payload[] = nullptr)
{
pqxx::connection &conn(t.conn());
std::string SQL{"NOTIFY \"" + channel + "\""};
if (payload != nullptr)
SQL += ", " + t.quote(payload);
TestReceiver receiver{t.conn(), channel};
// Clear out any previously pending notifications that might otherwise
// confuse the test.
conn.get_notifs();
// Notify, and receive.
t.exec(SQL);
t.commit();
int notifs{0};
for (int i{0}; (i < 10) and (notifs == 0);
++i, pqxx::internal::wait_for(1'000'000u))
notifs = conn.get_notifs();
PQXX_CHECK_EQUAL(notifs, 1, "Got wrong number of notifications.");
PQXX_CHECK_EQUAL(receiver.backend_pid, conn.backendpid(), "Bad pid.");
if (payload == nullptr)
PQXX_CHECK(std::empty(receiver.payload), "Unexpected payload.");
else
PQXX_CHECK_EQUAL(receiver.payload, payload, "Bad payload.");
}
void test_notification()
{
pqxx::connection conn;
TestReceiver receiver(conn, "mychannel");
PQXX_CHECK_EQUAL(receiver.channel(), "mychannel", "Bad channel.");
pqxx::work tx{conn};
test_receive(tx, "channel1");
pqxx::nontransaction u(conn);
test_receive(u, "channel2", "payload");
}
PQXX_REGISTER_TEST(test_notification);
} // namespace

View File

@@ -0,0 +1,64 @@
#include <chrono>
#include <pqxx/pipeline>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_pipeline()
{
pqxx::connection conn;
pqxx::work tx{conn};
// A pipeline grabs transaction focus, blocking regular queries and such.
pqxx::pipeline pipe(tx, "test_pipeline_detach");
PQXX_CHECK_THROWS(
tx.exec("SELECT 1"), std::logic_error,
"Pipeline does not block regular queries");
// Flushing a pipeline relinquishes transaction focus.
pipe.flush();
auto r{tx.exec("SELECT 2")};
PQXX_CHECK_EQUAL(
std::size(r), 1, "Wrong query result after flushing pipeline.");
PQXX_CHECK_EQUAL(
r[0][0].as<int>(), 2, "Query returns wrong data after flushing pipeline.");
// Inserting a query makes the pipeline grab transaction focus back.
auto q{pipe.insert("SELECT 2")};
PQXX_CHECK_THROWS(
tx.exec("SELECT 3"), std::logic_error,
"Pipeline does not block regular queries");
// Invoking complete() also detaches the pipeline from the transaction.
pipe.complete();
r = tx.exec("SELECT 4");
PQXX_CHECK_EQUAL(std::size(r), 1, "Wrong query result after complete().");
PQXX_CHECK_EQUAL(
r[0][0].as<int>(), 4, "Query returns wrong data after complete().");
// The complete() also received any pending query results from the backend.
r = pipe.retrieve(q);
PQXX_CHECK_EQUAL(std::size(r), 1, "Wrong result from pipeline.");
PQXX_CHECK_EQUAL(r[0][0].as<int>(), 2, "Pipeline returned wrong data.");
// We can cancel while the pipe is empty, and things will still work.
pipe.cancel();
// Issue a query and cancel it. Measure time to see that we don't really
// wait.
using clock = std::chrono::steady_clock;
auto const start{clock::now()};
pipe.retain(0);
pipe.insert("pg_sleep(10)");
pipe.cancel();
auto const finish{clock::now()};
auto const seconds{
std::chrono::duration_cast<std::chrono::seconds>(finish - start).count()};
PQXX_CHECK_LESS(seconds, 5, "Canceling a sleep took suspiciously long.");
}
} // namespace
PQXX_REGISTER_TEST(test_pipeline);

View File

@@ -0,0 +1,334 @@
#include <cassert>
#include <iostream>
#include <iterator>
#include <list>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
// Test program for libpqxx. Define and use prepared statements.
#define COMPARE_RESULTS(name, lhs, rhs) \
PQXX_CHECK_EQUAL( \
rhs, lhs, \
"Executing " name " as prepared statement yields different results.");
namespace
{
using namespace std::literals;
template<typename T> std::string stringize(pqxx::transaction_base &t, T i)
{
return stringize(t, pqxx::to_string(i));
}
// Substitute variables in raw query. This is not likely to be very robust,
// but it should do for just this test. The main shortcomings are escaping,
// and not knowing when to quote the variables.
// Note we do the replacement backwards (meaning forward_only iterators won't
// do!) to avoid substituting e.g. "$12" as "$1" first.
template<typename ITER>
std::string
subst(pqxx::transaction_base &t, std::string q, ITER patbegin, ITER patend)
{
ptrdiff_t i{distance(patbegin, patend)};
for (ITER arg{patend}; i > 0; --i)
{
--arg;
std::string const marker{"$" + pqxx::to_string(i)},
var{stringize(t, *arg)};
std::string::size_type const msz{std::size(marker)};
while (q.find(marker) != std::string::npos)
q.replace(q.find(marker), msz, var);
}
return q;
}
template<typename CNTNR>
std::string
subst(pqxx::transaction_base &t, std::string const &q, CNTNR const &patterns)
{
return subst(t, q, std::begin(patterns), std::end(patterns));
}
void test_registration_and_invocation()
{
constexpr auto count_to_5{"SELECT * FROM generate_series(1, 5)"};
pqxx::connection c;
pqxx::work tx1{c};
// Prepare a simple statement.
tx1.conn().prepare("CountToFive", count_to_5);
// The statement returns exactly what you'd expect.
COMPARE_RESULTS(
"CountToFive", tx1.exec_prepared("CountToFive"), tx1.exec(count_to_5));
// Re-preparing it is an error.
PQXX_CHECK_THROWS(
tx1.conn().prepare("CountToFive", count_to_5), pqxx::sql_error,
"Did not report re-definition of prepared statement.");
tx1.abort();
pqxx::work tx2{c};
// Executing a nonexistent prepared statement is also an error.
PQXX_CHECK_THROWS(
tx2.exec_prepared("NonexistentStatement"), pqxx::sql_error,
"Did not report invocation of nonexistent prepared statement.");
}
void test_basic_args()
{
pqxx::connection c;
c.prepare("EchoNum", "SELECT $1::int");
pqxx::work tx{c};
auto r{tx.exec_prepared("EchoNum", 7)};
PQXX_CHECK_EQUAL(
std::size(r), 1, "Did not get 1 row from prepared statement.");
PQXX_CHECK_EQUAL(std::size(r.front()), 1, "Did not get exactly one column.");
PQXX_CHECK_EQUAL(r[0][0].as<int>(), 7, "Got wrong result.");
auto rw{tx.exec_prepared1("EchoNum", 8)};
PQXX_CHECK_EQUAL(
std::size(rw), 1, "Did not get 1 column from exec_prepared1.");
PQXX_CHECK_EQUAL(rw[0].as<int>(), 8, "Got wrong result.");
}
void test_multiple_params()
{
pqxx::connection c;
c.prepare("CountSeries", "SELECT * FROM generate_series($1::int, $2::int)");
pqxx::work tx{c};
auto r{tx.exec_prepared_n(4, "CountSeries", 7, 10)};
PQXX_CHECK_EQUAL(
std::size(r), 4, "Wrong number of rows, but no error raised.");
PQXX_CHECK_EQUAL(r.front().front().as<int>(), 7, "Wrong $1.");
PQXX_CHECK_EQUAL(r.back().front().as<int>(), 10, "Wrong $2.");
c.prepare("Reversed", "SELECT * FROM generate_series($2::int, $1::int)");
r = tx.exec_prepared_n(3, "Reversed", 8, 6);
PQXX_CHECK_EQUAL(
r.front().front().as<int>(), 6, "Did parameters get reordered?");
PQXX_CHECK_EQUAL(
r.back().front().as<int>(), 8, "$2 did not come through properly.");
}
void test_nulls()
{
pqxx::connection c;
pqxx::work tx{c};
c.prepare("EchoStr", "SELECT $1::varchar");
auto rw{tx.exec_prepared1("EchoStr", nullptr)};
PQXX_CHECK(rw.front().is_null(), "nullptr did not translate to null.");
char const *n{nullptr};
rw = tx.exec_prepared1("EchoStr", n);
PQXX_CHECK(rw.front().is_null(), "Null pointer did not translate to null.");
}
void test_strings()
{
pqxx::connection c;
pqxx::work tx{c};
c.prepare("EchoStr", "SELECT $1::varchar");
auto rw{tx.exec_prepared1("EchoStr", "foo")};
PQXX_CHECK_EQUAL(
rw.front().as<std::string>(), "foo", "Wrong string result.");
char const nasty_string[]{R"--('\"\)--"};
rw = tx.exec_prepared1("EchoStr", nasty_string);
PQXX_CHECK_EQUAL(
rw.front().as<std::string>(), std::string(nasty_string),
"Prepared statement did not quote/escape correctly.");
rw = tx.exec_prepared1("EchoStr", std::string{nasty_string});
PQXX_CHECK_EQUAL(
rw.front().as<std::string>(), std::string(nasty_string),
"Quoting/escaping went wrong in std::string.");
char nonconst[]{"non-const C string"};
rw = tx.exec_prepared1("EchoStr", nonconst);
PQXX_CHECK_EQUAL(
rw.front().as<std::string>(), std::string(nonconst),
"Non-const C string passed incorrectly.");
}
void test_binary()
{
pqxx::connection c;
pqxx::work tx{c};
c.prepare("EchoBin", "SELECT $1::bytea");
constexpr char raw_bytes[]{"Binary\0bytes'\"with\tweird\xff bytes"};
std::string const input{raw_bytes, std::size(raw_bytes)};
#include "pqxx/internal/ignore-deprecated-pre.hxx"
{
pqxx::binarystring const bin{input};
auto rw{tx.exec_prepared1("EchoBin", bin)};
PQXX_CHECK_EQUAL(
pqxx::binarystring(rw[0]).str(), input,
"Binary string came out damaged.");
}
#include "pqxx/internal/ignore-deprecated-post.hxx"
{
std::basic_string<std::byte> bytes{
reinterpret_cast<std::byte const *>(raw_bytes), std::size(raw_bytes)};
auto bp{tx.exec_prepared1("EchoBin", bytes)};
auto bval{bp[0].as<std::basic_string<std::byte>>()};
PQXX_CHECK_EQUAL(
(std::string_view{
reinterpret_cast<char const *>(bval.c_str()), std::size(bval)}),
input, "Binary string parameter went wrong.");
}
// Now try it with a complex type that ultimately uses the conversions of
// std::basic_string<std::byte>, but complex enough that the call may
// convert the data to a text string on the libpqxx side. Which would be
// okay, except of course it's likely to be slower.
{
auto ptr{std::make_shared<std::basic_string<std::byte>>(
reinterpret_cast<std::byte const *>(raw_bytes), std::size(raw_bytes))};
auto rp{tx.exec_prepared1("EchoBin", ptr)};
auto pval{rp[0].as<std::basic_string<std::byte>>()};
PQXX_CHECK_EQUAL(
(std::string_view{
reinterpret_cast<char const *>(pval.c_str()), std::size(pval)}),
input, "Binary string as shared_ptr-to-optional went wrong.");
}
{
auto opt{std::optional<std::basic_string<std::byte>>{
std::in_place, reinterpret_cast<std::byte const *>(raw_bytes),
std::size(raw_bytes)}};
auto op{tx.exec_prepared1("EchoBin", opt)};
auto oval{op[0].as<std::basic_string<std::byte>>()};
PQXX_CHECK_EQUAL(
(std::string_view{
reinterpret_cast<char const *>(oval.c_str()), std::size(oval)}),
input, "Binary string as shared_ptr-to-optional went wrong.");
}
#if defined(PQXX_HAVE_CONCEPTS)
// By the way, it doesn't have to be a std::basic_string. Any contiguous
// range will do.
{
std::vector<std::byte> data{std::byte{'x'}, std::byte{'v'}};
auto op{tx.exec_prepared1("EchoBin", data)};
auto oval{op[0].as<std::basic_string<std::byte>>()};
PQXX_CHECK_EQUAL(
std::size(oval), 2u, "Binary data came back as wrong length.");
PQXX_CHECK_EQUAL(static_cast<int>(oval[0]), int('x'), "Wrong data.");
PQXX_CHECK_EQUAL(static_cast<int>(oval[1]), int('v'), "Wrong data.");
}
#endif
}
void test_params()
{
pqxx::connection c;
pqxx::work tx{c};
c.prepare("Concat2Numbers", "SELECT 10 * $1 + $2");
std::vector<int> values{3, 9};
pqxx::params params;
params.reserve(std::size(values));
params.append_multi(values);
auto const rw39{tx.exec_prepared1("Concat2Numbers", params)};
PQXX_CHECK_EQUAL(
rw39.front().as<int>(), 39,
"Dynamic prepared-statement parameters went wrong.");
c.prepare("Concat4Numbers", "SELECT 1000*$1 + 100*$2 + 10*$3 + $4");
auto const rw1396{tx.exec_prepared1("Concat4Numbers", 1, params, 6)};
PQXX_CHECK_EQUAL(
rw1396.front().as<int>(), 1396,
"Dynamic params did not interleave with static ones properly.");
}
void test_optional()
{
pqxx::connection c;
pqxx::work tx{c};
c.prepare("EchoNum", "SELECT $1::int");
pqxx::row rw{
tx.exec_prepared1("EchoNum", std::optional<int>{std::in_place, 10})};
PQXX_CHECK_EQUAL(
rw.front().as<int>(), 10,
"optional (with value) did not return the right value.");
rw = tx.exec_prepared1("EchoNum", std::optional<int>{});
PQXX_CHECK(
rw.front().is_null(), "optional without value did not come out as null.");
}
void test_prepared_statements()
{
test_registration_and_invocation();
test_basic_args();
test_multiple_params();
test_nulls();
test_strings();
test_binary();
test_params();
test_optional();
}
void test_placeholders_generates_names()
{
using pqxx::operator""_zv;
pqxx::placeholders name;
PQXX_CHECK_EQUAL(name.view(), "$1"_zv, "Bad placeholders initial zview.");
PQXX_CHECK_EQUAL(name.view(), "$1"sv, "Bad placeholders string_view.");
PQXX_CHECK_EQUAL(name.get(), "$1", "Bad placeholders::get().");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$2"_zv, "Incorrect placeholders::next().");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$3"_zv, "Incorrect placeholders::next().");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$4"_zv, "Incorrect placeholders::next().");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$5"_zv, "Incorrect placeholders::next().");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$6"_zv, "Incorrect placeholders::next().");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$7"_zv, "Incorrect placeholders::next().");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$8"_zv, "Incorrect placeholders::next().");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$9"_zv, "Incorrect placeholders::next().");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$10"_zv, "Incorrect placeholders carry.");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$11"_zv, "Incorrect placeholders 11.");
while (name.count() < 999) name.next();
PQXX_CHECK_EQUAL(name.view(), "$999"_zv, "Incorrect placeholders 999.");
name.next();
PQXX_CHECK_EQUAL(name.view(), "$1000"_zv, "Incorrect large placeholder.");
}
PQXX_REGISTER_TEST(test_prepared_statements);
PQXX_REGISTER_TEST(test_placeholders_generates_names);
} // namespace

View File

@@ -0,0 +1,555 @@
#include <pqxx/range>
#include <pqxx/strconv>
#include "../test_helpers.hxx"
namespace
{
void test_range_construct()
{
using optint = std::optional<int>;
using oibound = pqxx::inclusive_bound<std::optional<int>>;
using oxbound = pqxx::inclusive_bound<std::optional<int>>;
PQXX_CHECK_THROWS(
(pqxx::range<optint>{oibound{optint{}}, oibound{optint{}}}),
pqxx::argument_error, "Inclusive bound accepted a null.");
PQXX_CHECK_THROWS(
(pqxx::range<optint>{oxbound{optint{}}, oxbound{optint{}}}),
pqxx::argument_error, "Exclusive bound accepted a null.");
using ibound = pqxx::inclusive_bound<int>;
PQXX_CHECK_THROWS(
(pqxx::range<int>{ibound{1}, ibound{0}}), pqxx::range_error,
"Range constructor accepted backwards range.");
PQXX_CHECK_THROWS(
(pqxx::range<float>{
pqxx::inclusive_bound<float>{-1000.0},
pqxx::inclusive_bound<float>{-std::numeric_limits<float>::infinity()}}),
pqxx::range_error,
"Was able to construct range with infinity bound at the wrong end.");
}
void test_range_equality()
{
using range = pqxx::range<int>;
using ibound = pqxx::inclusive_bound<int>;
using xbound = pqxx::exclusive_bound<int>;
using ubound = pqxx::no_bound;
PQXX_CHECK_EQUAL(
range{}, range{}, "Default-constructed range is not consistent.");
PQXX_CHECK_EQUAL(
(range{xbound{0}, xbound{0}}), (range{xbound{5}, xbound{5}}),
"Empty ranges at different values are not equal.");
PQXX_CHECK_EQUAL(
(range{ubound{}, ubound{}}), (range{ubound{}, ubound{}}),
"Universal range is inconsistent.");
PQXX_CHECK_EQUAL(
(range{ibound{5}, ibound{8}}), (range{ibound{5}, ibound{8}}),
"Inclusive range is inconsistent.");
PQXX_CHECK_EQUAL(
(range{xbound{5}, xbound{8}}), (range{xbound{5}, xbound{8}}),
"Exclusive range is inconsistent.");
PQXX_CHECK_EQUAL(
(range{xbound{5}, ibound{8}}), (range{xbound{5}, ibound{8}}),
"Left-exclusive interval is not equal to itself.");
PQXX_CHECK_EQUAL(
(range{ibound{5}, xbound{8}}), (range{ibound{5}, xbound{8}}),
"Right-exclusive interval is not equal to itself.");
PQXX_CHECK_EQUAL(
(range{ubound{}, ibound{8}}), (range{ubound{}, ibound{8}}),
"Unlimited lower bound does not compare equal to same.");
PQXX_CHECK_EQUAL(
(range{ibound{8}, ubound{}}), (range{ibound{8}, ubound{}}),
"Unlimited upper bound does not compare equal to same.");
PQXX_CHECK_NOT_EQUAL(
(range{ibound{5}, ibound{8}}), (range{xbound{5}, ibound{8}}),
"Equality does not detect inclusive vs. exclusive lower bound.");
PQXX_CHECK_NOT_EQUAL(
(range{ibound{5}, ibound{8}}), (range{ubound{}, ibound{8}}),
"Equality does not detect inclusive vs. unlimited lower bound.");
PQXX_CHECK_NOT_EQUAL(
(range{xbound{5}, ibound{8}}), (range{ubound{}, ibound{8}}),
"Equality does not detect exclusive vs. unlimited lower bound.");
PQXX_CHECK_NOT_EQUAL(
(range{ibound{5}, ibound{8}}), (range{ibound{5}, xbound{8}}),
"Equality does not detect inclusive vs. exclusive upper bound.");
PQXX_CHECK_NOT_EQUAL(
(range{ibound{5}, ibound{8}}), (range{ibound{5}, ubound{}}),
"Equality does not detect inclusive vs. unlimited upper bound.");
PQXX_CHECK_NOT_EQUAL(
(range{ibound{5}, xbound{8}}), (range{ibound{5}, ubound{}}),
"Equality does not detect exclusive vs. unlimited upper bound.");
PQXX_CHECK_NOT_EQUAL(
(range{ibound{5}, ibound{8}}), (range{ibound{4}, ibound{8}}),
"Equality does not compare lower inclusive bound value.");
PQXX_CHECK_NOT_EQUAL(
(range{xbound{5}, ibound{8}}), (range{xbound{4}, ibound{8}}),
"Equality does not compare lower exclusive bound value.");
PQXX_CHECK_NOT_EQUAL(
(range{xbound{5}, ibound{8}}), (range{xbound{5}, ibound{7}}),
"Equality does not compare upper inclusive bound value.");
PQXX_CHECK_NOT_EQUAL(
(range{xbound{5}, xbound{8}}), (range{xbound{5}, xbound{7}}),
"Equality does not compare lower exclusive bound value.");
}
void test_range_empty()
{
using range = pqxx::range<int>;
using ibound = pqxx::inclusive_bound<int>;
using xbound = pqxx::exclusive_bound<int>;
using ubound = pqxx::no_bound;
PQXX_CHECK((range{}.empty()), "Default-constructed range is not empty.");
PQXX_CHECK(
(range{ibound{10}, xbound{10}}).empty(),
"Right-exclusive zero-length interval is not empty.");
PQXX_CHECK(
(range{xbound{10}, ibound{10}}).empty(),
"Left-exclusive zero-length interval is not empty.");
PQXX_CHECK(
(range{xbound{10}, xbound{10}}).empty(),
"Exclusive zero-length interval is not empty.");
PQXX_CHECK(
not(range{ibound{10}, ibound{10}}).empty(),
"Inclusive zero-length interval is empty.");
PQXX_CHECK(
not(range{xbound{10}, ibound{11}}.empty()),
"Interval is incorrectly empty.");
PQXX_CHECK(
not(range{ubound{}, ubound{}}.empty()),
"Double-unlimited interval is empty.");
PQXX_CHECK(
not(range{ubound{}, xbound{0}}.empty()),
"Left-unlimited interval is empty.");
PQXX_CHECK(
not(range{xbound{0}, ubound{}}.empty()),
"Right-unlimited interval is empty.");
}
void test_range_contains()
{
using range = pqxx::range<int>;
using ibound = pqxx::inclusive_bound<int>;
using xbound = pqxx::exclusive_bound<int>;
using ubound = pqxx::no_bound;
PQXX_CHECK(not(range{}.contains(-1)), "Empty range contains a value.");
PQXX_CHECK(not(range{}.contains(0)), "Empty range contains a value.");
PQXX_CHECK(not(range{}.contains(1)), "Empty range contains a value.");
PQXX_CHECK(
not(range{ibound{5}, ibound{8}}.contains(4)),
"Inclusive range contains value outside its left bound.");
PQXX_CHECK(
(range{ibound{5}, ibound{8}}.contains(5)),
"Inclusive range does not contain value on its left bound.");
PQXX_CHECK(
(range{ibound{5}, ibound{8}}.contains(6)),
"Inclusive range does not contain value inside it.");
PQXX_CHECK(
(range{ibound{5}, ibound{8}}.contains(8)),
"Inclusive range does not contain value on its right bound.");
PQXX_CHECK(
not(range{ibound{5}, ibound{8}}.contains(9)),
"Inclusive range contains value outside its right bound.");
PQXX_CHECK(
not(range{ibound{5}, xbound{8}}.contains(4)),
"Left-inclusive range contains value outside its left bound.");
PQXX_CHECK(
(range{ibound{5}, xbound{8}}.contains(5)),
"Left-inclusive range does not contain value on its left bound.");
PQXX_CHECK(
(range{ibound{5}, xbound{8}}.contains(6)),
"Left-inclusive range does not contain value inside it.");
PQXX_CHECK(
not(range{ibound{5}, xbound{8}}.contains(8)),
"Left-inclusive range contains value on its right bound.");
PQXX_CHECK(
not(range{ibound{5}, xbound{8}}.contains(9)),
"Left-inclusive range contains value outside its right bound.");
PQXX_CHECK(
not(range{xbound{5}, ibound{8}}.contains(4)),
"Right-inclusive range contains value outside its left bound.");
PQXX_CHECK(
not(range{xbound{5}, ibound{8}}.contains(5)),
"Right-inclusive range does contains value on its left bound.");
PQXX_CHECK(
(range{xbound{5}, ibound{8}}.contains(6)),
"Right-inclusive range does not contain value inside it.");
PQXX_CHECK(
(range{xbound{5}, ibound{8}}.contains(8)),
"Right-inclusive range does not contain value on its right bound.");
PQXX_CHECK(
not(range{xbound{5}, ibound{8}}.contains(9)),
"Right-inclusive range contains value outside its right bound.");
PQXX_CHECK(
not(range{xbound{5}, xbound{8}}.contains(4)),
"Exclusive range contains value outside its left bound.");
PQXX_CHECK(
not(range{xbound{5}, xbound{8}}.contains(5)),
"Exclusive range contains value on its left bound.");
PQXX_CHECK(
(range{xbound{5}, xbound{8}}.contains(6)),
"Exclusive range does not contain value inside it.");
PQXX_CHECK(
not(range{xbound{5}, xbound{8}}.contains(8)),
"Exclusive range does contains value on its right bound.");
PQXX_CHECK(
not(range{xbound{5}, xbound{8}}.contains(9)),
"Exclusive range contains value outside its right bound.");
PQXX_CHECK(
(range{ubound{}, ibound{8}}.contains(7)),
"Right-inclusive range does not contain value inside it.");
PQXX_CHECK(
(range{ubound{}, ibound{8}}.contains(8)),
"Right-inclusive range does not contain value on its right bound.");
PQXX_CHECK(
not(range{ubound{}, ibound{8}}.contains(9)),
"Right-inclusive range contains value outside its right bound.");
PQXX_CHECK(
(range{ubound{}, xbound{8}}.contains(7)),
"Right-exclusive range does not contain value inside it.");
PQXX_CHECK(
not(range{ubound{}, xbound{8}}.contains(8)),
"Right-exclusive range contains value on its right bound.");
PQXX_CHECK(
not(range{ubound{}, xbound{8}}.contains(9)),
"Right-exclusive range contains value outside its right bound.");
PQXX_CHECK(
not(range{ibound{5}, ubound{}}.contains(4)),
"Left-inclusive range contains value outside its left bound.");
PQXX_CHECK(
(range{ibound{5}, ubound{}}.contains(5)),
"Left-inclusive range does not contain value on its left bound.");
PQXX_CHECK(
(range{ibound{5}, ubound{}}.contains(6)),
"Left-inclusive range does not contain value inside it.");
PQXX_CHECK(
not(range{xbound{5}, ubound{}}.contains(4)),
"Left-exclusive range contains value outside its left bound.");
PQXX_CHECK(
not(range{xbound{5}, ubound{}}.contains(5)),
"Left-exclusive range contains value on its left bound.");
PQXX_CHECK(
(range{xbound{5}, ubound{}}.contains(6)),
"Left-exclusive range does not contain value inside it.");
PQXX_CHECK(
(range{ubound{}, ubound{}}.contains(-1)), "Value not in universal range.");
PQXX_CHECK(
(range{ubound{}, ubound{}}.contains(0)), "Value not in universal range.");
PQXX_CHECK(
(range{ubound{}, ubound{}}.contains(1)), "Value not in universal range.");
}
void test_float_range_contains()
{
using range = pqxx::range<double>;
using ibound = pqxx::inclusive_bound<double>;
using xbound = pqxx::exclusive_bound<double>;
using ubound = pqxx::no_bound;
using limits = std::numeric_limits<double>;
constexpr auto inf{limits::infinity()};
PQXX_CHECK(
not(range{ibound{4.0}, ibound{8.0}}.contains(3.9)),
"Float inclusive range contains value beyond its lower bound.");
PQXX_CHECK(
(range{ibound{4.0}, ibound{8.0}}.contains(4.0)),
"Float inclusive range does not contain its lower bound value.");
PQXX_CHECK(
(range{ibound{4.0}, ibound{8.0}}.contains(5.0)),
"Float inclusive range does not contain value inside it.");
PQXX_CHECK(
(range{ibound{0}, ibound{inf}}).contains(9999.0),
"Range to infinity did not include large number.");
PQXX_CHECK(
not(range{ibound{0}, ibound{inf}}.contains(-0.1)),
"Range to infinity includes number outside it.");
PQXX_CHECK(
(range{ibound{0}, xbound{inf}}.contains(9999.0)),
"Range to exclusive infinity did not include large number.");
PQXX_CHECK(
(range{ibound{0}, ibound{inf}}).contains(inf),
"Range to inclusive infinity does not include infinity.");
PQXX_CHECK(
not(range{ibound{0}, xbound{inf}}.contains(inf)),
"Range to exclusive infinity includes infinity.");
PQXX_CHECK(
(range{ibound{0}, ubound{}}).contains(inf),
"Right-unlimited range does not include infinity.");
PQXX_CHECK(
(range{ibound{-inf}, ibound{0}}).contains(-9999.0),
"Range from infinity did not include large negative number.");
PQXX_CHECK(
not(range{ibound{-inf}, ibound{0}}.contains(0.1)),
"Range from infinity includes number outside it.");
PQXX_CHECK(
(range{xbound{-inf}, ibound{0}}).contains(-9999.0),
"Range from exclusive infinity did not include large negative number.");
PQXX_CHECK(
(range{ibound{-inf}, ibound{0}}).contains(-inf),
"Range from inclusive infinity does not include negative infinity.");
PQXX_CHECK(
not(range{xbound{-inf}, ibound{0}}).contains(-inf),
"Range to infinity exclusive includes negative infinity.");
PQXX_CHECK(
(range{ubound{}, ibound{0}}).contains(-inf),
"Left-unlimited range does not include negative infinity.");
}
void test_range_subset()
{
using range = pqxx::range<int>;
using traits = pqxx::string_traits<range>;
std::string_view subsets[][2]{
{"empty", "empty"}, {"(,)", "empty"}, {"(0,1)", "empty"},
{"(,)", "[-10,10]"}, {"(,)", "(-10,10)"}, {"(,)", "(,)"},
{"(,10)", "(,10)"}, {"(,10)", "(,9)"}, {"(,10]", "(,10)"},
{"(,10]", "(,10]"}, {"(1,)", "(10,)"}, {"(1,)", "(9,)"},
{"[1,)", "(10,)"}, {"[1,)", "[10,)"}, {"[0,5]", "[1,4]"},
{"(0,5)", "[1,4]"},
};
for (auto const [super, sub] : subsets)
PQXX_CHECK(
traits::from_string(super).contains(traits::from_string(sub)),
pqxx::internal::concat(
"Range '", super, "' did not contain '", sub, "'."));
std::string_view non_subsets[][2]{
{"empty", "[0,0]"}, {"empty", "(,)"}, {"[-10,10]", "(,)"},
{"(-10,10)", "(,)"}, {"(,9)", "(,10)"}, {"(,10)", "(,10]"},
{"[1,4]", "[0,4]"}, {"[1,4]", "[1,5]"}, {"(0,10)", "[0,10]"},
{"(0,10)", "(0,10]"}, {"(0,10)", "[0,10)"},
};
for (auto const [super, sub] : non_subsets)
PQXX_CHECK(
not traits::from_string(super).contains(traits::from_string(sub)),
pqxx::internal::concat("Range '", super, "' contained '", sub, "'."));
}
void test_range_to_string()
{
using range = pqxx::range<int>;
using ibound = pqxx::inclusive_bound<int>;
using xbound = pqxx::exclusive_bound<int>;
using ubound = pqxx::no_bound;
PQXX_CHECK_EQUAL(
pqxx::to_string(range{}), "empty", "Empty range came out wrong.");
PQXX_CHECK_EQUAL(
pqxx::to_string(range{ibound{5}, ibound{8}}), "[5,8]",
"Inclusive range came out wrong.");
PQXX_CHECK_EQUAL(
pqxx::to_string(range{xbound{5}, ibound{8}}), "(5,8]",
"Left-exclusive range came out wrong.");
PQXX_CHECK_EQUAL(
pqxx::to_string(range{ibound{5}, xbound{8}}), "[5,8)",
"Right-exclusive range came out wrong.");
PQXX_CHECK_EQUAL(
pqxx::to_string(range{xbound{5}, xbound{8}}), "(5,8)",
"Exclusive range came out wrong.");
// Unlimited boundaries can use brackets or parentheses. Doesn't matter.
// We cheat and use some white-box knowledge of our implementation here.
PQXX_CHECK_EQUAL(
pqxx::to_string(range{ubound{}, ubound{}}), "(,)",
"Universal range came out unexpected.");
PQXX_CHECK_EQUAL(
pqxx::to_string(range{ubound{}, ibound{8}}), "(,8]",
"Left-unlimited range came out unexpected.");
PQXX_CHECK_EQUAL(
pqxx::to_string(range{ubound{}, xbound{8}}), "(,8)",
"Left-unlimited range came out unexpected.");
PQXX_CHECK_EQUAL(
pqxx::to_string(range{ibound{5}, ubound{}}), "[5,)",
"Right-unlimited range came out unexpected.");
PQXX_CHECK_EQUAL(
pqxx::to_string(range{xbound{5}, ubound{}}), "(5,)",
"Right-unlimited range came out unexpected.");
}
void test_parse_range()
{
using range = pqxx::range<int>;
using ubound = pqxx::no_bound;
using traits = pqxx::string_traits<range>;
constexpr std::string_view empties[]{"empty", "EMPTY", "eMpTy"};
for (auto empty : empties)
PQXX_CHECK(
traits::from_string(empty).empty(),
pqxx::internal::concat(
"This was supposed to produce an empty range: '", empty, "'"));
constexpr std::string_view universals[]{"(,)", "[,)", "(,]", "[,]"};
for (auto univ : universals)
PQXX_CHECK_EQUAL(
traits::from_string(univ), (range{ubound{}, ubound{}}),
pqxx::internal::concat(
"This was supposed to produce a universal range: '", univ, "'"));
PQXX_CHECK(
traits::from_string("(0,10]").lower_bound().is_exclusive(),
"Exclusive lower bound did not parse right.");
PQXX_CHECK(
traits::from_string("[0,10]").lower_bound().is_inclusive(),
"Inclusive lower bound did not parse right.");
PQXX_CHECK(
traits::from_string("(0,10)").upper_bound().is_exclusive(),
"Exclusive upper bound did not parse right.");
PQXX_CHECK(
traits::from_string("[0,10]").upper_bound().is_inclusive(),
"Inclusive upper bound did not parse right.");
PQXX_CHECK_EQUAL(
*traits::from_string("(\"0\",\"10\")").lower_bound().value(), 0,
"Quoted range boundary did not parse right.");
PQXX_CHECK_EQUAL(
*traits::from_string("(\"0\",\"10\")").upper_bound().value(), 10,
"Quoted upper boundary did not parse right.");
auto floats{
pqxx::string_traits<pqxx::range<double>>::from_string("(0,1.0)")};
PQXX_CHECK_GREATER(
*floats.lower_bound().value(), -0.001,
"Float lower bound is out of range.");
PQXX_CHECK_LESS(
*floats.lower_bound().value(), 0.001,
"Float lower bound is out of range.");
PQXX_CHECK_GREATER(
*floats.upper_bound().value(), 0.999,
"Float upper bound is out of range.");
PQXX_CHECK_LESS(
*floats.upper_bound().value(), 1.001,
"Float upper bound is out of range.");
}
void test_parse_bad_range()
{
using range = pqxx::range<int>;
using conv_err = pqxx::conversion_error;
using traits = pqxx::string_traits<range>;
constexpr std::string_view bad_ranges[]{
"", "x", "e", "empt", "emptyy", "()",
"[]", "(empty)", "(empty, 0)", "(0, empty)", ",", "(,",
",)", "(1,2,3)", "(4,5x)", "(null, 0)", "[0, 1.0]", "[1.0, 0]",
};
for (auto bad : bad_ranges)
PQXX_CHECK_THROWS(
pqxx::ignore_unused(traits::from_string(bad)), conv_err,
pqxx::internal::concat(
"This range wasn't supposed to parse: '", bad, "'"));
}
/// Parse ranges lhs and rhs, return their intersection as a string.
template<typename TYPE>
std::string intersect(std::string_view lhs, std::string_view rhs)
{
using traits = pqxx::string_traits<pqxx::range<TYPE>>;
return pqxx::to_string(traits::from_string(lhs) & traits::from_string(rhs));
}
void test_range_intersection()
{
// Intersections and their expected results, in text form.
// Each row contains two ranges, and their intersection.
std::string_view intersections[][3]{
{"empty", "empty", "empty"},
{"(,)", "empty", "empty"},
{"[,]", "empty", "empty"},
{"empty", "[0,10]", "empty"},
{"(,)", "(,)", "(,)"},
{"(,)", "(5,8)", "(5,8)"},
{"(,)", "[5,8)", "[5,8)"},
{"(,)", "(5,8]", "(5,8]"},
{"(,)", "[5,8]", "[5,8]"},
{"(-1000,10)", "(0,1000)", "(0,10)"},
{"[-1000,10)", "(0,1000)", "(0,10)"},
{"(-1000,10]", "(0,1000)", "(0,10]"},
{"[-1000,10]", "(0,1000)", "(0,10]"},
{"[0,100]", "[0,100]", "[0,100]"},
{"[0,100]", "[0,100)", "[0,100)"},
{"[0,100]", "(0,100]", "(0,100]"},
{"[0,100]", "(0,100)", "(0,100)"},
{"[0,10]", "[11,20]", "empty"},
{"[0,10]", "(11,20]", "empty"},
{"[0,10]", "[11,20)", "empty"},
{"[0,10]", "(11,20)", "empty"},
{"[0,10]", "[10,11]", "[10,10]"},
{"[0,10)", "[10,11]", "empty"},
{"[0,10]", "(10,11]", "empty"},
{"[0,10)", "(10,11]", "empty"},
};
for (auto [left, right, expected] : intersections)
{
PQXX_CHECK_EQUAL(
intersect<int>(left, right), expected,
pqxx::internal::concat(
"Intersection of '", left, "' and '", right,
" produced unexpected result."));
PQXX_CHECK_EQUAL(
intersect<int>(right, left), expected,
pqxx::internal::concat(
"Intersection of '", left, "' and '", right, " was asymmetric."));
}
}
void test_range_conversion()
{
std::string_view const ranges[]{
"empty", "(,)", "(,10)", "(0,)", "[0,10]", "[0,10)", "(0,10]", "(0,10)",
};
for (auto r : ranges)
{
auto const shortr{pqxx::from_string<pqxx::range<short>>(r)};
pqxx::range<int> intr{shortr};
PQXX_CHECK_EQUAL(
pqxx::to_string(intr), r, "Converted range looks different.");
}
}
PQXX_REGISTER_TEST(test_range_construct);
PQXX_REGISTER_TEST(test_range_equality);
PQXX_REGISTER_TEST(test_range_empty);
PQXX_REGISTER_TEST(test_range_contains);
PQXX_REGISTER_TEST(test_float_range_contains);
PQXX_REGISTER_TEST(test_range_subset);
PQXX_REGISTER_TEST(test_range_to_string);
PQXX_REGISTER_TEST(test_parse_range);
PQXX_REGISTER_TEST(test_parse_bad_range);
PQXX_REGISTER_TEST(test_range_intersection);
PQXX_REGISTER_TEST(test_range_conversion);
} // namespace

View File

@@ -0,0 +1,22 @@
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_read_transaction()
{
pqxx::connection conn;
pqxx::read_transaction tx{conn};
PQXX_CHECK_EQUAL(
tx.exec("SELECT 1")[0][0].as<int>(), 1,
"Bad result from read transaction.");
PQXX_CHECK_THROWS(
tx.exec("CREATE TABLE should_not_exist(x integer)"), pqxx::sql_error,
"Read-only transaction allows database to be modified.");
}
PQXX_REGISTER_TEST(test_read_transaction);
} // namespace

View File

@@ -0,0 +1,137 @@
#include <pqxx/stream_to>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_result_iteration()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::result r{tx.exec("SELECT generate_series(1, 3)")};
PQXX_CHECK(std::end(r) != std::begin(r), "Broken begin/end.");
PQXX_CHECK(std::rend(r) != std::rbegin(r), "Broken rbegin/rend.");
PQXX_CHECK(std::cbegin(r) == std::begin(r), "Wrong cbegin.");
PQXX_CHECK(std::cend(r) == std::end(r), "Wrong cend.");
PQXX_CHECK(std::crbegin(r) == std::rbegin(r), "Wrong crbegin.");
PQXX_CHECK(std::crend(r) == std::rend(r), "Wrong crend.");
PQXX_CHECK_EQUAL(r.front().front().as<int>(), 1, "Unexpected front().");
PQXX_CHECK_EQUAL(r.back().front().as<int>(), 3, "Unexpected back().");
}
void test_result_iter()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::result r{tx.exec("SELECT generate_series(1, 3)")};
int total{0};
for (auto const &[i] : r.iter<int>()) total += i;
PQXX_CHECK_EQUAL(total, 6, "iter() loop did not get the right values.");
}
void test_result_iterator_swap()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::result r{tx.exec("SELECT generate_series(1, 3)")};
auto head{std::begin(r)}, next{head + 1};
head.swap(next);
PQXX_CHECK_EQUAL(head[0].as<int>(), 2, "Result iterator swap is wrong.");
PQXX_CHECK_EQUAL(next[0].as<int>(), 1, "Result iterator swap is crazy.");
auto tail{std::rbegin(r)}, prev{tail + 1};
tail.swap(prev);
PQXX_CHECK_EQUAL(tail[0].as<int>(), 2, "Reverse iterator swap is wrong.");
PQXX_CHECK_EQUAL(prev[0].as<int>(), 3, "Reverse iterator swap is crazy.");
}
void test_result_iterator_assignment()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::result r{tx.exec("SELECT generate_series(1, 3)")};
pqxx::result::const_iterator fwd;
pqxx::result::const_reverse_iterator rev;
fwd = std::begin(r);
PQXX_CHECK_EQUAL(
fwd[0].as<int>(), std::begin(r)[0].as<int>(),
"Result iterator assignment is wrong.");
rev = std::rbegin(r);
PQXX_CHECK_EQUAL(
rev[0].as<int>(), std::rbegin(r)[0].as<int>(),
"Reverse iterator assignment is wrong.");
}
void check_employee(std::string name, int salary)
{
PQXX_CHECK(name == "x" or name == "y" or name == "z", "Unknown name.");
PQXX_CHECK(
salary == 1000 or salary == 1200 or salary == 1500, "Unknown salary.");
}
void test_result_for_each()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE employee(name varchar, salary int)");
auto fill{pqxx::stream_to::table(tx, {"employee"}, {"name", "salary"})};
fill.write_values("x", 1000);
fill.write_values("y", 1200);
fill.write_values("z", 1500);
fill.complete();
auto const res{tx.exec("SELECT name, salary FROM employee ORDER BY name")};
// Use for_each with a function.
res.for_each(check_employee);
// Use for_each with a simple lambda.
res.for_each(
[](std::string name, int salary) { check_employee(name, salary); });
// Use for_each with a lambda closure.
std::string names{};
int total{0};
res.for_each([&names, &total](std::string name, int salary) {
names.append(name);
total += salary;
});
PQXX_CHECK_EQUAL(
names, "xyz", "result::for_each did not accumulate names correctly.");
PQXX_CHECK_EQUAL(total, 1000 + 1200 + 1500, "Salaries added up wrong.");
// In addition to regular conversions, you can receive arguments as
// string_view, or as references.
names.clear();
total = 0;
res.for_each([&names, &total](std::string_view &&name, int const &salary) {
names.append(name);
total += salary;
});
PQXX_CHECK_EQUAL(
names, "xyz", "result::for_each did not accumulate names correctly.");
PQXX_CHECK_EQUAL(total, 1000 + 1200 + 1500, "Salaries added up wrong.");
}
PQXX_REGISTER_TEST(test_result_iteration);
PQXX_REGISTER_TEST(test_result_iter);
PQXX_REGISTER_TEST(test_result_iterator_swap);
PQXX_REGISTER_TEST(test_result_iterator_assignment);
PQXX_REGISTER_TEST(test_result_for_each);
} // namespace

View File

@@ -0,0 +1,157 @@
#include <pqxx/transaction>
#include "../test_helpers.hxx"
#include "pqxx/internal/ignore-deprecated-pre.hxx"
namespace pqxx
{
template<> struct nullness<row::const_iterator> : no_null<row::const_iterator>
{};
template<>
struct nullness<row::const_reverse_iterator>
: no_null<const_reverse_row_iterator>
{};
template<> struct string_traits<row::const_iterator>
{
static constexpr zview text{"[row::const_iterator]"};
static zview to_buf(char *, char *, row::const_iterator const &)
{
return text;
}
static char *into_buf(char *begin, char *end, row::const_iterator const &)
{
if ((end - begin) <= 30)
throw conversion_overrun{"Not enough buffer for const row iterator."};
std::memcpy(begin, text.c_str(), std::size(text) + 1);
return begin + std::size(text);
}
static constexpr std::size_t
size_buffer(row::const_iterator const &) noexcept
{
return std::size(text) + 1;
}
};
template<> struct string_traits<row::const_reverse_iterator>
{
static constexpr zview text{"[row::const_reverse_iterator]"};
static pqxx::zview
to_buf(char *, char *, row::const_reverse_iterator const &)
{
return text;
}
static char *
into_buf(char *begin, char *end, row::const_reverse_iterator const &)
{
if ((end - begin) <= 30)
throw conversion_overrun{"Not enough buffer for const row iterator."};
std::memcpy(begin, text.c_str(), std::size(text) + 1);
return begin + std::size(text);
}
static constexpr std::size_t
size_buffer(row::const_reverse_iterator const &) noexcept
{
return 100;
}
};
} // namespace pqxx
namespace
{
void test_result_slicing()
{
pqxx::connection conn;
pqxx::work tx{conn};
auto r{tx.exec("SELECT 1")};
PQXX_CHECK(not std::empty(r[0]), "A plain row shows up as empty.");
// Empty slice at beginning of row.
pqxx::row s{r[0].slice(0, 0)};
PQXX_CHECK(std::empty(s), "Empty slice does not show up as empty.");
PQXX_CHECK_EQUAL(std::size(s), 0, "Slicing produces wrong row size.");
PQXX_CHECK_EQUAL(
std::begin(s), std::end(s), "Slice begin()/end() are broken.");
PQXX_CHECK_EQUAL(
std::rbegin(s), std::rend(s), "Slice rbegin()/rend() are broken.");
PQXX_CHECK_THROWS(s.at(0), pqxx::range_error, "at() does not throw.");
pqxx::row slice;
PQXX_CHECK_THROWS(
slice = r[0].slice(0, 2), pqxx::range_error, "No range check.");
pqxx::ignore_unused(slice);
PQXX_CHECK_THROWS(
slice = r[0].slice(1, 0), pqxx::range_error, "Can reverse-slice.");
pqxx::ignore_unused(slice);
// Empty slice at end of row.
s = r[0].slice(1, 1);
PQXX_CHECK(std::empty(s), "empty() is broken.");
PQXX_CHECK_EQUAL(std::size(s), 0, "size() is broken.");
PQXX_CHECK_EQUAL(std::begin(s), std::end(s), "begin()/end() are broken.");
PQXX_CHECK_EQUAL(
std::rbegin(s), std::rend(s), "rbegin()/rend() are broken.");
PQXX_CHECK_THROWS(s.at(0), pqxx::range_error, "at() is inconsistent.");
// Slice that matches the entire row.
s = r[0].slice(0, 1);
PQXX_CHECK(not std::empty(s), "Nonempty slice shows up as empty.");
PQXX_CHECK_EQUAL(std::size(s), 1, "size() breaks for non-empty slice.");
PQXX_CHECK_EQUAL(std::begin(s) + 1, std::end(s), "Iteration is broken.");
PQXX_CHECK_EQUAL(
std::rbegin(s) + 1, std::rend(s), "Reverse iteration is broken.");
PQXX_CHECK_EQUAL(s.at(0).as<int>(), 1, "Accessing a slice is broken.");
PQXX_CHECK_EQUAL(s[0].as<int>(), 1, "operator[] is broken.");
PQXX_CHECK_THROWS(s.at(1).as<int>(), pqxx::range_error, "at() is off.");
// Meaningful slice at beginning of row.
r = tx.exec("SELECT 1, 2, 3");
s = r[0].slice(0, 1);
PQXX_CHECK(not std::empty(s), "Slicing confuses empty().");
PQXX_CHECK_THROWS(
s.at(1).as<int>(), pqxx::range_error, "at() does not enforce slice.");
// Meaningful slice that skips an initial column.
s = r[0].slice(1, 2);
PQXX_CHECK(
not std::empty(s), "Slicing away leading columns confuses empty().");
PQXX_CHECK_EQUAL(s[0].as<int>(), 2, "Slicing offset is broken.");
PQXX_CHECK_EQUAL(
std::begin(s)->as<int>(), 2, "Iteration uses wrong offset.");
PQXX_CHECK_EQUAL(
std::begin(s) + 1, std::end(s), "Iteration has wrong range.");
PQXX_CHECK_EQUAL(
std::rbegin(s) + 1, std::rend(s), "Reverse iteration has wrong range.");
PQXX_CHECK_THROWS(
s.at(1).as<int>(), pqxx::range_error, "Offset slicing is broken.");
// Column names in a slice.
r = tx.exec("SELECT 1 AS one, 2 AS two, 3 AS three");
s = r[0].slice(1, 2);
PQXX_CHECK_EQUAL(s["two"].as<int>(), 2, "Column addressing breaks.");
PQXX_CHECK_THROWS(
pqxx::ignore_unused(s.column_number("one")), pqxx::argument_error,
"Can access column name before slice.");
PQXX_CHECK_THROWS(
pqxx::ignore_unused(s.column_number("three")), pqxx::argument_error,
"Can access column name after slice.");
PQXX_CHECK_EQUAL(
s.column_number("Two"), 0, "Column name is case sensitive.");
// Identical column names.
r = tx.exec("SELECT 1 AS x, 2 AS x");
s = r[0].slice(1, 2);
PQXX_CHECK_EQUAL(s["x"].as<int>(), 2, "Identical column names break slice.");
}
PQXX_REGISTER_TEST(test_result_slicing);
} // namespace
#include "pqxx/internal/ignore-deprecated-post.hxx"

View File

@@ -0,0 +1,84 @@
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_row()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::result rows{tx.exec("SELECT 1, 2, 3")};
pqxx::row r{rows[0]};
PQXX_CHECK_EQUAL(std::size(r), 3, "Unexpected row size.");
PQXX_CHECK_EQUAL(r.at(0).as<int>(), 1, "Wrong value at index 0.");
PQXX_CHECK(std::begin(r) != std::end(r), "Broken row iteration.");
PQXX_CHECK(std::begin(r) < std::end(r), "Row begin does not precede end.");
PQXX_CHECK(std::cbegin(r) == std::begin(r), "Wrong cbegin.");
PQXX_CHECK(std::cend(r) == std::end(r), "Wrong cend.");
PQXX_CHECK(std::rbegin(r) != std::rend(r), "Broken reverse row iteration.");
PQXX_CHECK(std::crbegin(r) == std::rbegin(r), "Wrong crbegin.");
PQXX_CHECK(std::crend(r) == std::rend(r), "Wrong crend.");
PQXX_CHECK_EQUAL(r.front().as<int>(), 1, "Wrong row front().");
PQXX_CHECK_EQUAL(r.back().as<int>(), 3, "Wrong row back().");
}
void test_row_iterator()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::result rows{tx.exec("SELECT 1, 2, 3")};
auto i{std::begin(rows[0])};
PQXX_CHECK_EQUAL(i->as<int>(), 1, "Row iterator is wrong.");
auto i2{i};
PQXX_CHECK_EQUAL(i2->as<int>(), 1, "Row iterator copy is wrong.");
i2++;
PQXX_CHECK_EQUAL(i2->as<int>(), 2, "Row iterator increment is wrong.");
pqxx::row::const_iterator i3;
i3 = i2;
PQXX_CHECK_EQUAL(i3->as<int>(), 2, "Row iterator assignment is wrong.");
auto r{std::rbegin(rows[0])};
PQXX_CHECK_EQUAL(r->as<int>(), 3, "Row reverse iterator is wrong.");
auto r2{r};
PQXX_CHECK_EQUAL(r2->as<int>(), 3, "Row reverse iterator copy is wrong.");
r2++;
PQXX_CHECK_EQUAL(
r2->as<int>(), 2, "Row reverse iterator increment is wrong.");
pqxx::row::const_reverse_iterator r3;
r3 = r2;
PQXX_CHECK_EQUAL(
i3->as<int>(), 2, "Row reverse iterator assignment is wrong.");
}
void test_row_as()
{
using pqxx::operator"" _zv;
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::row const r{tx.exec1("SELECT 1, 2, 3")};
auto [one, two, three]{r.as<int, float, pqxx::zview>()};
static_assert(std::is_same_v<decltype(one), int>);
static_assert(std::is_same_v<decltype(two), float>);
static_assert(std::is_same_v<decltype(three), pqxx::zview>);
PQXX_CHECK_EQUAL(one, 1, "row::as() did not produce the right int.");
PQXX_CHECK_GREATER(two, 1.9, "row::as() did not produce the right float.");
PQXX_CHECK_LESS(two, 2.1, "row::as() did not produce the right float.");
PQXX_CHECK_EQUAL(
three, "3"_zv, "row::as() did not produce the right zview.");
PQXX_CHECK_EQUAL(
std::get<0>(tx.exec1("SELECT 999").as<int>()), 999,
"Unary tuple did not extract right.");
}
PQXX_REGISTER_TEST(test_row);
PQXX_REGISTER_TEST(test_row_iterator);
PQXX_REGISTER_TEST(test_row_as);
} // namespace

View File

@@ -0,0 +1,33 @@
#include <pqxx/separated_list>
#include "../test_helpers.hxx"
// Test program for separated_list.
namespace
{
void test_separated_list()
{
PQXX_CHECK_EQUAL(
pqxx::separated_list(",", std::vector<int>{}), "",
"Empty list came out wrong.");
PQXX_CHECK_EQUAL(
pqxx::separated_list(",", std::vector<int>{5}), "5",
"Single-element list came out wrong.");
PQXX_CHECK_EQUAL(
pqxx::separated_list(",", std::vector<int>{3, 6}), "3,6",
"Things go wrong once separators come in.");
std::vector<int> const nums{1, 2, 3};
PQXX_CHECK_EQUAL(
pqxx::separated_list(
"+", std::begin(nums), std::end(nums),
[](auto elt) { return *elt * 2; }),
"2+4+6", "Accessors don't seem to work.");
}
PQXX_REGISTER_TEST(test_separated_list);
} // namespace

View File

@@ -0,0 +1,20 @@
#include <pqxx/nontransaction>
#include <pqxx/subtransaction>
#include "../test_helpers.hxx"
namespace
{
void test_simultaneous_transactions()
{
pqxx::connection conn;
pqxx::nontransaction n1{conn};
PQXX_CHECK_THROWS(
pqxx::nontransaction n2{conn}, std::logic_error,
"Allowed to open simultaneous nontransactions.");
}
PQXX_REGISTER_TEST(test_simultaneous_transactions);
} // namespace

View File

@@ -0,0 +1,270 @@
#include <pqxx/cursor>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_forward_sql_cursor()
{
pqxx::connection conn;
pqxx::work tx{conn};
// Plain owned, scoped, forward-only read-only cursor.
pqxx::internal::sql_cursor forward(
tx, "SELECT generate_series(1, 4)", "forward",
pqxx::cursor_base::forward_only, pqxx::cursor_base::read_only,
pqxx::cursor_base::owned, false);
PQXX_CHECK_EQUAL(forward.pos(), 0, "Wrong initial position");
PQXX_CHECK_EQUAL(forward.endpos(), -1, "Wrong initial endpos()");
auto empty_result{forward.empty_result()};
PQXX_CHECK_EQUAL(std::size(empty_result), 0, "Empty result not empty");
auto displacement{0};
auto one{forward.fetch(1, displacement)};
PQXX_CHECK_EQUAL(std::size(one), 1, "Fetched wrong number of rows");
PQXX_CHECK_EQUAL(one[0][0].as<std::string>(), "1", "Unexpected result");
PQXX_CHECK_EQUAL(displacement, 1, "Wrong displacement");
PQXX_CHECK_EQUAL(forward.pos(), 1, "In wrong position");
auto offset{forward.move(1, displacement)};
PQXX_CHECK_EQUAL(offset, 1, "Unexpected offset from move()");
PQXX_CHECK_EQUAL(displacement, 1, "Unexpected displacement after move()");
PQXX_CHECK_EQUAL(forward.pos(), 2, "Wrong position after move()");
PQXX_CHECK_EQUAL(forward.endpos(), -1, "endpos() unexpectedly set");
auto row{forward.fetch(0, displacement)};
PQXX_CHECK_EQUAL(std::size(row), 0, "fetch(0, displacement) returns rows");
PQXX_CHECK_EQUAL(displacement, 0, "Unexpected displacement after fetch(0)");
PQXX_CHECK_EQUAL(forward.pos(), 2, "fetch(0, displacement) affected pos()");
row = forward.fetch(0);
PQXX_CHECK_EQUAL(std::size(row), 0, "fetch(0) fetched wrong number of rows");
PQXX_CHECK_EQUAL(forward.pos(), 2, "fetch(0) moved cursor");
PQXX_CHECK_EQUAL(forward.pos(), 2, "fetch(0) affected pos()");
offset = forward.move(1);
PQXX_CHECK_EQUAL(offset, 1, "move(1) returned unexpected value");
PQXX_CHECK_EQUAL(forward.pos(), 3, "move(1) after fetch(0) broke");
row = forward.fetch(1);
PQXX_CHECK_EQUAL(
std::size(row), 1, "fetch(1) returned wrong number of rows");
PQXX_CHECK_EQUAL(forward.pos(), 4, "fetch(1) results in bad pos()");
PQXX_CHECK_EQUAL(row[0][0].as<std::string>(), "4", "pos() is lying");
empty_result = forward.fetch(1, displacement);
PQXX_CHECK_EQUAL(std::size(empty_result), 0, "Got rows at end of cursor");
PQXX_CHECK_EQUAL(forward.pos(), 5, "Not at one-past-end position");
PQXX_CHECK_EQUAL(forward.endpos(), 5, "Failed to notice end position");
PQXX_CHECK_EQUAL(displacement, 1, "Wrong displacement at end position");
offset = forward.move(5, displacement);
PQXX_CHECK_EQUAL(offset, 0, "move() lied at end of result set");
PQXX_CHECK_EQUAL(forward.pos(), 5, "pos() is beyond end");
PQXX_CHECK_EQUAL(forward.endpos(), 5, "endpos() changed after end position");
PQXX_CHECK_EQUAL(displacement, 0, "Wrong displacement after end position");
// Move through entire result set at once.
pqxx::internal::sql_cursor forward2(
tx, "SELECT generate_series(1, 4)", "forward",
pqxx::cursor_base::forward_only, pqxx::cursor_base::read_only,
pqxx::cursor_base::owned, false);
// Move through entire result set at once.
offset = forward2.move(pqxx::cursor_base::all(), displacement);
PQXX_CHECK_EQUAL(offset, 4, "Unexpected number of rows in result set");
PQXX_CHECK_EQUAL(displacement, 5, "displacement != rows+1");
PQXX_CHECK_EQUAL(forward2.pos(), 5, "Bad pos() after skipping all rows");
PQXX_CHECK_EQUAL(forward2.endpos(), 5, "Bad endpos() after skipping");
pqxx::internal::sql_cursor forward3(
tx, "SELECT generate_series(1, 4)", "forward",
pqxx::cursor_base::forward_only, pqxx::cursor_base::read_only,
pqxx::cursor_base::owned, false);
// Fetch entire result set at once.
auto rows{forward3.fetch(pqxx::cursor_base::all(), displacement)};
PQXX_CHECK_EQUAL(
std::size(rows), 4, "Unexpected number of rows in result set");
PQXX_CHECK_EQUAL(displacement, 5, "displacement != rows+1");
PQXX_CHECK_EQUAL(forward3.pos(), 5, "Bad pos() after fetching all rows");
PQXX_CHECK_EQUAL(forward3.endpos(), 5, "Bad endpos() after fetching");
pqxx::internal::sql_cursor forward_empty(
tx, "SELECT generate_series(0, -1)", "forward_empty",
pqxx::cursor_base::forward_only, pqxx::cursor_base::read_only,
pqxx::cursor_base::owned, false);
offset = forward_empty.move(3, displacement);
PQXX_CHECK_EQUAL(forward_empty.pos(), 1, "Bad pos() at end of result");
PQXX_CHECK_EQUAL(forward_empty.endpos(), 1, "Bad endpos() in empty result");
PQXX_CHECK_EQUAL(displacement, 1, "Bad displacement in empty result");
PQXX_CHECK_EQUAL(offset, 0, "move() in empty result counted rows");
}
void test_scroll_sql_cursor()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::internal::sql_cursor scroll(
tx, "SELECT generate_series(1, 10)", "scroll",
pqxx::cursor_base::random_access, pqxx::cursor_base::read_only,
pqxx::cursor_base::owned, false);
PQXX_CHECK_EQUAL(scroll.pos(), 0, "Scroll cursor's initial pos() is wrong");
PQXX_CHECK_EQUAL(scroll.endpos(), -1, "New scroll cursor has endpos() set");
auto rows{scroll.fetch(pqxx::cursor_base::next())};
PQXX_CHECK_EQUAL(std::size(rows), 1, "Scroll cursor is broken");
PQXX_CHECK_EQUAL(scroll.pos(), 1, "Scroll cursor's pos() is broken");
PQXX_CHECK_EQUAL(scroll.endpos(), -1, "endpos() set prematurely");
// Turn cursor around. This is where we begin to feel SQL cursors'
// semantics: we pre-decrement, ending up on the position in front of the
// first row and returning no rows.
rows = scroll.fetch(pqxx::cursor_base::prior());
PQXX_CHECK_EQUAL(std::empty(rows), true, "Turning around on fetch() broke");
PQXX_CHECK_EQUAL(scroll.pos(), 0, "pos() is not back at zero");
PQXX_CHECK_EQUAL(
scroll.endpos(), -1, "endpos() set on wrong side of result");
// Bounce off the left-hand side of the result set. Can't move before the
// starting position.
auto offset{0}, displacement{0};
offset = scroll.move(-3, displacement);
PQXX_CHECK_EQUAL(offset, 0, "Rows found before beginning");
PQXX_CHECK_EQUAL(displacement, 0, "Failed to bounce off beginning");
PQXX_CHECK_EQUAL(scroll.pos(), 0, "pos() moved back from zero");
PQXX_CHECK_EQUAL(scroll.endpos(), -1, "endpos() set on left-side bounce");
// Try bouncing off the left-hand side a little harder. Take 4 paces away
// from the boundary and run into it.
offset = scroll.move(4, displacement);
PQXX_CHECK_EQUAL(offset, 4, "Offset mismatch");
PQXX_CHECK_EQUAL(displacement, 4, "Displacement mismatch");
PQXX_CHECK_EQUAL(scroll.pos(), 4, "Position mismatch");
PQXX_CHECK_EQUAL(scroll.endpos(), -1, "endpos() set at weird time");
offset = scroll.move(-10, displacement);
PQXX_CHECK_EQUAL(offset, 3, "Offset mismatch");
PQXX_CHECK_EQUAL(displacement, -4, "Displacement mismatch");
PQXX_CHECK_EQUAL(scroll.pos(), 0, "Hard bounce failed");
PQXX_CHECK_EQUAL(scroll.endpos(), -1, "endpos() set during hard bounce");
rows = scroll.fetch(3);
PQXX_CHECK_EQUAL(scroll.pos(), 3, "Bad pos()");
PQXX_CHECK_EQUAL(std::size(rows), 3, "Wrong number of rows");
PQXX_CHECK_EQUAL(rows[2][0].as<int>(), 3, "pos() does not match data");
rows = scroll.fetch(-1);
PQXX_CHECK_EQUAL(scroll.pos(), 2, "Bad pos()");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 2, "pos() does not match data");
rows = scroll.fetch(1);
PQXX_CHECK_EQUAL(scroll.pos(), 3, "Bad pos() after inverse turnaround");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 3, "Data position mismatch");
}
void test_adopted_sql_cursor()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0(
"DECLARE adopted SCROLL CURSOR FOR "
"SELECT generate_series(1, 3)");
pqxx::internal::sql_cursor adopted(tx, "adopted", pqxx::cursor_base::owned);
PQXX_CHECK_EQUAL(adopted.pos(), -1, "Adopted cursor has known pos()");
PQXX_CHECK_EQUAL(adopted.endpos(), -1, "Adopted cursor has known endpos()");
auto displacement{0};
auto rows{adopted.fetch(pqxx::cursor_base::all(), displacement)};
PQXX_CHECK_EQUAL(std::size(rows), 3, "Wrong number of rows in result");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 1, "Wrong result data");
PQXX_CHECK_EQUAL(rows[2][0].as<int>(), 3, "Wrong result data");
PQXX_CHECK_EQUAL(displacement, 4, "Wrong displacement");
PQXX_CHECK_EQUAL(
adopted.pos(), -1, "End-of-result set pos() on adopted cur");
PQXX_CHECK_EQUAL(adopted.endpos(), -1, "endpos() set too early");
rows = adopted.fetch(pqxx::cursor_base::backward_all(), displacement);
PQXX_CHECK_EQUAL(std::size(rows), 3, "Wrong number of rows in result");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 3, "Wrong result data");
PQXX_CHECK_EQUAL(rows[2][0].as<int>(), 1, "Wrong result data");
PQXX_CHECK_EQUAL(displacement, -4, "Wrong displacement");
PQXX_CHECK_EQUAL(adopted.pos(), 0, "Failed to recognize starting position");
PQXX_CHECK_EQUAL(adopted.endpos(), -1, "endpos() set too early");
auto offset{adopted.move(pqxx::cursor_base::all())};
PQXX_CHECK_EQUAL(offset, 3, "Unexpected move() offset");
PQXX_CHECK_EQUAL(adopted.pos(), 4, "Bad position on adopted cursor");
PQXX_CHECK_EQUAL(adopted.endpos(), 4, "endpos() not set properly");
// Owned adopted cursors are cleaned up on destruction.
pqxx::connection conn2;
pqxx::work tx2(conn2, "tx2");
tx2.exec0(
"DECLARE adopted2 CURSOR FOR "
"SELECT generate_series(1, 3)");
{
pqxx::internal::sql_cursor(tx2, "adopted2", pqxx::cursor_base::owned);
}
// Modern backends: accessing the cursor now is an error, as you'd expect.
PQXX_CHECK_THROWS(
tx2.exec("FETCH 1 IN adopted2"), pqxx::sql_error,
"Owned adopted cursor not cleaned up");
tx2.abort();
pqxx::work tx3(conn2, "tx3");
tx3.exec(
"DECLARE adopted3 CURSOR FOR "
"SELECT generate_series(1, 3)");
{
pqxx::internal::sql_cursor(tx3, "adopted3", pqxx::cursor_base::loose);
}
tx3.exec("MOVE 1 IN adopted3");
}
void test_hold_cursor()
{
pqxx::connection conn;
pqxx::work tx{conn};
// "With hold" cursor is kept after commit.
pqxx::internal::sql_cursor with_hold(
tx, "SELECT generate_series(1, 3)", "hold_cursor",
pqxx::cursor_base::forward_only, pqxx::cursor_base::read_only,
pqxx::cursor_base::owned, true);
tx.commit();
pqxx::work tx2(conn, "tx2");
auto rows{with_hold.fetch(1)};
PQXX_CHECK_EQUAL(
std::size(rows), 1, "Did not get 1 row from with-hold cursor");
// Cursor without hold is closed on commit.
pqxx::internal::sql_cursor no_hold(
tx2, "SELECT generate_series(1, 3)", "no_hold_cursor",
pqxx::cursor_base::forward_only, pqxx::cursor_base::read_only,
pqxx::cursor_base::owned, false);
tx2.commit();
pqxx::work tx3(conn, "tx3");
PQXX_CHECK_THROWS(
no_hold.fetch(1), pqxx::sql_error, "Cursor not closed on commit");
}
void cursor_tests()
{
test_forward_sql_cursor();
test_scroll_sql_cursor();
test_adopted_sql_cursor();
test_hold_cursor();
}
PQXX_REGISTER_TEST(cursor_tests);
} // namespace

View File

@@ -0,0 +1,85 @@
#include <pqxx/cursor>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_stateless_cursor()
{
pqxx::connection conn;
pqxx::work tx{conn};
pqxx::stateless_cursor<
pqxx::cursor_base::read_only, pqxx::cursor_base::owned>
empty(tx, "SELECT generate_series(0, -1)", "empty", false);
auto rows{empty.retrieve(0, 0)};
PQXX_CHECK_EQUAL(std::empty(rows), true, "Empty result not empty");
rows = empty.retrieve(0, 1);
PQXX_CHECK_EQUAL(std::size(rows), 0, "Empty result returned rows");
PQXX_CHECK_EQUAL(empty.size(), 0, "Empty cursor not empty");
PQXX_CHECK_THROWS(
empty.retrieve(1, 0), std::out_of_range, "Empty cursor tries to retrieve");
pqxx::stateless_cursor<
pqxx::cursor_base::read_only, pqxx::cursor_base::owned>
stateless(tx, "SELECT generate_series(0, 9)", "stateless", false);
PQXX_CHECK_EQUAL(stateless.size(), 10, "stateless_cursor::size() mismatch");
// Retrieve nothing.
rows = stateless.retrieve(1, 1);
PQXX_CHECK_EQUAL(std::size(rows), 0, "1-to-1 retrieval not empty");
// Retrieve two rows.
rows = stateless.retrieve(1, 3);
PQXX_CHECK_EQUAL(std::size(rows), 2, "Retrieved wrong number of rows");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 1, "Data/position mismatch");
PQXX_CHECK_EQUAL(rows[1][0].as<int>(), 2, "Data/position mismatch");
// Retrieve same rows in reverse.
rows = stateless.retrieve(2, 0);
PQXX_CHECK_EQUAL(std::size(rows), 2, "Retrieved wrong number of rows");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 2, "Data/position mismatch");
PQXX_CHECK_EQUAL(rows[1][0].as<int>(), 1, "Data/position mismatch");
// Retrieve beyond end.
rows = stateless.retrieve(9, 13);
PQXX_CHECK_EQUAL(std::size(rows), 1, "Row count wrong at end");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 9, "Data/pos mismatch at end");
// Retrieve beyond beginning.
rows = stateless.retrieve(0, -4);
PQXX_CHECK_EQUAL(std::size(rows), 1, "Row count wrong at beginning");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 0, "Data/pos mismatch at beginning");
// Retrieve entire result set backwards.
rows = stateless.retrieve(10, -15);
PQXX_CHECK_EQUAL(
std::size(rows), 10, "Reverse complete retrieval is broken");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 9, "Data mismatch");
PQXX_CHECK_EQUAL(rows[9][0].as<int>(), 0, "Data mismatch");
// Normal usage pattern: step through result set, 4 rows at a time.
rows = stateless.retrieve(0, 4);
PQXX_CHECK_EQUAL(std::size(rows), 4, "Wrong batch size");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 0, "Batch in wrong place");
PQXX_CHECK_EQUAL(rows[3][0].as<int>(), 3, "Batch in wrong place");
rows = stateless.retrieve(4, 8);
PQXX_CHECK_EQUAL(std::size(rows), 4, "Wrong batch size");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 4, "Batch in wrong place");
PQXX_CHECK_EQUAL(rows[3][0].as<int>(), 7, "Batch in wrong place");
rows = stateless.retrieve(8, 12);
PQXX_CHECK_EQUAL(std::size(rows), 2, "Wrong batch size");
PQXX_CHECK_EQUAL(rows[0][0].as<int>(), 8, "Batch in wrong place");
PQXX_CHECK_EQUAL(rows[1][0].as<int>(), 9, "Batch in wrong place");
}
PQXX_REGISTER_TEST(test_stateless_cursor);
} // namespace

View File

@@ -0,0 +1,143 @@
#include <memory>
#include <optional>
#include "../test_helpers.hxx"
namespace
{
enum colour
{
red,
green,
blue
};
enum class weather : short
{
hot,
cold,
wet
};
enum class many : unsigned long long
{
bottom = 0,
top = std::numeric_limits<unsigned long long>::max(),
};
} // namespace
namespace pqxx
{
PQXX_DECLARE_ENUM_CONVERSION(colour);
PQXX_DECLARE_ENUM_CONVERSION(weather);
PQXX_DECLARE_ENUM_CONVERSION(many);
} // namespace pqxx
namespace
{
void test_strconv_bool()
{
PQXX_CHECK_EQUAL(pqxx::to_string(false), "false", "Wrong to_string(false).");
PQXX_CHECK_EQUAL(pqxx::to_string(true), "true", "Wrong to_string(true).");
bool result;
pqxx::from_string("false", result);
PQXX_CHECK_EQUAL(result, false, "Wrong from_string('false').");
pqxx::from_string("FALSE", result);
PQXX_CHECK_EQUAL(result, false, "Wrong from_string('FALSE').");
pqxx::from_string("f", result);
PQXX_CHECK_EQUAL(result, false, "Wrong from_string('f').");
pqxx::from_string("F", result);
PQXX_CHECK_EQUAL(result, false, "Wrong from_string('F').");
pqxx::from_string("0", result);
PQXX_CHECK_EQUAL(result, false, "Wrong from_string('0').");
pqxx::from_string("true", result);
PQXX_CHECK_EQUAL(result, true, "Wrong from_string('true').");
pqxx::from_string("TRUE", result);
PQXX_CHECK_EQUAL(result, true, "Wrong from_string('TRUE').");
pqxx::from_string("t", result);
PQXX_CHECK_EQUAL(result, true, "Wrong from_string('t').");
pqxx::from_string("T", result);
PQXX_CHECK_EQUAL(result, true, "Wrong from_string('T').");
pqxx::from_string("1", result);
PQXX_CHECK_EQUAL(result, true, "Wrong from_string('1').");
}
void test_strconv_enum()
{
PQXX_CHECK_EQUAL(pqxx::to_string(red), "0", "Enum value did not convert.");
PQXX_CHECK_EQUAL(pqxx::to_string(green), "1", "Enum value did not convert.");
PQXX_CHECK_EQUAL(pqxx::to_string(blue), "2", "Enum value did not convert.");
colour col;
pqxx::from_string("2", col);
PQXX_CHECK_EQUAL(col, blue, "Could not recover enum value from string.");
}
void test_strconv_class_enum()
{
PQXX_CHECK_EQUAL(
pqxx::to_string(weather::hot), "0", "Class enum value did not convert.");
PQXX_CHECK_EQUAL(
pqxx::to_string(weather::wet), "2", "Enum value did not convert.");
weather w;
pqxx::from_string("2", w);
PQXX_CHECK_EQUAL(
w, weather::wet, "Could not recover class enum value from string.");
PQXX_CHECK_EQUAL(
pqxx::to_string(many::bottom), "0",
"Small wide enum did not convert right.");
PQXX_CHECK_EQUAL(
pqxx::to_string(many::top),
pqxx::to_string(std::numeric_limits<unsigned long long>::max()),
"Large wide enum did not convert right.");
}
void test_strconv_optional()
{
PQXX_CHECK_THROWS(
pqxx::to_string(std::optional<int>{}), pqxx::conversion_error,
"Converting an empty optional did not throw conversion error.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::optional<int>{std::in_place, 10}), "10",
"std::optional<int> does not convert right.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::optional<int>{std::in_place, -10000}), "-10000",
"std::optional<int> does not convert right.");
}
void test_strconv_smart_pointer()
{
PQXX_CHECK_THROWS(
pqxx::to_string(std::unique_ptr<int>{}), pqxx::conversion_error,
"Converting an empty unique_ptr did not throw conversion error.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::make_unique<int>(10)), "10",
"std::unique_ptr<int> does not convert right.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::make_unique<int>(-10000)), "-10000",
"std::unique_ptr<int> does not convert right.");
PQXX_CHECK_THROWS(
pqxx::to_string(std::shared_ptr<int>{}), pqxx::conversion_error,
"Converting an empty shared_ptr did not throw conversion error.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::make_shared<int>(10)), "10",
"std::shared_ptr<int> does not convert right.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::make_shared<int>(-10000)), "-10000",
"std::shared_ptr<int> does not convert right.");
}
PQXX_REGISTER_TEST(test_strconv_bool);
PQXX_REGISTER_TEST(test_strconv_enum);
PQXX_REGISTER_TEST(test_strconv_class_enum);
PQXX_REGISTER_TEST(test_strconv_optional);
PQXX_REGISTER_TEST(test_strconv_smart_pointer);
} // namespace

View File

@@ -0,0 +1,344 @@
#include <pqxx/stream_from>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
#include "../test_types.hxx"
#include <cstring>
#include <iostream>
#include <set>
#include <string>
#include <tuple>
#include <vector>
#include <optional>
namespace
{
void test_nonoptionals(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto extractor{pqxx::stream_from::query(
tx, "SELECT * FROM stream_from_test ORDER BY number0")};
PQXX_CHECK(extractor, "stream_from failed to initialize.");
std::tuple<int, std::string, int, ipv4, std::string, bytea> got_tuple;
try
{
// We can't read the "910" row -- it contains nulls, which our tuple does
// not accept.
extractor >> got_tuple;
PQXX_CHECK_NOTREACHED(
"Failed to fail to stream null values into null-less fields.");
}
catch (pqxx::conversion_error const &e)
{
std::string const what{e.what()};
if (what.find("null") == std::string::npos)
throw;
pqxx::test::expected_exception(
"Could not stream nulls into null-less fields: " + what);
}
// The stream is still good though.
// The second tuple is fine.
extractor >> got_tuple;
PQXX_CHECK(extractor, "Stream ended prematurely.");
PQXX_CHECK_EQUAL(std::get<0>(got_tuple), 1234, "Bad value.");
// Don't know much about the timestamp, but let's assume it starts with a
// year in the second millennium.
PQXX_CHECK(
std::get<1>(got_tuple).at(0) == '2', "Bad value. Expected timestamp.");
PQXX_CHECK_LESS(
std::size(std::get<1>(got_tuple)), 40u, "Unexpected length.");
PQXX_CHECK_GREATER(
std::size(std::get<1>(got_tuple)), 20u, "Unexpected length.");
PQXX_CHECK_EQUAL(std::get<2>(got_tuple), 4321, "Bad value.");
PQXX_CHECK_EQUAL(std::get<3>(got_tuple), (ipv4{8, 8, 8, 8}), "Bad value.");
PQXX_CHECK_EQUAL(std::get<4>(got_tuple), "hello\n \tworld", "Bad value.");
PQXX_CHECK_EQUAL(
std::get<5>(got_tuple), (bytea{'\x00', '\x01', '\x02'}), "Bad value.");
// The third tuple contains some nulls. For what it's worth, when we *know*
// that we're getting nulls, we can stream them into nullptr_t fields.
std::tuple<
int, std::string, std::nullptr_t, std::nullptr_t, std::string, bytea>
tup_w_nulls;
extractor >> tup_w_nulls;
PQXX_CHECK(extractor, "Stream ended prematurely.");
PQXX_CHECK_EQUAL(std::get<0>(tup_w_nulls), 5678, "Bad value.");
PQXX_CHECK(std::get<2>(tup_w_nulls) == nullptr, "Bad null.");
PQXX_CHECK(std::get<3>(tup_w_nulls) == nullptr, "Bad null.");
// We're at the end of the stream.
extractor >> tup_w_nulls;
PQXX_CHECK(not extractor, "Stream did not end.");
// Of course we can't stream a non-null value into a nullptr field.
auto ex2{pqxx::stream_from::query(tx, "SELECT 1")};
std::tuple<std::nullptr_t> null_tup;
try
{
ex2 >> null_tup;
PQXX_CHECK_NOTREACHED(
"stream_from should have refused to convert non-null value to "
"nullptr_t.");
}
catch (pqxx::conversion_error const &e)
{
std::string const what{e.what()};
if (what.find("null") == std::string::npos)
throw;
pqxx::test::expected_exception(
std::string{"Could not extract row: "} + what);
}
ex2 >> null_tup;
PQXX_CHECK(not ex2, "Stream did not end.");
PQXX_CHECK_SUCCEEDS(
tx.exec1("SELECT 1"), "Could not use transaction after stream_from.");
}
void test_bad_tuples(pqxx::connection &conn)
{
pqxx::work tx{conn};
auto extractor{pqxx::stream_from::table(tx, {"stream_from_test"})};
PQXX_CHECK(extractor, "stream_from failed to initialize");
std::tuple<int> got_tuple_too_short;
try
{
extractor >> got_tuple_too_short;
PQXX_CHECK_NOTREACHED("stream_from improperly read first row");
}
catch (pqxx::usage_error const &e)
{
std::string what{e.what()};
if (
what.find("1") == std::string::npos or
what.find("6") == std::string::npos)
throw;
pqxx::test::expected_exception("Tuple is wrong size: " + what);
}
std::tuple<int, std::string, int, ipv4, std::string, bytea, std::string>
got_tuple_too_long;
try
{
extractor >> got_tuple_too_long;
PQXX_CHECK_NOTREACHED("stream_from improperly read first row");
}
catch (pqxx::usage_error const &e)
{
std::string what{e.what()};
if (
what.find("6") == std::string::npos or
what.find("7") == std::string::npos)
throw;
pqxx::test::expected_exception("Could not extract row: " + what);
}
extractor.complete();
}
#define ASSERT_FIELD_EQUAL(OPT, VAL) \
PQXX_CHECK(static_cast<bool>(OPT), "unexpected null field"); \
PQXX_CHECK_EQUAL(*OPT, VAL, "field value mismatch")
#define ASSERT_FIELD_NULL(OPT) \
PQXX_CHECK(not static_cast<bool>(OPT), "expected null field")
template<template<typename...> class O>
void test_optional(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto extractor{pqxx::stream_from::query(
tx, "SELECT * FROM stream_from_test ORDER BY number0")};
PQXX_CHECK(extractor, "stream_from failed to initialize");
std::tuple<int, O<std::string>, O<int>, O<ipv4>, O<std::string>, O<bytea>>
got_tuple;
extractor >> got_tuple;
PQXX_CHECK(extractor, "stream_from failed to read third row");
PQXX_CHECK_EQUAL(std::get<0>(got_tuple), 910, "field value mismatch");
ASSERT_FIELD_NULL(std::get<1>(got_tuple));
ASSERT_FIELD_NULL(std::get<2>(got_tuple));
ASSERT_FIELD_NULL(std::get<3>(got_tuple));
ASSERT_FIELD_EQUAL(std::get<4>(got_tuple), "\\N");
ASSERT_FIELD_EQUAL(std::get<5>(got_tuple), bytea{});
extractor >> got_tuple;
PQXX_CHECK(extractor, "stream_from failed to read first row.");
PQXX_CHECK_EQUAL(std::get<0>(got_tuple), 1234, "Field value mismatch.");
PQXX_CHECK(
static_cast<bool>(std::get<1>(got_tuple)), "Unexpected null field.");
// PQXX_CHECK_EQUAL(*std::get<1>(got_tuple), , "field value mismatch");
ASSERT_FIELD_EQUAL(std::get<2>(got_tuple), 4321);
ASSERT_FIELD_EQUAL(std::get<3>(got_tuple), (ipv4{8, 8, 8, 8}));
ASSERT_FIELD_EQUAL(std::get<4>(got_tuple), "hello\n \tworld");
ASSERT_FIELD_EQUAL(std::get<5>(got_tuple), (bytea{'\x00', '\x01', '\x02'}));
extractor >> got_tuple;
PQXX_CHECK(extractor, "stream_from failed to read second row");
PQXX_CHECK_EQUAL(std::get<0>(got_tuple), 5678, "field value mismatch");
ASSERT_FIELD_EQUAL(std::get<1>(got_tuple), "2018-11-17 21:23:00");
ASSERT_FIELD_NULL(std::get<2>(got_tuple));
ASSERT_FIELD_NULL(std::get<3>(got_tuple));
ASSERT_FIELD_EQUAL(std::get<4>(got_tuple), "\u3053\u3093\u306b\u3061\u308f");
ASSERT_FIELD_EQUAL(
std::get<5>(got_tuple), (bytea{'f', 'o', 'o', ' ', 'b', 'a', 'r', '\0'}));
extractor >> got_tuple;
PQXX_CHECK(not extractor, "stream_from failed to detect end of stream");
extractor.complete();
}
void test_stream_from()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0(
"CREATE TEMP TABLE stream_from_test ("
"number0 INT NOT NULL,"
"ts1 TIMESTAMP NULL,"
"number2 INT NULL,"
"addr3 INET NULL,"
"txt4 TEXT NULL,"
"bin5 BYTEA NOT NULL"
")");
tx.exec_params(
"INSERT INTO stream_from_test VALUES ($1,$2,$3,$4,$5,$6)", 910, nullptr,
nullptr, nullptr, "\\N", bytea{});
tx.exec_params(
"INSERT INTO stream_from_test VALUES ($1,$2,$3,$4,$5,$6)", 1234, "now",
4321, ipv4{8, 8, 8, 8}, "hello\n \tworld", bytea{'\x00', '\x01', '\x02'});
tx.exec_params(
"INSERT INTO stream_from_test VALUES ($1,$2,$3,$4,$5,$6)", 5678,
"2018-11-17 21:23:00", nullptr, nullptr, "\u3053\u3093\u306b\u3061\u308f",
bytea{'f', 'o', 'o', ' ', 'b', 'a', 'r', '\0'});
tx.commit();
test_nonoptionals(conn);
test_bad_tuples(conn);
std::cout << "testing `std::unique_ptr` as optional...\n";
test_optional<std::unique_ptr>(conn);
std::cout << "testing `std::optional` as optional...\n";
test_optional<std::optional>(conn);
}
void test_stream_from_does_escaping()
{
std::string const input{"a\t\n\n\n \\b\nc"};
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE badstr (str text)");
tx.exec0("INSERT INTO badstr (str) VALUES (" + tx.quote(input) + ")");
auto reader{pqxx::stream_from::table(tx, {"badstr"})};
std::tuple<std::string> out;
reader >> out;
PQXX_CHECK_EQUAL(
std::get<0>(out), input, "stream_from got weird characters wrong.");
}
void test_stream_from_does_iteration()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE str (s text)");
tx.exec0("INSERT INTO str (s) VALUES ('foo')");
auto reader{pqxx::stream_from::table(tx, {"str"})};
int i{0};
std::string out;
for (std::tuple<std::string> t : reader.iter<std::string>())
{
i++;
out = std::get<0>(t);
}
PQXX_CHECK_EQUAL(i, 1, "Wrong number of iterations.");
PQXX_CHECK_EQUAL(out, "foo", "Got wrong string.");
tx.exec0("INSERT INTO str (s) VALUES ('bar')");
i = 0;
std::set<std::string> strings;
auto reader2{pqxx::stream_from::table(tx, {"str"})};
for (std::tuple<std::string> t : reader2.iter<std::string>())
{
i++;
strings.insert(std::get<0>(t));
}
PQXX_CHECK_EQUAL(i, 2, "Wrong number of iterations.");
PQXX_CHECK_EQUAL(
std::size(strings), 2u, "Wrong number of strings retrieved.");
PQXX_CHECK(strings.find("foo") != std::end(strings), "Missing key.");
PQXX_CHECK(strings.find("bar") != std::end(strings), "Missing key.");
}
void test_transaction_stream_from()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE sample (id integer, name varchar)");
tx.exec0("INSERT INTO sample (id, name) VALUES (321, 'something')");
int items{0};
int id{0};
std::string name;
for (auto [iid, iname] :
tx.stream<int, std::string_view>("SELECT id, name FROM sample"))
{
items++;
id = iid;
name = iname;
}
PQXX_CHECK_EQUAL(items, 1, "Wrong number of iterations.");
PQXX_CHECK_EQUAL(id, 321, "Got wrong int.");
PQXX_CHECK_EQUAL(name, std::string{"something"}, "Got wrong string.");
PQXX_CHECK_EQUAL(
tx.query_value<int>("SELECT 4"), 4,
"Loop did not relinquish transaction.");
}
void test_stream_from_read_row()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE sample (id integer, name varchar, opt integer)");
tx.exec0("INSERT INTO sample (id, name) VALUES (321, 'something')");
auto stream{pqxx::stream_from::table(tx, {"sample"})};
auto fields{stream.read_row()};
PQXX_CHECK_EQUAL(fields->size(), 3ul, "Wrong number of fields.");
PQXX_CHECK_EQUAL(
std::string((*fields)[0]), "321", "Integer field came out wrong.");
PQXX_CHECK_EQUAL(
std::string((*fields)[1]), "something", "Text field came out wrong.");
PQXX_CHECK(std::data((*fields)[2]) == nullptr, "Null field came out wrong.");
auto last{stream.read_row()};
PQXX_CHECK(last == nullptr, "No null pointer at end of stream.");
}
PQXX_REGISTER_TEST(test_stream_from);
PQXX_REGISTER_TEST(test_stream_from_does_escaping);
PQXX_REGISTER_TEST(test_stream_from_does_iteration);
PQXX_REGISTER_TEST(test_transaction_stream_from);
PQXX_REGISTER_TEST(test_stream_from_read_row);
} // namespace

View File

@@ -0,0 +1,445 @@
#include <iostream>
#include <optional>
#include <pqxx/stream_to>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
#include "../test_types.hxx"
namespace
{
std::string truncate_sql_error(std::string const &what)
{
auto trunc{what.substr(0, what.find('\n'))};
if (std::size(trunc) > 64)
trunc = trunc.substr(0, 61) + "...";
return trunc;
}
void test_nonoptionals(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
auto const nonascii{"\u3053\u3093\u306b\u3061\u308f"};
bytea const binary{'\x00', '\x01', '\x02'},
text{'f', 'o', 'o', ' ', 'b', 'a', 'r', '\0'};
inserter << std::make_tuple(
1234, "now", 4321, ipv4{8, 8, 4, 4}, "hello nonoptional world", binary);
inserter << std::make_tuple(
5678, "2018-11-17 21:23:00", nullptr, nullptr, nonascii, text);
inserter << std::make_tuple(910, nullptr, nullptr, nullptr, "\\N", bytea{});
inserter.complete();
auto r1{tx.exec1("SELECT * FROM stream_to_test WHERE number0 = 1234")};
PQXX_CHECK_EQUAL(r1[0].as<int>(), 1234, "Read back wrong first int.");
PQXX_CHECK_EQUAL(
r1[4].as<std::string>(), "hello nonoptional world",
"Read back wrong string.");
PQXX_CHECK_EQUAL(r1[3].as<ipv4>(), ipv4(8, 8, 4, 4), "Read back wrong ip.");
PQXX_CHECK_EQUAL(r1[5].as<bytea>(), binary, "Read back wrong bytea.");
auto r2{tx.exec1("SELECT * FROM stream_to_test WHERE number0 = 5678")};
PQXX_CHECK_EQUAL(r2[0].as<int>(), 5678, "Wrong int on second row.");
PQXX_CHECK(r2[2].is_null(), "Field 2 was meant to be null.");
PQXX_CHECK(r2[3].is_null(), "Field 3 was meant to be null.");
PQXX_CHECK_EQUAL(r2[4].as<std::string>(), nonascii, "Wrong non-ascii text.");
tx.commit();
}
void test_nonoptionals_fold(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
auto const nonascii{"\u3053\u3093\u306b\u3061\u308f"};
bytea const binary{'\x00', '\x01', '\x02'},
text{'f', 'o', 'o', ' ', 'b', 'a', 'r', '\0'};
inserter.write_values(
1234, "now", 4321, ipv4{8, 8, 4, 4}, "hello nonoptional world", binary);
inserter.write_values(
5678, "2018-11-17 21:23:00", nullptr, nullptr, nonascii, text);
inserter.write_values(910, nullptr, nullptr, nullptr, "\\N", bytea{});
inserter.complete();
auto r1{tx.exec1("SELECT * FROM stream_to_test WHERE number0 = 1234")};
PQXX_CHECK_EQUAL(r1[0].as<int>(), 1234, "Read back wrong first int.");
PQXX_CHECK_EQUAL(
r1[4].as<std::string>(), "hello nonoptional world",
"Read back wrong string.");
PQXX_CHECK_EQUAL(r1[3].as<ipv4>(), ipv4(8, 8, 4, 4), "Read back wrong ip.");
PQXX_CHECK_EQUAL(r1[5].as<bytea>(), binary, "Read back wrong bytera.");
auto r2{tx.exec1("SELECT * FROM stream_to_test WHERE number0 = 5678")};
PQXX_CHECK_EQUAL(r2[0].as<int>(), 5678, "Wrong int on second row.");
PQXX_CHECK(r2[2].is_null(), "Field 2 was meant to be null.");
PQXX_CHECK(r2[3].is_null(), "Field 3 was meant to be null.");
PQXX_CHECK_EQUAL(r2[4].as<std::string>(), nonascii, "Wrong non-ascii text.");
tx.commit();
}
/// Try to violate stream_to_test's not-null constraint using a stream_to.
void insert_bad_null_tuple(pqxx::stream_to &inserter)
{
inserter << std::make_tuple(
nullptr, "now", 4321, ipv4{8, 8, 8, 8}, "hello world",
bytea{'\x00', '\x01', '\x02'});
inserter.complete();
}
void test_bad_null(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
PQXX_CHECK_THROWS(
insert_bad_null_tuple(inserter), pqxx::not_null_violation,
"Did not expected not_null_violation when stream_to inserts a bad null.");
}
/// Try to violate stream_to_test's not-null construct using a stream_to.
void insert_bad_null_write(pqxx::stream_to &inserter)
{
inserter.write_values(
nullptr, "now", 4321, ipv4{8, 8, 8, 8}, "hello world",
bytea{'\x00', '\x01', '\x02'});
inserter.complete();
}
void test_bad_null_fold(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
PQXX_CHECK_THROWS(
insert_bad_null_write(inserter), pqxx::not_null_violation,
"Did not expected not_null_violation when stream_to inserts a bad null.");
}
void test_too_few_fields(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
try
{
inserter << std::make_tuple(1234, "now", 4321, ipv4{8, 8, 8, 8});
inserter.complete();
tx.commit();
PQXX_CHECK_NOTREACHED("stream_from improperly inserted row");
}
catch (pqxx::sql_error const &e)
{
std::string what{e.what()};
if (what.find("missing data for column") == std::string::npos)
throw;
pqxx::test::expected_exception(
"Could not insert row: " + truncate_sql_error(what));
}
}
void test_too_few_fields_fold(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
try
{
inserter.write_values(1234, "now", 4321, ipv4{8, 8, 8, 8});
inserter.complete();
tx.commit();
PQXX_CHECK_NOTREACHED("stream_from_fold improperly inserted row");
}
catch (pqxx::sql_error const &e)
{
std::string what{e.what()};
if (what.find("missing data for column") == std::string::npos)
throw;
pqxx::test::expected_exception(
"Fold - Could not insert row: " + truncate_sql_error(what));
}
}
void test_too_many_fields(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
try
{
inserter << std::make_tuple(
1234, "now", 4321, ipv4{8, 8, 8, 8}, "hello world",
bytea{'\x00', '\x01', '\x02'}, 5678);
inserter.complete();
tx.commit();
PQXX_CHECK_NOTREACHED("stream_from improperly inserted row");
}
catch (pqxx::sql_error const &e)
{
std::string what{e.what()};
if (what.find("extra data") == std::string::npos)
throw;
pqxx::test::expected_exception(
"Could not insert row: " + truncate_sql_error(what));
}
}
void test_too_many_fields_fold(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
try
{
inserter.write_values(
1234, "now", 4321, ipv4{8, 8, 8, 8}, "hello world",
bytea{'\x00', '\x01', '\x02'}, 5678);
inserter.complete();
tx.commit();
PQXX_CHECK_NOTREACHED("stream_from_fold improperly inserted row");
}
catch (pqxx::sql_error const &e)
{
std::string what{e.what()};
if (what.find("extra data") == std::string::npos)
throw;
pqxx::test::expected_exception(
"Fold - Could not insert row: " + truncate_sql_error(what));
}
}
void test_stream_to_does_nonnull_optional()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE foo(x integer, y text)");
auto inserter{pqxx::stream_to::table(tx, {"foo"})};
inserter.write_values(
std::optional<int>{368}, std::optional<std::string>{"Text"});
inserter.complete();
auto const row{tx.exec1("SELECT x, y FROM foo")};
PQXX_CHECK_EQUAL(
row[0].as<std::string>(), "368", "Non-null int optional came out wrong.");
PQXX_CHECK_EQUAL(
row[1].as<std::string>(), "Text",
"Non-null string optional came out wrong.");
}
template<template<typename...> class O>
void test_optional(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
inserter << std::make_tuple(
910, O<std::string>{pqxx::nullness<O<std::string>>::null()},
O<int>{pqxx::nullness<O<int>>::null()},
O<ipv4>{pqxx::nullness<O<ipv4>>::null()}, "\\N", bytea{});
inserter.complete();
tx.commit();
}
template<template<typename...> class O>
void test_optional_fold(pqxx::connection &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
inserter.write_values(
910, O<std::string>{pqxx::nullness<O<std::string>>::null()},
O<int>{pqxx::nullness<O<int>>::null()},
O<ipv4>{pqxx::nullness<O<ipv4>>::null()}, "\\N", bytea{});
inserter.complete();
tx.commit();
}
// As an alternative to a tuple, you can also insert a container.
void test_container_stream_to()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE test_container(a integer, b integer)");
auto inserter{pqxx::stream_to::table(tx, {"test_container"})};
inserter << std::vector{112, 244};
inserter.complete();
auto read{tx.exec1("SELECT * FROM test_container")};
PQXX_CHECK_EQUAL(
read[0].as<int>(), 112, "stream_to on container went wrong.");
PQXX_CHECK_EQUAL(
read[1].as<int>(), 244, "Second container field went wrong.");
tx.commit();
}
void test_variant_fold(pqxx::connection_base &connection)
{
pqxx::work tx{connection};
auto inserter{pqxx::stream_to::table(tx, {"stream_to_test"})};
PQXX_CHECK(inserter, "stream_to failed to initialize");
inserter.write_values(
std::variant<std::string, int>{1234},
std::variant<float, std::string>{"now"}, 4321, ipv4{8, 8, 8, 8},
"hello world", bytea{'\x00', '\x01', '\x02'});
inserter.write_values(
5678, "2018-11-17 21:23:00", nullptr, nullptr,
"\u3053\u3093\u306b\u3061\u308f",
bytea{'f', 'o', 'o', ' ', 'b', 'a', 'r', '\0'});
inserter.write_values(910, nullptr, nullptr, nullptr, "\\N", bytea{});
inserter.complete();
tx.commit();
}
void clear_table(pqxx::connection &conn)
{
pqxx::work tx{conn};
tx.exec0("DELETE FROM stream_to_test");
tx.commit();
}
void test_stream_to()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0(
"CREATE TEMP TABLE stream_to_test ("
"number0 INT NOT NULL,"
"ts1 TIMESTAMP NULL,"
"number2 INT NULL,"
"addr3 INET NULL,"
"txt4 TEXT NULL,"
"bin5 BYTEA NOT NULL"
")");
tx.commit();
test_nonoptionals(conn);
clear_table(conn);
test_nonoptionals_fold(conn);
clear_table(conn);
test_bad_null(conn);
clear_table(conn);
test_bad_null_fold(conn);
clear_table(conn);
test_too_few_fields(conn);
clear_table(conn);
test_too_few_fields_fold(conn);
clear_table(conn);
test_too_many_fields(conn);
clear_table(conn);
test_too_many_fields_fold(conn);
clear_table(conn);
test_optional<std::unique_ptr>(conn);
clear_table(conn);
test_optional_fold<std::unique_ptr>(conn);
clear_table(conn);
test_optional<std::optional>(conn);
clear_table(conn);
test_optional_fold<std::optional>(conn);
clear_table(conn);
test_variant_fold(conn);
}
void test_stream_to_factory_with_static_columns()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE pqxx_stream_to(a integer, b varchar)");
auto stream{pqxx::stream_to::table(tx, {"pqxx_stream_to"}, {"a", "b"})};
stream.write_values(3, "three");
stream.complete();
auto r{tx.exec1("SELECT a, b FROM pqxx_stream_to")};
PQXX_CHECK_EQUAL(r[0].as<int>(), 3, "Failed to stream_to a table.");
PQXX_CHECK_EQUAL(
r[1].as<std::string>(), "three",
"Failed to stream_to a string to a table.");
}
void test_stream_to_factory_with_dynamic_columns()
{
pqxx::connection conn;
pqxx::work tx{conn};
tx.exec0("CREATE TEMP TABLE pqxx_stream_to(a integer, b varchar)");
std::vector<std::string_view> columns{"a", "b"};
#if defined(PQXX_HAVE_CONCEPTS)
auto stream{pqxx::stream_to::table(tx, {"pqxx_stream_to"}, columns)};
#else
auto stream{pqxx::stream_to::raw_table(
tx, conn.quote_table({"pqxx_stream_to"}), conn.quote_columns(columns))};
#endif
stream.write_values(4, "four");
stream.complete();
auto r{tx.exec1("SELECT a, b FROM pqxx_stream_to")};
PQXX_CHECK_EQUAL(
r[0].as<int>(), 4, "Failed to stream_to a table with dynamic columns.");
PQXX_CHECK_EQUAL(
r[1].as<std::string>(), "four",
"Failed to stream_to a string to a table with dynamic columns.");
}
void test_stream_to_quotes_arguments()
{
pqxx::connection conn;
pqxx::work tx{conn};
std::string const table{R"--(pqxx_Stream"'x)--"}, column{R"--(a'"b)--"};
tx.exec0(
"CREATE TEMP TABLE " + tx.quote_name(table) + "(" + tx.quote_name(column) +
" integer)");
auto write{pqxx::stream_to::table(tx, {table}, {column})};
write.write_values<int>(12);
write.complete();
PQXX_CHECK_EQUAL(
tx.query_value<int>(
"SELECT " + tx.quote_name(column) + " FROM " + tx.quote_name(table)),
12, "Stream wrote wrong value.");
}
PQXX_REGISTER_TEST(test_stream_to);
PQXX_REGISTER_TEST(test_container_stream_to);
PQXX_REGISTER_TEST(test_stream_to_does_nonnull_optional);
PQXX_REGISTER_TEST(test_stream_to_factory_with_static_columns);
PQXX_REGISTER_TEST(test_stream_to_factory_with_dynamic_columns);
PQXX_REGISTER_TEST(test_stream_to_quotes_arguments);
} // namespace

View File

@@ -0,0 +1,178 @@
#include <cstdint>
#include <variant>
#include <pqxx/connection>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
// Some enums with string conversions.
enum EnumA
{
ea0,
ea1,
ea2
};
enum EnumB
{
eb0,
eb1,
eb2
};
namespace pqxx
{
PQXX_DECLARE_ENUM_CONVERSION(EnumA);
PQXX_DECLARE_ENUM_CONVERSION(EnumB);
} // namespace pqxx
namespace
{
// "A minimal difference."
constexpr double thres{0.00001};
void test_string_conversion()
{
PQXX_CHECK_EQUAL(
"C string array", pqxx::to_string("C string array"),
"C-style string constant does not convert to string properly.");
char text_array[]{"C char array"};
PQXX_CHECK_EQUAL(
"C char array", pqxx::to_string(text_array),
"C-style non-const char array does not convert to string properly.");
char const *text_ptr{"C string pointer"};
PQXX_CHECK_EQUAL(
"C string pointer", pqxx::to_string(text_ptr),
"C-style string pointer does not convert to string properly.");
std::string const cxx_string{"C++ string"};
PQXX_CHECK_EQUAL(
"C++ string", pqxx::to_string(cxx_string),
"C++-style string object does not convert to string properly.");
PQXX_CHECK_EQUAL("0", pqxx::to_string(0), "Zero does not convert right.");
PQXX_CHECK_EQUAL(
"1", pqxx::to_string(1), "Basic integer does not convert right.");
PQXX_CHECK_EQUAL("-1", pqxx::to_string(-1), "Negative numbers don't work.");
PQXX_CHECK_EQUAL(
"9999", pqxx::to_string(9999), "Larger numbers don't work.");
PQXX_CHECK_EQUAL(
"-9999", pqxx::to_string(-9999), "Larger negative numbers don't work.");
int x;
pqxx::from_string("0", x);
PQXX_CHECK_EQUAL(0, x, "Zero does not parse right.");
pqxx::from_string("1", x);
PQXX_CHECK_EQUAL(1, x, "Basic integer does not parse right.");
pqxx::from_string("-1", x);
PQXX_CHECK_EQUAL(-1, x, "Negative numbers don't work.");
pqxx::from_string("9999", x);
PQXX_CHECK_EQUAL(9999, x, "Larger numbers don't work.");
pqxx::from_string("-9999", x);
PQXX_CHECK_EQUAL(-9999, x, "Larger negative numbers don't work.");
// Bug #263 describes a case where this kind of overflow went undetected.
if (sizeof(unsigned int) == 4)
{
std::uint32_t u;
PQXX_CHECK_THROWS(
pqxx::from_string("4772185884", u), pqxx::conversion_error,
"Overflow not detected.");
}
// We can convert to and from long double. The implementation may fall
// back on a thread-local std::stringstream. Each call does its own
// cleanup, so the conversion works multiple times.
constexpr long double ld1{123456789.25}, ld2{9876543210.5};
constexpr char lds1[]{"123456789.25"}, lds2[]{"9876543210.5"};
PQXX_CHECK_EQUAL(
pqxx::to_string(ld1).substr(0, 12), lds1,
"Wrong conversion from long double.");
PQXX_CHECK_EQUAL(
pqxx::to_string(ld2).substr(0, 12), lds2,
"Wrong value on repeated conversion from long double.");
long double ldi1, ldi2;
pqxx::from_string(lds1, ldi1);
PQXX_CHECK_BOUNDS(
ldi1, ld1 - thres, ld1 + thres, "Wrong conversion to long double.");
pqxx::from_string(lds2, ldi2);
PQXX_CHECK_BOUNDS(
ldi2, ld2 - thres, ld2 + thres,
"Wrong repeated conversion to long double.");
// We can define string conversions for enums.
PQXX_CHECK_EQUAL(
pqxx::to_string(ea0), "0", "Enum-to-string conversion is broken.");
PQXX_CHECK_EQUAL(
pqxx::to_string(eb0), "0",
"Enum-to-string conversion is inconsistent between enum types.");
PQXX_CHECK_EQUAL(
pqxx::to_string(ea1), "1",
"Enum-to-string conversion breaks for nonzero value.");
EnumA ea;
pqxx::from_string("2", ea);
PQXX_CHECK_EQUAL(ea, ea2, "String-to-enum conversion is broken.");
}
void test_convert_variant_to_string()
{
PQXX_CHECK_EQUAL(
pqxx::to_string(std::variant<int, std::string>{99}), "99",
"First variant field did not convert right.");
PQXX_CHECK_EQUAL(
pqxx::to_string(std::variant<int, std::string>{"Text"}), "Text",
"Second variant field did not convert right.");
}
void test_integer_conversion()
{
PQXX_CHECK_EQUAL(
pqxx::from_string<int>("12"), 12, "Basic integer conversion failed.");
PQXX_CHECK_EQUAL(
pqxx::from_string<int>(" 12"), 12,
"Leading whitespace confused integer conversion.");
PQXX_CHECK_THROWS(
pqxx::ignore_unused(pqxx::from_string<int>("")), pqxx::conversion_error,
"Converting empty string to integer did not throw conversion error.");
PQXX_CHECK_THROWS(
pqxx::ignore_unused(pqxx::from_string<int>(" ")), pqxx::conversion_error,
"Converting whitespace to integer did not throw conversion error.");
PQXX_CHECK_EQUAL(
pqxx::from_string<int>("-6"), -6,
"Leading whitespace did not work with negative number.");
PQXX_CHECK_THROWS(
pqxx::ignore_unused(pqxx::from_string<int>("- 3")), pqxx::conversion_error,
"A space between negation and number was not properly flagged.");
PQXX_CHECK_THROWS(
pqxx::ignore_unused(pqxx::from_string<int>("-")), pqxx::conversion_error,
"Just a minus sign should not parse as an integer.");
}
void test_convert_null()
{
pqxx::connection conn;
pqxx::work tx{conn};
PQXX_CHECK_EQUAL(
tx.quote(nullptr), "NULL", "Null pointer did not come out as SQL 'null'.");
PQXX_CHECK_EQUAL(
tx.quote(std::nullopt), "NULL",
"std::nullopt did not come out as SQL 'null'.");
PQXX_CHECK_EQUAL(
tx.quote(std::monostate{}), "NULL",
"std::monostate did not come out as SQL 'null'.");
}
PQXX_REGISTER_TEST(test_string_conversion);
PQXX_REGISTER_TEST(test_convert_variant_to_string);
PQXX_REGISTER_TEST(test_integer_conversion);
PQXX_REGISTER_TEST(test_convert_null);
} // namespace

View File

@@ -0,0 +1,78 @@
#include <pqxx/subtransaction>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void make_table(pqxx::transaction_base &trans)
{
trans.exec0("CREATE TEMP TABLE foo (x INTEGER)");
}
void insert_row(pqxx::transaction_base &trans)
{
trans.exec0("INSERT INTO foo(x) VALUES (1)");
}
int count_rows(pqxx::transaction_base &trans)
{
return trans.query_value<int>("SELECT count(*) FROM foo");
}
void test_subtransaction_commits_if_commit_called(pqxx::connection &conn)
{
pqxx::work trans(conn);
make_table(trans);
{
pqxx::subtransaction sub(trans);
insert_row(sub);
sub.commit();
}
PQXX_CHECK_EQUAL(
count_rows(trans), 1, "Work done in committed subtransaction was lost.");
}
void test_subtransaction_aborts_if_abort_called(pqxx::connection &conn)
{
pqxx::work trans(conn);
make_table(trans);
{
pqxx::subtransaction sub(trans);
insert_row(sub);
sub.abort();
}
PQXX_CHECK_EQUAL(
count_rows(trans), 0, "Aborted subtransaction was not rolled back.");
}
void test_subtransaction_aborts_implicitly(pqxx::connection &conn)
{
pqxx::work trans(conn);
make_table(trans);
{
pqxx::subtransaction sub(trans);
insert_row(sub);
}
PQXX_CHECK_EQUAL(
count_rows(trans), 0,
"Uncommitted subtransaction was not rolled back uring destruction.");
}
void test_subtransaction()
{
pqxx::connection conn;
test_subtransaction_commits_if_commit_called(conn);
test_subtransaction_aborts_if_abort_called(conn);
test_subtransaction_aborts_implicitly(conn);
}
PQXX_REGISTER_TEST(test_subtransaction);
} // namespace

View File

@@ -0,0 +1,214 @@
#include "../test_helpers.hxx"
namespace
{
void empty() {}
void test_check_notreached()
{
// At a minimum, PQXX_CHECK_NOTREACHED must work.
bool failed{true};
try
{
PQXX_CHECK_NOTREACHED("(expected)");
failed = false;
}
catch (pqxx::test::test_failure const &)
{
// This is what we expect.
}
if (not failed)
throw pqxx::test::test_failure(
__FILE__, __LINE__, "PQXX_CHECK_NOTREACHED is broken.");
}
// Test PQXX_CHECK.
void test_check()
{
PQXX_CHECK(true, "PQXX_CHECK is broken.");
bool failed{true};
try
{
PQXX_CHECK(false, "(expected)");
failed = false;
}
catch (pqxx::test::test_failure const &)
{}
if (not failed)
PQXX_CHECK_NOTREACHED("PQXX_CHECK failed to notice failure.");
}
// Test PQXX_CHECK_THROWS_EXCEPTION.
void test_check_throws_exception()
{
// PQXX_CHECK_THROWS_EXCEPTION expects std::exception...
PQXX_CHECK_THROWS_EXCEPTION(
throw std::exception(),
"PQXX_CHECK_THROWS_EXCEPTION did not catch std::exception.");
// ...or any exception type derived from it.
PQXX_CHECK_THROWS_EXCEPTION(
throw pqxx::test::test_failure(__FILE__, __LINE__, "(expected)"),
"PQXX_CHECK_THROWS_EXCEPTION() failed to catch expected exception.");
// Any other type is an error.
bool failed{true};
try
{
PQXX_CHECK_THROWS_EXCEPTION(throw 1, "(expected)");
failed = false;
}
catch (pqxx::test::test_failure const &)
{}
PQXX_CHECK(
failed,
"PQXX_CHECK_THROWS_EXCEPTION did not complain about non-exception.");
// But there _must_ be an exception.
failed = true;
try
{
// If the test fails to throw, this throws a failure.
PQXX_CHECK_THROWS_EXCEPTION(empty(), "(expected)");
// So we shouldn't get to this point.
failed = false;
}
catch (pqxx::test::test_failure const &)
{
// Instead, we go straight here.
}
PQXX_CHECK(
failed, "PQXX_CHECK_THROWS_EXCEPTION did not notice missing exception.");
// PQXX_CHECK_THROWS_EXCEPTION can test itself...
PQXX_CHECK_THROWS_EXCEPTION(
PQXX_CHECK_THROWS_EXCEPTION(empty(), "(expected)"),
"PQXX_CHECK_THROWS_EXCEPTION failed to throw for missing exception.");
PQXX_CHECK_THROWS_EXCEPTION(
PQXX_CHECK_THROWS_EXCEPTION(throw 1, "(expected)"),
"PQXX_CHECK_THROWS_EXCEPTION ignored wrong exception type.");
}
// Test PQXX_CHECK_THROWS.
void test_check_throws()
{
PQXX_CHECK_THROWS(
throw pqxx::test::test_failure(__FILE__, __LINE__, "(expected)"),
pqxx::test::test_failure,
"PQXX_CHECK_THROWS() failed to catch expected exception.");
// Even if it's not std::exception-derived.
PQXX_CHECK_THROWS(throw 1, int, "(expected)");
// PQXX_CHECK_THROWS means there _must_ be an exception.
bool failed{true};
try
{
// If the test fails to throw, PQXX_CHECK_THROWS throws a failure.
PQXX_CHECK_THROWS(empty(), std::runtime_error, "(expected)");
// So we shouldn't get to this point.
failed = false;
}
catch (pqxx::test::test_failure const &)
{
// Instead, we go straight here.
}
PQXX_CHECK(failed, "PQXX_CHECK_THROWS did not notice missing exception.");
// The exception must be of the right type (or a subclass of the right type).
failed = true;
try
{
// If the test throws the wrong type, PQXX_CHECK_THROWS throws a failure.
PQXX_CHECK_THROWS(
throw std::exception(), pqxx::test::test_failure, "(expected)");
failed = false;
}
catch (pqxx::test::test_failure const &)
{
// Instead, we go straight here.
}
PQXX_CHECK(failed, "PQXX_CHECK_THROWS did not notice wrong exception type.");
// PQXX_CHECK_THROWS can test itself...
PQXX_CHECK_THROWS(
PQXX_CHECK_THROWS(empty(), pqxx::test::test_failure, "(expected)"),
pqxx::test::test_failure,
"PQXX_CHECK_THROWS failed to throw for missing exception.");
PQXX_CHECK_THROWS(
PQXX_CHECK_THROWS(throw 1, std::runtime_error, "(expected)"),
pqxx::test::test_failure,
"PQXX_CHECK_THROWS failed to throw for wrong exception type.");
}
void test_test_helpers()
{
test_check_notreached();
test_check();
test_check_throws_exception();
test_check_throws();
// Test other helpers against PQXX_CHECK_THROWS.
PQXX_CHECK_THROWS(
PQXX_CHECK_NOTREACHED("(expected)"), pqxx::test::test_failure,
"PQXX_CHECK_THROWS did not catch PQXX_CHECK_NOTREACHED.");
PQXX_CHECK_THROWS(
PQXX_CHECK(false, "(expected)"), pqxx::test::test_failure,
"PQXX_CHECK_THROWS did not catch failing PQXX_CHECK.");
PQXX_CHECK_THROWS(
PQXX_CHECK_THROWS(
PQXX_CHECK(true, "(shouldn't happen)"), pqxx::test::test_failure,
"(expected)"),
pqxx::test::test_failure,
"PQXX_CHECK_THROWS on successful PQXX_CHECK failed to throw.");
// PQXX_CHECK_EQUAL tests for equality. Its arguments need not be of the
// same type, as long as equality between them is defined.
PQXX_CHECK_EQUAL(1, 1, "PQXX_CHECK_EQUAL is broken.");
PQXX_CHECK_EQUAL(1, 1L, "PQXX_CHECK_EQUAL breaks on type mismatch.");
PQXX_CHECK_THROWS(
PQXX_CHECK_EQUAL(1, 2, "(expected)"), pqxx::test::test_failure,
"PQXX_CHECK_EQUAL fails to spot inequality.");
// PQXX_CHECK_NOT_EQUAL is like PQXX_CHECK_EQUAL, but tests for inequality.
PQXX_CHECK_NOT_EQUAL(1, 2, "PQXX_CHECK_NOT_EQUAL is broken.");
PQXX_CHECK_THROWS(
PQXX_CHECK_NOT_EQUAL(1, 1, "(expected)"), pqxx::test::test_failure,
"PQXX_CHECK_NOT_EQUAL fails to fail when arguments are equal.");
PQXX_CHECK_THROWS(
PQXX_CHECK_NOT_EQUAL(1, 1L, "(expected)"), pqxx::test::test_failure,
"PQXX_CHECK_NOT_EQUAL breaks on type mismatch.");
// PQXX_CHECK_BOUNDS checks a value against a range.
PQXX_CHECK_BOUNDS(2, 1, 3, "PQXX_CHECK_BOUNDS wrongly finds fault.");
PQXX_CHECK_THROWS(
PQXX_CHECK_BOUNDS(1, 2, 3, "(Expected)"), pqxx::test::test_failure,
"PQXX_CHECK_BOUNDS did not detect value below permitted range.");
// PQXX_CHECK_BOUNDS tests against a half-open interval.
PQXX_CHECK_BOUNDS(1, 1, 3, "PQXX_CHECK_BOUNDS goes wrong on lower bound.");
PQXX_CHECK_THROWS(
PQXX_CHECK_BOUNDS(3, 1, 3, "(Expected)"), pqxx::test::test_failure,
"PQXX_CHECK_BOUNDS interval is not half-open.");
// PQXX_CHECK_BOUNDS deals well with empty intervals.
PQXX_CHECK_THROWS(
PQXX_CHECK_BOUNDS(1, 2, 1, "(Expected)"), pqxx::test::test_failure,
"PQXX_CHECK_BOUNDS did not detect empty interval.");
}
PQXX_REGISTER_TEST(test_test_helpers);
} // namespace

View File

@@ -0,0 +1,23 @@
#include "../test_helpers.hxx"
#include <pqxx/util>
namespace
{
void test_thread_safety_model()
{
auto const model{pqxx::describe_thread_safety()};
if (model.safe_libpq and model.safe_kerberos)
PQXX_CHECK_EQUAL(
model.description, "",
"Thread-safety looks okay but model description is nonempty.");
else
PQXX_CHECK_NOT_EQUAL(
model.description, "",
"Thread-safety model is imperfect but lacks description.");
}
PQXX_REGISTER_TEST(test_thread_safety_model);
} // namespace

View File

@@ -0,0 +1,86 @@
#include <pqxx/time>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
#if defined(PQXX_HAVE_YEAR_MONTH_DAY)
using namespace std::literals;
void test_date_string_conversion()
{
pqxx::connection conn;
pqxx::work tx{conn};
std::tuple<int, unsigned, unsigned, std::string_view> const conversions[]{
{-542, 1, 1, "0543-01-01 BC"sv},
{-1, 2, 3, "0002-02-03 BC"sv},
{0, 9, 14, "0001-09-14 BC"sv},
{1, 12, 8, "0001-12-08"sv},
{2021, 10, 24, "2021-10-24"sv},
{10191, 8, 30, "10191-08-30"sv},
{-4712, 1, 1, "4713-01-01 BC"sv},
{32767, 12, 31, "32767-12-31"sv},
{2000, 2, 29, "2000-02-29"sv},
{2004, 2, 29, "2004-02-29"sv},
// This one won't work in postgres, but we can test the conversions.
{-32767, 11, 3, "32768-11-03 BC"sv},
};
for (auto const &[y, m, d, text] : conversions)
{
std::chrono::year_month_day const date{
std::chrono::year{y}, std::chrono::month{m}, std::chrono::day{d}};
PQXX_CHECK_EQUAL(
pqxx::to_string(date), text, "Date did not convert right.");
PQXX_CHECK_EQUAL(
pqxx::from_string<std::chrono::year_month_day>(text), date,
"Date did not parse right.");
if (int{date.year()} > -4712)
{
// We can't test this for years before 4713 BC (4712 BCE), because
// postgres doesn't handle earlier years.
PQXX_CHECK_EQUAL(
tx.query_value<std::string>(
"SELECT '" + pqxx::to_string(date) + "'::date"),
text, "Backend interpreted date differently.");
}
}
std::string_view const invalid[]{
""sv,
"yesterday"sv,
"1981-01"sv,
"2010"sv,
"2010-8-9"sv,
"1900-02-29"sv,
"2021-02-29"sv,
"2000-11-29-3"sv,
"1900-02-29"sv,
"2003-02-29"sv,
"12-12-12"sv,
"0000-09-16"sv,
"-01-01"sv,
"-1000-01-01"sv,
"1000-00-01"sv,
"1000-01-00"sv,
"2001y-01-01"sv,
"10-09-08"sv,
"0-01-01"sv,
"0000-01-01"sv,
"2021-13-01"sv,
"2021-+02-01"sv,
"2021-12-32"sv,
};
for (auto const text : invalid)
PQXX_CHECK_THROWS(
pqxx::ignore_unused(
pqxx::from_string<std::chrono::year_month_day>(text)),
pqxx::conversion_error,
pqxx::internal::concat("Invalid date '", text, "' parsed as if valid."));
}
PQXX_REGISTER_TEST(test_date_string_conversion);
#endif // PQXX_HAVE_YEAR_MONTH_DAY
} // namespace

View File

@@ -0,0 +1,113 @@
#include <pqxx/nontransaction>
#include <pqxx/robusttransaction>
#include "../test_helpers.hxx"
namespace
{
void test_nontransaction_continues_after_error()
{
pqxx::connection c;
pqxx::nontransaction tx{c};
PQXX_CHECK_EQUAL(
tx.query_value<int>("SELECT 9"), 9, "Simple query went wrong.");
PQXX_CHECK_THROWS(
tx.exec("SELECT 1/0"), pqxx::sql_error, "Expected error did not happen.");
PQXX_CHECK_EQUAL(
tx.query_value<int>("SELECT 5"), 5, "Wrong result after error.");
}
std::string const table{"pqxx_test_transaction"};
void delete_temp_table(pqxx::transaction_base &tx)
{
tx.exec0(std::string{"DROP TABLE IF EXISTS "} + table);
}
void create_temp_table(pqxx::transaction_base &tx)
{
tx.exec0("CREATE TEMP TABLE " + table + " (x integer)");
}
void insert_temp_table(pqxx::transaction_base &tx, int value)
{
tx.exec0(
"INSERT INTO " + table + " (x) VALUES (" + pqxx::to_string(value) + ")");
}
int count_temp_table(pqxx::transaction_base &tx)
{
return tx.query_value<int>("SELECT count(*) FROM " + table);
}
void test_nontransaction_autocommits()
{
pqxx::connection c;
pqxx::nontransaction tx1{c};
delete_temp_table(tx1);
create_temp_table(tx1);
tx1.commit();
pqxx::nontransaction tx2{c};
insert_temp_table(tx2, 4);
tx2.abort();
pqxx::nontransaction tx3{c};
PQXX_CHECK_EQUAL(
count_temp_table(tx3), 1,
"Did not keep effect of aborted nontransaction.");
delete_temp_table(tx3);
}
template<typename TX> void test_double_close()
{
pqxx::connection c;
TX tx1{c};
tx1.exec1("SELECT 1");
tx1.commit();
tx1.commit();
TX tx2{c};
tx2.exec1("SELECT 2");
tx2.abort();
tx2.abort();
TX tx3{c};
tx3.exec1("SELECT 3");
tx3.commit();
PQXX_CHECK_THROWS(
tx3.abort(), pqxx::usage_error, "Abort after commit not caught.");
;
TX tx4{c};
tx4.exec1("SELECT 4");
tx4.abort();
PQXX_CHECK_THROWS(
tx4.commit(), pqxx::usage_error, "Commit after abort not caught.");
}
void test_transaction()
{
test_nontransaction_continues_after_error();
test_nontransaction_autocommits();
test_double_close<pqxx::transaction<>>();
test_double_close<pqxx::read_transaction>();
test_double_close<pqxx::nontransaction>();
test_double_close<pqxx::robusttransaction<>>();
}
PQXX_REGISTER_TEST(test_transaction);
} // namespace

View File

@@ -0,0 +1,106 @@
#include <pqxx/stream_to>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
void test_exec0(pqxx::transaction_base &trans)
{
pqxx::result E{trans.exec0("SELECT * FROM pg_tables WHERE 0 = 1")};
PQXX_CHECK(std::empty(E), "Nonempty result from exec0.");
PQXX_CHECK_THROWS(
trans.exec0("SELECT 99"), pqxx::unexpected_rows,
"Nonempty exec0 result did not throw unexpected_rows.");
}
void test_exec1(pqxx::transaction_base &trans)
{
pqxx::row R{trans.exec1("SELECT 99")};
PQXX_CHECK_EQUAL(std::size(R), 1, "Wrong size result from exec1.");
PQXX_CHECK_EQUAL(R.front().as<int>(), 99, "Wrong result from exec1.");
PQXX_CHECK_THROWS(
trans.exec1("SELECT * FROM pg_tables WHERE 0 = 1"), pqxx::unexpected_rows,
"Empty exec1 result did not throw unexpected_rows.");
PQXX_CHECK_THROWS(
trans.exec1("SELECT * FROM generate_series(1, 2)"), pqxx::unexpected_rows,
"Two-row exec1 result did not throw unexpected_rows.");
}
void test_exec_n(pqxx::transaction_base &trans)
{
pqxx::result R{trans.exec_n(3, "SELECT * FROM generate_series(1, 3)")};
PQXX_CHECK_EQUAL(std::size(R), 3, "Wrong result size from exec_n.");
PQXX_CHECK_THROWS(
trans.exec_n(2, "SELECT * FROM generate_series(1, 3)"),
pqxx::unexpected_rows,
"exec_n did not throw unexpected_rows for an undersized result.");
PQXX_CHECK_THROWS(
trans.exec_n(4, "SELECT * FROM generate_series(1, 3)"),
pqxx::unexpected_rows,
"exec_n did not throw unexpected_rows for an oversized result.");
}
void test_query_value(pqxx::connection &conn)
{
pqxx::work tx{conn};
PQXX_CHECK_EQUAL(
tx.query_value<int>("SELECT 84 / 2"), 42,
"Got wrong value from query_value.");
PQXX_CHECK_THROWS(
tx.query_value<int>("SAVEPOINT dummy"), pqxx::unexpected_rows,
"Got field when none expected.");
PQXX_CHECK_THROWS(
tx.query_value<int>("SELECT generate_series(1, 2)"), pqxx::unexpected_rows,
"Failed to fail for multiple rows.");
PQXX_CHECK_THROWS(
tx.query_value<int>("SELECT 1, 2"), pqxx::usage_error,
"No error for too many fields.");
PQXX_CHECK_THROWS(
tx.query_value<int>("SELECT 3.141"), pqxx::conversion_error,
"Got int field from float string.");
}
void test_transaction_base()
{
pqxx::connection conn;
{
pqxx::work tx{conn};
test_exec_n(tx);
test_exec0(tx);
test_exec1(tx);
}
test_query_value(conn);
}
void test_transaction_for_each()
{
constexpr auto query{
"SELECT i, concat('x', (2*i)::text) "
"FROM generate_series(1, 3) AS i "
"ORDER BY i"};
pqxx::connection conn;
pqxx::work tx{conn};
std::string ints;
std::string strings;
tx.for_each(query, [&ints, &strings](int i, std::string const &s) {
ints += pqxx::to_string(i) + " ";
strings += s + " ";
});
PQXX_CHECK_EQUAL(ints, "1 2 3 ", "Unexpected int sequence.");
PQXX_CHECK_EQUAL(strings, "x2 x4 x6 ", "Unexpected string sequence.");
}
PQXX_REGISTER_TEST(test_transaction_base);
PQXX_REGISTER_TEST(test_transaction_for_each);
} // namespace

View File

@@ -0,0 +1,53 @@
#include <pqxx/stream_from>
#include <pqxx/transaction>
#include "../test_helpers.hxx"
namespace
{
auto make_focus(pqxx::dbtransaction &tx)
{
return pqxx::stream_from::query(tx, "SELECT * from generate_series(1, 10)");
}
void test_cannot_run_statement_during_focus()
{
pqxx::connection conn;
pqxx::transaction tx{conn};
tx.exec("SELECT 1");
auto focus{make_focus(tx)};
PQXX_CHECK_THROWS(
tx.exec("SELECT 1"), pqxx::usage_error,
"Command during focus did not throw expected error.");
}
void test_cannot_run_prepared_statement_during_focus()
{
pqxx::connection conn;
conn.prepare("foo", "SELECT 1");
pqxx::transaction tx{conn};
tx.exec_prepared("foo");
auto focus{make_focus(tx)};
PQXX_CHECK_THROWS(
tx.exec_prepared("foo"), pqxx::usage_error,
"Prepared statement during focus did not throw expected error.");
}
void test_cannot_run_params_statement_during_focus()
{
pqxx::connection conn;
pqxx::transaction tx{conn};
tx.exec_params("select $1", 10);
auto focus{make_focus(tx)};
PQXX_CHECK_THROWS(
tx.exec_params("select $1", 10), pqxx::usage_error,
"Parameterized statement during focus did not throw expected error.");
}
PQXX_REGISTER_TEST(test_cannot_run_statement_during_focus);
PQXX_REGISTER_TEST(test_cannot_run_prepared_statement_during_focus);
PQXX_REGISTER_TEST(test_cannot_run_params_statement_during_focus);
} // namespace

View File

@@ -0,0 +1,130 @@
#include <pqxx/transaction>
#include <pqxx/transactor>
#include "../test_helpers.hxx"
namespace
{
void test_transactor_newstyle_executes_simple_query()
{
pqxx::connection conn;
auto const r{pqxx::perform([&conn] {
return pqxx::work{conn}.exec("SELECT generate_series(1, 4)");
})};
PQXX_CHECK_EQUAL(std::size(r), 4, "Unexpected result size.");
PQXX_CHECK_EQUAL(r.columns(), 1, "Unexpected number of columns.");
PQXX_CHECK_EQUAL(r[0][0].as<int>(), 1, "Unexpected first row.");
PQXX_CHECK_EQUAL(r[3][0].as<int>(), 4, "Unexpected last row.");
}
void test_transactor_newstyle_can_return_void()
{
bool done{false};
pqxx::perform([&done]() noexcept { done = true; });
PQXX_CHECK(done, "Callback was not executed.");
}
void test_transactor_newstyle_completes_upon_success()
{
int attempts{0};
pqxx::perform([&attempts]() noexcept { attempts++; });
PQXX_CHECK_EQUAL(attempts, 1, "Successful transactor didn't run 1 time.");
}
void test_transactor_newstyle_retries_broken_connection()
{
int counter{0};
auto const &callback{[&counter] {
++counter;
if (counter == 1)
throw pqxx::broken_connection();
return counter;
}};
int const result{pqxx::perform(callback)};
PQXX_CHECK_EQUAL(result, 2, "Transactor run returned wrong result.");
PQXX_CHECK_EQUAL(counter, result, "Number of retries does not match.");
}
void test_transactor_newstyle_retries_rollback()
{
int counter{0};
auto const &callback{[&counter] {
++counter;
if (counter == 1)
throw pqxx::transaction_rollback("Simulated error");
return counter;
}};
int const result{pqxx::perform(callback)};
PQXX_CHECK_EQUAL(result, 2, "Transactor run returned wrong result.");
PQXX_CHECK_EQUAL(counter, result, "Number of retries does not match.");
}
void test_transactor_newstyle_does_not_retry_in_doubt_error()
{
int counter{0};
auto const &callback{[&counter] {
++counter;
throw pqxx::in_doubt_error("Simulated error");
}};
PQXX_CHECK_THROWS(
pqxx::perform(callback), pqxx::in_doubt_error,
"Transactor did not propagate in_doubt_error.");
PQXX_CHECK_EQUAL(counter, 1, "Transactor retried after in_doubt_error.");
}
void test_transactor_newstyle_does_not_retry_other_error()
{
int counter{0};
auto const &callback{[&counter] {
++counter;
throw std::runtime_error("Simulated error");
}};
PQXX_CHECK_THROWS(
pqxx::perform(callback), std::runtime_error,
"Transactor did not propagate std exception.");
PQXX_CHECK_EQUAL(counter, 1, "Transactor retried after std exception.");
}
void test_transactor_newstyle_repeats_up_to_given_number_of_attempts()
{
int const attempts{5};
int counter{0};
auto const &callback{[&counter] {
++counter;
throw pqxx::transaction_rollback("Simulated error");
}};
PQXX_CHECK_THROWS(
pqxx::perform(callback, attempts), pqxx::transaction_rollback,
"Not propagating original exception.");
PQXX_CHECK_EQUAL(counter, attempts, "Number of retries does not match.");
}
void test_transactor()
{
test_transactor_newstyle_executes_simple_query();
test_transactor_newstyle_can_return_void();
test_transactor_newstyle_completes_upon_success();
test_transactor_newstyle_retries_broken_connection();
test_transactor_newstyle_retries_rollback();
test_transactor_newstyle_does_not_retry_in_doubt_error();
test_transactor_newstyle_does_not_retry_other_error();
test_transactor_newstyle_repeats_up_to_given_number_of_attempts();
}
PQXX_REGISTER_TEST(test_transactor);
} // namespace

View File

@@ -0,0 +1,19 @@
#include "../test_helpers.hxx"
namespace
{
void test_type_name()
{
// It's hard to test in more detail, because spellings may differ.
// For instance, one compiler might call "const unsigned int*" what another
// might call "unsigned const *". And Visual Studio prefixes "class" to
// class types.
std::string const i{pqxx::type_name<int>};
PQXX_CHECK_LESS(std::size(i), 5u, "type_name<int> is suspiciously long.");
PQXX_CHECK_EQUAL(
i.substr(0, 1), "i", "type_name<int> does not start with 'i'.");
}
PQXX_REGISTER_TEST(test_type_name);
} // namespace

View File

@@ -0,0 +1,16 @@
#include <pqxx/zview>
#include "../test_helpers.hxx"
namespace
{
void test_zview_literal()
{
using pqxx::operator"" _zv;
PQXX_CHECK_EQUAL(("foo"_zv), pqxx::zview{"foo"}, "zview literal is broken.");
}
PQXX_REGISTER_TEST(test_zview_literal);
} // namespace