Skip to content

Commit da814e3

Browse files
author
Pierre-Luc Gagné
committed
Add SQL TIME support
1 parent b8b5dbb commit da814e3

5 files changed

Lines changed: 160 additions & 8 deletions

File tree

lib/include/ds_mysql/schema_generator.hpp

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,16 @@ struct sql_type_name {
9191
return "DATETIME";
9292
} else if constexpr (std::same_as<base_type, sql_timestamp>) {
9393
return "TIMESTAMP";
94+
} else if constexpr (std::same_as<base_type, sql_time>) {
95+
return "TIME";
9496
} else {
9597
static_assert(false,
9698
"Unsupported type for SQL mapping. Supported: uint32_t, int32_t, uint64_t, "
9799
"int64_t, float, double, float_type<P,S>, double_type<P,S>, decimal_type<P,S>, "
98100
"bool, varchar_field<N>, text_field (TEXT), "
99101
"mediumtext_field (MEDIUMTEXT, MySQL), longtext_field (LONGTEXT, MySQL), "
100-
"std::chrono::system_clock::time_point, sql_datetime, sql_timestamp, and their "
101-
"std::optional variants");
102+
"std::chrono::system_clock::time_point, sql_datetime, sql_timestamp, sql_time, "
103+
"and their std::optional variants");
102104
}
103105
} // end else (not ColumnFieldType)
104106
}
@@ -166,6 +168,14 @@ namespace sql_type_format {
166168
return "TIMESTAMP(" + std::to_string(fractional_second_precision) + ")";
167169
}
168170

171+
[[nodiscard]] inline std::string time_type() {
172+
return "TIME";
173+
}
174+
175+
[[nodiscard]] inline std::string time_type(uint32_t fractional_second_precision) {
176+
return "TIME(" + std::to_string(fractional_second_precision) + ")";
177+
}
178+
169179
} // namespace sql_type_format
170180

171181
// ===================================================================

lib/include/ds_mysql/sql.hpp

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,26 @@ inline std::string format_datetime(std::chrono::system_clock::time_point tp, uin
212212
return std::format("'{:%Y-%m-%d %H:%M:%S}.{:0{}d}'", secs, scaled_fraction, precision);
213213
}
214214

