Loading...
Searching...
No Matches
array.hxx
/* Handling of SQL arrays.
*
* DO NOT INCLUDE THIS FILE DIRECTLY; include pqxx/field instead.
*
* Copyright (c) 2000-2024, Jeroen T. Vermeulen.
*
* See COPYING for copyright license. If you did not receive a file called
* COPYING with this source code, please notify the distributor of this
* mistake, or contact the author.
*/
#ifndef PQXX_H_ARRAY
#define PQXX_H_ARRAY
#if !defined(PQXX_HEADER_PRE)
# error "Include libpqxx headers as <pqxx/header>, not <pqxx/header.hxx>."
#endif
#include <algorithm>
#include <cassert>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "pqxx/connection.hxx"
#include "pqxx/internal/array-composite.hxx"
#include "pqxx/internal/encoding_group.hxx"
#include "pqxx/internal/encodings.hxx"
namespace pqxx
{
// TODO: Specialise for string_view/zview, allocate all strings in one buffer.
template<
typename ELEMENT, std::size_t DIMENSIONS = 1u,
char SEPARATOR = array_separator<ELEMENT>>
class array final
{
public:
array(std::string_view data, connection const &cx) :
array{data, pqxx::internal::enc_group(cx.encoding_id())}
{}
constexpr std::size_t dimensions() noexcept { return DIMENSIONS; }
std::array<std::size_t, DIMENSIONS> const &sizes() noexcept
{
return m_extents;
}
template<typename... INDEX> ELEMENT const &at(INDEX... index) const
{
static_assert(sizeof...(index) == DIMENSIONS);
check_bounds(index...);
return m_elts.at(locate(index...));
}
template<typename... INDEX> ELEMENT const &operator[](INDEX... index) const
{
static_assert(sizeof...(index) == DIMENSIONS);
return m_elts[locate(index...)];
}
constexpr auto cbegin() const noexcept { return m_elts.cbegin(); }
constexpr auto cend() const noexcept { return m_elts.cend(); }
constexpr auto crbegin() const noexcept { return m_elts.crbegin(); }
constexpr auto crend() const noexcept { return m_elts.crend(); }
constexpr std::size_t size() const noexcept { return m_elts.size(); }
constexpr auto ssize() const noexcept
{
return static_cast<std::ptrdiff_t>(size());
}
constexpr auto front() const noexcept { return m_elts.front(); }
constexpr auto back() const noexcept { return m_elts.back(); }
private:
void check_dims(std::string_view data)
{
auto sz{std::size(data)};
if (sz < DIMENSIONS * 2)
throw conversion_error{pqxx::internal::concat(
"Trying to parse a ", DIMENSIONS, "-dimensional array out of '", data,
"'.")};
// Making some assumptions here:
// * The array holds no extraneous whitespace.
// * None of the sub-arrays can be null.
// * Only ASCII characters start off with a byte in the 0-127 range.
//
// Given those, the input must start with a sequence of DIMENSIONS bytes
// with the ASCII value for '{'; and likewise it must end with a sequence
// of DIMENSIONS bytes with the ASCII value for '}'.
if (data[0] != '{')
throw conversion_error{"Malformed array: does not start with '{'."};
for (std::size_t i{0}; i < DIMENSIONS; ++i)
if (data[i] != '{')
throw conversion_error{pqxx::internal::concat(
"Expecting ", DIMENSIONS, "-dimensional array, but found ", i, ".")};
if (data[DIMENSIONS] == '{')
throw conversion_error{pqxx::internal::concat(
"Tried to parse ", DIMENSIONS,
"-dimensional array from array data that has more dimensions.")};
for (std::size_t i{0}; i < DIMENSIONS; ++i)
if (data[sz - 1 - i] != '}')
throw conversion_error{
"Malformed array: does not end in the right number of '}'."};
}
// Allow fields to construct arrays passing the encoding group.
// Couldn't make this work through a call gate, thanks to the templating.
friend class ::pqxx::field;
array(std::string_view data, pqxx::internal::encoding_group enc)
{
using group = pqxx::internal::encoding_group;
switch (enc)
{
case group::MONOBYTE: parse<group::MONOBYTE>(data); break;
case group::BIG5: parse<group::BIG5>(data); break;
case group::EUC_CN: parse<group::EUC_CN>(data); break;
case group::EUC_JP: parse<group::EUC_JP>(data); break;
case group::EUC_KR: parse<group::EUC_KR>(data); break;
case group::EUC_TW: parse<group::EUC_TW>(data); break;
case group::GB18030: parse<group::GB18030>(data); break;
case group::GBK: parse<group::GBK>(data); break;
case group::JOHAB: parse<group::JOHAB>(data); break;
case group::MULE_INTERNAL: parse<group::MULE_INTERNAL>(data); break;
case group::SJIS: parse<group::SJIS>(data); break;
case group::UHC: parse<group::UHC>(data); break;
case group::UTF8: parse<group::UTF8>(data); break;
default: PQXX_UNREACHABLE; break;
}
}
std::size_t parse_field_end(std::string_view data, std::size_t here) const
{
auto const sz{std::size(data)};
if (here < sz)
switch (data[here])
{
case SEPARATOR:
++here;
if (here >= sz)
throw conversion_error{"Array looks truncated."};
switch (data[here])
{
case SEPARATOR:
throw conversion_error{"Array contains double separator."};
case '}': throw conversion_error{"Array contains trailing separator."};
default: break;
}
break;
case '}': break;
default:
throw conversion_error{pqxx::internal::concat(
"Unexpected character in array: ",
static_cast<unsigned>(static_cast<unsigned char>(data[here])),
" where separator or closing brace expected.")};
}
return here;
}
constexpr std::size_t estimate_elements(std::string_view data) const noexcept
{
// Dirty trick: just count the number of bytes that look as if they may be
// separators. At the very worst we may overestimate by a factor of two or
// so, in exceedingly rare cases, on some encodings.
auto const separators{
std::count(std::begin(data), std::end(data), SEPARATOR)};
// The number of dimensions makes no difference here. It's still one
// separator between consecutive elements, just possibly with some extra
// braces as well.
return static_cast<std::size_t>(separators + 1);
}
template<pqxx::internal::encoding_group ENC>
void parse(std::string_view data)
{
static_assert(DIMENSIONS > 0u, "Can't create a zero-dimensional array.");
auto const sz{std::size(data)};
check_dims(data);
m_elts.reserve(estimate_elements(data));
// We discover the array's extents along each of the dimensions, starting
// with the final dimension and working our way towards the first. At any
// given point during parsing, we know the extents starting at this
// dimension.
std::size_t know_extents_from{DIMENSIONS};
// Currently parsing this dimension. We start off at -1, relying on C++'s
// well-defined rollover for unsigned numbers.
// The actual outermost dimension of the array is 0, and the innermost is
// at the end. But, the array as a whole is enclosed in braces just like
// each row. So we act like there's an anomalous "outer" dimension holding
// the entire array.
constexpr std::size_t outer{std::size_t{0u} - std::size_t{1u}};
// We start parsing at the fictional outer dimension. The input begins
// with opening braces, one for each dimension, so we'll start off by
// bumping all the way to the innermost dimension.
std::size_t dim{outer};
// Extent counters, one per "real" dimension.
// Note initialiser syntax; this zero-initialises all elements.
std::array<std::size_t, DIMENSIONS> extents{};
// Current parsing position.
std::size_t here{0};
PQXX_ASSUME(here <= sz);
while (here < sz)
{
if (data[here] == '{')
{
if (dim == outer)
{
// This must be the initial opening brace.
if (know_extents_from != DIMENSIONS)
throw conversion_error{
"Array text representation closed and reopened its outside "
"brace pair."};
assert(here == 0);
PQXX_ASSUME(here == 0);
}
else
{
if (dim >= (DIMENSIONS - 1))
throw conversion_error{
"Array seems to have inconsistent number of dimensions."};
++extents[dim];
}
// (Rolls over to zero if we're coming from the outer dimension.)
++dim;
extents[dim] = 0u;
++here;
}
else if (data[here] == '}')
{
if (dim == outer)
throw conversion_error{"Array has spurious '}'."};
if (dim < know_extents_from)
{
// We just finished parsing our first row in this dimension.
// Now we know the array dimension's extent.
m_extents[dim] = extents[dim];
know_extents_from = dim;
}
else
{
if (extents[dim] != m_extents[dim])
throw conversion_error{"Rows in array have inconsistent sizes."};
}
// Bump back down to the next-lower dimension. Which may be the outer
// dimension, through underflow.
--dim;
++here;
here = parse_field_end(data, here);
}
else
{
// Found an array element. The actual elements always live in the
// "inner" dimension.
if (dim != DIMENSIONS - 1)
throw conversion_error{
"Malformed array: found element where sub-array was expected."};
assert(dim != outer);
++extents[dim];
std::size_t end;
switch (data[here])
{
case '\0': throw conversion_error{"Unexpected zero byte in array."};
case ',': throw conversion_error{"Array contains empty field."};
case '"': {
// Double-quoted string. We parse it into a buffer before parsing
// the resulting string as an element. This seems wasteful: the
// string might not contain any special characters. So it's
// tempting to check, and try to use a string_view and avoid a
// useless copy step. But. Even besides the branch prediction
// risk, the very fact that the back-end chose to quote the string
// indicates that there is some kind of special character in there.
// So in practice, this optimisation would only apply if the only
// special characters in the string were commas.
end = pqxx::internal::scan_double_quoted_string<ENC>(
std::data(data), std::size(data), here);
// TODO: scan_double_quoted_string() with reusable buffer.
std::string const buf{
pqxx::internal::parse_double_quoted_string<ENC>(
std::data(data), end, here)};
m_elts.emplace_back(from_string<ELEMENT>(buf));
}
break;
default: {
// Unquoted string. An unquoted string is always literal, no
// escaping or encoding, so we don't need to parse it into a
// buffer. We can just read it as a string_view.
end = pqxx::internal::scan_unquoted_string<ENC, SEPARATOR, '}'>(
std::data(data), std::size(data), here);
std::string_view const field{
std::string_view{std::data(data) + here, end - here}};
if (field == "NULL")
{
if constexpr (nullness<ELEMENT>::has_null)
m_elts.emplace_back(nullness<ELEMENT>::null());
else
throw unexpected_null{pqxx::internal::concat(
"Array contains a null ", type_name<ELEMENT>,
". Consider making it an array of std::optional<",
type_name<ELEMENT>, "> instead.")};
}
else
m_elts.emplace_back(from_string<ELEMENT>(field));
}
}
here = end;
PQXX_ASSUME(here <= sz);
here = parse_field_end(data, here);
}
}
if (dim != outer)
throw conversion_error{"Malformed array; may be truncated."};
assert(know_extents_from == 0);
PQXX_ASSUME(know_extents_from == 0);
init_factors();
}
void init_factors() noexcept
{
std::size_t factor{1};
for (std::size_t dim{DIMENSIONS - 1}; dim > 0; --dim)
{
factor *= m_extents[dim];
m_factors[dim - 1] = factor;
}
}
template<typename... INDEX> std::size_t locate(INDEX... index) const noexcept
{
static_assert(
sizeof...(index) == DIMENSIONS,
"Indexing array with wrong number of dimensions.");
return add_index(index...);
}
template<typename OUTER, typename... INDEX>
constexpr std::size_t add_index(OUTER outer, INDEX... indexes) const noexcept
{
std::size_t const first{check_cast<std::size_t>(outer, "array index"sv)};
if constexpr (sizeof...(indexes) == 0)
{
return first;
}
else
{
static_assert(sizeof...(indexes) < DIMENSIONS);
// (Offset by 1 here because the outer dimension is not in there.)
constexpr auto dimension{DIMENSIONS - (sizeof...(indexes) + 1)};
static_assert(dimension < DIMENSIONS);
return first * m_factors[dimension] + add_index(indexes...);
}
}
template<typename OUTER, typename... INDEX>
constexpr void check_bounds(OUTER outer, INDEX... indexes) const
{
std::size_t const first{check_cast<std::size_t>(outer, "array index"sv)};
static_assert(sizeof...(indexes) < DIMENSIONS);
// (Offset by 1 here because the outer dimension is not in there.)
constexpr auto dimension{DIMENSIONS - (sizeof...(indexes) + 1)};
static_assert(dimension < DIMENSIONS);
if (first >= m_extents[dimension])
throw range_error{pqxx::internal::concat(
"Array index for dimension ", dimension, " is out of bounds: ", first,
" >= ", m_extents[dimension])};
// Now check the rest of the indexes, if any.
if constexpr (sizeof...(indexes) > 0)
check_bounds(indexes...);
}
std::vector<ELEMENT> m_elts;
std::array<std::size_t, DIMENSIONS> m_extents;
std::array<std::size_t, DIMENSIONS - 1> m_factors;
};
class PQXX_LIBEXPORT array_parser
{
public:
enum class juncture
{
row_start,
row_end,
null_value,
string_value,
done,
};
explicit array_parser(
std::string_view input,
internal::encoding_group = internal::encoding_group::MONOBYTE);
std::pair<juncture, std::string> get_next() { return (this->*m_impl)(); }
private:
std::string_view m_input;
std::size_t m_pos = 0u;
using implementation = std::pair<juncture, std::string> (array_parser::*)();
static implementation
specialize_for_encoding(pqxx::internal::encoding_group enc);
implementation m_impl;
template<pqxx::internal::encoding_group>
std::pair<juncture, std::string> parse_array_step();
template<pqxx::internal::encoding_group>
std::string::size_type scan_double_quoted_string() const;
template<pqxx::internal::encoding_group>
std::string parse_double_quoted_string(std::string::size_type end) const;
template<pqxx::internal::encoding_group>
std::string::size_type scan_unquoted_string() const;
template<pqxx::internal::encoding_group>
std::string_view parse_unquoted_string(std::string::size_type end) const;
template<pqxx::internal::encoding_group>
std::string::size_type scan_glyph(std::string::size_type pos) const;
template<pqxx::internal::encoding_group>
std::string::size_type
scan_glyph(std::string::size_type pos, std::string::size_type end) const;
};
} // namespace pqxx
#endif
pqxx::array::back
constexpr auto back() const noexcept
Refer to the last element, if any.
Definition array.hxx:158
pqxx::array::cend
constexpr auto cend() const noexcept
Return end point of iteration.
Definition array.hxx:117
pqxx::array::crbegin
constexpr auto crbegin() const noexcept
Begin reverse iteration.
Definition array.hxx:119
pqxx::array::operator[]
ELEMENT const & operator[](INDEX... index) const
Access element (without bounds check).
Definition array.hxx:103
pqxx::array::size
constexpr std::size_t size() const noexcept
Number of elements in the array.
Definition array.hxx:127
pqxx::array::ssize
constexpr auto ssize() const noexcept
Number of elements in the array (as a signed number).
Definition array.hxx:145
pqxx::array::dimensions
constexpr std::size_t dimensions() noexcept
How many dimensions does this array have?
Definition array.hxx:75
pqxx::array::cbegin
constexpr auto cbegin() const noexcept
Begin iteration of individual elements.
Definition array.hxx:115
pqxx::array::crend
constexpr auto crend() const noexcept
Return end point of reverse iteration.
Definition array.hxx:121
pqxx::array::sizes
std::array< std::size_t, DIMENSIONS > const & sizes() noexcept
Return the sizes of this array in each of its dimensions.
Definition array.hxx:82
pqxx::array::array
array(std::string_view data, connection const &cx)
Parse an SQL array, read as text from a pqxx::result or stream.
Definition array.hxx:68
pqxx::array::front
constexpr auto front() const noexcept
Refer to the first element, if any.
Definition array.hxx:153
pqxx::internal
Internal items for libpqxx' own use. Do not use these yourself.
Definition encodings.cxx:33
pqxx::internal::concat
std::string concat(TYPE... item)
Efficiently combine a bunch of items into one big string.
Definition concat.hxx:31
pqxx::internal::parse_unquoted_string
std::string_view parse_unquoted_string(char const input[], std::size_t end, std::size_t pos)
Parse an unquoted array entry or cfield of a composite-type field.
Definition array-composite.hxx:149
pqxx::internal::scan_double_quoted_string
std::size_t scan_double_quoted_string(char const input[], std::size_t size, std::size_t pos)
Definition array-composite.hxx:20
pqxx::internal::scan_unquoted_string
std::size_t scan_unquoted_string(char const input[], std::size_t size, std::size_t pos)
Find the end of an unquoted string in an array or composite-type value.
Definition array-composite.hxx:131
pqxx::internal::parse_double_quoted_string
std::string parse_double_quoted_string(char const input[], std::size_t end, std::size_t pos)
Un-quote and un-escape a double-quoted SQL string.
Definition array-composite.hxx:84
pqxx
The home of all libpqxx classes, functions, templates, etc.
Definition array.cxx:27
pqxx::type_name
std::string const type_name
A human-readable name for a type, used in error messages and such.
Definition strconv.hxx:80
pqxx::array_separator
constexpr char array_separator
Element separator between SQL array elements of this type.
Definition strconv.hxx:559
pqxx::from_string
T from_string(field const &value)
Convert a field's value to type T.
Definition field.hxx:548
pqxx::check_cast
TO check_cast(FROM value, std::string_view description)
Cast a numeric value to another type, or throw if it underflows/overflows.
Definition util.hxx:153
pqxx::nullness::null
static TYPE null()
Return a null value.
pqxx::nullness::has_null
static bool has_null
Does this type have a null value?
Definition strconv.hxx:93