#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.");
  }
}


constexpr void test_range_is_constexpr()
{
// Test compile-time operations.
//
// A few things in the standard library need to be constexpr for this to work,
// so we only test it in C++20.
#if __cplusplus >= 202002L
  using range = pqxx::range<int>;
  using ibound = pqxx::inclusive_bound<int>;

  constexpr ibound one{1}, three{3};
  constexpr range oneone{one, one}, onethree{one, three};
  static_assert(oneone == oneone);
  static_assert(oneone != onethree);
  static_assert(onethree.contains(oneone));
#endif
}


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);
PQXX_REGISTER_TEST(test_range_is_constexpr);
} // namespace