215+
inline std::string format_time(std::chrono::microseconds dur, uint32_t fractional_second_precision = 0) {
216+
auto const precision = normalize_fractional_second_precision(fractional_second_precision);
217+
bool const negative = dur.count() < 0;
218+
auto const abs_dur = negative ? -dur : dur;
219+
auto const total_secs = std::chrono::duration_cast<std::chrono::seconds>(abs_dur).count();
220+
auto const hours = total_secs / 3600;
221+
auto const mins = (total_secs % 3600) / 60;
222+
auto const secs = total_secs % 60;
223+
std::string result = std::format("'{}{:02d}:{:02d}:{:02d}", negative ? "-" : "", hours, mins, secs);
224+
if (precision > 0U) {
225+
constexpr std::array<uint32_t, 7> divisors{1000000U, 100000U, 10000U, 1000U, 100U, 10U, 1U};
226+
auto const fractional_us =
227+
static_cast<uint32_t>((abs_dur - std::chrono::duration_cast<std::chrono::seconds>(abs_dur)).count());
228+
auto const scaled_fraction = fractional_us / divisors[precision];
229+
result += std::format(".{:0{}d}", scaled_fraction, precision);
230+
}
231+
result += '\'';
232+
return result;
233+
}
234+
215235
/**
216236
* to_sql_value — convert a typed C++ value to its SQL literal representation.
217237
*
@@ -248,6 +268,8 @@ std::string to_sql_value(T const& v) {
248268
return format_datetime(v.time_point(), v.fractional_second_precision());
249269
} else if constexpr (std::same_as<T, std::chrono::system_clock::time_point>) {
250270
return format_datetime(v);
271+
} else if constexpr (std::same_as<T, sql_time>) {
272+
return format_time(v.duration(), v.fractional_second_precision());
251273
} else if constexpr (std::same_as<T, bool>) {
252274
return v ? "1" : "0";
253275
} else if constexpr (is_formatted_numeric_type_v<T>) {
@@ -266,7 +288,7 @@ std::string to_sql_value(T const& v) {
266288
"Supported: column_field<T>, optional<T>, sql_datetime, sql_timestamp, bool, "
267289
"integral types, floating-point types, float_type<P,S>, double_type<P,S>, "
268290
"decimal_type<P,S>, varchar_field<N>, std::string, "
269-
"std::chrono::system_clock::time_point");
291+
"std::chrono::system_clock::time_point, sql_time");
270292
}
271293
}
272294

@@ -277,10 +299,10 @@ std::string to_sql_value(T const& v) {
277299
// Constrains template parameters that must produce a valid SQL literal.
278300
// ===================================================================
279301
template <typename T>
280-
concept SqlValue =
281-
ColumnFieldType<T> || is_optional_v<T> || std::same_as<T, sql_datetime> || std::same_as<T, sql_timestamp> ||
282-
std::same_as<T, std::chrono::system_clock::time_point> || std::same_as<T, bool> || std::integral<T> ||
283-
std::floating_point<T> || is_formatted_numeric_type_v<T> || is_varchar_field_v<T> || std::same_as<T, std::string>;
302+
concept SqlValue = ColumnFieldType<T> || is_optional_v<T> || std::same_as<T, sql_datetime> ||
303+
std::same_as<T, sql_timestamp> || std::same_as<T, std::chrono::system_clock::time_point> ||
304+
std::same_as<T, sql_time> || std::same_as<T, bool> || std::integral<T> || std::floating_point<T> ||
305+
is_formatted_numeric_type_v<T> || is_varchar_field_v<T> || std::same_as<T, std::string>;
284306

285307
// ===================================================================
286308
// where_condition — a typed SQL WHERE fragment
@@ -4933,12 +4955,33 @@ T from_mysql_value_nonnull(std::string_view sv) {
49334955
std::tm tm{};
49344956
ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
49354957
return std::chrono::system_clock::from_time_t(std::mktime(&tm));
4958+
} else if constexpr (std::same_as<T, sql_time>) {
4959+
bool const negative = !sv.empty() && sv[0] == '-';
4960+
std::string_view const s = negative ? sv.substr(1) : sv;
4961+
auto const c1 = s.find(':');
4962+
auto const c2 = s.find(':', c1 + 1);
4963+
int64_t const hours = std::stoll(std::string{s.substr(0, c1)});
4964+
int64_t const mins = std::stoll(std::string{s.substr(c1 + 1, c2 - c1 - 1)});
4965+
auto const secs_frac = s.substr(c2 + 1);
4966+
auto const dot = secs_frac.find('.');
4967+
int64_t const secs =
4968+
std::stoll(std::string{secs_frac.substr(0, dot == std::string_view::npos ? secs_frac.size() : dot)});
4969+
int64_t frac_us = 0;
4970+
if (dot != std::string_view::npos) {
4971+
std::string frac{secs_frac.substr(dot + 1)};
4972+
frac.resize(6, '0');
4973+
frac_us = std::stoll(frac);
4974+
}
4975+
int64_t total_us = (hours * 3600LL + mins * 60LL + secs) * 1000000LL + frac_us;
4976+
if (negative)
4977+
total_us = -total_us;
4978+
return sql_time{std::chrono::microseconds{total_us}};
49364979
} else {
49374980
static_assert(false,
49384981
"Unsupported type for MySQL deserialization. "
49394982
"Supported: uint32_t, int32_t, uint64_t, int64_t, float, double, float_type<P,S>, "
49404983
"double_type<P,S>, decimal_type<P,S>, bool, std::string, varchar_field<N>, "
4941-
"std::chrono::system_clock::time_point, "
4984+
"std::chrono::system_clock::time_point (for DATETIME/TIMESTAMP), sql_time, "
49424985
"and their std::optional variants.");
49434986
}
49444987
}

lib/include/ds_mysql/sql_temporal.hpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,38 @@ class sql_timestamp {
8787
uint32_t fractional_second_precision_;
8888
};
8989

90+
/**
91+
* sql_time — a typed SQL TIME value holding a signed duration.
92+
*
93+
* MySQL's TIME type stores a duration in the range -838:59:59 to 838:59:59,
94+
* with optional fractional-second precision (0–6).
95+
*
96+
* Rendered as 'HHH:MM:SS[.fraction]' (or '-HHH:MM:SS[.fraction]' for negative
97+
* durations) in SQL literals.
98+
*
99+
* Example:
100+
* using namespace std::chrono_literals;
101+
* row.duration_.value = sql_time{8h + 30min}; // → '08:30:00'
102+
* row.duration_.value = sql_time{-(10h + 5min), 3}; // → '-10:05:00.000'
103+
*/
104+
class sql_time {
105+
public:
106+
sql_time() noexcept = default;
107+
explicit sql_time(std::chrono::microseconds duration, uint32_t fractional_second_precision = 0) noexcept
108+
: duration_(duration), fractional_second_precision_(fractional_second_precision) {
109+
}
110+
111+
[[nodiscard]] std::chrono::microseconds duration() const noexcept {
112+
return duration_;
113+
}
114+
115+
[[nodiscard]] uint32_t fractional_second_precision() const noexcept {
116+
return fractional_second_precision_;
117+
}
118+
119+
private:
120+
std::chrono::microseconds duration_{};
121+
uint32_t fractional_second_precision_{0};
122+
};
123+
90124
} // namespace ds_mysql

tests/unit/test_ddl.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ struct temporal_table {
7878
COLUMN_FIELD(updated_at, sql_timestamp)
7979
};
8080

