Lightweight 0.20260625.0
Loading...
Searching...
No Matches
Utils.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5// <sql.h> below is not self-contained on Windows: the SDK's <sqltypes.h> resolves SQLLEN/SQLULEN/
6// SQLHWND/GUID against the Windows prelude, so <Windows.h> has to come first. Every other header
7// here that reaches an ODBC header opens with the same block; without it this header would only
8// compile when some other header happened to establish the prelude earlier in the translation unit.
9#if defined(_WIN32) || defined(_WIN64)
10 #include <Windows.h>
11#endif
12
13#include "Api.hpp"
14#include "Description.hpp"
15
16#include <reflection-cpp/reflection.hpp>
17
18#include <algorithm>
19#include <array>
20#include <cerrno>
21#include <charconv>
22#include <cstdlib>
23#include <optional>
24#include <ranges>
25#include <source_location>
26#include <string>
27#include <string_view>
28#include <system_error>
29#include <type_traits>
30#include <unordered_map>
31#include <utility>
32
33// libc++ exposes the locale-aware strtod_l / newlocale family via <xlocale.h>; glibc declares them in
34// <stdlib.h>/<locale.h>. We only need them on the fallback path below (no float std::from_chars).
35#if !(defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 201611L)
36 #include <clocale>
37 #if defined(__APPLE__)
38 #include <xlocale.h>
39 #else
40 #include <locale.h>
41 #endif
42#endif
43
44#include <sql.h>
45
46#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
47 #include <experimental/meta>
48#endif
49
50namespace Lightweight
51{
52
53namespace detail
54{
55
56 template <typename T, typename... Comps>
57 concept OneOf = (std::same_as<T, Comps> || ...);
58
59 template <typename T>
60 constexpr auto AlwaysFalse = std::false_type::value;
61
62 constexpr auto Finally(auto&& cleanupRoutine) noexcept
63 {
64 // NOLINTNEXTLINE(cppcoreguidelines-special-member-functions)
65 struct Finally
66 {
67 std::remove_cvref_t<decltype(cleanupRoutine)> cleanup;
68 ~Finally()
69 {
70 cleanup();
71 }
72 };
73 return Finally { std::forward<decltype(cleanupRoutine)>(cleanupRoutine) };
74 }
75
76 /// Parses a floating-point value from the character range @c [first, last) in a locale-independent way.
77 ///
78 /// This is the single, shared replacement for @c std::from_chars on floating-point types, which is
79 /// unavailable on libc++ before macOS 26. Unlike a bare @c std::strtod it neither depends on the active
80 /// @c LC_NUMERIC locale nor silently accepts trailing garbage, so it round-trips the @c '.' decimal point
81 /// the library always emits regardless of the host locale.
82 ///
83 /// @tparam T A floating-point type (@c float, @c double, or @c long double).
84 /// @param first Pointer to the first character of the numeric text.
85 /// @param last Pointer one past the last character of the numeric text.
86 /// @return The parsed value, or @c std::nullopt if the text is not a complete, in-range number.
87 template <typename T>
88 requires std::is_floating_point_v<T>
89 [[nodiscard]] inline std::optional<T> ParseFloat(char const* first, char const* last) noexcept
90 {
91 if (first == last)
92 return std::nullopt;
93#if defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 201611L
94 // std::from_chars is locale-independent, allocation-free, and reports both partial parses (ptr) and
95 // range errors (ec) — the preferred path wherever the float overloads exist.
96 T value {};
97 auto const [ptr, ec] = std::from_chars(first, last, value);
98 if (ec != std::errc {} || ptr != last)
99 return std::nullopt;
100 return value;
101#else
102 // libc++ fallback: strtod_l with a persistent "C" locale gives locale-independent parsing; from_chars
103 // float overloads are unavailable here. Copy into a NUL-terminated buffer (the range need not be) and
104 // require the parse to consume all of it (mirrors the from_chars ptr==last check). On-stack for the
105 // common short numeric text; only the rare over-long token allocates.
106 static ::locale_t const cLocale = ::newlocale(LC_NUMERIC_MASK, "C", static_cast<::locale_t>(nullptr));
107 auto const length = static_cast<std::size_t>(last - first);
108 std::array<char, 64> stackBuffer {};
109 std::string heapBuffer;
110 char const* text = nullptr;
111 if (length < stackBuffer.size())
112 {
113 std::ranges::copy(first, last, stackBuffer.begin());
114 stackBuffer[length] = '\0';
115 text = stackBuffer.data();
116 }
117 else
118 {
119 heapBuffer.assign(first, last);
120 text = heapBuffer.c_str();
121 }
122
123 char* parseEnd = nullptr;
124 errno = 0;
125 T value {};
126 if constexpr (std::is_same_v<T, float>)
127 value = ::strtof_l(text, &parseEnd, cLocale);
128 else if constexpr (std::is_same_v<T, long double>)
129 value = ::strtold_l(text, &parseEnd, cLocale);
130 else
131 value = static_cast<T>(::strtod_l(text, &parseEnd, cLocale));
132
133 if (errno == ERANGE || parseEnd != text + length)
134 return std::nullopt;
135 return value;
136#endif
137 }
138
139 // is_specialization_of<> is inspired by:
140 // https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2098r1.pdf
141
142 template <template <typename...> class T, typename U>
143 struct is_specialization_of: std::false_type
144 {
145 };
146
147 template <template <typename...> class T, typename... Us>
148 struct is_specialization_of<T, T<Us...>>: std::true_type
149 {
150 };
151
152 template <typename T>
153 struct MemberClassTypeHelper;
154
155 template <typename M, typename T>
156 struct MemberClassTypeHelper<M T::*>
157 {
158 using type = std::remove_cvref_t<T>;
159 };
160
161 template <typename Record>
162 struct RecordTableNameImpl
163 {
164 static constexpr std::string_view Value = []() {
165 if constexpr (requires { Record::TableName; })
166 return Record::TableName;
167 else
168 return []() {
169#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
170 return std::meta::identifier_of(^^Record);
171#else
172 auto const typeName = Reflection::TypeNameOf<Record>;
173 if (auto const i = typeName.rfind(':'); i != std::string_view::npos)
174 return typeName.substr(i + 1);
175 return typeName;
176#endif
177 }();
178 }();
179 };
180
181 // specialization for the case when we use tuple as
182 // a record, then we use the first element of the tuple
183 // to get the table name
184 template <typename First, typename Second>
185 struct RecordTableNameImpl<std::tuple<First, Second>>
186 {
187 static constexpr std::string_view Value = []() {
188 if constexpr (requires { First::TableName; })
189 return First::TableName;
190 else
191 return []() {
192#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
193 return std::meta::identifier_of(^^First);
194#else
195 auto const typeName = Reflection::TypeNameOf<First>;
196 if (auto const i = typeName.rfind(':'); i != std::string_view::npos)
197 return typeName.substr(i + 1);
198 return typeName;
199#endif
200 }();
201 }();
202 };
203
204 template <typename FieldType>
205 constexpr auto ColumnNameOverride = []() consteval {
206 if constexpr (requires { FieldType::ColumnNameOverride; })
207 return FieldType::ColumnNameOverride;
208 else
209 return std::string_view {};
210 }();
211#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
212 template <auto reflection>
213 struct FieldNameOfImpl
214 {
215 using R = typename[:std::meta::type_of(reflection):];
216 static constexpr std::string_view value = []() constexpr -> std::string_view {
217 if constexpr (requires { R::ColumnNameOverride; })
218 {
219 if constexpr (!R::ColumnNameOverride.empty())
220 return R::ColumnNameOverride;
221 }
222 return std::meta::identifier_of(reflection);
223 }();
224 };
225#else
226 template <typename ReferencedFieldType, auto F>
227 struct FieldNameOfImpl;
228
229 template <typename T, auto F, typename R>
230 struct FieldNameOfImpl<R T::*, F>
231 {
232 static constexpr std::string_view value = []() constexpr -> std::string_view {
233 if constexpr (requires { R::ColumnNameOverride; })
234 {
235 if constexpr (!R::ColumnNameOverride.empty())
236 return R::ColumnNameOverride;
237 }
238 return Reflection::NameOf<F>;
239 }();
240 };
241#endif // LIGHTWEIGHT_CXX26_REFLECTION
242
243 template <std::size_t I, typename Record>
244 consteval std::string_view FieldNameAt()
245 {
246 // Prefer the pre-baked SQL column name from a generated descriptor (avoids the
247 // expensive MangledName-based MemberNameOf evaluation); fall back to reflection.
248 if constexpr (HasDescription<Record>)
249 {
250 return Description<Record>::FieldNames[I];
251 }
252 else
253 {
254 using FieldType = Reflection::MemberTypeOf<I, Record>;
255
256 if constexpr (!std::string_view(ColumnNameOverride<FieldType>).empty())
257 {
258 return FieldType::ColumnNameOverride;
259 }
260 return Reflection::MemberNameOf<I, Record>;
261 }
262 }
263} // namespace detail
264
265/// @brief Returns the SQL field name of the given field index in the record.
266///
267/// @ingroup DataMapper
268template <std::size_t I, typename Record>
269constexpr inline std::string_view FieldNameAt = detail::FieldNameAt<I, Record>();
270
271/// @brief Holds the SQL tabl ename for the given record type.
272///
273/// @ingroup DataMapper
274template <typename Record>
275constexpr std::string_view RecordTableName = detail::RecordTableNameImpl<Record>::Value;
276
277template <template <typename...> class S, class T>
278concept IsSpecializationOf = detail::is_specialization_of<S, T>::value;
279
280#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
281
282/// @brief Returns the name of the field referenced by the given pointer-to-member.
283///
284/// This also supports custom column name overrides.
285template <std::meta::info ReflectionOfField>
286constexpr inline std::string_view FieldNameOf = detail::FieldNameOfImpl<ReflectionOfField>::value;
287
288template <auto Member>
289using MemberClassType = typename[:std::meta::parent_of(Member):];
290
291template <auto Member>
292constexpr size_t MemberIndexOf = []() consteval -> size_t {
293 int index { -1 };
294 auto members = nonstatic_data_members_of(parent_of(Member), std::meta::access_context::current());
295 if (auto it = std::ranges::find(members, Member); it != members.end())
296 {
297 index = std::distance(members.begin(), it);
298 return static_cast<size_t>(index);
299 }
300 return -1;
301}();
302#else // not LIGHTWEIGHT_CXX26_REFLECTION
303
304/// @brief Returns the name of the field referenced by the given pointer-to-member.
305///
306/// This also supports custom column name overrides.
307template <auto ReferencedField>
308constexpr inline std::string_view FieldNameOf = detail::FieldNameOfImpl<decltype(ReferencedField), ReferencedField>::value;
309
310template <auto Member>
311constexpr size_t MemberIndexOf = Reflection::MemberIndexOf<Member>;
312
313template <typename T>
314using MemberClassType = detail::MemberClassTypeHelper<T>::type;
315
316#endif // LIGHTWEIGHT_CXX26_REFLECTION
317
318/// @brief SqlQualifiedTableColumnName represents a column name qualified with a table name.
319///
320/// This is the single structural representation of a `table.column` reference used
321/// throughout the query builder API. The builder is responsible for quoting; do not
322/// pre-quote the values stored here.
323///
324/// @ingroup QueryBuilder
326{
327 /// The table name.
328 std::string_view tableName;
329 /// The column name.
330 std::string_view columnName;
331
332 /// Three-way comparison operator.
333 constexpr std::weak_ordering operator<=>(SqlQualifiedTableColumnName const&) const noexcept = default;
334};
335
336/// @brief Holds the fully qualified column reference (table + column) for the given field.
337/// @tparam ReferencedField A pointer-to-member identifying the field.
338///
339/// @code
340/// constexpr auto ref = FullyQualifiedNameOf<&Person::id>;
341/// static_assert(ref.tableName == "Person");
342/// static_assert(ref.columnName == "id");
343/// @endcode
344///
345/// The result is an `SqlQualifiedTableColumnName` accepted by every column-name
346/// entry point in the builder (`Field`, `Fields`, `Where`, `OrderBy`, `GroupBy`,
347/// `Aggregate::*`, joins). The builder applies the quoting.
348///
349/// @ingroup DataMapper
350template <auto ReferencedField>
352#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
353 .tableName = RecordTableName<typename[:std::meta::parent_of(ReferencedField):]>,
354#else
355 .tableName = RecordTableName<MemberClassType<decltype(ReferencedField)>>,
356#endif
357 .columnName = FieldNameOf<ReferencedField>,
358};
359
360namespace detail
361{
362 template <auto ReferencedField>
363 struct FullyQualifiedQuotedNameOfImpl
364 {
365#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
366 static constexpr auto ClassName = RecordTableName<typename[:std::meta::parent_of(ReferencedField):]>;
367#else
368 static constexpr auto ClassName = RecordTableName<MemberClassType<decltype(ReferencedField)>>;
369#endif
370 static constexpr auto FieldName = FieldNameOf<ReferencedField>;
371 static constexpr auto StorageSize = ClassName.size() + FieldName.size() + 6;
372
373 // Holds the full field name in the format "ClassName"."FieldName"
374 static constexpr auto Storage = []() constexpr -> std::array<char, StorageSize> {
375 // clang-format off
376 auto storage = std::array<char, StorageSize> {};
377 std::ranges::copy("\"", storage.begin());
378 std::ranges::copy(ClassName, storage.begin() + 1);
379 std::ranges::copy("\".\"", storage.begin() + 1 + ClassName.size());
380 std::ranges::copy(FieldName, storage.begin() + 1 + ClassName.size() + 3);
381 std::ranges::copy("\"", storage.begin() + 1 + ClassName.size() + 3 + FieldName.size());
382 storage.back() = '\0';
383 // clang-format on
384 return storage;
385 }();
386 static constexpr auto value = std::string_view(Storage.data(), Storage.size() - 1);
387 };
388
389 template <auto ReferencedField>
390 constexpr inline auto FullyQualifiedQuotedNameOf = FullyQualifiedQuotedNameOfImpl<ReferencedField>::value;
391
392 template <auto... ReferencedFields>
393 struct FullyQualifiedNamesOfImpl
394 {
395 static constexpr auto StorageSize =
396 1 + (2 * (sizeof...(ReferencedFields) - 1)) + (0 + ... + FullyQualifiedQuotedNameOf<ReferencedFields>.size());
397
398 static constexpr std::array<char, StorageSize> Storage = []() consteval {
399 auto result = std::array<char, StorageSize> {};
400 size_t offset = 0;
401 (
402 [&] {
403 if (offset > 0)
404 {
405 constexpr auto Delimiter = std::string_view(", ");
406 std::ranges::copy(Delimiter, result.begin() + offset);
407 offset += Delimiter.size();
408 }
409 std::ranges::copy(FullyQualifiedQuotedNameOf<ReferencedFields>, result.begin() + offset);
410 offset += FullyQualifiedQuotedNameOf<ReferencedFields>.size();
411 }(),
412 ...);
413 result.back() = '\0';
414 return result;
415 }();
416
417 static constexpr auto value = std::string_view(Storage.data(), Storage.size() - 1);
418 };
419
420 /// Pre-quoted, comma-joined fully qualified field names of the given fields.
421 /// Internal helper used by DataMapper to embed column lists into SQL text directly.
422 template <auto... ReferencedFields>
423 constexpr inline auto FullyQualifiedNamesOf = FullyQualifiedNamesOfImpl<ReferencedFields...>::value;
424
425} // namespace detail
426
427// Only ever taken by const reference below, so a forward declaration is enough - including
428// SqlError.hpp here would be a needless dependency for every consumer of Utils.hpp.
429struct SqlErrorInfo;
430
431/// @brief How a failed ODBC return code should surface to the caller.
432///
433/// @see ClassifyOdbcResult
434enum class SqlFailureAction : uint8_t
435{
436 /// The call succeeded; the caller should carry on.
437 None,
438
439 /// Throw @c std::invalid_argument - a soft failure the caller is expected to recover from.
440 ThrowInvalidArgument,
441
442 /// Throw @c SqlException - a genuine SQL error.
443 ThrowSqlException,
444};
445
446/// @brief Decides how a failed ODBC return code should surface, given the diagnostics already
447/// retrieved from the driver.
448///
449/// This is the error-handling *policy* of the library, split out from @ref RequireSuccess so it can
450/// be exercised without a database: it performs no I/O and touches no ODBC handle, so a test drives
451/// it by constructing a @ref SqlErrorInfo - the approach `SqlErrorDetectionTests.cpp` already uses
452/// for the error-classification helpers.
453///
454/// @param result The ODBC return code being classified.
455/// @param errorInfo The diagnostics corresponding to @p result.
456/// @return The action the caller should take.
457///
458/// @note Deliberately not @c constexpr: @ref SqlErrorInfo holds @c std::string members, so a call
459/// can never be a constant expression, and the keyword would only mislead.
460[[nodiscard]] LIGHTWEIGHT_API SqlFailureAction ClassifyOdbcResult(SQLRETURN result, SqlErrorInfo const& errorInfo) noexcept;
461
462LIGHTWEIGHT_API void LogIfFailed(SQLHSTMT hStmt, SQLRETURN error, std::source_location sourceLocation);
463
464/// @brief Substitutes a scripted failure for an ODBC call that actually succeeded.
465///
466/// Exists so error-recovery paths can be driven from a test. Some failures cannot be provoked
467/// through a real driver at all: a backup worker's transient-error retry arm needs a class-08 or
468/// HYT00 SQLSTATE, but every fault reachable from a test file (unreachable driver, unwritable path,
469/// dropped table) surfaces as HY000, which the retry policy classifies as non-transient. Without a
470/// seam those arms are unreachable rather than merely untested.
471///
472/// Production code never installs one: with no source configured, @c RequireSuccess consults
473/// nothing and returns on success exactly as before. This mirrors @c SqlDiagnosticSource and
474/// @c SqlLogger::SetLogger, injection mechanisms the project already uses.
475///
476/// @see SetFaultSource
478{
479 public:
480 SqlFaultSource() = default;
481 SqlFaultSource(SqlFaultSource const&) = delete;
482 SqlFaultSource& operator=(SqlFaultSource const&) = delete;
483 SqlFaultSource(SqlFaultSource&&) = delete;
484 SqlFaultSource& operator=(SqlFaultSource&&) = delete;
485 virtual ~SqlFaultSource() = default;
486
487 /// @brief Decides whether the next statement-handle check should fail.
488 ///
489 /// Called by @c RequireSuccess for a call that the driver reported as successful. Returning
490 /// an engaged optional makes @c RequireSuccess throw @c SqlException carrying that
491 /// diagnostic, as though the driver had failed.
492 ///
493 /// @warning @c RequireSuccess also guards @c SQLAllocHandle during statement construction,
494 /// where the handle is not yet valid. Failing indiscriminately therefore leaves a
495 /// half-constructed @c SqlStatement whose destructor cannot unwind. Use
496 /// @p sourceLocation to fail only the call sites a test means to target — the
497 /// function name of a real execution is @c ExecuteDirect / @c Execute / @c Prepare,
498 /// never a constructor.
499 ///
500 /// @param hStmt The statement handle being checked. A fake may ignore it.
501 /// @param sourceLocation Where in the library the check is happening.
502 /// @return The error to inject, or @c std::nullopt to let the successful call through.
503 [[nodiscard]] virtual std::optional<SqlErrorInfo> NextFailure(SQLHSTMT hStmt,
504 std::source_location const& sourceLocation) = 0;
505};
506
507/// @brief Installs a fault source process-wide.
508///
509/// Intended for tests. Ownership is not transferred and remains with the caller, which must keep
510/// @p source alive until it is cleared. Pass @c nullptr to disable fault injection.
511///
512/// @param source The source to install, or @c nullptr to disable.
513LIGHTWEIGHT_API void SetFaultSource(SqlFaultSource* source) noexcept;
514
515/// @brief Returns the currently installed fault source, or @c nullptr if none is installed.
516[[nodiscard]] LIGHTWEIGHT_API SqlFaultSource* GetFaultSource() noexcept;
517
518/// @brief Throws unless the given ODBC return code indicates success.
519///
520/// Retrieves the diagnostics for @p hStmt and applies @ref ClassifyOdbcResult to decide how the
521/// failure surfaces: @c std::invalid_argument for a soft failure the caller is expected to recover
522/// from, @c SqlException otherwise.
523///
524/// @param hStmt The statement handle @p error came from.
525/// @param error The ODBC return code to check.
526/// @param sourceLocation Where the check originated; reported in the exception message. Defaults to
527/// the caller's location.
528LIGHTWEIGHT_API void RequireSuccess(SQLHSTMT hStmt,
529 SQLRETURN error,
530 std::source_location sourceLocation = std::source_location::current());
531
532/// Defines the naming convention for use (e.g. for C++ column names or table names in C++ struct names).
533enum class FormatType : uint8_t
534{
535 /// Preserve the original naming convention.
536 preserve,
537
538 /// Ensure the name is formatted in snake_case naming convention.
539 snakeCase,
540
541 /// Ensure the name is formatted in CamelCase naming convention.
542 camelCase,
543};
544
545/// @brief Converts a string to a format that is more suitable for C++ code.
546LIGHTWEIGHT_API std::string FormatName(std::string const& name, FormatType formatType);
547
548/// @brief Converts a string to a format that is more suitable for C++ code.
549LIGHTWEIGHT_API std::string FormatName(std::string_view name, FormatType formatType);
550
551/// Maintains collisions to create unique names
553{
554 public:
555 /// Tests if the given name is already registered.
556 [[nodiscard]] LIGHTWEIGHT_API bool IsColliding(std::string const& name) const noexcept;
557
558 /// Tries to declare a name and returns it, otherwise returns std::nullopt.
559 [[nodiscard]] LIGHTWEIGHT_API std::optional<std::string> TryDeclareName(std::string name);
560
561 /// Creates a name that is definitely not colliding.
562 [[nodiscard]] LIGHTWEIGHT_API std::string DeclareName(std::string name);
563
564 private:
565 std::unordered_map<std::string, size_t> _collisionMap;
566};
567
568} // namespace Lightweight
Substitutes a scripted failure for an ODBC call that actually succeeded.
Definition Utils.hpp:478
virtual std::optional< SqlErrorInfo > NextFailure(SQLHSTMT hStmt, std::source_location const &sourceLocation)=0
Decides whether the next statement-handle check should fail.
Maintains collisions to create unique names.
Definition Utils.hpp:553
LIGHTWEIGHT_API std::string DeclareName(std::string name)
Creates a name that is definitely not colliding.
LIGHTWEIGHT_API bool IsColliding(std::string const &name) const noexcept
Tests if the given name is already registered.
LIGHTWEIGHT_API std::optional< std::string > TryDeclareName(std::string name)
Tries to declare a name and returns it, otherwise returns std::nullopt.
constexpr std::string_view RecordTableName
Holds the SQL tabl ename for the given record type.
Definition Utils.hpp:275
constexpr std::string_view FieldNameAt
Returns the SQL field name of the given field index in the record.
Definition Utils.hpp:269
constexpr auto FullyQualifiedNameOf
Holds the fully qualified column reference (table + column) for the given field.
Definition Utils.hpp:351
SqlQualifiedTableColumnName represents a column name qualified with a table name.
Definition Utils.hpp:326
std::string_view tableName
The table name.
Definition Utils.hpp:328
constexpr std::weak_ordering operator<=>(SqlQualifiedTableColumnName const &) const noexcept=default
Three-way comparison operator.
std::string_view columnName
The column name.
Definition Utils.hpp:330