Lightweight 0.20260617.0
Loading...
Searching...
No Matches
Utils.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "Api.hpp"
6#include "Description.hpp"
7
8#include <reflection-cpp/reflection.hpp>
9
10#include <algorithm>
11#include <array>
12#include <cerrno>
13#include <charconv>
14#include <cstdlib>
15#include <optional>
16#include <ranges>
17#include <source_location>
18#include <string>
19#include <string_view>
20#include <system_error>
21#include <type_traits>
22#include <unordered_map>
23#include <utility>
24
25// libc++ exposes the locale-aware strtod_l / newlocale family via <xlocale.h>; glibc declares them in
26// <stdlib.h>/<locale.h>. We only need them on the fallback path below (no float std::from_chars).
27#if !(defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 201611L)
28 #include <clocale>
29 #if defined(__APPLE__)
30 #include <xlocale.h>
31 #else
32 #include <locale.h>
33 #endif
34#endif
35
36#include <sql.h>
37
38#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
39 #include <experimental/meta>
40#endif
41
42namespace Lightweight
43{
44
45namespace detail
46{
47
48 template <typename T, typename... Comps>
49 concept OneOf = (std::same_as<T, Comps> || ...);
50
51 template <typename T>
52 constexpr auto AlwaysFalse = std::false_type::value;
53
54 constexpr auto Finally(auto&& cleanupRoutine) noexcept
55 {
56 // NOLINTNEXTLINE(cppcoreguidelines-special-member-functions)
57 struct Finally
58 {
59 std::remove_cvref_t<decltype(cleanupRoutine)> cleanup;
60 ~Finally()
61 {
62 cleanup();
63 }
64 };
65 return Finally { std::forward<decltype(cleanupRoutine)>(cleanupRoutine) };
66 }
67
68 /// Parses a floating-point value from the character range @c [first, last) in a locale-independent way.
69 ///
70 /// This is the single, shared replacement for @c std::from_chars on floating-point types, which is
71 /// unavailable on libc++ before macOS 26. Unlike a bare @c std::strtod it neither depends on the active
72 /// @c LC_NUMERIC locale nor silently accepts trailing garbage, so it round-trips the @c '.' decimal point
73 /// the library always emits regardless of the host locale.
74 ///
75 /// @tparam T A floating-point type (@c float, @c double, or @c long double).
76 /// @param first Pointer to the first character of the numeric text.
77 /// @param last Pointer one past the last character of the numeric text.
78 /// @return The parsed value, or @c std::nullopt if the text is not a complete, in-range number.
79 template <typename T>
80 requires std::is_floating_point_v<T>
81 [[nodiscard]] inline std::optional<T> ParseFloat(char const* first, char const* last) noexcept
82 {
83 if (first == last)
84 return std::nullopt;
85#if defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 201611L
86 // std::from_chars is locale-independent, allocation-free, and reports both partial parses (ptr) and
87 // range errors (ec) — the preferred path wherever the float overloads exist.
88 T value {};
89 auto const [ptr, ec] = std::from_chars(first, last, value);
90 if (ec != std::errc {} || ptr != last)
91 return std::nullopt;
92 return value;
93#else
94 // libc++ fallback: strtod_l with a persistent "C" locale gives locale-independent parsing; from_chars
95 // float overloads are unavailable here. Copy into a NUL-terminated buffer (the range need not be) and
96 // require the parse to consume all of it (mirrors the from_chars ptr==last check). On-stack for the
97 // common short numeric text; only the rare over-long token allocates.
98 static ::locale_t const cLocale = ::newlocale(LC_NUMERIC_MASK, "C", static_cast<::locale_t>(nullptr));
99 auto const length = static_cast<std::size_t>(last - first);
100 std::array<char, 64> stackBuffer {};
101 std::string heapBuffer;
102 char const* text = nullptr;
103 if (length < stackBuffer.size())
104 {
105 std::ranges::copy(first, last, stackBuffer.begin());
106 stackBuffer[length] = '\0';
107 text = stackBuffer.data();
108 }
109 else
110 {
111 heapBuffer.assign(first, last);
112 text = heapBuffer.c_str();
113 }
114
115 char* parseEnd = nullptr;
116 errno = 0;
117 T value {};
118 if constexpr (std::is_same_v<T, float>)
119 value = ::strtof_l(text, &parseEnd, cLocale);
120 else if constexpr (std::is_same_v<T, long double>)
121 value = ::strtold_l(text, &parseEnd, cLocale);
122 else
123 value = static_cast<T>(::strtod_l(text, &parseEnd, cLocale));
124
125 if (errno == ERANGE || parseEnd != text + length)
126 return std::nullopt;
127 return value;
128#endif
129 }
130
131 // is_specialization_of<> is inspired by:
132 // https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2098r1.pdf
133
134 template <template <typename...> class T, typename U>
135 struct is_specialization_of: std::false_type
136 {
137 };
138
139 template <template <typename...> class T, typename... Us>
140 struct is_specialization_of<T, T<Us...>>: std::true_type
141 {
142 };
143
144 template <typename T>
145 struct MemberClassTypeHelper;
146
147 template <typename M, typename T>
148 struct MemberClassTypeHelper<M T::*>
149 {
150 using type = std::remove_cvref_t<T>;
151 };
152
153 template <typename Record>
154 struct RecordTableNameImpl
155 {
156 static constexpr std::string_view Value = []() {
157 if constexpr (requires { Record::TableName; })
158 return Record::TableName;
159 else
160 return []() {
161#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
162 return std::meta::identifier_of(^^Record);
163#else
164 auto const typeName = Reflection::TypeNameOf<Record>;
165 if (auto const i = typeName.rfind(':'); i != std::string_view::npos)
166 return typeName.substr(i + 1);
167 return typeName;
168#endif
169 }();
170 }();
171 };
172
173 // specialization for the case when we use tuple as
174 // a record, then we use the first element of the tuple
175 // to get the table name
176 template <typename First, typename Second>
177 struct RecordTableNameImpl<std::tuple<First, Second>>
178 {
179 static constexpr std::string_view Value = []() {
180 if constexpr (requires { First::TableName; })
181 return First::TableName;
182 else
183 return []() {
184#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
185 return std::meta::identifier_of(^^First);
186#else
187 auto const typeName = Reflection::TypeNameOf<First>;
188 if (auto const i = typeName.rfind(':'); i != std::string_view::npos)
189 return typeName.substr(i + 1);
190 return typeName;
191#endif
192 }();
193 }();
194 };
195
196 template <typename FieldType>
197 constexpr auto ColumnNameOverride = []() consteval {
198 if constexpr (requires { FieldType::ColumnNameOverride; })
199 return FieldType::ColumnNameOverride;
200 else
201 return std::string_view {};
202 }();
203#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
204 template <auto reflection>
205 struct FieldNameOfImpl
206 {
207 using R = typename[:std::meta::type_of(reflection):];
208 static constexpr std::string_view value = []() constexpr -> std::string_view {
209 if constexpr (requires { R::ColumnNameOverride; })
210 {
211 if constexpr (!R::ColumnNameOverride.empty())
212 return R::ColumnNameOverride;
213 }
214 return std::meta::identifier_of(reflection);
215 }();
216 };
217#else
218 template <typename ReferencedFieldType, auto F>
219 struct FieldNameOfImpl;
220
221 template <typename T, auto F, typename R>
222 struct FieldNameOfImpl<R T::*, F>
223 {
224 static constexpr std::string_view value = []() constexpr -> std::string_view {
225 if constexpr (requires { R::ColumnNameOverride; })
226 {
227 if constexpr (!R::ColumnNameOverride.empty())
228 return R::ColumnNameOverride;
229 }
230 return Reflection::NameOf<F>;
231 }();
232 };
233#endif // LIGHTWEIGHT_CXX26_REFLECTION
234
235 template <std::size_t I, typename Record>
236 consteval std::string_view FieldNameAt()
237 {
238 // Prefer the pre-baked SQL column name from a generated descriptor (avoids the
239 // expensive MangledName-based MemberNameOf evaluation); fall back to reflection.
240 if constexpr (HasDescription<Record>)
241 {
242 return Description<Record>::FieldNames[I];
243 }
244 else
245 {
246 using FieldType = Reflection::MemberTypeOf<I, Record>;
247
248 if constexpr (!std::string_view(ColumnNameOverride<FieldType>).empty())
249 {
250 return FieldType::ColumnNameOverride;
251 }
252 return Reflection::MemberNameOf<I, Record>;
253 }
254 }
255} // namespace detail
256
257/// @brief Returns the SQL field name of the given field index in the record.
258///
259/// @ingroup DataMapper
260template <std::size_t I, typename Record>
261constexpr inline std::string_view FieldNameAt = detail::FieldNameAt<I, Record>();
262
263/// @brief Holds the SQL tabl ename for the given record type.
264///
265/// @ingroup DataMapper
266template <typename Record>
267constexpr std::string_view RecordTableName = detail::RecordTableNameImpl<Record>::Value;
268
269template <template <typename...> class S, class T>
270concept IsSpecializationOf = detail::is_specialization_of<S, T>::value;
271
272#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
273
274/// @brief Returns the name of the field referenced by the given pointer-to-member.
275///
276/// This also supports custom column name overrides.
277template <std::meta::info ReflectionOfField>
278constexpr inline std::string_view FieldNameOf = detail::FieldNameOfImpl<ReflectionOfField>::value;
279
280template <auto Member>
281using MemberClassType = typename[:std::meta::parent_of(Member):];
282
283template <auto Member>
284constexpr size_t MemberIndexOf = []() consteval -> size_t {
285 int index { -1 };
286 auto members = nonstatic_data_members_of(parent_of(Member), std::meta::access_context::current());
287 if (auto it = std::ranges::find(members, Member); it != members.end())
288 {
289 index = std::distance(members.begin(), it);
290 return static_cast<size_t>(index);
291 }
292 return -1;
293}();
294#else // not LIGHTWEIGHT_CXX26_REFLECTION
295
296/// @brief Returns the name of the field referenced by the given pointer-to-member.
297///
298/// This also supports custom column name overrides.
299template <auto ReferencedField>
300constexpr inline std::string_view FieldNameOf = detail::FieldNameOfImpl<decltype(ReferencedField), ReferencedField>::value;
301
302template <auto Member>
303constexpr size_t MemberIndexOf = Reflection::MemberIndexOf<Member>;
304
305template <typename T>
306using MemberClassType = detail::MemberClassTypeHelper<T>::type;
307
308#endif // LIGHTWEIGHT_CXX26_REFLECTION
309
310/// @brief SqlQualifiedTableColumnName represents a column name qualified with a table name.
311///
312/// This is the single structural representation of a `table.column` reference used
313/// throughout the query builder API. The builder is responsible for quoting; do not
314/// pre-quote the values stored here.
315///
316/// @ingroup QueryBuilder
318{
319 /// The table name.
320 std::string_view tableName;
321 /// The column name.
322 std::string_view columnName;
323
324 /// Three-way comparison operator.
325 constexpr std::weak_ordering operator<=>(SqlQualifiedTableColumnName const&) const noexcept = default;
326};
327
328/// @brief Holds the fully qualified column reference (table + column) for the given field.
329/// @tparam ReferencedField A pointer-to-member identifying the field.
330///
331/// @code
332/// constexpr auto ref = FullyQualifiedNameOf<&Person::id>;
333/// static_assert(ref.tableName == "Person");
334/// static_assert(ref.columnName == "id");
335/// @endcode
336///
337/// The result is an `SqlQualifiedTableColumnName` accepted by every column-name
338/// entry point in the builder (`Field`, `Fields`, `Where`, `OrderBy`, `GroupBy`,
339/// `Aggregate::*`, joins). The builder applies the quoting.
340///
341/// @ingroup DataMapper
342template <auto ReferencedField>
344#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
345 .tableName = RecordTableName<typename[:std::meta::parent_of(ReferencedField):]>,
346#else
347 .tableName = RecordTableName<MemberClassType<decltype(ReferencedField)>>,
348#endif
349 .columnName = FieldNameOf<ReferencedField>,
350};
351
352namespace detail
353{
354 template <auto ReferencedField>
355 struct FullyQualifiedQuotedNameOfImpl
356 {
357#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
358 static constexpr auto ClassName = RecordTableName<typename[:std::meta::parent_of(ReferencedField):]>;
359#else
360 static constexpr auto ClassName = RecordTableName<MemberClassType<decltype(ReferencedField)>>;
361#endif
362 static constexpr auto FieldName = FieldNameOf<ReferencedField>;
363 static constexpr auto StorageSize = ClassName.size() + FieldName.size() + 6;
364
365 // Holds the full field name in the format "ClassName"."FieldName"
366 static constexpr auto Storage = []() constexpr -> std::array<char, StorageSize> {
367 // clang-format off
368 auto storage = std::array<char, StorageSize> {};
369 std::ranges::copy("\"", storage.begin());
370 std::ranges::copy(ClassName, storage.begin() + 1);
371 std::ranges::copy("\".\"", storage.begin() + 1 + ClassName.size());
372 std::ranges::copy(FieldName, storage.begin() + 1 + ClassName.size() + 3);
373 std::ranges::copy("\"", storage.begin() + 1 + ClassName.size() + 3 + FieldName.size());
374 storage.back() = '\0';
375 // clang-format on
376 return storage;
377 }();
378 static constexpr auto value = std::string_view(Storage.data(), Storage.size() - 1);
379 };
380
381 template <auto ReferencedField>
382 constexpr inline auto FullyQualifiedQuotedNameOf = FullyQualifiedQuotedNameOfImpl<ReferencedField>::value;
383
384 template <auto... ReferencedFields>
385 struct FullyQualifiedNamesOfImpl
386 {
387 static constexpr auto StorageSize =
388 1 + (2 * (sizeof...(ReferencedFields) - 1)) + (0 + ... + FullyQualifiedQuotedNameOf<ReferencedFields>.size());
389
390 static constexpr std::array<char, StorageSize> Storage = []() consteval {
391 auto result = std::array<char, StorageSize> {};
392 size_t offset = 0;
393 (
394 [&] {
395 if (offset > 0)
396 {
397 constexpr auto Delimiter = std::string_view(", ");
398 std::ranges::copy(Delimiter, result.begin() + offset);
399 offset += Delimiter.size();
400 }
401 std::ranges::copy(FullyQualifiedQuotedNameOf<ReferencedFields>, result.begin() + offset);
402 offset += FullyQualifiedQuotedNameOf<ReferencedFields>.size();
403 }(),
404 ...);
405 result.back() = '\0';
406 return result;
407 }();
408
409 static constexpr auto value = std::string_view(Storage.data(), Storage.size() - 1);
410 };
411
412 /// Pre-quoted, comma-joined fully qualified field names of the given fields.
413 /// Internal helper used by DataMapper to embed column lists into SQL text directly.
414 template <auto... ReferencedFields>
415 constexpr inline auto FullyQualifiedNamesOf = FullyQualifiedNamesOfImpl<ReferencedFields...>::value;
416
417} // namespace detail
418
419LIGHTWEIGHT_API void LogIfFailed(SQLHSTMT hStmt, SQLRETURN error, std::source_location sourceLocation);
420
421LIGHTWEIGHT_API void RequireSuccess(SQLHSTMT hStmt,
422 SQLRETURN error,
423 std::source_location sourceLocation = std::source_location::current());
424
425/// Defines the naming convention for use (e.g. for C++ column names or table names in C++ struct names).
426enum class FormatType : uint8_t
427{
428 /// Preserve the original naming convention.
429 preserve,
430
431 /// Ensure the name is formatted in snake_case naming convention.
432 snakeCase,
433
434 /// Ensure the name is formatted in CamelCase naming convention.
435 camelCase,
436};
437
438/// @brief Converts a string to a format that is more suitable for C++ code.
439LIGHTWEIGHT_API std::string FormatName(std::string const& name, FormatType formatType);
440
441/// @brief Converts a string to a format that is more suitable for C++ code.
442LIGHTWEIGHT_API std::string FormatName(std::string_view name, FormatType formatType);
443
444/// Maintains collisions to create unique names
446{
447 public:
448 /// Tests if the given name is already registered.
449 [[nodiscard]] LIGHTWEIGHT_API bool IsColliding(std::string const& name) const noexcept;
450
451 /// Tries to declare a name and returns it, otherwise returns std::nullopt.
452 [[nodiscard]] LIGHTWEIGHT_API std::optional<std::string> TryDeclareName(std::string name);
453
454 /// Creates a name that is definitely not colliding.
455 [[nodiscard]] LIGHTWEIGHT_API std::string DeclareName(std::string name);
456
457 private:
458 std::unordered_map<std::string, size_t> _collisionMap;
459};
460
461} // namespace Lightweight
Maintains collisions to create unique names.
Definition Utils.hpp:446
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:267
constexpr std::string_view FieldNameAt
Returns the SQL field name of the given field index in the record.
Definition Utils.hpp:261
constexpr auto FullyQualifiedNameOf
Holds the fully qualified column reference (table + column) for the given field.
Definition Utils.hpp:343
SqlQualifiedTableColumnName represents a column name qualified with a table name.
Definition Utils.hpp:318
std::string_view tableName
The table name.
Definition Utils.hpp:320
constexpr std::weak_ordering operator<=>(SqlQualifiedTableColumnName const &) const noexcept=default
Three-way comparison operator.
std::string_view columnName
The column name.
Definition Utils.hpp:322