Skip to content

Commit 7860227

Browse files
Pierre-Luc Gagnéclaude
andcommitted
feat: add date_type for MySQL DATE columns
Adds date_type backed by std::chrono::sys_days with full support for DDL generation, serialization, deserialization, and optional variants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e0ee50 commit 7860227

8 files changed

Lines changed: 176 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88

99
## [Unreleased]
1010

11+
### Added
12+
13+
- `date_type` for MySQL `DATE` columns — stores `std::chrono::sys_days`, supports serialization (`'YYYY-MM-DD'`), deserialization, and `std::optional<date_type>` for nullable columns
14+
1115
---
1216

1317
## [4.5.0] – 2026-03-29

lib/include/ds_mysql/column_field_base_temporal.hpp

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,94 @@ struct base<std::optional<timestamp_type<Fsp>>> : column_field_tag {
210210
}
211211
};
212212

213+
// ===================================================================
214+
// base<date_type> specializations
215+
// ===================================================================
216+
217+
template <>
218+
struct base<date_type> : column_field_tag {
219+
using value_type = date_type;
220+
221+
date_type value{};
222+
223+
constexpr base() = default;
224+
base(std::chrono::sys_days d) noexcept : value(d) {
225+
}
226+
base(date_type v) noexcept : value(v) {
227+
}
228+
229+
base& operator=(std::chrono::sys_days d) noexcept {
230+
value = date_type{d};
231+
return *this;
232+
}
233+
base& operator=(date_type v) noexcept {
234+
value = v;
235+
return *this;
236+
}
237+
238+
[[nodiscard]] constexpr date_type const& get() const noexcept {
239+
return value;
240+
}
241+
[[nodiscard]] constexpr date_type& get() noexcept {
242+
return value;
243+
}
244+
245+
constexpr operator date_type const&() const noexcept {
246+
return value;
247+
}
248+
constexpr operator date_type&() noexcept {
249+
return value;
250+
}
251+
};
252+
253+
template <>
254+
struct base<std::optional<date_type>> : column_field_tag {
255+
using value_type = std::optional<date_type>;
256+
257+
std::optional<date_type> value{};
258+
259+
constexpr base() = default;
260+
constexpr base(std::nullopt_t) noexcept : value(std::nullopt) {
261+
}
262+
base(std::chrono::sys_days d) noexcept : value(date_type{d}) {
263+
}
264+
base(date_type v) noexcept : value(std::move(v)) {
265+
}
266+
base(std::optional<date_type> v) noexcept : value(std::move(v)) {
267+
}
268+
269+
constexpr base& operator=(std::nullopt_t) noexcept {
270+
value = std::nullopt;
271+
return *this;
272+
}
273+
constexpr base& operator=(std::chrono::sys_days d) noexcept {
274+
value = date_type{d};
275+
return *this;
276+
}
277+
constexpr base& operator=(date_type v) noexcept {
278+
value = std::move(v);
279+
return *this;
280+
}
281+
constexpr base& operator=(std::optional<date_type> v) noexcept {
282+
value = std::move(v);
283+
return *this;
284+
}
285+
286+
[[nodiscard]] constexpr std::optional<date_type> const& get() const noexcept {
287+
return value;
288+
}
289+
[[nodiscard]] constexpr std::optional<date_type>& get() noexcept {
290+
return value;
291+
}
292+
293+
constexpr operator std::optional<date_type> const&() const noexcept {
294+
return value;
295+
}
296+
constexpr operator std::optional<date_type>&() noexcept {
297+
return value;
298+
}
299+
};
300+
213301
} // namespace column_field_detail
214302

215303
} // namespace ds_mysql

lib/include/ds_mysql/schema_generator.hpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ struct sql_type_name {
9696
} else {
9797
return "TIME(" + std::to_string(base_type::column_fsp) + ")";
9898
}
99+
} else if constexpr (is_date_type_v<base_type>) {
100+
return "DATE";
99101
} else {
100102
static_assert(false,
101103
"Unsupported type for SQL mapping. Supported: uint32_t, int32_t, uint64_t, "
@@ -105,7 +107,7 @@ struct sql_type_name {
105107
"bool, varchar_type<N>, text_type (TEXT), "
106108
"mediumtext_type (MEDIUMTEXT, MySQL), longtext_type (LONGTEXT, MySQL), "
107109
"std::chrono::system_clock::time_point, datetime_type<Fsp>, timestamp_type<Fsp>, "
108-
"time_type<Fsp>, and their std::optional variants");
110+
"time_type<Fsp>, date_type, and their std::optional variants");
109111
}
110112
} // end else (not ColumnFieldType)
111113
}
@@ -213,6 +215,10 @@ namespace sql_type_format {
213215
return "TIME(" + std::to_string(fractional_second_precision) + ")";
214216
}
215217

