relx 0.1.0
A Modern C++23 Type-Safe SQL Query Builder
Loading...
Searching...
No Matches
chrono_traits.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "core.hpp"
4
5#include <chrono>
6#include <iomanip>
7#include <sstream>
8#include <stdexcept>
9#include <string>
10
11namespace relx::schema {
12
14template <>
15struct column_traits<std::chrono::system_clock::time_point> {
16 static constexpr auto sql_type_name = "TIMESTAMPTZ";
17 static constexpr bool nullable = false;
18
19 static std::string to_sql_string(const std::chrono::system_clock::time_point& value) {
20 // Extract the time_t part and microseconds part
21 auto time_t_val = std::chrono::system_clock::to_time_t(value);
22 auto time_point_from_time_t = std::chrono::system_clock::from_time_t(time_t_val);
23 auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(
24 value - time_point_from_time_t)
25 .count();
26
27 // Format as ISO 8601 timestamp with timezone (T separator and Z suffix)
28 std::stringstream ss;
29 auto tm = *std::gmtime(&time_t_val);
30 ss << "'" << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S");
31
32 // Add fractional seconds if present
33 if (microseconds > 0) {
34 ss << "." << std::setfill('0') << std::setw(6) << microseconds;
35 }
36
37 ss << "Z'";
38 return ss.str();
39 }
40
41 static std::chrono::system_clock::time_point from_sql_string(const std::string& value) {
42 // Handle PostgreSQL TIMESTAMPTZ formats more robustly
43 // Common formats:
44 // - 2023-12-25T10:30:45Z
45 // - 2023-12-25T10:30:45.123Z
46 // - 2023-12-25T10:30:45+00:00
47 // - 2023-12-25 10:30:45+00
48
49 std::string clean_value = value;
50 std::chrono::microseconds fractional_seconds{0};
51
52 // Remove quotes if present
53 if (!clean_value.empty() && clean_value.front() == '\'' && clean_value.back() == '\'') {
54 clean_value = clean_value.substr(1, clean_value.length() - 2);
55 }
56
57 // Extract and remove fractional seconds if present
58 auto dot_pos = clean_value.find('.');
59 if (dot_pos != std::string::npos) {
60 auto end_pos = clean_value.find_first_of("Z+-", dot_pos);
61 if (end_pos == std::string::npos) {
62 end_pos = clean_value.length();
63 }
64
65 std::string frac_str = clean_value.substr(dot_pos + 1, end_pos - dot_pos - 1);
66 // Pad or truncate to 6 digits (microseconds)
67 if (frac_str.length() > 6) {
68 frac_str = frac_str.substr(0, 6);
69 } else {
70 frac_str.append(6 - frac_str.length(), '0');
71 }
72
73 fractional_seconds = std::chrono::microseconds{std::stoi(frac_str)};
74 clean_value.erase(dot_pos, end_pos - dot_pos);
75 if (end_pos < value.length()) {
76 clean_value += value.substr(end_pos);
77 }
78 }
79
80 // Parse timezone information and calculate UTC offset
81 std::chrono::minutes timezone_offset{0}; // Offset to subtract from local time to get UTC
82
83 // Look for timezone info at the end of the string
84 auto tz_pos = clean_value.find_last_of("Z");
85 if (tz_pos != std::string::npos && tz_pos == clean_value.length() - 1) {
86 // Found Z at the end - this means UTC, no offset needed
87 clean_value = clean_value.substr(0, tz_pos);
88 } else {
89 // Look for +/- timezone offset (e.g. +05:00, -08:00, +0530)
90 tz_pos = clean_value.find_last_of("+-");
91 if (tz_pos != std::string::npos && tz_pos > 10) { // Make sure it's not part of the date
92 std::string tz_str = clean_value.substr(tz_pos);
93 clean_value = clean_value.substr(0, tz_pos);
94
95 // Parse timezone offset: +05:00, -08:00, +0530, etc.
96 bool is_positive = (tz_str[0] == '+');
97 std::string offset_str = tz_str.substr(1); // Remove +/- sign
98
99 if (offset_str.empty()) {
100 throw std::invalid_argument("Empty timezone offset: " + tz_str);
101 }
102
103 int hours = 0, minutes = 0;
104
105 try {
106 if (offset_str.find(':') != std::string::npos) {
107 // Format: HH:MM or H:MM
108 auto colon_pos = offset_str.find(':');
109 if (offset_str.find(':', colon_pos + 1) != std::string::npos) {
110 throw std::invalid_argument("Too many colons in timezone: " + tz_str);
111 }
112 hours = std::stoi(offset_str.substr(0, colon_pos));
113 auto minute_str = offset_str.substr(colon_pos + 1);
114 if (minute_str.empty()) {
115 throw std::invalid_argument("Missing minutes after colon: " + tz_str);
116 }
117 minutes = std::stoi(minute_str);
118 } else if (offset_str.length() == 4) {
119 // Format: HHMM
120 hours = std::stoi(offset_str.substr(0, 2));
121 minutes = std::stoi(offset_str.substr(2, 2));
122 } else if (offset_str.length() == 2) {
123 // Format: HH
124 hours = std::stoi(offset_str);
125 } else if (offset_str.length() == 1) {
126 // Format: H (single digit hour)
127 hours = std::stoi(offset_str);
128 } else {
129 throw std::invalid_argument("Invalid timezone format: " + tz_str);
130 }
131 } catch (const std::invalid_argument& e) {
132 throw std::invalid_argument("Invalid timezone format: " + tz_str);
133 } catch (const std::out_of_range& e) {
134 throw std::invalid_argument("Timezone values out of range: " + tz_str);
135 }
136
137 // Validate timezone values
138 if (hours > 14 || hours < 0) {
139 throw std::invalid_argument("Invalid timezone hour offset (must be 0-14): " + tz_str);
140 }
141 if (minutes >= 60 || minutes < 0) {
142 throw std::invalid_argument("Invalid timezone minute offset (must be 0-59): " + tz_str);
143 }
144
145 // Calculate total offset in minutes
146 int total_minutes = hours * 60 + minutes;
147 if (!is_positive) {
148 total_minutes = -total_minutes;
149 }
150
151 // To convert to UTC: UTC_time = local_time - offset
152 // So if we have +05:00, we subtract 5 hours to get UTC
153 timezone_offset = std::chrono::minutes{total_minutes};
154 }
155 // If no timezone info found, assume UTC (timezone_offset remains 0)
156 }
157
158 // Parse the base timestamp
159 std::tm tm = {};
160 std::istringstream ss(clean_value);
161
162 bool parsed = false;
163
164 // Try ISO format first: YYYY-MM-DDTHH:MM:SS
165 if (clean_value.find('T') != std::string::npos) {
166 ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
167 parsed = !ss.fail();
168 }
169
170 // Try space-separated format: YYYY-MM-DD HH:MM:SS
171 if (!parsed) {
172 ss.clear();
173 ss.str(clean_value);
174 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
175 parsed = !ss.fail();
176 }
177
178 if (!parsed) {
179 throw std::invalid_argument("Failed to parse timestamp: " + value);
180 }
181
182 // Convert to time_point (treating as UTC since PostgreSQL TIMESTAMPTZ is timezone-aware)
183 // Set remaining fields properly
184 tm.tm_isdst = 0; // Not DST since we're working with UTC
185 tm.tm_wday = 0; // Day of week (not used for conversion)
186 tm.tm_yday = 0; // Day of year (not used for conversion)
187
188 // Convert using mktime but adjust for UTC
189 // Since mktime assumes local time, we need to convert to UTC
190 std::time_t time_t_val;
191
192#ifdef _WIN32
193 // Windows has _mkgmtime for UTC conversion
194 time_t_val = _mkgmtime(&tm);
195#else
196// Unix-like systems: use timegm if available, otherwise calculate offset
197#if defined(__GLIBC__) || defined(__APPLE__) || defined(__FreeBSD__)
198 time_t_val = timegm(&tm);
199#else
200 // Fallback: use mktime and adjust for timezone
201 time_t_val = mktime(&tm);
202 if (time_t_val != -1) {
203 // Get timezone offset to adjust to UTC
204 auto gmt_tm = *gmtime(&time_t_val);
205 auto local_tm = *localtime(&time_t_val);
206
207 // Calculate the offset between local time and GMT
208 auto gmt_time = mktime(&gmt_tm);
209 auto local_time = mktime(&local_tm);
210 auto offset = local_time - gmt_time;
211 time_t_val -= offset;
212 }
213#endif
214#endif
215
216 if (time_t_val == -1) {
217 throw std::invalid_argument("Invalid timestamp value: " + value);
218 }
219
220 auto time_point = std::chrono::system_clock::from_time_t(time_t_val);
221
222 // Apply timezone offset to convert to UTC
223 // timezone_offset is the offset to subtract from local time to get UTC
224 auto utc_time_point = time_point - timezone_offset;
225
226 return utc_time_point + fractional_seconds;
227 }
228};
229
231template <>
232struct column_traits<std::chrono::year_month_day> {
233 static constexpr auto sql_type_name = "DATE";
234 static constexpr bool nullable = false;
235
236 static std::string to_sql_string(const std::chrono::year_month_day& value) {
237 std::stringstream ss;
238 ss << "'" << static_cast<int>(value.year()) << "-" << std::setfill('0') << std::setw(2)
239 << static_cast<unsigned>(value.month()) << "-" << std::setfill('0') << std::setw(2)
240 << static_cast<unsigned>(value.day()) << "'";
241 return ss.str();
242 }
243
244 static std::chrono::year_month_day from_sql_string(const std::string& value) {
245 // Parse YYYY-MM-DD format
246 int year, month, day;
247 char dash1, dash2;
248
249 std::istringstream ss(value);
250 ss >> year >> dash1 >> month >> dash2 >> day;
251
252 return std::chrono::year_month_day{std::chrono::year{year},
253 std::chrono::month{static_cast<unsigned>(month)},
254 std::chrono::day{static_cast<unsigned>(day)}};
255 }
256};
257
258} // namespace relx::schema
STL namespace.
static std::string to_sql_string(const std::chrono::system_clock::time_point &value)
static std::chrono::system_clock::time_point from_sql_string(const std::string &value)
static std::string to_sql_string(const std::chrono::year_month_day &value)
static std::chrono::year_month_day from_sql_string(const std::string &value)
Contains schema definition components.
Definition core.hpp:18
static constexpr bool nullable
Whether this type can be NULL.
Definition core.hpp:23
static constexpr auto sql_type_name
The SQL type name for this C++ type.
Definition core.hpp:20