Lightweight 0.20261105.1
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 a handful of
6// symbols (SQLHWND/GUID/etc.) against the Windows prelude. SqlOdbcPrelude.hpp supplies minimal,
7// ABI-stable shims for exactly those symbols instead of pulling in the full <Windows.h> aggregate
8// — see its header comment for the rationale (#547). Every other header here that reaches an ODBC
9// header opens with the same include.
10#include "Api.hpp"
11#include "Description.hpp"
12#include "SqlError.hpp"
13#include "SqlOdbcPrelude.hpp"
14
15#include <reflection-cpp/reflection.hpp>
16
17#include <algorithm>
18#include <array>
19#include <cerrno>
20#include <charconv>
21#include <cstddef>
22#include <cstdlib>
23#include <optional>
24#include <ranges>
25#include <source_location>
26#include <span>
27#include <string>
28#include <string_view>
29#include <system_error>
30#include <type_traits>
31#include <unordered_map>
32#include <utility>
33
34// libc++ exposes the locale-aware strtod_l / newlocale family via <xlocale.h>; glibc declares them in
35// <stdlib.h>/<locale.h>. We only need them on the fallback path below (no float std::from_chars).
36#if !(defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 201611L)
37 #include <clocale>
38 #if defined(__APPLE__)
39 #include <xlocale.h>
40 #else
41 #include <locale.h>
42 #endif
43#endif
44
45#include <sql.h>
46
47#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
48 #include <experimental/meta>
49#endif
50
51namespace Lightweight
52{
53
54namespace detail
55{
56
57 template <typename T, typename... Comps>
58 concept OneOf = (std::same_as<T, Comps> || ...);
59
60 /// Appends @p bytes to @p text as upper-case hex digits, two per byte.
61 /// @param text Receives the digits.
62 /// @param bytes The bytes to spell out.
63 inline void AppendUpperHex(std::string& text, std::span<std::byte const> bytes)
64 {
65 constexpr auto Digits = std::string_view { "0123456789ABCDEF" };
66 text.reserve(text.size() + (2 * bytes.size()));
67 for (auto const byte: bytes)
68 {
69 auto const value = std::to_integer<unsigned>(byte);
70 text += Digits[value >> 4U];
71 text += Digits[value & 0x0FU];
72 }
73 }
74
75 template <typename T>
76 constexpr auto AlwaysFalse = std::false_type::value;
77
78 constexpr auto Finally(auto&& cleanupRoutine) noexcept
79 {
80 // NOLINTNEXTLINE(cppcoreguidelines-special-member-functions)
81 struct Finally
82 {
83 std::remove_cvref_t<decltype(cleanupRoutine)> cleanup;
84 ~Finally()
85 {
86 cleanup();
87 }
88 };
89 return Finally { std::forward<decltype(cleanupRoutine)>(cleanupRoutine) };
90 }
91
92 /// Parses a floating-point value from the character range @c [first, last) in a locale-independent way.
93 ///
94 /// This is the single, shared replacement for @c std::from_chars on floating-point types, which is
95 /// unavailable on libc++ before macOS 26. Unlike a bare @c std::strtod it neither depends on the active
96 /// @c LC_NUMERIC locale nor silently accepts trailing garbage, so it round-trips the @c '.' decimal point
97 /// the library always emits regardless of the host locale.
98 ///
99 /// @tparam T A floating-point type (@c float, @c double, or @c long double).
100 /// @param first Pointer to the first character of the numeric text.
101 /// @param last Pointer one past the last character of the numeric text.
102 /// @return The parsed value, or @c std::nullopt if the text is not a complete, in-range number.
103 template <typename T>
104 requires std::is_floating_point_v<T>
105 [[nodiscard]] inline std::optional<T> ParseFloat(char const* first, char const* last) noexcept
106 {
107 if (first == last)
108 return std::nullopt;
109#if defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 201611L
110 // std::from_chars is locale-independent, allocation-free, and reports both partial parses (ptr) and
111 // range errors (ec) — the preferred path wherever the float overloads exist.
112 T value {};
113 auto const [ptr, ec] = std::from_chars(first, last, value);
114 if (ec != std::errc {} || ptr != last)
115 return std::nullopt;
116 return value;
117#else
118 // libc++ fallback: strtod_l with a persistent "C" locale gives locale-independent parsing; from_chars
119 // float overloads are unavailable here. Copy into a NUL-terminated buffer (the range need not be) and
120 // require the parse to consume all of it (mirrors the from_chars ptr==last check). On-stack for the
121 // common short numeric text; only the rare over-long token allocates.
122 static ::locale_t const cLocale = ::newlocale(LC_NUMERIC_MASK, "C", static_cast<::locale_t>(nullptr));
123 auto const length = static_cast<std::size_t>(last - first);
124 std::array<char, 64> stackBuffer {};
125 std::string heapBuffer;
126 char const* text = nullptr;
127 if (length < stackBuffer.size())
128 {
129 std::ranges::copy(first, last, stackBuffer.begin());
130 stackBuffer[length] = '\0';
131 text = stackBuffer.data();
132 }
133 else
134 {
135 heapBuffer.assign(first, last);
136 text = heapBuffer.c_str();
137 }
138
139 char* parseEnd = nullptr;
140 errno = 0;
141 T value {};
142 if constexpr (std::is_same_v<T, float>)
143 value = ::strtof_l(text, &parseEnd, cLocale);
144 else if constexpr (std::is_same_v<T, long double>)
145 value = ::strtold_l(text, &parseEnd, cLocale);
146 else
147 value = static_cast<T>(::strtod_l(text, &parseEnd, cLocale));
148
149 if (errno == ERANGE || parseEnd != text + length)
150 return std::nullopt;
151 return value;
152#endif
153 }
154
155 // is_specialization_of<> is inspired by:
156 // https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2098r1.pdf
157
158 template <template <typename...> class T, typename U>
159 struct is_specialization_of: std::false_type
160 {
161 };
162
163 template <template <typename...> class T, typename... Us>
164 struct is_specialization_of<T, T<Us...>>: std::true_type
165 {
166 };
167
168 template <typename T>
169 struct MemberClassTypeHelper;
170
171 template <typename M, typename T>
172 struct MemberClassTypeHelper<M T::*>
173 {
174 using type = std::remove_cvref_t<T>;
175 };
176
177 template <typename Record>
178 struct RecordTableNameImpl
179 {
180 static constexpr std::string_view Value = []() {
181 if constexpr (requires { Record::TableName; })
182 return Record::TableName;
183 else
184 return []() {
185#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
186 return std::meta::identifier_of(^^Record);
187#else
188 auto const typeName = Reflection::TypeNameOf<Record>;
189 if (auto const i = typeName.rfind(':'); i != std::string_view::npos)
190 return typeName.substr(i + 1);
191 return typeName;
192#endif
193 }();
194 }();
195 };
196
197 // specialization for the case when we use tuple as
198 // a record, then we use the first element of the tuple
199 // to get the table name
200 template <typename First, typename Second>
201 struct RecordTableNameImpl<std::tuple<First, Second>>
202 {
203 static constexpr std::string_view Value = []() {
204 if constexpr (requires { First::TableName; })
205 return First::TableName;
206 else
207 return []() {
208#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
209 return std::meta::identifier_of(^^First);
210#else
211 auto const typeName = Reflection::TypeNameOf<First>;
212 if (auto const i = typeName.rfind(':'); i != std::string_view::npos)
213 return typeName.substr(i + 1);
214 return typeName;
215#endif
216 }();
217 }();
218 };
219
220 template <typename FieldType>
221 constexpr auto ColumnNameOverride = []() consteval {
222 if constexpr (requires { FieldType::ColumnNameOverride; })
223 return FieldType::ColumnNameOverride;
224 else
225 return std::string_view {};
226 }();
227#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
228 template <auto reflection>
229 struct FieldNameOfImpl
230 {
231 using R = typename[:std::meta::type_of(reflection):];
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 std::meta::identifier_of(reflection);
239 }();
240 };
241#else
242 template <typename ReferencedFieldType, auto F>
243 struct FieldNameOfImpl;
244
245 template <typename T, auto F, typename R>
246 struct FieldNameOfImpl<R T::*, F>
247 {
248 static constexpr std::string_view value = []() constexpr -> std::string_view {
249 if constexpr (requires { R::ColumnNameOverride; })
250 {
251 if constexpr (!R::ColumnNameOverride.empty())
252 return R::ColumnNameOverride;
253 }
254 return Reflection::NameOf<F>;
255 }();
256 };
257#endif // LIGHTWEIGHT_CXX26_REFLECTION
258
259 template <std::size_t I, typename Record>
260 consteval std::string_view FieldNameAt()
261 {
262 // Prefer the pre-baked SQL column name from a generated descriptor (avoids the
263 // expensive MangledName-based MemberNameOf evaluation); fall back to reflection.
264 if constexpr (HasDescription<Record>)
265 {
266 return Description<Record>::FieldNames[I];
267 }
268 else
269 {
270 using FieldType = Reflection::MemberTypeOf<I, Record>;
271
272 if constexpr (!std::string_view(ColumnNameOverride<FieldType>).empty())
273 {
274 return FieldType::ColumnNameOverride;
275 }
276 return Reflection::MemberNameOf<I, Record>;
277 }
278 }
279} // namespace detail
280
281/// @brief Returns the SQL field name of the given field index in the record.
282///
283/// @ingroup DataMapper
284template <std::size_t I, typename Record>
285constexpr inline std::string_view FieldNameAt = detail::FieldNameAt<I, Record>();
286
287/// @brief Holds the SQL tabl ename for the given record type.
288///
289/// @ingroup DataMapper
290template <typename Record>
291constexpr std::string_view RecordTableName = detail::RecordTableNameImpl<Record>::Value;
292
293template <template <typename...> class S, class T>
294concept IsSpecializationOf = detail::is_specialization_of<S, T>::value;
295
296#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
297
298/// @brief Returns the name of the field referenced by the given pointer-to-member.
299///
300/// This also supports custom column name overrides.
301template <std::meta::info ReflectionOfField>
302constexpr inline std::string_view FieldNameOf = detail::FieldNameOfImpl<ReflectionOfField>::value;
303
304template <auto Member>
305using MemberClassType = typename[:std::meta::parent_of(Member):];
306
307template <auto Member>
308constexpr size_t MemberIndexOf = []() consteval -> size_t {
309 int index { -1 };
310 auto members = nonstatic_data_members_of(std::meta::parent_of(Member), std::meta::access_context::current());
311 if (auto it = std::ranges::find(members, Member); it != members.end())
312 {
313 index = std::distance(members.begin(), it);
314 return static_cast<size_t>(index);
315 }
316 return -1;
317}();
318#else // not LIGHTWEIGHT_CXX26_REFLECTION
319
320/// @brief Returns the name of the field referenced by the given pointer-to-member.
321///
322/// This also supports custom column name overrides.
323template <auto ReferencedField>
324constexpr inline std::string_view FieldNameOf = detail::FieldNameOfImpl<decltype(ReferencedField), ReferencedField>::value;
325
326template <auto Member>
327constexpr size_t MemberIndexOf = Reflection::MemberIndexOf<Member>;
328
329template <typename T>
330using MemberClassType = detail::MemberClassTypeHelper<T>::type;
331
332#endif // LIGHTWEIGHT_CXX26_REFLECTION
333
334/// @brief SqlQualifiedTableColumnName represents a column name qualified with a table name.
335///
336/// This is the single structural representation of a `table.column` reference used
337/// throughout the query builder API. The builder is responsible for quoting; do not
338/// pre-quote the values stored here.
339///
340/// @ingroup QueryBuilder
342{
343 /// The table name.
344 std::string_view tableName;
345 /// The column name.
346 std::string_view columnName;
347
348 /// Three-way comparison operator.
349 constexpr std::weak_ordering operator<=>(SqlQualifiedTableColumnName const&) const noexcept = default;
350};
351
352/// @brief Holds the fully qualified column reference (table + column) for the given field.
353/// @tparam ReferencedField A pointer-to-member identifying the field.
354///
355/// @code
356/// constexpr auto ref = FullyQualifiedNameOf<&Person::id>;
357/// static_assert(ref.tableName == "Person");
358/// static_assert(ref.columnName == "id");
359/// @endcode
360///
361/// The result is an `SqlQualifiedTableColumnName` accepted by every column-name
362/// entry point in the builder (`Field`, `Fields`, `Where`, `OrderBy`, `GroupBy`,
363/// `Aggregate::*`, joins). The builder applies the quoting.
364///
365/// @ingroup DataMapper
366template <auto ReferencedField>
368#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
369 .tableName = RecordTableName<typename[:std::meta::parent_of(ReferencedField):]>,
370#else
371 .tableName = RecordTableName<MemberClassType<decltype(ReferencedField)>>,
372#endif
373 .columnName = FieldNameOf<ReferencedField>,
374};
375
376namespace detail
377{
378 template <auto ReferencedField>
379 struct FullyQualifiedQuotedNameOfImpl
380 {
381#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
382 static constexpr auto ClassName = RecordTableName<typename[:std::meta::parent_of(ReferencedField):]>;
383#else
384 static constexpr auto ClassName = RecordTableName<MemberClassType<decltype(ReferencedField)>>;
385#endif
386 static constexpr auto FieldName = FieldNameOf<ReferencedField>;
387 static constexpr auto StorageSize = ClassName.size() + FieldName.size() + 6;
388
389 // Holds the full field name in the format "ClassName"."FieldName"
390 static constexpr auto Storage = []() constexpr -> std::array<char, StorageSize> {
391 // clang-format off
392 auto storage = std::array<char, StorageSize> {};
393 std::ranges::copy("\"", storage.begin());
394 std::ranges::copy(ClassName, storage.begin() + 1);
395 std::ranges::copy("\".\"", storage.begin() + 1 + ClassName.size());
396 std::ranges::copy(FieldName, storage.begin() + 1 + ClassName.size() + 3);
397 std::ranges::copy("\"", storage.begin() + 1 + ClassName.size() + 3 + FieldName.size());
398 storage.back() = '\0';
399 // clang-format on
400 return storage;
401 }();
402 static constexpr auto value = std::string_view(Storage.data(), Storage.size() - 1);
403 };
404
405 template <auto ReferencedField>
406 constexpr inline auto FullyQualifiedQuotedNameOf = FullyQualifiedQuotedNameOfImpl<ReferencedField>::value;
407
408 template <auto... ReferencedFields>
409 struct FullyQualifiedNamesOfImpl
410 {
411 static constexpr auto StorageSize =
412 1 + (2 * (sizeof...(ReferencedFields) - 1)) + (0 + ... + FullyQualifiedQuotedNameOf<ReferencedFields>.size());
413
414 static constexpr std::array<char, StorageSize> Storage = []() consteval {
415 auto result = std::array<char, StorageSize> {};
416 size_t offset = 0;
417 (
418 [&] {
419 if (offset > 0)
420 {
421 constexpr auto Delimiter = std::string_view(", ");
422 std::ranges::copy(Delimiter, result.begin() + offset);
423 offset += Delimiter.size();
424 }
425 std::ranges::copy(FullyQualifiedQuotedNameOf<ReferencedFields>, result.begin() + offset);
426 offset += FullyQualifiedQuotedNameOf<ReferencedFields>.size();
427 }(),
428 ...);
429 result.back() = '\0';
430 return result;
431 }();
432
433 static constexpr auto value = std::string_view(Storage.data(), Storage.size() - 1);
434 };
435
436 /// Pre-quoted, comma-joined fully qualified field names of the given fields.
437 /// Internal helper used by DataMapper to embed column lists into SQL text directly.
438 template <auto... ReferencedFields>
439 constexpr inline auto FullyQualifiedNamesOf = FullyQualifiedNamesOfImpl<ReferencedFields...>::value;
440
441} // namespace detail
442
443// SqlFaultSource::NextFailure returns std::optional<SqlErrorInfo> by value, and instantiating
444// std::optional<T> with an incomplete T is undefined ([optional.optional]/3) - so a forward
445// declaration is not enough here and this header must carry the full definition.
446
447/// @brief How a failed ODBC return code should surface to the caller.
448///
449/// @see ClassifyOdbcResult
450enum class SqlFailureAction : uint8_t
451{
452 /// The call succeeded; the caller should carry on.
453 None,
454
455 /// Throw @c std::invalid_argument - a soft failure the caller is expected to recover from.
456 ThrowInvalidArgument,
457
458 /// Throw @c SqlException - a genuine SQL error.
459 ThrowSqlException,
460};
461
462/// @brief Decides how a failed ODBC return code should surface, given the diagnostics already
463/// retrieved from the driver.
464///
465/// This is the error-handling *policy* of the library, split out from @ref RequireSuccess so it can
466/// be exercised without a database: it performs no I/O and touches no ODBC handle, so a test drives
467/// it by constructing a @ref SqlErrorInfo - the approach `SqlErrorDetectionTests.cpp` already uses
468/// for the error-classification helpers.
469///
470/// @param result The ODBC return code being classified.
471/// @param errorInfo The diagnostics corresponding to @p result.
472/// @return The action the caller should take.
473///
474/// @note Deliberately not @c constexpr: @ref SqlErrorInfo holds @c std::string members, so a call
475/// can never be a constant expression, and the keyword would only mislead.
476[[nodiscard]] LIGHTWEIGHT_API SqlFailureAction ClassifyOdbcResult(SQLRETURN result, SqlErrorInfo const& errorInfo) noexcept;
477
478LIGHTWEIGHT_API void LogIfFailed(SQLHSTMT hStmt, SQLRETURN error, std::source_location sourceLocation);
479
480/// @brief Substitutes a scripted failure for an ODBC call that actually succeeded.
481///
482/// Exists so error-recovery paths can be driven from a test. Some failures cannot be provoked
483/// through a real driver at all: a backup worker's transient-error retry arm needs a class-08 or
484/// HYT00 SQLSTATE, but every fault reachable from a test file (unreachable driver, unwritable path,
485/// dropped table) surfaces as HY000, which the retry policy classifies as non-transient. Without a
486/// seam those arms are unreachable rather than merely untested.
487///
488/// Production code never installs one: with no source configured, @c RequireSuccess consults
489/// nothing and returns on success exactly as before. This mirrors @c SqlDiagnosticSource and
490/// @c SqlLogger::SetLogger, injection mechanisms the project already uses.
491///
492/// @see SetFaultSource
494{
495 public:
496 SqlFaultSource() = default;
497 SqlFaultSource(SqlFaultSource const&) = delete;
498 SqlFaultSource& operator=(SqlFaultSource const&) = delete;
499 SqlFaultSource(SqlFaultSource&&) = delete;
500 SqlFaultSource& operator=(SqlFaultSource&&) = delete;
501 virtual ~SqlFaultSource() = default;
502
503 /// @brief Decides whether the next statement-handle check should fail.
504 ///
505 /// Called by @c RequireSuccess for a call that the driver reported as successful. Returning
506 /// an engaged optional makes @c RequireSuccess throw @c SqlException carrying that
507 /// diagnostic, as though the driver had failed.
508 ///
509 /// @note Never called for a null statement handle. @c RequireSuccess also guards
510 /// @c SQLAllocHandle during statement construction, where failing would leave a
511 /// half-constructed @c SqlStatement whose destructor cannot release the handle; it
512 /// therefore skips injection entirely while @p hStmt is still @c SQL_NULL_HSTMT. A fake
513 /// may narrow further via @p sourceLocation — the function name of a real execution is
514 /// @c ExecuteDirect / @c Execute / @c Prepare — but does not have to in order to be safe.
515 ///
516 /// @param hStmt The statement handle being checked, never @c SQL_NULL_HSTMT. A fake may ignore it.
517 /// @param sourceLocation Where in the library the check is happening.
518 /// @return The error to inject, or @c std::nullopt to let the successful call through.
519 [[nodiscard]] virtual std::optional<SqlErrorInfo> NextFailure(SQLHSTMT hStmt,
520 std::source_location const& sourceLocation) = 0;
521
522 /// @brief Decides whether the next connection-handle check should fail.
523 ///
524 /// The connection-side counterpart of @ref NextFailure, consulted by
525 /// @c detail::CheckOdbcConnectionCall for a call that the driver reported as successful on a
526 /// @c SQLHDBC rather than a @c SQLHSTMT (e.g. a pre-connect attribute set). Returning an engaged
527 /// optional makes the caller treat the call as failed, exactly as an injected @ref NextFailure
528 /// does for a statement check.
529 ///
530 /// Defaults to never injecting, so a fake that predates connection-handle support and overrides
531 /// only @ref NextFailure keeps compiling and behaving exactly as before.
532 ///
533 /// @note Never called for a null connection handle, for the same reason @ref NextFailure is never
534 /// called for a null statement handle: a fault injected while a @c SqlConnection is still
535 /// being constructed or is in the middle of tearing down its handle could leave the handle
536 /// leaked or double-released.
537 ///
538 /// @param hDbc The connection handle being checked, never @c SQL_NULL_HDBC. A fake may ignore it.
539 /// @param sourceLocation Where in the library the check is happening.
540 /// @return The error to inject, or @c std::nullopt to let the successful call through.
541 [[nodiscard]] virtual std::optional<SqlErrorInfo> NextConnectionFailure(
542 [[maybe_unused]] SQLHDBC hDbc, [[maybe_unused]] std::source_location const& sourceLocation)
543 {
544 return std::nullopt;
545 }
546};
547
548/// @brief Installs a fault source process-wide.
549///
550/// Intended for tests. Ownership is not transferred and remains with the caller, which must keep
551/// @p source alive until it is cleared. Pass @c nullptr to disable fault injection.
552///
553/// @param source The source to install, or @c nullptr to disable.
554LIGHTWEIGHT_API void SetFaultSource(SqlFaultSource* source) noexcept;
555
556/// @brief Returns the currently installed fault source, or @c nullptr if none is installed.
557[[nodiscard]] LIGHTWEIGHT_API SqlFaultSource* GetFaultSource() noexcept;
558
559/// @brief Throws unless the given ODBC return code indicates success.
560///
561/// Retrieves the diagnostics for @p hStmt and applies @ref ClassifyOdbcResult to decide how the
562/// failure surfaces: @c std::invalid_argument for a soft failure the caller is expected to recover
563/// from, @c SqlException otherwise.
564///
565/// @param hStmt The statement handle @p error came from.
566/// @param error The ODBC return code to check.
567/// @param sourceLocation Where the check originated; reported in the exception message. Defaults to
568/// the caller's location.
569LIGHTWEIGHT_API void RequireSuccess(SQLHSTMT hStmt,
570 SQLRETURN error,
571 std::source_location sourceLocation = std::source_location::current());
572
573namespace detail
574{
575 /// @brief Verdict of a non-throwing checked ODBC call, carrying the diagnostic that justifies a
576 /// failure verdict whenever that failure was injected rather than real.
577 ///
578 /// A bare @c bool is not enough here. When an installed @ref SqlFaultSource overrides a real
579 /// success into a failure, the underlying ODBC call genuinely succeeded, so the handle holds no
580 /// diagnostic records at all: a caller that recovers by *inspecting* the error (rather than
581 /// merely branching on it) would read an empty or stale diagnostic off the handle and take the
582 /// wrong recovery arm. Carrying the injected error alongside the verdict is what makes such an
583 /// arm reachable from a test, which is the whole point of the seam.
584 ///
585 /// @see CheckOdbcCall, CheckOdbcConnectionCall
586 struct OdbcCallOutcome
587 {
588 /// Whether the checked call should be treated as having succeeded.
589 bool succeeded {};
590
591 /// The injected diagnostic, engaged only when a @ref SqlFaultSource turned a real success
592 /// into a failure. Never engaged for a genuine failure — that one's diagnostic is on the
593 /// handle, where the caller can read it. Prefer @ref EffectiveError over branching on this
594 /// directly.
595 std::optional<SqlErrorInfo> injectedError {};
596
597 /// @return @ref succeeded, so an outcome can be tested directly in an `if`.
598 [[nodiscard]] explicit constexpr operator bool() const noexcept
599 {
600 return succeeded;
601 }
602
603 /// @brief The diagnostic describing this failure, from whichever source actually has one.
604 ///
605 /// @param readFromHandle Invoked only when the failure was not injected, to read the real
606 /// handle's diagnostic (e.g. @c SqlErrorInfo::FromStatementHandle or
607 /// @c SqlConnection::LastError). Deliberately lazy: reading
608 /// diagnostics off a handle is an ODBC round-trip, and there is
609 /// nothing to read when the failure was injected.
610 /// @return The injected diagnostic when there is one, otherwise @p readFromHandle's result.
611 template <typename ReadFromHandle>
612 requires std::is_invocable_r_v<SqlErrorInfo, ReadFromHandle const&>
613 [[nodiscard]] SqlErrorInfo EffectiveError(ReadFromHandle const& readFromHandle) const
614 {
615 return injectedError.has_value() ? *injectedError : readFromHandle();
616 }
617 };
618
619 /// @brief Non-throwing checked-call helper for inline `SQL_SUCCEEDED` recovery checks against a
620 /// statement handle.
621 ///
622 /// Some recovery arms cannot go through @ref RequireSuccess because they must not throw: the
623 /// caller recovers by branching (e.g. declining to retry, or declining to pool a handle), and
624 /// that recovery is the whole point of being able to drive the call to its failure branch from a
625 /// test. This helper gives such a call site the same fault-injection seam @ref RequireSuccess
626 /// already offers, without the throw: the returned outcome carries the @c SQL_SUCCEEDED verdict
627 /// for @p result, unless an installed @ref SqlFaultSource overrides a real success into an
628 /// injected failure via @ref SqlFaultSource::NextFailure — in which case it also carries that
629 /// injected diagnostic, since the handle has none to offer.
630 ///
631 /// Mirrors @ref RequireSuccess's safety property: never consults the fault source while @p hStmt
632 /// is @c SQL_NULL_HSTMT, so a call made before a statement handle is fully valid cannot be
633 /// disturbed by injection.
634 ///
635 /// @param result The ODBC return code to check.
636 /// @param hStmt The statement handle @p result came from.
637 /// @param sourceLocation Where the check originated; forwarded to the fault source. Defaults to
638 /// the caller's location.
639 /// @return The verdict for @p result, plus the injected diagnostic when one was injected.
640 [[nodiscard]] LIGHTWEIGHT_API OdbcCallOutcome
641 CheckOdbcCall(SQLRETURN result, SQLHSTMT hStmt, std::source_location sourceLocation = std::source_location::current());
642
643 /// @brief Non-throwing checked-call helper for inline `SQL_SUCCEEDED` recovery checks against a
644 /// connection handle.
645 ///
646 /// The connection-handle counterpart of @ref CheckOdbcCall(SQLRETURN, SQLHSTMT,
647 /// std::source_location): same contract, keyed on a @c SQLHDBC via
648 /// @ref SqlFaultSource::NextConnectionFailure instead of @ref SqlFaultSource::NextFailure. Never
649 /// consults the fault source while @p hDbc is @c SQL_NULL_HDBC, for the same reason the
650 /// statement-handle overload skips a null @c SQLHSTMT.
651 ///
652 /// @param result The ODBC return code to check.
653 /// @param hDbc The connection handle @p result came from.
654 /// @param sourceLocation Where the check originated; forwarded to the fault source. Defaults to
655 /// the caller's location.
656 /// @return The verdict for @p result, plus the injected diagnostic when one was injected.
657 [[nodiscard]] LIGHTWEIGHT_API OdbcCallOutcome CheckOdbcConnectionCall(
658 SQLRETURN result, SQLHDBC hDbc, std::source_location sourceLocation = std::source_location::current());
659} // namespace detail
660
661/// Defines the naming convention for use (e.g. for C++ column names or table names in C++ struct names).
662enum class FormatType : uint8_t
663{
664 /// Preserve the original naming convention.
665 preserve,
666
667 /// Ensure the name is formatted in snake_case naming convention.
668 snakeCase,
669
670 /// Ensure the name is formatted in CamelCase naming convention.
671 camelCase,
672};
673
674/// @brief Converts a string to a format that is more suitable for C++ code.
675LIGHTWEIGHT_API std::string FormatName(std::string const& name, FormatType formatType);
676
677/// @brief Converts a string to a format that is more suitable for C++ code.
678LIGHTWEIGHT_API std::string FormatName(std::string_view name, FormatType formatType);
679
680/// Maintains collisions to create unique names
682{
683 public:
684 /// Tests if the given name is already registered.
685 [[nodiscard]] LIGHTWEIGHT_API bool IsColliding(std::string const& name) const noexcept;
686
687 /// Tries to declare a name and returns it, otherwise returns std::nullopt.
688 [[nodiscard]] LIGHTWEIGHT_API std::optional<std::string> TryDeclareName(std::string name);
689
690 /// Creates a name that is definitely not colliding.
691 [[nodiscard]] LIGHTWEIGHT_API std::string DeclareName(std::string name);
692
693 private:
694 std::unordered_map<std::string, size_t> _collisionMap;
695};
696
697} // namespace Lightweight
Substitutes a scripted failure for an ODBC call that actually succeeded.
Definition Utils.hpp:494
virtual std::optional< SqlErrorInfo > NextConnectionFailure(SQLHDBC hDbc, std::source_location const &sourceLocation)
Decides whether the next connection-handle check should fail.
Definition Utils.hpp:541
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:682
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:291
constexpr std::string_view FieldNameAt
Returns the SQL field name of the given field index in the record.
Definition Utils.hpp:285
constexpr auto FullyQualifiedNameOf
Holds the fully qualified column reference (table + column) for the given field.
Definition Utils.hpp:367
@ None
The field is not maintained by the data mapper.
SqlQualifiedTableColumnName represents a column name qualified with a table name.
Definition Utils.hpp:342
std::string_view tableName
The table name.
Definition Utils.hpp:344
constexpr std::weak_ordering operator<=>(SqlQualifiedTableColumnName const &) const noexcept=default
Three-way comparison operator.
std::string_view columnName
The column name.
Definition Utils.hpp:346