218+
[[nodiscard]] inline std::string date_type() {
219+
return "DATE";
220+
}
221+
216222
} // namespace sql_type_format
217223

218224
// ===================================================================

lib/include/ds_mysql/sql_core.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ template <typename T>
235235
return format_datetime(v);
236236
} else if constexpr (is_time_type_v<T>) {
237237
return format_time(v.duration(), v.fractional_second_precision());
238+
} else if constexpr (is_date_type_v<T>) {
239+
return std::format("'{:%Y-%m-%d}'", v.days());
238240
} else if constexpr (std::same_as<T, bool>) {
239241
return v ? "1" : "0";
240242
} else if constexpr (is_formatted_numeric_type_v<T>) {
@@ -250,7 +252,7 @@ template <typename T>
250252
} else {
251253
static_assert(false,
252254
"to_sql_value: unsupported type. "
253-
"Supported: column_field<T>, optional<T>, datetime_type, timestamp_type, bool, "
255+
"Supported: column_field<T>, optional<T>, datetime_type, timestamp_type, date_type, bool, "
254256
"integral types, floating-point types, float_type<P,S>, double_type<P,S>, "
255257
"decimal_type<P,S>, varchar_type<N>, text_type, std::string, "
256258
"std::chrono::system_clock::time_point, time_type");
@@ -265,7 +267,7 @@ template <typename T>
265267
// ===================================================================
266268
template <typename T>
267269
concept SqlValue =
268-
ColumnFieldType<T> || is_optional_v<T> || is_datetime_type_v<T> || is_timestamp_type_v<T> ||
270+
ColumnFieldType<T> || is_optional_v<T> || is_datetime_type_v<T> || is_timestamp_type_v<T> || is_date_type_v<T> ||
269271
std::same_as<T, std::chrono::system_clock::time_point> || is_time_type_v<T> || std::same_as<T, bool> ||
270272
std::integral<T> || std::floating_point<T> || is_formatted_numeric_type_v<T> || is_varchar_type_v<T> ||
271273
is_text_type_v<T> || std::same_as<T, std::string>;

lib/include/ds_mysql/sql_dql.hpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2892,6 +2892,14 @@ T from_mysql_value_nonnull(std::string_view sv) {
28922892
std::tm tm{};
28932893
ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
28942894
return std::chrono::system_clock::from_time_t(std::mktime(&tm));
2895+
} else if constexpr (is_date_type_v<T>) {
2896+
std::istringstream ss{std::string{sv}};
2897+
std::tm tm{};
2898+
ss >> std::get_time(&tm, "%Y-%m-%d");
2899+
tm.tm_hour = 0;
2900+
tm.tm_min = 0;
2901+
tm.tm_sec = 0;
2902+
return T{std::chrono::floor<std::chrono::days>(std::chrono::system_clock::from_time_t(std::mktime(&tm)))};
28952903
} else if constexpr (is_time_type_v<T>) {
28962904
bool const negative = !sv.empty() && sv[0] == '-';
28972905
std::string_view const s = negative ? sv.substr(1) : sv;
@@ -2918,7 +2926,7 @@ T from_mysql_value_nonnull(std::string_view sv) {
29182926
"Unsupported type for MySQL deserialization. "
29192927
"Supported: uint32_t, int32_t, uint64_t, int64_t, float, double, float_type<P,S>, "
29202928
"double_type<P,S>, decimal_type<P,S>, bool, std::string, varchar_type<N>, text_type, "
2921-
"std::chrono::system_clock::time_point (for DATETIME/TIMESTAMP), time_type, "
2929+
"std::chrono::system_clock::time_point (for DATETIME/TIMESTAMP), date_type, time_type, "
29222930
"and their std::optional variants.");
29232931
}
29242932
}

lib/include/ds_mysql/sql_temporal.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@ class time_type {
103103
uint32_t fractional_second_precision_;
104104
};
105105

106+
class date_type {
107+
public:
108+
date_type() noexcept : days_() {
109+
}
110+
explicit date_type(std::chrono::sys_days d) noexcept : days_(d) {
111+
}
112+
113+
[[nodiscard]] std::chrono::sys_days days() const noexcept {
114+
return days_;
115+
}
116+
117+
private:
118+
std::chrono::sys_days days_{};
119+
};
120+
106121
using datetime_type_default = datetime_type<>;
107122
using timestamp_type_default = timestamp_type<>;
108123
using time_type_default = time_type<>;
@@ -132,4 +147,11 @@ struct is_time_type<time_type<Fsp>> : std::true_type {};
132147
template <typename T>
133148
inline constexpr bool is_time_type_v = is_time_type<T>::value;
134149

