2
0
mirror of https://github.com/boostorg/mysql.git synced 2026-02-15 13:12:21 +00:00

Refactored binary protocol magic numbers

Fixed wider ranges for DATE and DATETIME
Removed old binary (de)serialization functions
This commit is contained in:
ruben
2020-05-08 16:54:22 +01:00
parent d71695b3d4
commit a76caa8a83
10 changed files with 238 additions and 172 deletions

View File

@@ -1,23 +1,13 @@
Sanitize
Refactor binary deserialization tests
serialize_binary_value(), get_binary_value_size() instead of global serialize for floats and times
Move serialization tests to just binary serialization tests
Verify that deserialization test fixture works
Add float, double, times error tests in serialization.cpp
Refactor binary deserialization constants
Change text row to use parameterized tests
Refactor deserialize_binary_value to have similar implementation to the text one
Better resilience of deserialization
Make float inf and nan output NULL?
Lift range check on dates, datetimes, times
Make invalid dates and datetimes output NULL
Integ tests for zero and invalid dates
Complete DATETIME and TIME error tests
Complete DATETIME and TIME deserialize_binary_value error tests
Complete serialize_binary_value tests
Change deserialize_text_value to allow zero and invalid dates/datetimes
Integ tests for zero and invalid dates
Remove deserialization test error fixture
Change text row to use parameterized tests
See if we have any trouble with user input in binary serialization: make assertions
Test with an unknown protocol type
Random input tests
Better docs
Wandbox
Breaking up the tutorial in pieces
Explaining the different overloads and async methods available
Try to include Readme instead of copying it
@@ -54,6 +44,7 @@ Other possible features
Lower C++ std requirements
Status flags accessors in resultset (for OK_Packet)
Technical debt
Random input tests
Change channel read to make less syscalls
Test dashboard
Sanitizers

View File

@@ -74,8 +74,8 @@ constexpr std::uint8_t auth_more_data_header = 0x01;
constexpr std::string_view fast_auth_complete_challenge = "\3";
// Column flags
namespace column_flags
{
namespace column_flags {
constexpr std::uint16_t not_null = 1; // Field can't be NULL.
constexpr std::uint16_t pri_key = 2; // Field is part of a primary key.
constexpr std::uint16_t unique_key = 4; // Field is part of a unique key.
@@ -92,16 +92,73 @@ constexpr std::uint16_t no_default_value = 4096; // Field doesn't have defau
constexpr std::uint16_t on_update_now = 8192; // Field is set to NOW on UPDATE.
constexpr std::uint16_t part_key = 16384; // Intern; Part of some key.
constexpr std::uint16_t num = 32768; // Field is num (for clients)
}
} // column_flags
// Prepared statements
namespace cursor_types
{
namespace cursor_types {
constexpr std::uint8_t no_cursor = 0;
constexpr std::uint8_t read_only = 1;
constexpr std::uint8_t for_update = 2;
constexpr std::uint8_t scrollable = 4;
}
} // cursor_types
// Text protocol deserialization constants
namespace textc {
constexpr unsigned max_decimals = 6u;
constexpr std::size_t year_sz = 4;
constexpr std::size_t month_sz = 2;
constexpr std::size_t day_sz = 2;
constexpr std::size_t hours_min_sz = 2; // in TIME, it may be longer
constexpr std::size_t mins_sz = 2;
constexpr std::size_t secs_sz = 2;
constexpr std::size_t date_sz = year_sz + month_sz + day_sz + 2; // delimiters
constexpr std::size_t time_min_sz = hours_min_sz + mins_sz + secs_sz + 2; // delimiters
constexpr std::size_t time_max_sz = time_min_sz + max_decimals + 3; // sign, period, hour extra character
constexpr std::size_t datetime_min_sz = date_sz + time_min_sz + 1; // delimiter
constexpr unsigned max_hour = 838;
constexpr unsigned max_min = 59;
constexpr unsigned max_sec = 59;
constexpr unsigned max_micro = 999999;
} // textc
// Binary protocol (de)serialization constants
namespace binc {
constexpr std::size_t length_sz = 1; // length byte, for date, datetime and time
constexpr std::size_t year_sz = 2;
constexpr std::size_t month_sz = 1;
constexpr std::size_t date_day_sz = 1;
constexpr std::size_t time_days_sz = 4;
constexpr std::size_t hours_sz = 1;
constexpr std::size_t mins_sz = 1;
constexpr std::size_t secs_sz = 1;
constexpr std::size_t micros_sz = 4;
constexpr std::size_t time_sign_sz = 1;
constexpr std::size_t date_sz = year_sz + month_sz + date_day_sz; // does not include length
constexpr std::size_t datetime_d_sz = date_sz;
constexpr std::size_t datetime_dhms_sz = datetime_d_sz + hours_sz + mins_sz + secs_sz;
constexpr std::size_t datetime_dhmsu_sz = datetime_dhms_sz + micros_sz;
constexpr std::size_t time_dhms_sz = time_sign_sz + time_days_sz + hours_sz + mins_sz + secs_sz;
constexpr std::size_t time_dhmsu_sz = time_dhms_sz + micros_sz;
constexpr std::size_t max_days = 34; // equivalent to the 839 hours, in the broken format
constexpr unsigned max_hour = 23;
constexpr unsigned max_min = 59;
constexpr unsigned max_sec = 59;
constexpr unsigned max_micro = 999999;
} // binc
} // detail

