Lightweight 0.20260921.0
Loading...
Searching...
No Matches
SqlDynamicNumeric.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "../Api.hpp"
6#include "../SqlColumnTypeDefinitions.hpp"
7#include "../SqlError.hpp"
8#include "Core.hpp"
9
10#include <algorithm>
11#include <array>
12#include <cstdint>
13#include <format>
14#include <optional>
15#include <span>
16#include <string>
17#include <string_view>
18
19namespace Lightweight
20{
21
22/// The largest number of decimal digits @ref SqlDynamicNumeric carries exactly.
23///
24/// Bounded by the signed 64-bit integer the unscaled value is stored in. This matches
25/// @ref SqlMaxNumericPrecision, the equivalent ceiling on @ref SqlNumeric, so neither type silently
26/// accepts a column the other would reject.
27inline constexpr std::uint8_t SqlMaxDynamicNumericPrecision = 19;
28
29namespace detail
30{
31 /// Removes leading and trailing ASCII whitespace.
32 [[nodiscard]] constexpr std::string_view TrimAsciiWhitespace(std::string_view text) noexcept
33 {
34 auto const isSpace = [](char c) noexcept {
35 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
36 };
37 while (!text.empty() && isSpace(text.front()))
38 text.remove_prefix(1);
39 while (!text.empty() && isSpace(text.back()))
40 text.remove_suffix(1);
41 return text;
42 }
43
44 /// Appends one decimal digit to @p value, reporting overflow instead of wrapping.
45 ///
46 /// @retval false The result would exceed `std::int64_t`; @p value is left unchanged.
47 [[nodiscard]] constexpr bool TryAppendDecimalDigit(std::int64_t& value, std::int64_t digit) noexcept
48 {
49 if (value > (INT64_MAX - digit) / 10)
50 return false;
51 value = (value * 10) + digit;
52 return true;
53 }
54
55 /// Powers of ten a signed 64-bit integer holds exactly: `10^0` through `10^18`.
56 ///
57 /// Indexed by scale, so an exact rescale multiplies once. Repeating a multiplication or division
58 /// by ten instead rounds at every step and drifts: 123456789 at scale 9 comes back as
59 /// 0.12345678900000004 rather than 0.123456789.
60 ///
61 /// @note The table deliberately stops one short of @ref SqlMaxDynamicNumericPrecision. `10^19`
62 /// exceeds `INT64_MAX`, so an entry for it could only hold a wrong value — and a saturating
63 /// generator would make it silently alias `10^18`, scaling a DECIMAL(19, 19) by ten.
64 /// @ref DoublePowersOfTen covers that last scale, where the value is representable.
65 inline constexpr std::array<std::int64_t, SqlMaxDynamicNumericPrecision> PowersOfTen = [] {
66 auto powers = std::array<std::int64_t, SqlMaxDynamicNumericPrecision> {};
67 auto value = std::int64_t { 1 };
68 for (auto& power: powers)
69 {
70 power = value;
71 // Guards only the step past the final entry, whose result is never stored.
72 if (value <= INT64_MAX / 10)
73 value *= 10;
74 }
75 return powers;
76 }();
77
78 /// Powers of ten as exact doubles, covering every scale @ref SqlDynamicNumeric admits.
79 ///
80 /// A double represents `10^n` exactly for n up to 22 — the significand only has to hold `5^n` —
81 /// so even the widest scale still converts in a single division.
82 inline constexpr std::array<double, SqlMaxDynamicNumericPrecision + 1> DoublePowersOfTen = [] {
83 auto powers = std::array<double, SqlMaxDynamicNumericPrecision + 1> {};
84 auto value = 1.0;
85 for (auto& power: powers)
86 {
87 power = value;
88 value *= 10.0;
89 }
90 return powers;
91 }();
92
93 /// Multiplies @p value by `10^count`, reporting overflow instead of wrapping.
94 ///
95 /// Handles negative values, so the caller need not strip the sign first.
96 ///
97 /// @retval false The result would exceed `std::int64_t`; @p value is left unchanged.
98 [[nodiscard]] constexpr bool TryScaleByPowerOfTen(std::int64_t& value, std::uint8_t count) noexcept
99 {
100 // 10^count is beyond the exact table, so it overflows int64 for every value but zero —
101 // which needs no scaling at all.
102 if (count >= PowersOfTen.size())
103 return value == 0;
104
105 auto const factor = PowersOfTen[count];
106 // Compare against the bound that matches the sign: INT64_MIN has no positive counterpart,
107 // so negating first would itself overflow.
108 if (value > 0 && value > INT64_MAX / factor)
109 return false;
110 if (value < 0 && value < INT64_MIN / factor)
111 return false;
112
113 value *= factor;
114 return true;
115 }
116
117 /// Accumulated state of @ref ParseUnscaledDecimal while it walks the literal.
118 struct DecimalParseState
119 {
120 std::int64_t unscaled = 0;
121 std::uint8_t fractionDigits = 0;
122 bool sawDecimalPoint = false;
123 bool sawDigit = false;
124 };
125
126 /// Folds one character of a decimal literal into @p state.
127 ///
128 /// @retval false The character is not valid at this position, or the value overflowed.
129 [[nodiscard]] constexpr bool TryConsumeDecimalCharacter(DecimalParseState& state,
130 char character,
131 std::uint8_t targetScale) noexcept
132 {
133 if (character == '.')
134 {
135 if (state.sawDecimalPoint)
136 return false;
137 state.sawDecimalPoint = true;
138 return true;
139 }
140
141 if (character < '0' || character > '9')
142 return false;
143
144 if (state.sawDecimalPoint)
145 {
146 // Digits past the target scale would change the value, so refuse rather than truncate.
147 if (state.fractionDigits == targetScale)
148 return false;
149 ++state.fractionDigits;
150 }
151
152 state.sawDigit = true;
153 return TryAppendDecimalDigit(state.unscaled, static_cast<std::int64_t>(character - '0'));
154 }
155
156 /// Counts the digits after the decimal point in a plain decimal literal.
157 ///
158 /// Used to recover the scale when a driver does not report one: PostgreSQL's `numeric` without a
159 /// typmod, and expression columns such as `SELECT SUM(amount)`, carry no declared scale, but the
160 /// literal the driver returns still states it exactly.
161 ///
162 /// @return Zero if there is no fractional part, or the text is not a plain decimal literal.
163 [[nodiscard]] constexpr std::uint8_t FractionDigitsOf(std::string_view text) noexcept
164 {
165 // Trim exactly as ParseUnscaledDecimal does. A driver that pads the literal would otherwise
166 // make the two disagree about the same string: this would score "12.34 " as zero fractional
167 // digits, and the parse would then reject the digits it was never told to expect.
168 text = TrimAsciiWhitespace(text);
169
170 auto const decimalPoint = text.find('.');
171 if (decimalPoint == std::string_view::npos)
172 return 0;
173
174 auto digits = std::size_t { 0 };
175 for (char const character: text.substr(decimalPoint + 1))
176 {
177 if (character < '0' || character > '9')
178 return 0;
179 ++digits;
180 }
181 return static_cast<std::uint8_t>(std::min<std::size_t>(digits, SqlMaxDynamicNumericPrecision));
182 }
183
184 /// Parses a plain decimal literal into its unscaled integer representation at @p targetScale.
185 ///
186 /// Accepts an optional sign, an optional integral part and an optional fractional part
187 /// (`"-12.340"`, `"+.5"`, `"7"`). Surrounding whitespace is ignored. Scientific notation is
188 /// deliberately rejected: ODBC drivers do not emit it for DECIMAL/NUMERIC columns, and accepting
189 /// it would mean guessing at a value the database expressed exactly.
190 ///
191 /// The result is the mathematical value multiplied by `10^targetScale`, so `"1.5"` at scale 4
192 /// yields `15000`.
193 ///
194 /// @param text The decimal literal to parse.
195 /// @param targetScale Number of fractional digits the returned value is scaled to.
196 /// @retval std::nullopt The text is not a plain decimal literal, carries more fractional digits
197 /// than @p targetScale, or the scaled result overflows `std::int64_t`.
198 [[nodiscard]] constexpr std::optional<std::int64_t> ParseUnscaledDecimal(std::string_view text,
199 std::uint8_t targetScale) noexcept
200 {
201 text = TrimAsciiWhitespace(text);
202 if (text.empty())
203 return std::nullopt;
204
205 bool isNegative = false;
206 if (text.front() == '+' || text.front() == '-')
207 {
208 isNegative = text.front() == '-';
209 text.remove_prefix(1);
210 }
211
212 auto state = DecimalParseState {};
213 for (char const character: text)
214 if (!TryConsumeDecimalCharacter(state, character, targetScale))
215 return std::nullopt;
216
217 if (!state.sawDigit)
218 return std::nullopt;
219
220 if (!TryScaleByPowerOfTen(state.unscaled, static_cast<std::uint8_t>(targetScale - state.fractionDigits)))
221 return std::nullopt;
222
223 return isNegative ? -state.unscaled : state.unscaled;
224 }
225} // namespace detail
226
227/// @brief A fixed-point decimal whose precision and scale are known only at run time.
228///
229/// This is the dynamic counterpart to @ref SqlNumeric. Where `SqlNumeric<Precision, Scale>` fixes both
230/// at compile time, a value fetched into a @ref SqlVariant learns its precision and scale from the
231/// result-set metadata, so they have to travel with the value.
232///
233/// The number is held exactly, as the unscaled integer `value * 10^scale`, and never passes through a
234/// binary floating-point type. That is the point of the type: `DECIMAL(19, 4)` — what MS SQL Server's
235/// `money` maps to — carries more significant digits than a `double` can represent, so reading such a
236/// column as `double` loses cents on large amounts.
237///
238/// @ingroup DataTypes
240{
241 /// The value, scaled by `10^scale`. `12.34` at scale 2 is stored as `1234`.
242 std::int64_t unscaledValue = 0;
243
244 /// Total number of decimal digits the source column holds.
245 std::uint8_t precision = 0;
246
247 /// Number of digits after the decimal point.
248 std::uint8_t scale = 0;
249
250 /// Compares two values by mathematical magnitude, so the same number at different scales compares
251 /// equal (`1.50` at scale 2 equals `1.5` at scale 1).
252 [[nodiscard]] constexpr bool operator==(SqlDynamicNumeric const& other) const noexcept
253 {
254 if (scale == other.scale)
255 return unscaledValue == other.unscaledValue;
256
257 // Lift the coarser-scaled value up to the finer scale, so 1.5 (scale 1) equals 1.50 (scale 2).
258 auto const coarser = scale < other.scale ? *this : other;
259 auto const finer = scale < other.scale ? other : *this;
260
261 // TryScaleByPowerOfTen handles a negative value directly; negating INT64_MIN to strip the
262 // sign first would itself be undefined.
263 auto rescaled = coarser.unscaledValue;
264 if (!detail::TryScaleByPowerOfTen(rescaled, static_cast<std::uint8_t>(finer.scale - coarser.scale)))
265 return false;
266
267 return rescaled == finer.unscaledValue;
268 }
269
270 /// Inequality, derived from @ref operator==.
271 [[nodiscard]] constexpr bool operator!=(SqlDynamicNumeric const& other) const noexcept
272 {
273 return !(*this == other);
274 }
275
276 /// Converts to a floating-point approximation.
277 ///
278 /// @note Lossy for values needing more significant digits than a `double` carries; read
279 /// @ref unscaledValue together with @ref scale when exactness matters.
280 [[nodiscard]] constexpr double ToDouble() const noexcept
281 {
282 // A scale past SqlMaxDynamicNumericPrecision describes no value this type can hold.
283 if (scale == 0 || scale >= detail::DoublePowersOfTen.size())
284 return static_cast<double>(unscaledValue);
285
286 // One division, not `scale` of them: dividing by ten repeatedly rounds at every step, so
287 // 123456789 at scale 9 would come back as 0.12345678900000004. The divisor comes from the
288 // double table rather than the int64 one, which stops at 10^18 and so cannot express the
289 // divisor for a DECIMAL(19, 19).
290 return static_cast<double>(unscaledValue) / detail::DoublePowersOfTen[scale];
291 }
292
293 /// Renders the exact decimal representation, including the trailing zeros implied by @ref scale.
294 [[nodiscard]] std::string ToString() const
295 {
296 auto const negative = unscaledValue < 0;
297 // Negate in the unsigned domain so INT64_MIN does not overflow.
298 auto const magnitude =
299 negative ? 0ULL - static_cast<std::uint64_t>(unscaledValue) : static_cast<std::uint64_t>(unscaledValue);
300
301 auto digits = std::to_string(magnitude);
302 if (scale > 0)
303 {
304 if (digits.size() <= scale)
305 digits.insert(0, std::string(scale - digits.size() + 1, '0'));
306 digits.insert(digits.size() - scale, ".");
307 }
308 return negative ? "-" + digits : digits;
309 }
310
311 /// Parses an exact decimal literal, scaling it to @p scale.
312 ///
313 /// @param text The decimal literal, as an ODBC driver renders a DECIMAL/NUMERIC column.
314 /// @param precision Total digit count to record on the result.
315 /// @param scale Fractional digit count to scale the value to.
316 /// @retval std::nullopt @p text is not a plain decimal literal, or does not fit the scale.
317 [[nodiscard]] static constexpr std::optional<SqlDynamicNumeric> FromString(std::string_view text,
318 std::uint8_t precision,
319 std::uint8_t scale) noexcept
320 {
321 auto const unscaled = detail::ParseUnscaledDecimal(text, scale);
322 if (!unscaled)
323 return std::nullopt;
324 return SqlDynamicNumeric { .unscaledValue = *unscaled, .precision = precision, .scale = scale };
325 }
326};
327
328/// Binds @ref SqlDynamicNumeric as an exact decimal literal.
329///
330/// Text is deliberately the wire format in both directions. `SQL_C_NUMERIC` needs the application to
331/// publish precision and scale on the descriptor before each call and is honoured inconsistently —
332/// the SQL Server driver returns scale-0 values without it and the SQLite driver has no native
333/// support at all, which is why `SqlDataBinder<SqlNumeric<P, S>>` routes both backends around it.
334/// Every supported driver converts between a decimal column and its literal exactly, with no binary
335/// floating-point step, so this path keeps all digits on all backends.
336template <>
337struct SqlDataBinder<SqlDynamicNumeric>
338{
339 /// Widest decimal ODBC admits, so no supported backend can overflow the bind buffer.
340 static constexpr auto ColumnType = SqlColumnTypeDefinitions::Decimal { .precision = 38, .scale = 0 };
341
342 /// Binds the value as an input parameter, rendered as an exact decimal literal.
343 static LIGHTWEIGHT_FORCE_INLINE SQLRETURN InputParameter(SQLHSTMT stmt,
344 SQLUSMALLINT column,
345 SqlDynamicNumeric const& value,
346 SqlDataBinderCallback& cb) noexcept
347 {
348 // Stage the literal on the statement rather than in the value: the buffer only has to
349 // outlive the execute, and carrying it inside the type would put ~130 bytes of ODBC scratch
350 // into every SqlVariant — and so into every row of every prefetch block.
351 auto const literal = value.ToString();
352 auto* const buffer = reinterpret_cast<char*>(cb.ProvideBatchStagingBuffer(literal.size() + 1));
353 std::ranges::copy(literal, buffer);
354 buffer[literal.size()] = '\0';
355
356 auto* const indicator = cb.ProvideInputIndicator();
357 *indicator = static_cast<SQLLEN>(literal.size());
358
359 auto const columnSize = static_cast<SQLULEN>(value.precision != 0 ? value.precision : SqlMaxDynamicNumericPrecision);
360 return SQLBindParameter(stmt,
361 column,
362 SQL_PARAM_INPUT,
363 SQL_C_CHAR,
364 SQL_NUMERIC,
365 columnSize,
366 static_cast<SQLSMALLINT>(value.scale),
367 (SQLPOINTER) buffer,
368 static_cast<SQLLEN>(literal.size() + 1),
369 indicator);
370 }
371
372 /// A decimal column's text, as delivered by a single SQLGetData call.
373 struct Literal
374 {
375 /// What SQLGetData reported, or SQL_ERROR when the text did not fit the caller's buffer.
376 SQLRETURN returnCode = SQL_SUCCESS;
377 /// The literal, or empty when the column is NULL, the read failed, or the text was truncated.
378 std::string_view text;
379 /// Whether the column was NULL. Reported here rather than left to the caller's @c indicator,
380 /// which @c ReadLiteral accepts as null — a caller passing none could not otherwise tell a
381 /// NULL apart from a failed read.
382 bool isNull = false;
383 };
384
385 /// Reads the column's decimal literal into @p buffer, issuing exactly one SQLGetData.
386 ///
387 /// ODBC allows a second SQLGetData on the same column only while a character or binary value is
388 /// still being delivered in parts. Once it has arrived in full, a conforming driver answers
389 /// SQL_NO_DATA — SQL Server's does. So a caller that wants the value exactly and, failing that,
390 /// approximately must derive both from this one read rather than retrieving the column twice.
391 ///
392 /// @param stmt The ODBC statement handle to read from.
393 /// @param column The 1-based index of the column to read.
394 /// @param indicator Where to report the length; may be null, in which case the length is still
395 /// needed internally and is discarded afterwards.
396 /// @param buffer Storage for the text; @c Literal::text points into it, so it must outlive the
397 /// returned value. 128 bytes is always enough: ODBC caps DECIMAL precision at 38,
398 /// leaving room for the sign, decimal point, terminator and driver padding.
399 [[nodiscard]] static LIGHTWEIGHT_FORCE_INLINE Literal ReadLiteral(SQLHSTMT stmt,
400 SQLUSMALLINT column,
401 SQLLEN* indicator,
402 std::span<char> buffer) noexcept
403 {
404 // SQLGetData needs somewhere to report the length even when the caller does not want it, and
405 // the truncation check below is not optional — it is what stops a clipped literal parsing
406 // into a plausible-looking wrong number.
407 SQLLEN discardedIndicator = 0;
408 auto* const lengthIndicator = indicator ? indicator : &discardedIndicator;
409
410 auto const returnCode =
411 SQLGetData(stmt, column, SQL_C_CHAR, buffer.data(), static_cast<SQLLEN>(buffer.size()), lengthIndicator);
412 if (!SQL_SUCCEEDED(returnCode))
413 return { .returnCode = returnCode, .text = {}, .isNull = false };
414 if (*lengthIndicator == SQL_NULL_DATA)
415 return { .returnCode = returnCode, .text = {}, .isNull = true };
416
417 // The indicator reports the bytes *available*, not the bytes written: on truncation it
418 // exceeds the buffer (alongside SQL_SUCCESS_WITH_INFO, which SQL_SUCCEEDED accepts), and it
419 // is SQL_NO_TOTAL when the driver cannot say. Neither is a usable length, so measure the
420 // NUL-terminated text the driver actually wrote, and refuse a value that did not fit rather
421 // than parsing a truncated literal into a plausible-looking wrong number.
422 auto const truncated = *lengthIndicator == SQL_NO_TOTAL
423 || (*lengthIndicator >= 0 && static_cast<std::size_t>(*lengthIndicator) >= buffer.size());
424 if (truncated)
425 return { .returnCode = SQL_ERROR, .text = {}, .isNull = false };
426
427 // Trim here, once, so every consumer sees the same literal. Drivers pad; the exact parser
428 // trims and the double fallback's std::from_chars does not, so an untrimmed view would make
429 // the two disagree about the very same bytes.
430 auto const terminator = std::ranges::find(buffer, '\0');
431 auto const raw = std::string_view(buffer.data(), static_cast<std::size_t>(terminator - buffer.begin()));
432 return { .returnCode = returnCode, .text = detail::TrimAsciiWhitespace(raw), .isNull = false };
433 }
434
435 /// Converts a decimal literal into an exact value, using the column's declared precision and scale.
436 ///
437 /// @retval std::nullopt The value needs more digits than the unscaled 64-bit carrier holds; see
438 /// @c SqlMaxDynamicNumericPrecision.
439 [[nodiscard]] static LIGHTWEIGHT_FORCE_INLINE std::optional<SqlDynamicNumeric> FromColumnLiteral(
440 SQLHSTMT stmt, SQLUSMALLINT column, std::string_view literal) noexcept
441 {
442 SQLLEN declaredPrecision = 0;
443 SQLLEN declaredScale = 0;
444 (void) SQLColAttributeW(stmt, column, SQL_DESC_PRECISION, nullptr, SQLSMALLINT { 0 }, nullptr, &declaredPrecision);
445 (void) SQLColAttributeW(stmt, column, SQL_DESC_SCALE, nullptr, SQLSMALLINT { 0 }, nullptr, &declaredScale);
446
447 // Clamp before narrowing: a driver may report a scale or precision wider than this type
448 // admits, and a bare cast to uint8_t would wrap it into a small, plausible-looking value.
449 auto const toDigitCount = [](SQLLEN reported) noexcept {
450 return static_cast<std::uint8_t>(std::clamp<SQLLEN>(reported, 0, SqlMaxDynamicNumericPrecision));
451 };
452
453 // Not every column has a declared scale — PostgreSQL's unconstrained `numeric` and any
454 // expression column report none — but the literal always states it. Take whichever is
455 // larger so the fractional digits present in the text are never discarded.
456 auto const scale = std::max(toDigitCount(declaredScale), detail::FractionDigitsOf(literal));
457 auto const precision = toDigitCount(declaredPrecision);
458
459 return SqlDynamicNumeric::FromString(literal, precision, scale);
460 }
461
462 /// Retrieves the column as an exact decimal without throwing, for callers that can degrade.
463 ///
464 /// @retval SQL_ERROR The driver truncated the literal, or the value needs more digits than the
465 /// unscaled 64-bit carrier holds (see @c SqlMaxDynamicNumericPrecision).
466 /// No ODBC diagnostic is posted for this — the driver did not fail, the value
467 /// simply does not fit — so read the return code rather than the statement's
468 /// last error. @ref GetColumn turns it into a described exception;
469 /// @ref SqlVariant reuses @ref ReadLiteral and falls back to `double` instead.
470 static LIGHTWEIGHT_FORCE_INLINE SQLRETURN TryGetColumn(SQLHSTMT stmt,
471 SQLUSMALLINT column,
472 SqlDynamicNumeric* result,
473 SQLLEN* indicator,
474 SqlDataBinderCallback const& /*cb*/) noexcept
475 {
476 std::array<char, 128> buffer {};
477 auto const literal = ReadLiteral(stmt, column, indicator, buffer);
478 if (literal.text.empty())
479 {
480 // Leave a defined value behind on NULL or a failed read, so a result reused across a
481 // fetch loop cannot report the previous row's number.
482 *result = SqlDynamicNumeric {};
483 return literal.returnCode;
484 }
485
486 auto const parsed = FromColumnLiteral(stmt, column, literal.text);
487 if (!parsed)
488 return SQL_ERROR;
489
490 *result = *parsed;
491 return literal.returnCode;
492 }
493
494 /// Retrieves the column as an exact decimal, preserving every digit the column declares.
495 ///
496 /// @throws SqlException When the value cannot be represented exactly. The driver posts no
497 /// diagnostic in that case — it did not fail — so this synthesizes one rather than
498 /// letting the caller surface an empty, or worse a stale, error from the handle.
499 static LIGHTWEIGHT_FORCE_INLINE SQLRETURN GetColumn(
500 SQLHSTMT stmt, SQLUSMALLINT column, SqlDynamicNumeric* result, SQLLEN* indicator, SqlDataBinderCallback const& cb)
501 {
502 auto const returnCode = TryGetColumn(stmt, column, result, indicator, cb);
503 if (returnCode != SQL_ERROR)
504 return returnCode;
505
506 throw SqlException(SqlErrorInfo {
507 .nativeErrorCode = 0,
508 .sqlState = "22003", // numeric value out of range
509 .message = std::format("Column {} holds a decimal that SqlDynamicNumeric cannot represent exactly: "
510 "it carries more than {} significant digits, or the driver truncated the "
511 "literal. Read the column as double or std::string to accept an "
512 "approximation.",
513 column,
514 SqlMaxDynamicNumericPrecision),
515 });
516 }
517
518 /// Renders the value for SQL trace logs.
519 static LIGHTWEIGHT_FORCE_INLINE std::string Inspect(SqlDynamicNumeric const& value)
520 {
521 return value.ToString();
522 }
523};
524
525} // namespace Lightweight
526
527template <>
528struct std::formatter<Lightweight::SqlDynamicNumeric>: std::formatter<std::string>
529{
530 LIGHTWEIGHT_FORCE_INLINE auto format(Lightweight::SqlDynamicNumeric const& value, format_context& ctx) const
531 -> format_context::iterator
532 {
533 return std::formatter<std::string>::format(value.ToString(), ctx);
534 }
535};
std::string_view text
The literal, or empty when the column is NULL, the read failed, or the text was truncated.
static LIGHTWEIGHT_FORCE_INLINE std::string Inspect(SqlDynamicNumeric const &value)
Renders the value for SQL trace logs.
static LIGHTWEIGHT_FORCE_INLINE SQLRETURN InputParameter(SQLHSTMT stmt, SQLUSMALLINT column, SqlDynamicNumeric const &value, SqlDataBinderCallback &cb) noexcept
Binds the value as an input parameter, rendered as an exact decimal literal.
static LIGHTWEIGHT_FORCE_INLINE SQLRETURN TryGetColumn(SQLHSTMT stmt, SQLUSMALLINT column, SqlDynamicNumeric *result, SQLLEN *indicator, SqlDataBinderCallback const &) noexcept
static LIGHTWEIGHT_FORCE_INLINE std::optional< SqlDynamicNumeric > FromColumnLiteral(SQLHSTMT stmt, SQLUSMALLINT column, std::string_view literal) noexcept
static LIGHTWEIGHT_FORCE_INLINE SQLRETURN GetColumn(SQLHSTMT stmt, SQLUSMALLINT column, SqlDynamicNumeric *result, SQLLEN *indicator, SqlDataBinderCallback const &cb)
static LIGHTWEIGHT_FORCE_INLINE Literal ReadLiteral(SQLHSTMT stmt, SQLUSMALLINT column, SQLLEN *indicator, std::span< char > buffer) noexcept
A fixed-point decimal whose precision and scale are known only at run time.
constexpr bool operator==(SqlDynamicNumeric const &other) const noexcept
std::string ToString() const
Renders the exact decimal representation, including the trailing zeros implied by scale.
static constexpr std::optional< SqlDynamicNumeric > FromString(std::string_view text, std::uint8_t precision, std::uint8_t scale) noexcept
std::int64_t unscaledValue
The value, scaled by 10^scale. 12.34 at scale 2 is stored as 1234.
std::uint8_t scale
Number of digits after the decimal point.
std::uint8_t precision
Total number of decimal digits the source column holds.
constexpr double ToDouble() const noexcept
constexpr bool operator!=(SqlDynamicNumeric const &other) const noexcept
Inequality, derived from operator==.
Represents an ODBC SQL error.
Definition SqlError.hpp:32
SQLINTEGER nativeErrorCode
The native ODBC error code.
Definition SqlError.hpp:34