150+
template <typename T>
151+
struct is_date_type : std::false_type {};
152+
template <>
153+
struct is_date_type<date_type> : std::true_type {};
154+
template <typename T>
155+
inline constexpr bool is_date_type_v = is_date_type<T>::value;
156+
135157
} // namespace ds_mysql

tests/unit/test_ddl.cpp

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ struct time_table {
9898
COLUMN_FIELD(end_time, std::optional<time_type<>>)
9999
};
100100

101+
struct date_table {
102+
COLUMN_FIELD(id, uint32_t)
103+
COLUMN_FIELD(birth_date, date_type)
104+
COLUMN_FIELD(expiry_date, std::optional<date_type>)
105+
};
106+
101107
struct fsp_temporal_table {
102108
COLUMN_FIELD(id, uint32_t)
103109
COLUMN_FIELD(created_at, datetime_type<6>)
@@ -368,6 +374,17 @@ suite<"DDL"> ddl_suite = [] {
368374
<< sql;
369375
};
370376

377+
"create_table date type - emits DATE definitions"_test = [] {
378+
auto const sql = create_table(date_table{}).build_sql();
379+
expect(sql ==
380+
"CREATE TABLE date_table (\n"
381+
" id INT UNSIGNED NOT NULL,\n"
382+
" birth_date DATE NOT NULL,\n"
383+
" expiry_date DATE\n"
384+
");\n"s)
385+
<< sql;
386+
};
387+
371388
"sql_type_for - templated temporal types embed FSP in the type string"_test = [] {
372389
expect(sql_type_for<datetime_type<0>>() == "DATETIME"s);
373390
expect(sql_type_for<datetime_type<3>>() == "DATETIME(3)"s);
@@ -381,9 +398,12 @@ suite<"DDL"> ddl_suite = [] {
381398
expect(sql_type_for<time_type<3>>() == "TIME(3)"s);
382399
expect(sql_type_for<time_type<6>>() == "TIME(6)"s);
383400

384-
// std::optional wrappers preserve the FSP
401+
expect(sql_type_for<date_type>() == "DATE"s);
402+
403+
// std::optional wrappers preserve the type
385404
expect(sql_type_for<std::optional<datetime_type<6>>>() == "DATETIME(6)"s);
386405
expect(sql_type_for<std::optional<time_type<4>>>() == "TIME(4)"s);
406+
expect(sql_type_for<std::optional<date_type>>() == "DATE"s);
387407
};
388408

389409
"create_table with templated temporal FSP columns - emits types with precision"_test = [] {

tests/unit/test_dml.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,27 @@ suite<"DML"> dml_suite = [] {
199199
expect(t5.duration() == microseconds{(838 * 3600LL + 59 * 60LL + 59LL) * 1000000LL});
200200
};
201201

202+
"date_type serialization formats as YYYY-MM-DD"_test = [] {
203+
using namespace std::chrono;
204+
auto const d1 = date_type{sys_days{year{2024} / January / 15}};
205+
expect(sql_detail::to_sql_value(d1) == "'2024-01-15'"s);
206+
207+
auto const d2 = date_type{sys_days{year{1999} / December / 31}};
208+
expect(sql_detail::to_sql_value(d2) == "'1999-12-31'"s);
209+
210+
// Default-constructed date_type → epoch
211+
expect(sql_detail::to_sql_value(date_type{}) == "'1970-01-01'"s);
212+
};
213+
214+
"date_type deserialization parses MySQL DATE strings correctly"_test = [] {
215+
using namespace std::chrono;
216+
auto const d1 = ::ds_mysql::detail::from_mysql_value_nonnull<date_type>("2024-01-15");
217+
expect(d1.days() == sys_days{year{2024} / January / 15});
218+
219+
auto const d2 = ::ds_mysql::detail::from_mysql_value_nonnull<date_type>("1999-12-31");
220+
expect(d2.days() == sys_days{year{1999} / December / 31});
221+
};
222+
202223
"formatted numeric wrapper types serialize and deserialize like their underlying values"_test = [] {
203224
expect(sql_type_for<float_type<>>() == "FLOAT"s);
204225
expect(sql_type_for<float_type<12, 4>>() == "FLOAT(12,4)"s);

0 commit comments

Comments
 (0)