81+
struct time_table {
82+
COLUMN_FIELD(id, uint32_t)
83+
COLUMN_FIELD(start_time, sql_time)
84+
COLUMN_FIELD(end_time, std::optional<sql_time>)
85+
};
86+
8187
struct symbol_with_indexes {
8288
COLUMN_FIELD(id, int32_t)
8389
COLUMN_FIELD(exchange_id, std::optional<int32_t>)
@@ -194,6 +200,10 @@ suite<"DDL"> ddl_suite = [] {
194200

195201
expect(sql_type_format::timestamp_type() == "TIMESTAMP"s);
196202
expect(sql_type_format::timestamp_type(6) == "TIMESTAMP(6)"s);
203+
204+
expect(sql_type_format::time_type() == "TIME"s);
205+
expect(sql_type_format::time_type(3) == "TIME(3)"s);
206+
expect(sql_type_format::time_type(6) == "TIME(6)"s);
197207
};
198208

199209
"create_table temporal types - emits DATETIME and TIMESTAMP definitions"_test = [] {
@@ -207,6 +217,17 @@ suite<"DDL"> ddl_suite = [] {
207217
<< sql;
208218
};
209219

220+
"create_table time type - emits TIME definitions"_test = [] {
221+
auto const sql = create_table<time_table>().build_sql();
222+
expect(sql ==
223+
"CREATE TABLE time_table (\n"
224+
" id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,\n"
225+
" start_time TIME NOT NULL,\n"
226+
" end_time TIME\n"
227+
");\n"s)
228+
<< sql;
229+
};
230+
210231
"create_table with numeric sql formatting options - emits FLOAT/DOUBLE/DECIMAL definitions"_test = [] {
211232
auto const sql = create_table<numeric_format_table>().build_sql();
212233
expect(sql ==

tests/unit/test_dml.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,50 @@ suite<"DML"> dml_suite = [] {
146146
expect(sql_detail::to_sql_value(sql_timestamp{sql_now, 5}) == "CURRENT_TIMESTAMP(5)"s);
147147
};
148148

149+
"sql_time serialization reflects duration and fractional-second precision"_test = [] {
150+
using namespace std::chrono;
151+
// 08:30:00
152+
auto const t1 = sql_time{microseconds{(8 * 3600 + 30 * 60) * 1000000LL}};
153+
expect(sql_detail::to_sql_value(t1) == "'08:30:00'"s);
154+
155+
// 00:00:00
156+
expect(sql_detail::to_sql_value(sql_time{}) == "'00:00:00'"s);
157+
158+
// 838:59:59 (MySQL maximum)
159+
auto const t_max = sql_time{microseconds{(838 * 3600LL + 59 * 60LL + 59LL) * 1000000LL}};
160+
expect(sql_detail::to_sql_value(t_max) == "'838:59:59'"s);
161+
162+
// Negative duration: -10:05:00
163+
auto const t_neg = sql_time{microseconds{-(10 * 3600 + 5 * 60) * 1000000LL}};
164+
expect(sql_detail::to_sql_value(t_neg) == "'-10:05:00'"s);
165+
166+
// Fractional seconds precision=3: 01:02:03.456000 → '01:02:03.456'
167+
auto const t_frac = sql_time{microseconds{(3600LL + 2 * 60LL + 3LL) * 1000000LL + 456789LL}, 3};
168+
expect(sql_detail::to_sql_value(t_frac) == "'01:02:03.456'"s);
169+
170+
// Fractional seconds precision=6: '01:02:03.456789'
171+
auto const t_frac6 = sql_time{microseconds{(3600LL + 2 * 60LL + 3LL) * 1000000LL + 456789LL}, 6};
172+
expect(sql_detail::to_sql_value(t_frac6) == "'01:02:03.456789'"s);
173+
};
174+
175+
"sql_time deserialization parses MySQL TIME strings correctly"_test = [] {
176+
using namespace std::chrono;
177+
auto const t1 = ::ds_mysql::detail::from_mysql_value_nonnull<sql_time>("08:30:00");
178+
expect(t1.duration() == microseconds{(8 * 3600 + 30 * 60) * 1000000LL});
179+
180+
auto const t2 = ::ds_mysql::detail::from_mysql_value_nonnull<sql_time>("00:00:00");
181+
expect(t2.duration() == microseconds{0});
182+
183+
auto const t3 = ::ds_mysql::detail::from_mysql_value_nonnull<sql_time>("-10:05:00");
184+
expect(t3.duration() == microseconds{-(10 * 3600 + 5 * 60) * 1000000LL});
185+
186+
auto const t4 = ::ds_mysql::detail::from_mysql_value_nonnull<sql_time>("01:02:03.456789");
187+
expect(t4.duration() == microseconds{(3600LL + 2 * 60LL + 3LL) * 1000000LL + 456789LL});
188+
189+
auto const t5 = ::ds_mysql::detail::from_mysql_value_nonnull<sql_time>("838:59:59");
190+
expect(t5.duration() == microseconds{(838 * 3600LL + 59 * 60LL + 59LL) * 1000000LL});
191+
};
192+
149193
"formatted numeric wrapper types serialize and deserialize like their underlying values"_test = [] {
150194
expect(sql_type_for<float_type<>>() == "FLOAT"s);
151195
expect(sql_type_for<float_type<12, 4>>() == "FLOAT(12,4)"s);

0 commit comments

Comments
 (0)