Lightweight 0.20261105.1
Loading...
Searching...
No Matches
SqlGuid.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "../SqlColumnTypeDefinitions.hpp"
6#include "Core.hpp"
7
8#include <algorithm>
9#include <array>
10#include <bit>
11#include <charconv>
12#include <format>
13#include <optional>
14#include <ranges>
15#include <span>
16#include <string>
17
18namespace Lightweight
19{
20
21/// Represents a GUID (Globally Unique Identifier).
22///
23/// @ingroup DataTypes
24struct LIGHTWEIGHT_API SqlGuid
25{
26 /// The raw GUID data bytes.
27 uint8_t data[16] {};
28
29 /// Creates a new non-empty GUID.
30 static SqlGuid Create() noexcept;
31
32 /// Parses a GUID from its 36-character `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` text form.
33 ///
34 /// Any hex value is accepted, whatever its version and variant nibbles, as SQL Server's
35 /// `uniqueidentifier` does. `data[i]` receives the i-th hex pair of the text, left to right.
36 ///
37 /// @param text The GUID text, upper or lower case.
38 /// @return The parsed GUID, or `std::nullopt` if @p text is not a well-formed GUID.
39 static std::optional<SqlGuid> TryParse(std::string_view const& text) noexcept;
40
41 /// Parses a GUID from a string. Use with caution and always prefer TryParse at all cost.
42 ///
43 /// Accepts the same texts as TryParse(); for any other text it returns an unspecified sentinel.
44 /// @param text The GUID text.
45 /// @return The parsed GUID.
46 static SqlGuid constexpr UnsafeParse(std::string_view const& text) noexcept;
47
48 /// Three-way comparison operator.
49 constexpr std::weak_ordering operator<=>(SqlGuid const& other) const noexcept = default;
50
51 /// Equality comparison operator.
52 constexpr bool operator==(SqlGuid const& other) const noexcept
53 {
54 return (*this <=> other) == std::weak_ordering::equivalent;
55 }
56
57 /// Inequality comparison operator.
58 constexpr bool operator!=(SqlGuid const& other) const noexcept
59 {
60 return !(*this == other);
61 }
62
63 /// Tests if the GUID is non-empty.
64 constexpr explicit operator bool() const noexcept
65 {
66 return *this != SqlGuid {};
67 }
68
69 /// Tests if the GUID is empty.
70 constexpr bool operator!() const noexcept
71 {
72 return !static_cast<bool>(*this);
73 }
74};
75
76namespace detail
77{
78 /// Tests whether @p character is a hexadecimal digit.
79 /// @param character The character to test.
80 /// @return `true` for `0-9`, `a-f` and `A-F`.
81 constexpr bool IsHexDigit(char character) noexcept
82 {
83 return (character >= '0' && character <= '9') || (character >= 'a' && character <= 'f')
84 || (character >= 'A' && character <= 'F');
85 }
86
87 /// Tests whether @p text has the 8-4-4-4-12 hex-digit form of a GUID.
88 /// @param text The text to test.
89 /// @return `true` if @p text is 36 characters, dashes at 8, 13, 18 and 23, hex digits elsewhere.
90 constexpr bool IsWellFormedGuidText(std::string_view text) noexcept
91 {
92 constexpr auto IsDashPosition = [](size_t position) {
93 return position == 8 || position == 13 || position == 18 || position == 23;
94 };
95 if (text.size() != 36)
96 return false;
97 return std::ranges::all_of(std::views::iota(size_t { 0 }, text.size()), [&](size_t position) {
98 return IsDashPosition(position) ? text[position] == '-' : IsHexDigit(text[position]);
99 });
100 }
101
102 /// Returns the value of the hex digit @p character, which must satisfy IsHexDigit().
103 /// @param character The hex digit.
104 /// @return Its value, 0 to 15.
105 constexpr uint8_t HexDigitValue(char character) noexcept
106 {
107 if (character >= '0' && character <= '9')
108 return static_cast<uint8_t>(character - '0');
109 if (character >= 'a' && character <= 'f')
110 return static_cast<uint8_t>(character - 'a' + 10);
111 return static_cast<uint8_t>(character - 'A' + 10);
112 }
113} // namespace detail
114
115constexpr SqlGuid SqlGuid::UnsafeParse(std::string_view const& text) noexcept
116{
117 // Format: xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx. The version (M) and variant (N) nibbles are
118 // not checked: SQL Server stores any value, and older Lightweight builds wrote GUIDs with
119 // byte-swapped Data1, Data2 and Data3, which leaves an arbitrary digit where the version sits.
120 if (!detail::IsWellFormedGuidText(text))
121 return { "\x01" };
122
123 SqlGuid guid {};
124 // clang-format off
125 size_t i = 0;
126 for (size_t const index: std::array<size_t, 16> { 0, 2, 4, 6,
127 9, 11,
128 14, 16,
129 19, 21,
130 24, 26, 28, 30, 32, 34 })
131 {
132 guid.data[i] = static_cast<uint8_t>((detail::HexDigitValue(text[index]) << 4)
133 | detail::HexDigitValue(text[index + 1]));
134 i++;
135 }
136 // clang-format on
137
138 return guid;
139}
140
141} // namespace Lightweight
142
143template <>
144struct std::formatter<Lightweight::SqlGuid>: std::formatter<std::string>
145{
146 LIGHTWEIGHT_FORCE_INLINE auto format(Lightweight::SqlGuid const& guid, format_context& ctx) const
147 -> format_context::iterator
148 {
149 // clang-format off
150 return formatter<std::string>::format(std::format(
151 "{:08X}-{:04X}-{:04X}-{:04X}-{:012X}",
152 (uint32_t) guid.data[3] | (uint32_t) guid.data[2] << 8 |
153 (uint32_t) guid.data[1] << 16 | (uint32_t) guid.data[0] << 24,
154 (uint16_t) guid.data[5] | (uint16_t) guid.data[4] << 8,
155 (uint16_t) guid.data[7] | (uint16_t) guid.data[6] << 8,
156 (uint16_t) guid.data[9] | (uint16_t) guid.data[8] << 8,
157 (uint64_t) guid.data[15] | (uint64_t) guid.data[14] << 8 |
158 (uint64_t) guid.data[13] << 16 | (uint64_t) guid.data[12] << 24 |
159 (uint64_t) guid.data[11] << 32 | (uint64_t) guid.data[10] << 40
160 ),
161 ctx
162 );
163 // clang-format on
164 }
165};
166
167namespace Lightweight
168{
169
170inline LIGHTWEIGHT_FORCE_INLINE std::string to_string(SqlGuid const& guid)
171{
172 return std::format("{}", guid);
173}
174
175namespace detail
176{
177 /// Swaps `data[0..7]` between `SqlGuid`'s canonical byte order (the order `TryParse()`,
178 /// `UnsafeParse()`, and the `std::formatter` specialization agree on: `data[i]` is the i-th
179 /// hex-pair of the textual representation, left to right) and the physical `SQL_C_GUID` /
180 /// Win32 `GUID` wire layout that ODBC drivers expect for `Data1`/`Data2`/`Data3` (those three
181 /// fields are transmitted in native/little-endian byte order, not textual order). `data[8..15]`
182 /// (`Data4`) is a plain byte array in both representations and is left untouched.
183 ///
184 /// Self-inverse: applying this twice restores the original bytes. Every call site that binds
185 /// or fetches raw `SQL_C_GUID` bytes (bypassing `SqlDataBinder<SqlGuid>`'s own conversion) must
186 /// apply this exactly once when crossing that boundary, in either direction.
187 ///
188 /// @param data The 16-byte GUID payload to convert in place.
189 inline void SwapGuidWireByteOrder(std::span<uint8_t, 16> data) noexcept
190 {
191 // The reversal below undoes exactly the byte-order change a little-endian host applies
192 // when it stores Data1/Data2/Data3 as native integers; on a big-endian host those fields
193 // are already in textual order and reversing them would corrupt the GUID instead. Assume
194 // little-endian, matching the project-wide precedent in SqlNumeric.hpp.
195 static_assert(std::endian::native == std::endian::little);
196
197 // Data1 (4 bytes), Data2 (2) and Data3 (2) differ between the two representations;
198 // Data4 (the trailing 8) is a plain byte array in both and is left untouched.
199 std::ranges::reverse(data.first<4>());
200 std::ranges::reverse(data.subspan<4, 2>());
201 std::ranges::reverse(data.subspan<6, 2>());
202 }
203} // namespace detail
204
205template <>
206struct LIGHTWEIGHT_API SqlDataBinder<SqlGuid>
207{
208 static constexpr auto ColumnType = SqlColumnTypeDefinitions::Guid {};
209
210 static SQLRETURN InputParameter(SQLHSTMT stmt,
211 SQLUSMALLINT column,
212 SqlGuid const& value,
213 SqlDataBinderCallback& cb) noexcept;
214
215 static SQLRETURN OutputColumn(
216 SQLHSTMT stmt, SQLUSMALLINT column, SqlGuid* result, SQLLEN* indicator, SqlDataBinderCallback& cb) noexcept;
217
218 static SQLRETURN GetColumn(
219 SQLHSTMT stmt, SQLUSMALLINT column, SqlGuid* result, SQLLEN* indicator, SqlDataBinderCallback const& cb) noexcept;
220
221 static LIGHTWEIGHT_FORCE_INLINE std::string Inspect(SqlGuid const& value) noexcept
222 {
223 return std::format("{}", value);
224 }
225};
226
227} // namespace Lightweight
constexpr bool operator!=(SqlGuid const &other) const noexcept
Inequality comparison operator.
Definition SqlGuid.hpp:58
static SqlGuid constexpr UnsafeParse(std::string_view const &text) noexcept
Definition SqlGuid.hpp:115
constexpr bool operator!() const noexcept
Tests if the GUID is empty.
Definition SqlGuid.hpp:70
static SqlGuid Create() noexcept
Creates a new non-empty GUID.
uint8_t data[16]
The raw GUID data bytes.
Definition SqlGuid.hpp:27