View File

@@ -11,6 +11,7 @@
#include <variant>
#include "boost/mysql/detail/protocol/serialization.hpp"
#include "boost/mysql/detail/protocol/null_bitmap_traits.hpp"
#include "boost/mysql/detail/protocol/constants.hpp"
#include "boost/mysql/detail/auxiliar/tmp.hpp"
namespace boost {
@@ -62,9 +63,8 @@ errc deserialize_binary_value_to_variant_float(
if (!ctx.enough_size(sizeof(T)))
return errc::incomplete_message;
T v;
// Endianness conversion. Boost.Endian support for floats start at 1.71
T v;
#if BOOST_ENDIAN_BIG_BYTE
char buf [sizeof(T)];
std::memcpy(buf, ctx.first(), sizeof(T));
@@ -73,59 +73,78 @@ errc deserialize_binary_value_to_variant_float(
#else
std::memcpy(&v, ctx.first(), sizeof(T));
#endif
ctx.advance(sizeof(T));
// Nans and infs not allowed in SQL
if (std::isnan(v) || std::isinf(v))
{
//output = nullptr;
return errc::protocol_value_error;
}
else
{
output = v;
}
// Done
ctx.advance(sizeof(T));
output = v;
return errc::ok;
}
// Time types
inline errc deserialize_binary_ymd(
deserialization_context& ctx,
::date::year_month_day& output
)
{
int2 year;
int1 month;
int1 day;
auto err = deserialize_fields(ctx, year, month, day);
if (err != errc::ok)
return err;
output = ::date::year_month_day (
::date::year(year.value),
::date::month(month.value),
::date::day(day.value)
);
return errc::ok;
}
inline errc deserialize_binary_value_to_variant_date(
deserialization_context& ctx,
value& output
) noexcept
{
int1 length;
int2 year;
int1 month;
int1 day;
// Deserialize length
int1 length;
auto err = deserialize(length, ctx);
if (err != errc::ok)
return err;
// A zero date. This is represented in C++ as a NULL
if (length.value < 4)
// Check for zero dates, represented in C++ as a NULL
if (length.value < binc::date_sz)
{
output = nullptr;
return errc::ok;
}
// Deserialize rest of fields
err = deserialize_fields(ctx, year, month, day);
::date::year_month_day ymd;
err = deserialize_binary_ymd(ctx, ymd);
if (err != errc::ok)
return err;
::date::year_month_day ymd (::date::year(year.value), ::date::month(month.value), ::date::day(day.value));
if (!ymd.ok()) // invalid date. We represent this as a NULL
// Check for invalid dates, represented as NULL in C++
if (!ymd.ok())
{
output = nullptr;
return errc::ok;
}
// Range check
date d (ymd);
if (d < min_date || d > max_date)
return errc::protocol_value_error;
output = d;
// Convert to sys_days (date)
output = static_cast<date>(ymd);
return errc::ok;
}
@@ -134,58 +153,72 @@ inline errc deserialize_binary_value_to_variant_datetime(
value& output
) noexcept
{
int1 length;
int2 year;
int1 month;
int1 day;
int1 hours;
int1 minutes;
int1 seconds;
int4 micros;
using namespace binc;
// Deserialize length
int1 length;
auto err = deserialize(length, ctx);
if (err != errc::ok)
return err;
// A zero datetime, represented as NULL in C++
if (length.value < 4)
// Check for zero datetimes, represented as NULL in C++
if (length.value < datetime_d_sz)
{
output = nullptr;
return errc::ok;
}
// Date part
err = deserialize_fields(ctx, year, month, day);
// Deserialize date
::date::year_month_day ymd;
err = deserialize_binary_ymd(ctx, ymd);
if (err != errc::ok)
return err;
::date::year_month_day ymd (::date::year(year.value), ::date::month(month.value), ::date::day(day.value));
// Check for invalid dates, represented in C++ as NULL
if (!ymd.ok())
{
output = nullptr;
return errc::ok;
}
// Rest of fields
if (length.value >= 7)
// If the DATETIME contains no value for these fields, they are zero
int1 hours (0);
int1 minutes (0);
int1 seconds (0);
int4 micros (0);
// Hours, minutes, seconds
if (length.value >= datetime_dhms_sz)
{
err = deserialize_fields(ctx, hours, minutes, seconds);
if (err != errc::ok)
return err;
}
if (length.value >= 11)
// Microseconds
if (length.value >= datetime_dhmsu_sz)
{
err = deserialize(micros, ctx);
if (err != errc::ok)
return err;
}
// TODO: range check
// Range check
if (hours.value > max_hour ||
minutes.value > max_min ||
seconds.value > max_sec ||
micros.value > max_micro)
{
return errc::protocol_value_error;
}
// Compose the final datetime. Doing time of day and date separately to avoid overflow
auto time_of_day_part = std::chrono::hours(hours.value) + std::chrono::minutes(minutes.value) +
std::chrono::seconds(seconds.value) + std::chrono::microseconds(micros.value);
output = date(ymd) + time_of_day_part;
auto time_of_day =
std::chrono::hours(hours.value) +
std::chrono::minutes(minutes.value) +
std::chrono::seconds(seconds.value) +
std::chrono::microseconds(micros.value);
output = static_cast<date>(ymd) + time_of_day;
return errc::ok;
}
@@ -194,12 +227,15 @@ inline errc deserialize_binary_value_to_variant_time(
value& output
) noexcept
{
// Length
using namespace binc;
// Deserialize length
int1 length;
auto err = deserialize(length, ctx);
if (err != errc::ok)
return err;
// If the TIME contains no value for these fields, they are zero
int1 is_negative (0);
int4 days (0);
int1 hours (0);
@@ -207,7 +243,8 @@ inline errc deserialize_binary_value_to_variant_time(
int1 seconds(0);
int4 microseconds(0);
if (length.value >= 8)
// Sign, days, hours, minutes, seconds
if (length.value >= time_dhms_sz)
{
err = deserialize_fields(
ctx,
@@ -220,20 +257,33 @@ inline errc deserialize_binary_value_to_variant_time(
if (err != errc::ok)
return err;
}
if (length.value >= 12)
// Microseconds
if (length.value >= time_dhmsu_sz)
{
err = deserialize(microseconds, ctx);
if (err != errc::ok)
return err;
}
output = (is_negative.value ? -1 : 1) * (
// Range check
if (days.value > max_days ||
hours.value > max_hour ||
minutes.value > max_min ||
seconds.value > max_sec ||
microseconds.value > max_micro)
{
return errc::protocol_value_error;
}
// Compose the final time
output = time((is_negative.value ? -1 : 1) * (
::date::days(days.value) +
std::chrono::hours(hours.value) +
std::chrono::minutes(minutes.value) +
std::chrono::seconds(seconds.value) +
std::chrono::microseconds(microseconds.value)
);
));
return errc::ok;
}

View File

@@ -8,54 +8,13 @@
#ifndef INCLUDE_BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_BINARY_SERIALIZATION_IPP_
#define INCLUDE_BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_BINARY_SERIALIZATION_IPP_
#include "boost/mysql/detail/protocol/constants.hpp"
namespace boost {
namespace mysql {
namespace detail {
// Does not add the length prefix byte
inline void serialize_binary_ymd(
const ::date::year_month_day& ymd,
serialization_context& ctx
) noexcept
{
serialize_fields(
ctx,
int2(static_cast<std::uint16_t>(static_cast<int>(ymd.year()))),
int1(static_cast<std::uint8_t>(static_cast<unsigned>(ymd.month()))),
int1(static_cast<std::uint8_t>(static_cast<unsigned>(ymd.day())))
);
}
struct broken_datetime
{
::date::year_month_day ymd;
::date::time_of_day<std::chrono::microseconds> tod;
broken_datetime(const datetime& input) noexcept :
ymd(::date::floor<::date::days>(input)),
tod(input - ::date::floor<::date::days>(input))
{
}
};
struct broken_time
{
::date::days days;
std::chrono::hours hours;
std::chrono::minutes minutes;
std::chrono::seconds seconds;
std::chrono::microseconds microseconds;
broken_time(const time& input) noexcept :
days(std::chrono::duration_cast<::date::days>(input)),
hours(std::chrono::duration_cast<std::chrono::hours>(input % ::date::days(1))),
minutes(std::chrono::duration_cast<std::chrono::minutes>(input % std::chrono::hours(1))),
seconds(std::chrono::duration_cast<std::chrono::seconds>(input % std::chrono::minutes(1))),
microseconds(input % std::chrono::seconds(1))
{
}
};
// Floating point types
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>>
serialize_binary_value_impl(
@@ -74,13 +33,32 @@ serialize_binary_value_impl(
#endif
}
// Time types
// Does not add the length prefix byte
inline void serialize_binary_ymd(
const ::date::year_month_day& ymd,
serialization_context& ctx
) noexcept
{
serialize_fields(
ctx,
int2(static_cast<std::uint16_t>(static_cast<int>(ymd.year()))),
int1(static_cast<std::uint8_t>(static_cast<unsigned>(ymd.month()))),
int1(static_cast<std::uint8_t>(static_cast<unsigned>(ymd.day())))
);
}
inline void serialize_binary_value_impl(
const date& input,
serialization_context& ctx
)
{
serialize(int1(4), ctx); // length byte
serialize_binary_ymd(::date::year_month_day (input), ctx);
::date::year_month_day ymd (input);
assert(ymd.ok());
serialize(int1(binc::date_sz), ctx);
serialize_binary_ymd(ymd, ctx);
}
inline void serialize_binary_value_impl(
@@ -88,16 +66,21 @@ inline void serialize_binary_value_impl(
serialization_context& ctx
)
{
broken_datetime brokendt (input);
auto micros = static_cast<std::uint32_t>(brokendt.tod.subseconds().count());
serialize(int1(11), ctx);
serialize_binary_ymd(brokendt.ymd, ctx);
// Break datetime
auto days = ::date::floor<::date::days>(input);
::date::year_month_day ymd (days);
::date::time_of_day<std::chrono::microseconds> tod (input - days);
assert(ymd.ok());
// Serialize
serialize(int1(binc::datetime_dhmsu_sz), ctx);
serialize_binary_ymd(ymd, ctx);
serialize_fields(
ctx,
int1(static_cast<std::uint8_t>(brokendt.tod.hours().count())),
int1(static_cast<std::uint8_t>(brokendt.tod.minutes().count())),
int1(static_cast<std::uint8_t>(brokendt.tod.seconds().count())),
int4(micros)
int1(static_cast<std::uint8_t>(tod.hours().count())),
int1(static_cast<std::uint8_t>(tod.minutes().count())),
int1(static_cast<std::uint8_t>(tod.seconds().count())),
int4(static_cast<std::uint32_t>(tod.subseconds().count()))
);
}
@@ -106,18 +89,24 @@ inline void serialize_binary_value_impl(
serialization_context& ctx
)
{
broken_time broken (input);
// Break time
auto days = std::chrono::duration_cast<::date::days>(input);
auto hours = std::chrono::duration_cast<std::chrono::hours>(input % ::date::days(1));
auto minutes = std::chrono::duration_cast<std::chrono::minutes>(input % std::chrono::hours(1));
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(input % std::chrono::minutes(1));
auto microseconds = input % std::chrono::seconds(1);
int1 is_negative (input.count() < 0 ? 1 : 0);
auto micros = static_cast<std::uint32_t>(std::abs(broken.microseconds.count()));
// Serialize
serialize_fields(
ctx,
int1(12), // length
int1(binc::time_dhmsu_sz),
is_negative,
int4(static_cast<std::uint32_t>(std::abs(broken.days.count()))),
int1(static_cast<std::uint8_t>(std::abs(broken.hours.count()))),
int1(static_cast<std::uint8_t>(std::abs(broken.minutes.count()))),
int1(static_cast<std::uint8_t>(std::abs(broken.seconds.count()))),
int4(micros)
int4(static_cast<std::uint32_t>(std::abs(days.count()))),
int1(static_cast<std::uint8_t>(std::abs(hours.count()))),
int1(static_cast<std::uint8_t>(std::abs(minutes.count()))),
int1(static_cast<std::uint8_t>(std::abs(seconds.count()))),
int4(static_cast<std::uint32_t>(std::abs(microseconds.count())))
);
}
@@ -133,9 +122,9 @@ struct size_visitor
std::size_t operator()(T) noexcept { return sizeof(T); }
std::size_t operator()(std::string_view v) noexcept { return get_size(string_lenenc(v), ctx); }
std::size_t operator()(const date&) noexcept { return 5; }
std::size_t operator()(const datetime&) noexcept { return 12; }
std::size_t operator()(const time&) noexcept { return 13; }
std::size_t operator()(const date&) noexcept { return binc::date_sz + binc::length_sz; }
std::size_t operator()(const datetime&) noexcept { return binc::datetime_dhmsu_sz + binc::length_sz; }
std::size_t operator()(const time&) noexcept { return binc::time_dhmsu_sz + binc::length_sz; }
std::size_t operator()(std::nullptr_t) noexcept { return 0; }
};

View File

@@ -11,36 +11,12 @@
#include <cstdlib>
#include <cmath>
#include <boost/lexical_cast/try_lexical_convert.hpp>
#include <date/date.h>
#include "boost/mysql/detail/protocol/constants.hpp"
namespace boost {
namespace mysql {
namespace detail {
// Text protocol deserialization constants
namespace textc {
constexpr unsigned max_decimals = 6u;
constexpr std::size_t year_sz = 4;
constexpr std::size_t month_sz = 2;
constexpr std::size_t day_sz = 2;
constexpr std::size_t hours_min_sz = 2; // in TIME, it may be longer
constexpr std::size_t mins_sz = 2;
constexpr std::size_t secs_sz = 2;
constexpr std::size_t date_sz = year_sz + month_sz + day_sz + 2; // delimiters
constexpr std::size_t time_min_sz = hours_min_sz + mins_sz + secs_sz + 2; // delimiters
constexpr std::size_t time_max_sz = time_min_sz + max_decimals + 3; // sign, period, hour extra character
constexpr std::size_t datetime_min_sz = date_sz + time_min_sz + 1; // delimiter
constexpr unsigned max_hour = 838;
constexpr unsigned max_min = 59;
constexpr unsigned max_sec = 59;
constexpr unsigned max_micro = 999999;
} // textc
inline unsigned sanitize_decimals(unsigned decimals)
{
return std::min(decimals, textc::max_decimals);
@@ -78,11 +54,10 @@ inline errc deserialize_text_value_impl(
return errc::protocol_value_error;
// Range check
if (result > max_date || result < min_date)
to = result;
if (to < min_date || to > max_date)
return errc::protocol_value_error;
// Done
to = result;
return errc::ok;
}

View File

@@ -15,7 +15,7 @@ namespace mysql {
namespace detail {
// Max/min
constexpr date min_date = ::date::day(1)/::date::January/::date::year(100); // some implementations support less than the official
constexpr date min_date = ::date::day(1)/::date::January/::date::year(0); // slightly more flexible than official min
constexpr date max_date = ::date::day(31)/::date::December/::date::year(9999);
constexpr datetime min_datetime = min_date;
constexpr datetime max_datetime = max_date + std::chrono::hours(24) - std::chrono::microseconds(1);

View File

@@ -133,9 +133,13 @@ INSTANTIATE_TEST_SUITE_P(DATE, DeserializeBinaryValueErrorTest, Values(
err_binary_value_testcase("empty", {}, protocol_field_type::date, errc::incomplete_message),
err_binary_value_testcase("incomplete_year", {0x04, 0xff},
protocol_field_type::date, errc::incomplete_message),
err_binary_value_testcase("year_gt_max", {0x04, 0x10, 0x27, 0x03, 0x1c}, // year 10000
err_binary_value_testcase("no_month_day", {0x04, 0x09, 0x27},
protocol_field_type::date, errc::incomplete_message),
err_binary_value_testcase("no_day", {0x04, 0x09, 0x27, 0x01},
protocol_field_type::date, errc::incomplete_message),
err_binary_value_testcase("gt_max", {0x04, 0x10, 0x27, 0x0c, 0x1f}, // year 10000
protocol_field_type::date),
err_binary_value_testcase("year_lt_min", {0x04, 0x63, 0x00, 0x03, 0x1c}, // year 99
err_binary_value_testcase("protocol_max", {0x04, 0xff, 0xff, 0x0c, 0x1f},
protocol_field_type::date)
));

View File

@@ -138,8 +138,8 @@ INSTANTIATE_TEST_SUITE_P(DOUBLE, DeserializeBinaryValueTest, Values(
INSTANTIATE_TEST_SUITE_P(DATE, DeserializeBinaryValueTest, ::testing::Values(
binary_value_testcase("regular", {0x04, 0xda, 0x07, 0x03, 0x1c},
makedate(2010, 3, 28), protocol_field_type::date),
binary_value_testcase("min", {0x04, 0xe8, 0x03, 0x01, 0x01},
makedate(1000, 1, 1), protocol_field_type::date),
binary_value_testcase("min", {0x04, 0x00, 0x00, 0x01, 0x01},
makedate(0, 1, 1), protocol_field_type::date),
binary_value_testcase("max", {0x04, 0x0f, 0x27, 0x0c, 0x1f},
makedate(9999, 12, 31), protocol_field_type::date)
), test_name_generator);

View File

@@ -166,13 +166,14 @@ INSTANTIATE_TEST_SUITE_P(DATE, DeserializeTextValueErrorTest, Values(
err_text_value_testcase("too_few_groups", "2020-00005", protocol_field_type::date),
err_text_value_testcase("incomplete_year", "999-05-005", protocol_field_type::date),
err_text_value_testcase("null_value", makesv("2020-05-\02"), protocol_field_type::date),
err_text_value_testcase("lt_min", "0099-05-02", protocol_field_type::date),
err_text_value_testcase("lt_min", "-001-05-02", protocol_field_type::date),
err_text_value_testcase("gt_max", "10000-05-2", protocol_field_type::date),
err_text_value_testcase("long_month", "2010-005-2", protocol_field_type::date),
err_text_value_testcase("long_day", "2010-5-002", protocol_field_type::date),
err_text_value_testcase("negative_year", "-999-05-02", protocol_field_type::date),
err_text_value_testcase("negative_month", "2010--5-02", protocol_field_type::date),
err_text_value_testcase("negative_day", "2010-05-- 2", protocol_field_type::date)
err_text_value_testcase("negative_day", "2010-05--2", protocol_field_type::date),
err_text_value_testcase("hex", "ffff-ff-ff", protocol_field_type::date)
), test_name_generator);
std::vector<err_text_value_testcase> make_datetime_err_cases(
@@ -223,7 +224,7 @@ std::vector<err_text_value_testcase> make_datetime_err_cases(
err_text_value_testcase("invalid_sec", "2020-05-02 22:06:60", t),
err_text_value_testcase("negative_sec", "2020-05-02 22:06:-1", t),
err_text_value_testcase("negative_micro", "2020-05-02 22:06:01.-1", t, 0, 2),
err_text_value_testcase("lt_min", "0090-01-01 00:00:00.00", t, 0, 2),
err_text_value_testcase("lt_min", "-001-01-01 00:00:00.00", t, 0, 2),
err_text_value_testcase("gt_max", "10000-01-01 00:00:00.00", t, 0, 2)
};
}

View File

@@ -188,9 +188,8 @@ INSTANTIATE_TEST_SUITE_P(DOUBLE, DeserializeTextValueTest, ValuesIn(
INSTANTIATE_TEST_SUITE_P(DATE, DeserializeTextValueTest, Values(
text_value_testcase("regular_date", "2019-02-28", makedate(2019, 2, 28), protocol_field_type::date),
text_value_testcase("leap_year", "1788-02-29", makedate(1788, 2, 29), protocol_field_type::date),
text_value_testcase("min", "1000-01-01", makedate(1000, 1, 1), protocol_field_type::date),
text_value_testcase("max", "9999-12-31", makedate(9999, 12, 31), protocol_field_type::date),
text_value_testcase("unofficial_min", "0100-01-01", makedate(100, 1, 1), protocol_field_type::date)
text_value_testcase("min", "0000-01-01", makedate(0, 1, 1), protocol_field_type::date),
text_value_testcase("max", "9999-12-31", makedate(9999, 12, 31), protocol_field_type::date)
), test_name_generator);
std::vector<text_value_testcase> make_datetime_cases(