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