Lightweight 0.20260625.0
Loading...
Searching...
No Matches
SqlNumeric.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "../SqlColumnTypeDefinitions.hpp"
6#include "../SqlError.hpp"
7#include "Int128.hpp"
8#include "Primitives.hpp"
9
10#include <bit>
11#include <cmath>
12#include <compare>
13#include <concepts>
14#include <cstddef>
15#include <cstring>
16#include <format>
17#include <source_location>
18#include <string>
19
20namespace Lightweight
21{
22
23static_assert(sizeof(Int128) == sizeof(SQL_NUMERIC_STRUCT::val));
24
25namespace detail
26{
27
28 /// Number of whole decimal digits that fit into a binary magnitude of `bits` bits.
29 ///
30 /// `floor(bits * log10(2))`, evaluated with integer arithmetic (`log10(2) ~= 0.30103`) so that
31 /// it stays usable in a constant expression without pulling in `<cmath>` at compile time.
32 ///
33 /// @param bits Width of the binary magnitude, excluding any sign bit.
34 /// @return Number of decimal digits that magnitude represents without loss.
35 [[nodiscard]] constexpr std::size_t DecimalDigitsForBits(std::size_t bits) noexcept
36 {
37 return (bits * 30103) / 100'000;
38 }
39
40} // namespace detail
41
42/// Widest `Precision` a `SqlNumeric` may declare, in decimal digits.
43///
44/// **This is the same value on every toolchain.** A column's precision is a property of the
45/// database schema, so the type that maps it must not depend on which compiler builds the client:
46/// a `ddl2cpp`-generated record has to compile everywhere or the generator is useless. `Int128`
47/// exists to make that true — it is `__int128_t` where the compiler has one and a software
48/// stand-in (`detail::Int128Soft`) where it does not, so the unscaled carrier is 128 bits wide
49/// under MSVC and clang-cl as well.
50///
51/// The bound is deliberately *not* derived from `SQL_MAX_NUMERIC_LEN`. That macro is the size **in
52/// bytes** of `SQL_NUMERIC_STRUCT::val` (16, i.e. a 128-bit mantissa), not a decimal precision;
53/// comparing a digit count against it is a category error that rejects perfectly ordinary columns
54/// such as `DECIMAL(18, 2)`.
55///
56/// Bounding by the mantissa's theoretical capacity instead (128 bits -> 38 digits) is the same
57/// category error in the other direction: 38 is what the ODBC *struct* can hold, not what this
58/// implementation can read back. What narrows it is the **readable width**: every accessor except
59/// `ToUnscaledValue()` and `ToString()` — that is, `ToFloat`, `ToDouble` and `ToLongDouble` —
60/// divides through `long double`, so no more digits can be read back through those than that
61/// type's significand holds. The widest one in use is the 80-bit x87 `long double`, whose 64-bit
62/// significand gives `DecimalDigitsForBits(64)` == 19; beyond that nothing is readable on *any*
63/// platform, e.g. a fetched `SqlNumeric<20, 0>` holding 99999999999999999999 reads back as
64/// 100000000000000000000.
65///
66/// Hence 19, everywhere. `DECIMAL(18, s)` and MS SQL Server's `money` (`DECIMAL(19, 4)`) both
67/// compile on every supported toolchain; a wider column must be read as a string.
68///
69/// NB: 19 is the point past which nothing is readable *anywhere*. It is emphatically not a promise
70/// that any given platform delivers 19 digits through the *floating-point* accessors: those are
71/// bounded by the width of `long double`, which is 53 bits on MSVC and on Clang for Apple Silicon.
72/// The width they guarantee everywhere is `std::numeric_limits<double>::digits10`. Above that,
73/// `ToUnscaledValue()` and `ToString()` are the accessors to rely on: neither divides through a
74/// floating-point type, so both carry all 19 digits on every toolchain.
75/// `docs/data-binder.md` tabulates what each accessor delivers.
76///
77/// NB: `inline` is load-bearing. At namespace scope `constexpr` implies `const`, hence internal
78/// linkage, and an exported template in the module interface (`SqlNumeric`, via its static_assert)
79/// may not reference an internal-linkage entity.
80inline constexpr std::size_t SqlMaxNumericPrecision =
81 detail::DecimalDigitsForBits(64); // 19 — bounded by the `long double` significand every accessor divides through
82
83/// Represents a fixed-point number with a given precision and scale.
84///
85/// Precision is *exactly* the total number of digits in the number,
86/// including the digits after the decimal point.
87///
88/// Scale is the number of digits after the decimal point, and may be anywhere in `[0, Precision]`.
89/// `Scale == Precision` denotes a purely fractional number, e.g. `SqlNumeric<4, 4>` covers
90/// `[0.0000, 0.9999]` — the C++ equivalent of SQL's `DECIMAL(4, 4)`.
91///
92/// @ingroup DataTypes
93template <std::size_t ThePrecision, std::size_t TheScale>
95{
96 /// Number of total digits
97 static constexpr auto Precision = ThePrecision;
98
99 /// Number of digits after the decimal point
100 static constexpr auto Scale = TheScale;
101
102 /// The SQL column type definition for this numeric type.
103 static constexpr auto ColumnType = SqlColumnTypeDefinitions::Decimal { .precision = Precision, .scale = TheScale };
104
105 static_assert(Precision > 0, "A fixed-point number must have at least one digit.");
106 // NB: This bound is 19 on every toolchain, so `SqlNumeric<19, 4>` — what ddl2cpp emits for MS
107 // SQL Server's `money` — compiles everywhere. It is the same value under MSVC and clang-cl
108 // because `Int128` supplies a software 128-bit carrier where the compiler has no native one;
109 // see SqlMaxNumericPrecision for the derivation.
110 static_assert(Precision <= SqlMaxNumericPrecision,
111 "Precision exceeds the number of decimal digits this implementation can carry. Read the column as a "
112 "string instead, or narrow the column.");
113 // `DECIMAL(p, s)` requires 0 <= s <= p; `s == p` denotes a purely fractional number (e.g.
114 // DECIMAL(4, 4) holds [0, 1) with four fractional digits) and is legal in every supported
115 // backend. No conversion path here needs an integral digit: the value is kept as the unscaled
116 // integer `value * 10^Scale`, and every accessor divides that by `10^Scale` again.
117 static_assert(Scale <= Precision, "Scale counts digits after the decimal point and cannot exceed Precision.");
118
119 /// The SQL numeric struct for ODBC binding.
120 SQL_NUMERIC_STRUCT sqlValue {};
121
122 /// Cached native value for drivers without SQL_NUMERIC_STRUCT support.
123 double nativeValue {};
124
125 /// Default constructor.
126 constexpr SqlNumeric() noexcept = default;
127 /// Move constructor.
128 constexpr SqlNumeric(SqlNumeric&&) noexcept = default;
129 /// Move assignment operator.
130 constexpr SqlNumeric& operator=(SqlNumeric&&) noexcept = default;
131 /// Copy constructor.
132 constexpr SqlNumeric(SqlNumeric const&) noexcept = default;
133 /// Copy assignment operator.
134 constexpr SqlNumeric& operator=(SqlNumeric const&) noexcept = default;
135 constexpr ~SqlNumeric() noexcept = default;
136
137 /// Constructs a numeric from a floating point value.
138 constexpr SqlNumeric(std::floating_point auto value) noexcept
139 {
140 assign(value);
141 }
142
143 /// Constructs a numeric from a SQL_NUMERIC_STRUCT.
144 constexpr explicit SqlNumeric(SQL_NUMERIC_STRUCT const& value) noexcept:
145 sqlValue(value)
146 {
147 }
148
149 // For encoding/decoding purposes, we assume little-endian.
150 static_assert(std::endian::native == std::endian::little);
151
152 /// Assigns a value to the numeric.
153 LIGHTWEIGHT_FORCE_INLINE constexpr void assign(std::floating_point auto inputValue) noexcept
154 {
155 nativeValue = static_cast<decltype(nativeValue)>(inputValue);
156
157 sqlValue = {};
158 sqlValue.sign = std::signbit(inputValue) ? 0 : 1;
159 sqlValue.precision = static_cast<SQLCHAR>(Precision);
160 sqlValue.scale = static_cast<SQLSCHAR>(Scale);
161
162 auto const unscaledValue = std::roundl(static_cast<long double>(std::abs(inputValue) * std::powl(10.0L, Scale)));
163
164 // `Int128` is 128 bits wide on every toolchain, so the unscaled value of even the widest
165 // declarable precision fits, and the conversion stays in range. `sqlValue.val` is
166 // little-endian and exactly this wide (asserted above).
167 auto const num = static_cast<Int128>(unscaledValue);
168 std::memcpy(sqlValue.val, &num, sizeof(num));
169 }
170
171 /// Assigns a floating point value to the numeric.
172 LIGHTWEIGHT_FORCE_INLINE constexpr SqlNumeric& operator=(std::floating_point auto value) noexcept
173 {
174 assign(value);
175 return *this;
176 }
177
178 /// Converts the numeric to an unscaled integer value.
179 ///
180 /// Along with `ToString()`, which renders from this value, it is one of the two accessors that
181 /// do not divide through a floating-point type, and therefore carries every digit up to
182 /// `SqlMaxNumericPrecision` on every toolchain. The floating-point accessors do not.
183 ///
184 /// @return `value * 10^Scale` as a signed 128-bit integer.
185 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE Int128 ToUnscaledValue() const noexcept
186 {
187 if (nativeValue != 0.0)
188 return static_cast<Int128>(std::roundl(nativeValue * std::powl(10.0L, Scale)));
189
190 // `sqlValue.val` sits at offset 3 of a 1-aligned struct, so it cannot be dereferenced
191 // through a 128-bit pointer without a misaligned load; copy it out instead. Both `Int128`
192 // implementations are little-endian 16-byte two's complement, matching the field's layout.
193 auto magnitude = Int128 {};
194 std::memcpy(&magnitude, sqlValue.val, sizeof(magnitude));
195
196 return sqlValue.sign ? magnitude : -magnitude;
197 }
198
199 /// Converts the numeric to a floating point value.
200 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE float ToFloat() const noexcept
201 {
202 return static_cast<float>(ToUnscaledValue()) / std::powf(10, sqlValue.scale);
203 }
204
205 /// Converts the numeric to a double precision floating point value.
206 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE double ToDouble() const noexcept
207 {
208 return static_cast<double>(ToUnscaledValue()) / std::pow(10, sqlValue.scale);
209 }
210
211 /// Converts the numeric to a long double precision floating point value.
212 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE long double ToLongDouble() const noexcept
213 {
214 return static_cast<long double>(ToUnscaledValue()) / std::pow(10, sqlValue.scale);
215 }
216
217 /// Converts the numeric to a floating point value.
218 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE explicit operator float() const noexcept
219 {
220 return ToFloat();
221 }
222
223 /// Converts the numeric to a double precision floating point value.
224 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE explicit operator double() const noexcept
225 {
226 return ToDouble();
227 }
228
229 /// Converts the numeric to a long double precision floating point value.
230 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE explicit operator long double() const noexcept
231 {
232 return ToLongDouble();
233 }
234
235 /// Converts the numeric to a string, exactly.
236 ///
237 /// Rendered from the unscaled integer rather than by formatting `ToLongDouble()`, so every digit
238 /// the carrier holds survives on every toolchain. Formatting through `long double` would drop
239 /// the low digits wherever that type is narrow (53 bits on MSVC and on Clang for Apple Silicon)
240 /// or wherever the standard library's formatter narrows to `double` regardless.
241 ///
242 /// @return The value in plain decimal notation with exactly `Scale` fractional digits.
243 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE std::string ToString() const
244 {
245 auto const unscaled = ToUnscaledValue();
246 auto digits = detail::Int128ToString(unscaled);
247
248 auto const negative = !digits.empty() && digits.front() == '-';
249 if (negative)
250 digits.erase(digits.begin());
251
252 if constexpr (Scale == 0)
253 return negative ? "-" + digits : digits;
254
255 // Left-pad so there is at least one integral digit to the left of the point, which is what a
256 // purely fractional type (Scale == Precision, e.g. DECIMAL(4, 4)) always needs.
257 if (digits.size() <= Scale)
258 digits.insert(digits.begin(), Scale + 1 - digits.size(), '0');
259
260 digits.insert(digits.size() - Scale, 1, '.');
261
262 return negative ? "-" + digits : digits;
263 }
264
265 /// Three-way comparison operator.
266 ///
267 /// Comparing two `SqlNumeric` values yields `std::partial_ordering` because the
268 /// underlying conversion to `double` admits NaN inputs. In practice every value
269 /// produced by this type is finite, so the result is totally ordered.
270 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE std::partial_ordering operator<=>(SqlNumeric const& other) const noexcept
271 {
272 return ToDouble() <=> other.ToDouble();
273 }
274
275 /// Equality comparison operator.
276 template <std::size_t OtherPrecision, std::size_t OtherScale>
277 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE bool operator==(
278 SqlNumeric<OtherPrecision, OtherScale> const& other) const noexcept
279 {
280 return ToFloat() == other.ToFloat();
281 }
282};
283
284template <typename T>
285concept SqlNumericType = requires(T t) {
286 { T::Precision } -> std::convertible_to<std::size_t>;
287 { T::Scale } -> std::convertible_to<std::size_t>;
288} && std::same_as<T, SqlNumeric<T::Precision, T::Scale>>;
289
290// clang-format off
291template <std::size_t Precision, std::size_t Scale>
292struct SqlDataBinder<SqlNumeric<Precision, Scale>>
293{
294 using ValueType = SqlNumeric<Precision, Scale>;
295
296 static constexpr auto ColumnType = SqlColumnTypeDefinitions::Decimal { .precision = Precision, .scale = Scale };
297
298 static void RequireSuccess(SQLHSTMT stmt, SQLRETURN error, std::source_location sourceLocation = std::source_location::current())
299 {
300 if (SQL_SUCCEEDED(error))
301 return;
302
303 throw SqlException(SqlErrorInfo::FromStatementHandle(stmt), sourceLocation);
304 }
305
306 static constexpr bool NativeNumericSupportIsBroken(SqlServerType serverType) noexcept
307 {
308 // SQLite's ODBC driver does not support SQL_NUMERIC_STRUCT (it's all just floating point numbers).
309 // Microsoft SQL Server's ODBC driver also has issues (keeps reporting out of bound, on Linux at least).
310 return serverType == SqlServerType::SQLITE || serverType == SqlServerType::MICROSOFT_SQL;
311 }
312
313 static LIGHTWEIGHT_FORCE_INLINE SQLRETURN InputParameter(SQLHSTMT stmt,
314 SQLUSMALLINT column,
315 ValueType const& value,
316 SqlDataBinderCallback& cb) noexcept
317 {
318 if (NativeNumericSupportIsBroken(cb.ServerType()))
319 {
320 return SQLBindParameter(stmt,
321 column,
322 SQL_PARAM_INPUT,
323 SQL_C_DOUBLE,
324 SQL_DOUBLE,
325 0,
326 0,
327 (SQLPOINTER) &value.nativeValue,
328 sizeof(value.nativeValue),
329 nullptr);
330 }
331
332 // Bind with the type's compile-time Precision/Scale rather than value.sqlValue.precision/scale:
333 // the latter is 0 for a default-constructed (never-assigned) value. On the native row-wise batch
334 // path a single bind descriptor (taken from row 0) governs the whole array, so a default-constructed
335 // row 0 would otherwise mis-bind every row. The template constants are correct for every value of
336 // SqlNumeric<Precision, Scale> by definition (assign() always sets these same values).
337 return SQLBindParameter(stmt,
338 column,
339 SQL_PARAM_INPUT,
340 SQL_C_NUMERIC,
341 SQL_NUMERIC,
342 static_cast<SQLULEN>(Precision),
343 static_cast<SQLSMALLINT>(Scale),
344 (SQLPOINTER) &value,
345 sizeof(value),
346 nullptr);
347 }
348
349
350 static LIGHTWEIGHT_FORCE_INLINE SQLRETURN OutputColumn(
351 SQLHSTMT stmt, SQLUSMALLINT column, ValueType* result, SQLLEN* indicator, SqlDataBinderCallback& cb) noexcept
352 {
353 if (NativeNumericSupportIsBroken(cb.ServerType()))
354 {
355 result->sqlValue = { .precision = Precision, .scale = Scale, .sign = 0, .val = {} };
356 return SQLBindCol(stmt, column, SQL_C_DOUBLE, &result->nativeValue, sizeof(result->nativeValue), indicator);
357 }
358
359 SQLHDESC hDesc {};
360 RequireSuccess(stmt, SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, (SQLPOINTER) &hDesc, 0, nullptr));
361 RequireSuccess(stmt, SQLSetDescField(hDesc, (SQLSMALLINT) column, SQL_DESC_PRECISION, (SQLPOINTER) Precision, 0)); // NOLINT(performance-no-int-to-ptr)
362 RequireSuccess(stmt, SQLSetDescField(hDesc, (SQLSMALLINT) column, SQL_DESC_SCALE, (SQLPOINTER) Scale, 0)); // NOLINT(performance-no-int-to-ptr)
363
364 return SQLBindCol(stmt, column, SQL_C_NUMERIC, &result->sqlValue, sizeof(ValueType), indicator);
365 }
366
367 static LIGHTWEIGHT_FORCE_INLINE SQLRETURN GetColumn(SQLHSTMT stmt, SQLUSMALLINT column, ValueType* result, SQLLEN* indicator, SqlDataBinderCallback const& cb) noexcept
368 {
369 if (NativeNumericSupportIsBroken(cb.ServerType()))
370 {
371 result->sqlValue = { .precision = Precision, .scale = Scale, .sign = 0, .val = {} };
372 return SQLGetData(stmt, column, SQL_C_DOUBLE, &result->nativeValue, sizeof(result->nativeValue), indicator);
373 }
374
375 SQLHDESC hDesc {};
376 RequireSuccess(stmt, SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, (SQLPOINTER) &hDesc, 0, nullptr));
377 RequireSuccess(stmt, SQLSetDescField(hDesc, (SQLSMALLINT) column, SQL_DESC_PRECISION, (SQLPOINTER) Precision, 0)); // NOLINT(performance-no-int-to-ptr)
378 RequireSuccess(stmt, SQLSetDescField(hDesc, (SQLSMALLINT) column, SQL_DESC_SCALE, (SQLPOINTER) Scale, 0)); // NOLINT(performance-no-int-to-ptr)
379
380 return SQLGetData(stmt, column, SQL_C_NUMERIC, &result->sqlValue, sizeof(ValueType), indicator);
381 }
382
383 static LIGHTWEIGHT_FORCE_INLINE std::string Inspect(ValueType const& value) noexcept
384 {
385 return value.ToString();
386 }
387};
388
389// SqlNumeric binds a fixed-width inline struct and is row-wise batchable for non-nullable columns. It
390// is flagged as numeric so the std::optional batch path excludes it (its contained value is not bound
391// at a uniform offset/representation across backends).
392template <std::size_t ThePrecision, std::size_t TheScale>
393inline constexpr bool SqlIsNativeRowBindableValue<SqlNumeric<ThePrecision, TheScale>> = true;
394template <std::size_t ThePrecision, std::size_t TheScale>
395inline constexpr bool SqlIsNumericValue<SqlNumeric<ThePrecision, TheScale>> = true;
396// clang-format off
397
398} // namespace Lightweight
399
400template <Lightweight::SqlNumericType Type>
401struct std::formatter<Type>: std::formatter<std::string>
402{
403 template <typename FormatContext>
404 auto format(Type const& value, FormatContext& ctx) const
405 {
406 return formatter<std::string>::format(value.ToString(), ctx);
407 }
408};
static SqlErrorInfo FromStatementHandle(SQLHSTMT hStmt)
Constructs an ODBC error info object from the given ODBC statement handle.
Definition SqlError.hpp:48
LIGHTWEIGHT_FORCE_INLINE long double ToLongDouble() const noexcept
Converts the numeric to a long double precision floating point value.
LIGHTWEIGHT_FORCE_INLINE constexpr void assign(std::floating_point auto inputValue) noexcept
Assigns a value to the numeric.
double nativeValue
Cached native value for drivers without SQL_NUMERIC_STRUCT support.
LIGHTWEIGHT_FORCE_INLINE bool operator==(SqlNumeric< OtherPrecision, OtherScale > const &other) const noexcept
Equality comparison operator.
LIGHTWEIGHT_FORCE_INLINE double ToDouble() const noexcept
Converts the numeric to a double precision floating point value.
LIGHTWEIGHT_FORCE_INLINE std::partial_ordering operator<=>(SqlNumeric const &other) const noexcept
constexpr SqlNumeric(SQL_NUMERIC_STRUCT const &value) noexcept
Constructs a numeric from a SQL_NUMERIC_STRUCT.
LIGHTWEIGHT_FORCE_INLINE std::string ToString() const
static constexpr auto Precision
Number of total digits.
constexpr SqlNumeric() noexcept=default
Default constructor.
SQL_NUMERIC_STRUCT sqlValue
The SQL numeric struct for ODBC binding.
static constexpr auto Scale
Number of digits after the decimal point.
LIGHTWEIGHT_FORCE_INLINE float ToFloat() const noexcept
Converts the numeric to a floating point value.
LIGHTWEIGHT_FORCE_INLINE Int128 ToUnscaledValue() const noexcept
static constexpr auto ColumnType
The SQL column type definition for this numeric type.
LIGHTWEIGHT_FORCE_INLINE constexpr SqlNumeric & operator=(std::floating_point auto value) noexcept
Assigns a floating point value to the numeric.