Lightweight 0.20261105.1
Loading...
Searching...
No Matches
BasicStringBinder.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "Core.hpp"
6#include "UnicodeConverter.hpp"
7
8#include <algorithm>
9#include <cassert>
10#include <cstddef>
11#include <cstring>
12#include <memory>
13#include <optional>
14#include <ranges>
15#include <utility>
16
17namespace Lightweight
18{
19
20/// @brief Magic number, which is used to determine the optimal maximum size of a column.
21///
22/// Columns may be larger than this value, but this is the optimal maximum size for performance,
23/// and usually also means that values are stored in the same row as the rest of the data, or not.
24constexpr inline std::size_t SqlOptimalMaxColumnSize = 4000;
25
26namespace detail
27{
28 /// Reads raw column data of an array-like type, growing the array until the value fits.
29 ///
30 /// Takes the data source as a parameter so a test can make it report what no conforming driver
31 /// reports, which is the only way to reach the checks on the length indicator.
32 ///
33 /// @param getData Called as `getData(SQLPOINTER buffer, SQLLEN bufferBytes, SQLLEN* indicator)`; must not throw.
34 /// @param result The array-like type to store the data in.
35 /// @param indicator The indicator to store the length of the data.
36 ///
37 /// @return SQLRETURN indicating the result of the operation. An indicator no length can be derived
38 /// from, or a length the array cannot hold, gives SQL_ERROR. The driver then posted no
39 /// diagnostic for it, so the error the caller raises has an empty or stale SQLSTATE.
40 template <auto CType, typename ArrayType, typename GetData>
41 requires requires(ArrayType& arr) {
42 { arr.data() } -> std::convertible_to<typename ArrayType::value_type*>;
43 { arr.size() } -> std::convertible_to<std::size_t>;
44 { arr.resize(std::declval<std::size_t>()) };
45 }
46 SQLRETURN ReadRawColumnArrayData(GetData const& getData, ArrayType* result, SQLLEN* indicator) noexcept
47 {
48 using CharType = ArrayType::value_type;
49
50 // Character C types NUL-terminate every SQLGetData transfer, consuming the last buffer
51 // slot; SQL_C_BINARY transfers raw bytes WITHOUT a terminator, so every buffer slot holds
52 // data. Getting this wrong for binary loses the byte at each continuation boundary.
53 constexpr size_t TrailingNulTerminators = CType == SQL_C_BINARY ? 0 : 1;
54
55 // A length the array cannot hold, such as one a driver misreports as huge, must not escape as an
56 // exception: this function is noexcept, so that would terminate the process.
57 try
58 {
59 *indicator = 0;
60
61 // Resize the string to the size of the data
62 // Get the data (take care of SQL_NULL_DATA and SQL_NO_TOTAL)
63 auto sqlResult = getData(
64 static_cast<SQLPOINTER>(result->data()), static_cast<SQLLEN>(result->size() * sizeof(CharType)), indicator);
65
66 if (sqlResult == SQL_SUCCESS || sqlResult == SQL_NO_DATA)
67 {
68 // Data has been read successfully on first call to SQLGetData, or there is no data to read.
69 if (*indicator == SQL_NULL_DATA)
70 result->clear();
71 else if (*indicator < 0)
72 return SQL_ERROR; // SQL_NO_TOTAL is valid only with SQL_SUCCESS_WITH_INFO.
73 else
74 result->resize(static_cast<size_t>(*indicator) / sizeof(CharType));
75 return sqlResult;
76 }
77
78 if (sqlResult == SQL_SUCCESS_WITH_INFO && *indicator < 0 && *indicator != SQL_NO_TOTAL)
79 {
80 if (*indicator != SQL_NULL_DATA)
81 return SQL_ERROR;
82 result->clear();
83 return sqlResult;
84 }
85
86 if (sqlResult == SQL_SUCCESS_WITH_INFO && *indicator != SQL_NO_TOTAL
87 && (static_cast<size_t>(*indicator) / sizeof(CharType)) + TrailingNulTerminators > result->size())
88 {
89 // Truncation with a known total. Note the TrailingNulTerminators term: a CHARACTER value whose
90 // length exactly equals the buffer still truncates (the driver spends the last slot
91 // on the NUL terminator), so the continuation must run for indicator == buffer size.
92 // We have a truncation and the server knows how much data is left.
93 auto const totalCharCount = static_cast<size_t>(*indicator) / sizeof(CharType);
94 auto const charsWritten = result->size() - TrailingNulTerminators;
95 result->resize(totalCharCount + TrailingNulTerminators);
96 auto* bufferCont = result->data() + charsWritten;
97 auto const bufferCharsAvailable = (totalCharCount + TrailingNulTerminators) - charsWritten;
98 sqlResult = getData(bufferCont, static_cast<SQLLEN>(bufferCharsAvailable * sizeof(CharType)), indicator);
99 if (SQL_SUCCEEDED(sqlResult))
100 {
101 if (*indicator < 0)
102 return SQL_ERROR;
103 result->resize(charsWritten + (static_cast<size_t>(*indicator) / sizeof(CharType)));
104 }
105 return sqlResult;
106 }
107
108 size_t writeIndex = 0;
109 // Chars offered to the previous SQLGetData call; a truncated transfer filled all but the terminator.
110 size_t lastTransferChars = result->size();
111 while (sqlResult == SQL_SUCCESS_WITH_INFO && *indicator == SQL_NO_TOTAL)
112 {
113 // We have a truncation and the server does not know how much data is left.
114 writeIndex += lastTransferChars - TrailingNulTerminators;
115 result->resize(result->size() * 2);
116 auto* const bufferStart = result->data() + writeIndex;
117 lastTransferChars = result->size() - writeIndex;
118 sqlResult = getData(bufferStart, static_cast<SQLLEN>(lastTransferChars * sizeof(CharType)), indicator);
119 }
120 // The unknown-total loop exits with the final transfer's char count in the indicator; trim
121 // the over-allocated buffer to the bytes actually written.
122 if (writeIndex > 0 && SQL_SUCCEEDED(sqlResult) && *indicator != SQL_NULL_DATA && *indicator != SQL_NO_TOTAL)
123 {
124 if (*indicator < 0)
125 return SQL_ERROR;
126 result->resize(writeIndex + (static_cast<size_t>(*indicator) / sizeof(CharType)));
127 }
128 return sqlResult;
129 }
130 catch (...)
131 {
132 return SQL_ERROR;
133 }
134 }
135
136 /// Helper function to get raw column data of an array-like type.
137 ///
138 /// @param stmt The SQL statement handle.
139 /// @param column The column number to retrieve data from.
140 /// @param result The array-like type to store the data in.
141 /// @param indicator The indicator to store the length of the data.
142 ///
143 /// @return SQLRETURN indicating the result of the operation.
144 template <auto CType, typename ArrayType>
145 SQLRETURN GetRawColumnArrayData(SQLHSTMT stmt, SQLUSMALLINT column, ArrayType* result, SQLLEN* indicator) noexcept
146 {
147 return ReadRawColumnArrayData<CType>(
148 [stmt, column](SQLPOINTER buffer, SQLLEN bufferBytes, SQLLEN* reported) noexcept {
149 return SQLGetData(stmt, column, CType, buffer, bufferBytes, reported);
150 },
151 result,
152 indicator);
153 }
154
155 /// Size of the first buffer a growing SQL_C_CHAR read of @p column starts with, including the
156 /// null terminator.
157 ///
158 /// Reading in chunks - a small buffer, grown as the driver reports truncation - is defined only
159 /// for character and binary sources. For any other source the value must fit the first buffer:
160 /// MS SQL Server fails a uniqueidentifier (36 characters) read into a short one with 22003
161 /// "Numeric value out of range", and the failed call counts as the column's retrieval, so no
162 /// retry with a larger buffer can follow (#602). Such columns start at their display width.
163 ///
164 /// @param stmt The statement holding the result set.
165 /// @param column The 1-based column index.
166 /// @return The initial buffer size, in bytes.
167 inline size_t InitialCharBufferSize(SQLHSTMT stmt, SQLUSMALLINT column) noexcept
168 {
169 constexpr size_t DefaultSize = 15;
170
171 SQLLEN sqlType {};
172 if (!SQL_SUCCEEDED(SQLColAttribute(stmt, column, SQL_DESC_CONCISE_TYPE, nullptr, 0, nullptr, &sqlType)))
173 return DefaultSize;
174 switch (sqlType)
175 {
176 case SQL_CHAR:
177 case SQL_VARCHAR:
178 case SQL_LONGVARCHAR:
179 case SQL_WCHAR:
180 case SQL_WVARCHAR:
181 case SQL_WLONGVARCHAR:
182 case SQL_BINARY:
183 case SQL_VARBINARY:
184 case SQL_LONGVARBINARY:
185 return DefaultSize;
186 default:
187 break;
188 }
189
190 SQLLEN displaySize {};
191 if (!SQL_SUCCEEDED(SQLColAttribute(stmt, column, SQL_DESC_DISPLAY_SIZE, nullptr, 0, nullptr, &displaySize)))
192 return DefaultSize;
193 return (std::max) (DefaultSize, static_cast<size_t>((std::max) (displaySize, SQLLEN { 0 })) + 1);
194 }
195
196 template <typename Utf16StringType>
197 SQLRETURN GetColumnUtf16(SQLHSTMT stmt,
198 SQLUSMALLINT column,
199 Utf16StringType* result,
200 SQLLEN* indicator,
201 SqlDataBinderCallback const& /*cb*/) noexcept
202 {
203 if constexpr (requires { Utf16StringType::Capacity; })
204 result->resize(Utf16StringType::Capacity);
205 else if (result->size() == 0)
206 result->resize(255);
207
208 return GetRawColumnArrayData<SQL_C_WCHAR>(stmt, column, result, indicator);
209 }
210
211 template <typename StringType>
212 SQLRETURN BindOutputColumnNonUtf16Unicode(
213 SQLHSTMT stmt, SQLUSMALLINT column, StringType* result, SQLLEN* indicator, SqlDataBinderCallback& cb) noexcept
214 {
215 using CharType = StringType::value_type;
216
217 auto u16String = std::make_shared<std::u16string>();
218 if (!result->empty())
219 u16String->resize(result->size());
220 else
221 u16String->resize(255);
222
223 cb.PlanPostProcessOutputColumn([stmt, column, result, indicator, u16String = u16String]() {
224 if (*indicator == SQL_NULL_DATA)
225 u16String->clear();
226 else if (*indicator == SQL_NO_TOTAL)
227 ; // We don't know the size of the data
228 else if (std::cmp_less_equal(*indicator, u16String->size() * sizeof(char16_t)))
229 u16String->resize(static_cast<size_t>(*indicator) / sizeof(char16_t));
230 else
231 {
232 auto const totalCharsRequired = static_cast<size_t>(*indicator) / sizeof(char16_t);
233 *indicator += sizeof(char16_t); // Add space to hold the null terminator
234 u16String->resize(totalCharsRequired);
235 auto const sqlResult = SQLGetData(stmt, column, SQL_C_WCHAR, u16String->data(), *indicator, indicator);
236 (void) sqlResult;
237 assert(SQL_SUCCEEDED(sqlResult));
238 assert(std::cmp_equal(*indicator, totalCharsRequired * sizeof(char16_t)));
239 }
240
241 if constexpr (sizeof(typename StringType::value_type) == 1)
242 *result = ToUtf8(*u16String);
243 else if constexpr (sizeof(typename StringType::value_type) == 4)
244 {
245 // *result = ToUtf32(*u16String);
246 auto const u32String = ToUtf32(*u16String);
247 *result = StringType {
248 (CharType const*) u32String.data(),
249 (CharType const*) u32String.data() + u32String.size(),
250 };
251 }
252
253 // The UTF-16 binder applies StringTraits::PostProcessOutputColumn (if present) so that
254 // FIXED_SIZE_RIGHT_TRIMMED strings strip the trailing CHAR(N)/NCHAR(N) padding the server
255 // returned. The UTF-32 / UTF-8 path constructs *result above, which preserves the padding,
256 // so we have to invoke the same post-process here. The synthetic indicator is the
257 // result's own byte count: PostProcessOutputColumn divides by sizeof(CharType) to get
258 // the char count, and TrimRight then walks back over trailing whitespace/null.
259 using StringTraits = SqlBasicStringOperations<StringType>;
260 if constexpr (requires { StringTraits::PostProcessOutputColumn(result, *indicator); })
261 {
262 if (*indicator != SQL_NULL_DATA && *indicator != SQL_NO_TOTAL)
263 {
264 auto const syntheticIndicator = static_cast<SQLLEN>(StringTraits::Size(result) * sizeof(CharType));
265 StringTraits::PostProcessOutputColumn(result, syntheticIndicator);
266 }
267 }
268 });
269
270 return SQLBindCol(stmt,
271 column,
272 SQL_C_WCHAR,
273 static_cast<SQLPOINTER>(u16String->data()),
274 static_cast<SQLLEN>(u16String->size() * sizeof(char16_t)),
275 indicator);
276 }
277
278} // namespace detail
279
280// SqlDataBinder<> specialization for ANSI character strings
281template <typename AnsiStringType>
282 requires SqlBasicStringBinderConcept<AnsiStringType, char>
283struct SqlDataBinder<AnsiStringType>
284{
285 using ValueType = AnsiStringType;
286 using CharType = char;
287 using StringTraits = SqlBasicStringOperations<AnsiStringType>;
288
289 static constexpr auto ColumnType = StringTraits::ColumnType;
290
291 static LIGHTWEIGHT_FORCE_INLINE SQLRETURN InputParameter(SQLHSTMT stmt,
292 SQLUSMALLINT column,
293 AnsiStringType const& value,
294 SqlDataBinderCallback& cb) noexcept
295 {
296 // PostgreSQL: route narrow strings through UTF-16 + SQL_C_WCHAR. Even with the
297 // DBC handle in Unicode-app mode (SQLDriverConnectW), psqlODBC on Windows still
298 // interprets SQL_C_CHAR data as the system codepage (cp1252), not UTF-8 — so a
299 // 200-byte UTF-8 payload representing 100 supplementary-plane characters gets
300 // re-encoded into 400 bytes and trips VARCHAR(100)'s character-count check.
301 // Going via SQL_C_WCHAR makes the driver itself do the encoding conversion to
302 // the server's encoding (UTF-8) using proper Unicode rules. We treat the
303 // incoming `std::string` as UTF-8 by convention — that is what every other
304 // call path in the library produces (file readers, query formatters, etc.).
305 if (cb.ServerType() == SqlServerType::POSTGRESQL)
306 {
307 auto u16String = std::make_shared<std::u16string>(ToUtf16(std::u8string_view {
308 reinterpret_cast<char8_t const*>(StringTraits::Data(&value)), StringTraits::Size(&value) }));
309 cb.PlanPostExecuteCallback([u16String = u16String]() {}); // keep buffer alive
310 auto const charCount = u16String->size();
311 auto const byteCount = charCount * sizeof(char16_t);
312 auto const sqlType =
313 static_cast<SQLSMALLINT>(charCount > SqlOptimalMaxColumnSize ? SQL_WLONGVARCHAR : SQL_WVARCHAR);
314
315 SQLLEN* indicator = cb.ProvideInputIndicator();
316 *indicator = static_cast<SQLLEN>(byteCount);
317
318 return SQLBindParameter(stmt,
319 column,
320 SQL_PARAM_INPUT,
321 SQL_C_WCHAR,
322 sqlType,
323 detail::NonZeroColumnSize(charCount),
324 0,
325 (SQLPOINTER) u16String->data(),
326 static_cast<SQLLEN>(byteCount),
327 indicator);
328 }
329
330 auto const charCount = StringTraits::Size(&value);
331 auto const sqlType = static_cast<SQLSMALLINT>(charCount > SqlOptimalMaxColumnSize ? SQL_LONGVARCHAR : SQL_VARCHAR);
332
333 SQLLEN* indicator = cb.ProvideInputIndicator();
334 *indicator = static_cast<SQLLEN>(charCount);
335
336 return SQLBindParameter(stmt,
337 column,
338 SQL_PARAM_INPUT,
339 SQL_C_CHAR,
340 sqlType,
341 detail::NonZeroColumnSize(charCount),
342 0,
343 (SQLPOINTER) StringTraits::Data(&value),
344 0,
345 indicator);
346 }
347
348 static LIGHTWEIGHT_FORCE_INLINE SQLRETURN BatchInputParameter(SQLHSTMT stmt,
349 SQLUSMALLINT column,
350 AnsiStringType const* values,
351 size_t rowCount,
352 SqlDataBinderCallback& cb) noexcept
353 {
354 size_t maxLen = 0;
355 SQLLEN* indicators = cb.ProvideInputIndicators(rowCount);
356 for (size_t i = 0; i < rowCount; ++i)
357 {
358 auto const len = StringTraits::Size(&values[i]);
359 indicators[i] = static_cast<SQLLEN>(len);
360 if (len > maxLen)
361 maxLen = len;
362 }
363
364 auto const sqlType = static_cast<SQLSMALLINT>(maxLen > SqlOptimalMaxColumnSize ? SQL_LONGVARCHAR : SQL_VARCHAR);
365
366 return SQLBindParameter(stmt,
367 column,
368 SQL_PARAM_INPUT,
369 SQL_C_CHAR,
370 sqlType,
371 detail::NonZeroColumnSize(maxLen),
372 0,
373 (SQLPOINTER) values,
374 sizeof(AnsiStringType),
375 indicators);
376 }
377
378 /// Binds an inline fixed-capacity string column of a native row-wise batch zero-copy.
379 ///
380 /// Only available for fixed-capacity string types (those exposing @c ::Capacity, e.g.
381 /// @c SqlFixedString / @c SqlAnsiString), whose character buffer is stored inline in the object —
382 /// so each row's characters are addressed at @c Data(elem0) + i*rowStride and bound directly,
383 /// while per-row lengths are supplied through a temporary row-strided indicator buffer. Heap-backed
384 /// strings (e.g. @c std::string) do not satisfy the constraint and use the soft batch path.
385 ///
386 /// @note Like the column-major batch binder, this binds @c SQL_C_CHAR without the PostgreSQL
387 /// UTF-16 round-trip, so it is correct for ASCII/single-byte content (matching existing batch
388 /// semantics); non-ASCII payloads on PostgreSQL should use the per-row path.
389 /// @note PRE (guaranteed by the caller): row-wise binding with @c SQL_ATTR_PARAM_BIND_TYPE ==
390 /// @p rowStride and @c rowStride % alignof(SQLLEN) == 0.
391 static SQLRETURN BatchRowWiseInputParameter(SQLHSTMT stmt,
392 SQLUSMALLINT column,
393 AnsiStringType const* elem0,
394 std::size_t rowStride,
395 std::size_t rowCount,
396 SqlDataBinderCallback& cb) noexcept
397 requires requires { AnsiStringType::Capacity; }
398 {
399 auto const* sourceBytes = reinterpret_cast<std::byte const*>(elem0);
400 auto* const indicatorBytes = cb.ProvideBatchStagingBuffer(((rowCount - 1) * rowStride) + sizeof(SQLLEN));
401 for (auto const i: std::views::iota(std::size_t { 0 }, rowCount))
402 {
403 auto const& str = *reinterpret_cast<AnsiStringType const*>(sourceBytes + (i * rowStride));
404 *reinterpret_cast<SQLLEN*>(indicatorBytes + (i * rowStride)) = static_cast<SQLLEN>(StringTraits::Size(&str));
405 }
406
407 return BatchRowWiseBindInline(stmt, column, elem0, reinterpret_cast<SQLLEN*>(indicatorBytes));
408 }
409
410 /// Issues the row-wise @c SQLBindParameter for an inline fixed-capacity string array.
411 ///
412 /// @p elem0 points at row 0's string; the driver strides its character buffer by the statement's
413 /// @c SQL_ATTR_PARAM_BIND_TYPE. @p indicators is a caller-prepared row-strided buffer whose per-row
414 /// slot already holds the string length, or @c SQL_NULL_DATA for a NULL row. Shared by the
415 /// non-optional row-wise binder and the @c std::optional row-wise binder so the bind incantation
416 /// (and its @c Capacity / buffer-length arithmetic) lives in exactly one place.
417 static SQLRETURN BatchRowWiseBindInline(SQLHSTMT stmt,
418 SQLUSMALLINT column,
419 AnsiStringType const* elem0,
420 SQLLEN* indicators) noexcept
421 requires requires { AnsiStringType::Capacity; }
422 {
423 return SQLBindParameter(stmt,
424 column,
425 SQL_PARAM_INPUT,
426 SQL_C_CHAR,
427 SQL_VARCHAR,
428 AnsiStringType::Capacity,
429 0,
430 (SQLPOINTER) StringTraits::Data(elem0),
431 static_cast<SQLLEN>(AnsiStringType::Capacity) + 1,
432 indicators);
433 }
434
435 /// Binds an array of @c std::optional<fixed-capacity-string> in a native row-wise batch zero-copy.
436 ///
437 /// Mirrors @ref BatchRowWiseInputParameter but is NULL-aware: each row's indicator slot holds the
438 /// contained string's length when the optional is engaged, or @c SQL_NULL_DATA when disengaged. The
439 /// bind then targets row 0's contained character buffer and the driver strides by @p rowStride. Only
440 /// the address of the contained string is taken — disengaged storage is never read (its indicator is
441 /// @c SQL_NULL_DATA, so the driver ignores those bytes).
442 ///
443 /// This is the delegation target for @c SqlDataBinder<std::optional<T>>::BatchRowWiseInputParameter
444 /// when @p T is a length-bearing inline type (i.e. a fixed-capacity string).
445 ///
446 /// @note PRE (guaranteed by the caller): row-wise binding with @c SQL_ATTR_PARAM_BIND_TYPE ==
447 /// @p rowStride and @c rowStride % alignof(SQLLEN) == 0.
448 static SQLRETURN BatchRowWiseInputParameterOptional(SQLHSTMT stmt,
449 SQLUSMALLINT column,
450 std::optional<AnsiStringType> const* elem0,
451 std::size_t rowStride,
452 std::size_t rowCount,
453 SqlDataBinderCallback& cb) noexcept
454 requires requires { AnsiStringType::Capacity; }
455 {
456 using OptionalType = std::optional<AnsiStringType>;
457
458 // Offset of the contained string within the optional (see detail::OptionalValueOffset).
459 auto const valueOffset = detail::OptionalValueOffset<AnsiStringType>();
460
461 auto const* sourceBytes = reinterpret_cast<std::byte const*>(elem0);
462 auto* const indicatorBytes = cb.ProvideBatchStagingBuffer(((rowCount - 1) * rowStride) + sizeof(SQLLEN));
463 for (auto const i: std::views::iota(std::size_t { 0 }, rowCount))
464 {
465 auto const& optional = *reinterpret_cast<OptionalType const*>(sourceBytes + (i * rowStride));
466 *reinterpret_cast<SQLLEN*>(indicatorBytes + (i * rowStride)) =
467 optional.has_value() ? static_cast<SQLLEN>(StringTraits::Size(std::addressof(*optional)))
468 : SQLLEN { SQL_NULL_DATA };
469 }
470
471 // Bind at row 0's contained string buffer (address only; disengaged storage is never read).
472 auto const* containedRow0 = reinterpret_cast<AnsiStringType const*>(sourceBytes + valueOffset);
473 return BatchRowWiseBindInline(stmt, column, containedRow0, reinterpret_cast<SQLLEN*>(indicatorBytes));
474 }
475
476 static LIGHTWEIGHT_FORCE_INLINE SQLRETURN OutputColumn(
477 SQLHSTMT stmt, SQLUSMALLINT column, AnsiStringType* result, SQLLEN* indicator, SqlDataBinderCallback& cb) noexcept
478 {
479 // PostgreSQL: read narrow strings back through UTF-16 + SQL_C_WCHAR, mirroring InputParameter.
480 // psqlODBC on Windows transcodes SQL_C_CHAR data to the system codepage (cp1252), which mangles
481 // the UTF-8 bytes the write path stored (café -> caf? ?). Going via SQL_C_WCHAR and converting
482 // UTF-16 -> UTF-8 here keeps the narrow round-trip byte-exact under the library's UTF-8 convention.
483 // (Self-contained rather than reusing BindOutputColumnNonUtf16Unicode, which assumes a
484 // std::basic_string result and so does not cover SqlText / fixed-capacity narrow strings.)
485 if (cb.ServerType() == SqlServerType::POSTGRESQL)
486 {
487 auto u16String = std::make_shared<std::u16string>();
488 u16String->resize(StringTraits::Size(result) != 0 ? StringTraits::Size(result) : 255);
489
490 cb.PlanPostProcessOutputColumn([stmt, column, result, indicator, u16String]() {
491 if (*indicator == SQL_NULL_DATA)
492 u16String->clear();
493 else if (*indicator == SQL_NO_TOTAL)
494 ; // Length unknown; keep what the driver already wrote into the bound buffer.
495 else if (std::cmp_less_equal(*indicator, u16String->size() * sizeof(char16_t)))
496 u16String->resize(static_cast<size_t>(*indicator) / sizeof(char16_t));
497 else
498 {
499 // Truncation with a known total: grow and re-fetch the whole value.
500 auto const totalChars = static_cast<size_t>(*indicator) / sizeof(char16_t);
501 u16String->resize(totalChars + 1);
502 auto const rv = SQLGetData(stmt,
503 column,
504 SQL_C_WCHAR,
505 u16String->data(),
506 static_cast<SQLLEN>((totalChars + 1) * sizeof(char16_t)),
507 indicator);
508 (void) rv;
509 assert(SQL_SUCCEEDED(rv));
510 u16String->resize(totalChars);
511 }
512
513 auto const u8String = ToUtf8(*u16String);
514 // Resize may clamp to a fixed-capacity string's Capacity; copy only the post-clamp
515 // length so a UTF-8 expansion never overruns the inline buffer.
516 StringTraits::Resize(result, static_cast<SQLLEN>(u8String.size()));
517 std::memcpy(StringTraits::Data(result), u8String.data(), StringTraits::Size(result));
518
519 if constexpr (requires { StringTraits::PostProcessOutputColumn(result, *indicator); })
520 if (*indicator != SQL_NULL_DATA && *indicator != SQL_NO_TOTAL)
521 {
522 auto const syntheticIndicator = static_cast<SQLLEN>(StringTraits::Size(result));
523 StringTraits::PostProcessOutputColumn(result, syntheticIndicator);
524 }
525 });
526
527 return SQLBindCol(stmt,
528 column,
529 SQL_C_WCHAR,
530 static_cast<SQLPOINTER>(u16String->data()),
531 static_cast<SQLLEN>(u16String->size() * sizeof(char16_t)),
532 indicator);
533 }
534
535 if constexpr (requires { AnsiStringType::Capacity; })
536 StringTraits::Resize(result, AnsiStringType::Capacity);
537 else if (StringTraits::Size(result) == 0)
538 StringTraits::Resize(result, 255);
539
540 if constexpr (requires { StringTraits::PostProcessOutputColumn(result, *indicator); })
541 cb.PlanPostProcessOutputColumn(
542 [indicator, result]() { StringTraits::PostProcessOutputColumn(result, *indicator); });
543 else
544 cb.PlanPostProcessOutputColumn(
545 [stmt, column, indicator, result]() { PostProcessOutputColumn(stmt, column, result, indicator); });
546
547 // Per ODBC spec, BufferLength for SQL_C_CHAR must include space for the
548 // null terminator. SqlFixedString<N> backs its data with _data[N+1] for
549 // exactly this reason; pass Capacity+1 to let the driver write a full
550 // N-char value plus null. Dynamic strings keep the current behaviour.
551 SQLLEN const bufferLength = [&]() -> SQLLEN {
552 if constexpr (requires { AnsiStringType::Capacity; })
553 return static_cast<SQLLEN>(AnsiStringType::Capacity) + 1;
554 else
555 return static_cast<SQLLEN>(StringTraits::Size(result));
556 }();
557
558 return SQLBindCol(stmt, column, SQL_C_CHAR, (SQLPOINTER) StringTraits::Data(result), bufferLength, indicator);
559 }
560
561 static void PostProcessOutputColumn(SQLHSTMT stmt, SQLUSMALLINT column, AnsiStringType* result, SQLLEN* indicator)
562 {
563 // Now resize the string to the actual length of the data
564 // NB: If the indicator is greater than the buffer size, we have a truncation.
565 if (*indicator == SQL_NO_TOTAL)
566 {
567 // We have a truncation and the server does not know how much data is left.
568 StringTraits::Resize(result, static_cast<SQLLEN>(StringTraits::Size(result)) - 1);
569 }
570 else if (*indicator == SQL_NULL_DATA)
571 {
572 // We have a NULL value
573 StringTraits::Resize(result, 0);
574 }
575 else if (*indicator <= static_cast<SQLLEN>(StringTraits::Size(result)))
576 {
577 StringTraits::Resize(result, *indicator);
578 }
579 else
580 {
581 // We have a truncation and the server knows how much data is left.
582 // Extend the buffer and fetch the rest via SQLGetData.
583
584 auto const totalCharsRequired = *indicator;
585 StringTraits::Resize(result, totalCharsRequired + 1);
586 auto const sqlResult =
587 SQLGetData(stmt, column, SQL_C_CHAR, StringTraits::Data(result), totalCharsRequired + 1, indicator);
588 (void) sqlResult;
589 assert(SQL_SUCCEEDED(sqlResult));
590 assert(*indicator == totalCharsRequired);
591 StringTraits::Resize(result, totalCharsRequired);
592 }
593 }
594
595 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
596 static SQLRETURN GetColumn(SQLHSTMT stmt,
597 SQLUSMALLINT column,
598 AnsiStringType* result,
599 SQLLEN* indicator,
600 SqlDataBinderCallback const& cb) noexcept
601 {
602 // PostgreSQL: fetch narrow strings via UTF-16 + SQL_C_WCHAR and convert to UTF-8, symmetric
603 // with InputParameter / OutputColumn. A raw SQL_C_CHAR fetch would come back transcoded to
604 // the Windows system codepage (cp1252) by psqlODBC, mangling the stored UTF-8 bytes.
605 if (cb.ServerType() == SqlServerType::POSTGRESQL)
606 {
607 auto u16String = std::u16string {};
608 auto const sqlResult = detail::GetColumnUtf16(stmt, column, &u16String, indicator, cb);
609 if (!SQL_SUCCEEDED(sqlResult))
610 return sqlResult;
611
612 auto const u8String = ToUtf8(u16String);
613 // Resize may clamp to a fixed-capacity string's Capacity, so copy only what fits
614 // (StringTraits::Size reflects the post-clamp length) to avoid overrunning the buffer.
615 StringTraits::Resize(result, static_cast<SQLLEN>(u8String.size()));
616 std::memcpy(StringTraits::Data(result), u8String.data(), StringTraits::Size(result));
617 // Apply the type's post-processing (e.g. FIXED_SIZE_RIGHT_TRIMMED strips the CHAR(N)
618 // padding the server returned), matching the non-PostgreSQL GetColumn path. The
619 // synthetic indicator is the result's own byte count.
620 if constexpr (requires { StringTraits::PostProcessOutputColumn(result, *indicator); })
621 if (*indicator != SQL_NULL_DATA && *indicator != SQL_NO_TOTAL)
622 StringTraits::PostProcessOutputColumn(result, static_cast<SQLLEN>(StringTraits::Size(result)));
623 return sqlResult;
624 }
625
626 if constexpr (requires { AnsiStringType::Capacity; })
627 {
628 StringTraits::Resize(result, AnsiStringType::Capacity);
629 // BufferLength for SQL_C_CHAR must include space for the null terminator;
630 // _data is sized N+1 to accommodate it.
631 SQLRETURN const rv =
632 SQLGetData(stmt, column, SQL_C_CHAR, StringTraits::Data(result), AnsiStringType::Capacity + 1, indicator);
633 if (rv == SQL_SUCCESS || rv == SQL_NO_DATA)
634 {
635 if (*indicator == SQL_NULL_DATA)
636 StringTraits::Resize(result, 0);
637 else if (*indicator != SQL_NO_TOTAL)
638 StringTraits::Resize(
639 result, static_cast<SQLLEN>((std::min) (AnsiStringType::Capacity, static_cast<size_t>(*indicator))));
640 }
641 if constexpr (requires { StringTraits::PostProcessOutputColumn(result, *indicator); })
642 StringTraits::PostProcessOutputColumn(result, *indicator);
643 return rv;
644 }
645 else
646 {
647 StringTraits::Reserve(result, detail::InitialCharBufferSize(stmt, column));
648 size_t writeIndex = 0;
649 *indicator = 0;
650 while (true)
651 {
652 auto* const bufferStart = StringTraits::Data(result) + writeIndex;
653 size_t const bufferSize = StringTraits::Size(result) - writeIndex;
654 SQLRETURN const rv =
655 SQLGetData(stmt, column, SQL_C_CHAR, bufferStart, static_cast<SQLLEN>(bufferSize), indicator);
656 switch (rv)
657 {
658 case SQL_SUCCESS:
659 case SQL_NO_DATA:
660 // last successive call
661 if (*indicator != SQL_NULL_DATA)
662 {
663 StringTraits::Resize(result, static_cast<SQLLEN>(writeIndex) + *indicator);
664 *indicator = static_cast<SQLLEN>(StringTraits::Size(result));
665 }
666 return SQL_SUCCESS;
667 case SQL_SUCCESS_WITH_INFO: {
668 // more data pending
669 if (*indicator == SQL_NO_TOTAL)
670 {
671 // We have a truncation and the server does not know how much data is left.
672 writeIndex += bufferSize - 1;
673 StringTraits::Resize(result, static_cast<SQLLEN>((2 * writeIndex) + 1));
674 }
675 else if (std::cmp_greater_equal(*indicator, bufferSize))
676 {
677 // We have a truncation and the server knows how much data is left.
678 writeIndex += bufferSize - 1;
679 StringTraits::Resize(result, static_cast<SQLLEN>(writeIndex) + *indicator);
680 }
681 else
682 {
683 // We have no truncation and the server knows how much data is left.
684 StringTraits::Resize(result, static_cast<SQLLEN>(writeIndex) + *indicator - 1);
685 return SQL_SUCCESS;
686 }
687 break;
688 }
689 default:
690 if constexpr (requires { StringTraits::PostProcessOutputColumn(result, *indicator); })
691 StringTraits::PostProcessOutputColumn(result, *indicator);
692 return rv;
693 }
694 }
695 }
696 }
697
698 static LIGHTWEIGHT_FORCE_INLINE std::string_view Inspect(AnsiStringType const& value) noexcept
699 {
700 return { StringTraits::Data(&value), StringTraits::Size(&value) };
701 }
702};
703
704// SqlDataBinder<> specialization for UTF-16 strings
705template <typename Utf16StringType>
706 requires(SqlBasicStringBinderConcept<Utf16StringType, char16_t>
707 || (SqlBasicStringBinderConcept<Utf16StringType, unsigned short>)
708 || (SqlBasicStringBinderConcept<Utf16StringType, wchar_t> && sizeof(wchar_t) == 2))
709struct SqlDataBinder<Utf16StringType>
710{
711 using ValueType = Utf16StringType;
712 using CharType = std::remove_cvref_t<decltype(std::declval<Utf16StringType>()[0])>;
713 using StringTraits = SqlBasicStringOperations<Utf16StringType>;
714
715 static constexpr auto ColumnType = StringTraits::ColumnType;
716
717 static constexpr auto CType = SQL_C_WCHAR;
718
719 static SQLRETURN InputParameter(SQLHSTMT stmt,
720 SQLUSMALLINT column,
721 Utf16StringType const& value,
722 SqlDataBinderCallback& cb) noexcept
723 {
724 // Bind UTF-16 input directly via SQL_C_WCHAR for every backend; the driver
725 // converts to the server's encoding, including correct handling of UTF-16
726 // surrogate pairs for supplementary-plane code points (emoji, CJK extension B+).
727 using CharType = StringTraits::CharType;
728 auto const* data = StringTraits::Data(&value);
729 auto const sizeInBytes = StringTraits::Size(&value) * sizeof(CharType);
730 auto const charCount = StringTraits::Size(&value);
731 auto const sqlType = static_cast<SQLSMALLINT>(charCount > SqlOptimalMaxColumnSize ? SQL_WLONGVARCHAR : SQL_WVARCHAR);
732
733 SQLLEN* indicator = cb.ProvideInputIndicator();
734 *indicator = static_cast<SQLLEN>(sizeInBytes);
735
736 return SQLBindParameter(stmt,
737 column,
738 SQL_PARAM_INPUT,
739 CType,
740 sqlType,
741 detail::NonZeroColumnSize(charCount),
742 0,
743 (SQLPOINTER) data,
744 static_cast<SQLLEN>(sizeInBytes),
745 indicator);
746 }
747
748 static SQLRETURN OutputColumn(
749 SQLHSTMT stmt, SQLUSMALLINT column, Utf16StringType* result, SQLLEN* indicator, SqlDataBinderCallback& cb) noexcept
750 {
751 if constexpr (requires { Utf16StringType::Capacity; })
752 StringTraits::Resize(result, Utf16StringType::Capacity);
753 else if (StringTraits::Size(result) == 0)
754 StringTraits::Resize(result, 255);
755
756 if constexpr (requires { StringTraits::PostProcessOutputColumn(result, *indicator); })
757 {
758 cb.PlanPostProcessOutputColumn(
759 [indicator, result]() { StringTraits::PostProcessOutputColumn(result, *indicator); });
760 }
761 else
762 {
763 cb.PlanPostProcessOutputColumn([stmt, column, indicator, result]() {
764 // Now resize the string to the actual length of the data
765 // NB: If the indicator is greater than the buffer size, we have a truncation.
766 if (*indicator == SQL_NULL_DATA)
767 StringTraits::Resize(result, 0);
768 else if (*indicator == SQL_NO_TOTAL)
769 ; // We don't know the size of the data
770 else if (*indicator <= static_cast<SQLLEN>(result->size() * sizeof(char16_t)))
771 result->resize(static_cast<size_t>(*indicator) / sizeof(char16_t));
772 else
773 {
774 auto const totalCharsRequired = static_cast<size_t>(*indicator) / sizeof(char16_t);
775 *indicator += sizeof(char16_t); // Add space to hold the null terminator
776 result->resize(totalCharsRequired);
777 auto const sqlResult = SQLGetData(stmt, column, SQL_C_WCHAR, result->data(), *indicator, indicator);
778 (void) sqlResult;
779 assert(SQL_SUCCEEDED(sqlResult));
780 assert(std::cmp_equal(*indicator, totalCharsRequired * sizeof(char16_t)));
781 }
782 });
783 }
784 return SQLBindCol(stmt,
785 column,
786 CType,
787 (SQLPOINTER) StringTraits::Data(result),
788 (SQLLEN) ((StringTraits::Size(result) + 1) * sizeof(CharType)),
789 indicator);
790 }
791
792 static SQLRETURN GetColumn(SQLHSTMT stmt,
793 SQLUSMALLINT column,
794 Utf16StringType* result,
795 SQLLEN* indicator,
796 SqlDataBinderCallback const& cb) noexcept
797 {
798 return detail::GetColumnUtf16(stmt, column, result, indicator, cb);
799 }
800
801 static LIGHTWEIGHT_FORCE_INLINE std::string Inspect(Utf16StringType const& value) noexcept
802 {
803 auto u8String = ToUtf8(detail::SqlViewHelper<Utf16StringType>::View(value));
804 return std::string(reinterpret_cast<char const*>(u8String.data()), u8String.size());
805 }
806};
807
808// SqlDataBinder<> specialization for UTF-32 strings
809template <typename Utf32StringType>
810 requires(SqlBasicStringBinderConcept<Utf32StringType, char32_t>
811 || (SqlBasicStringBinderConcept<Utf32StringType, uint32_t>)
812 || (SqlBasicStringBinderConcept<Utf32StringType, wchar_t> && sizeof(wchar_t) == 4))
813struct SqlDataBinder<Utf32StringType>
814{
815 using ValueType = Utf32StringType;
816 using CharType = Utf32StringType::value_type;
817 using StringTraits = SqlBasicStringOperations<Utf32StringType>;
818
819 static constexpr auto ColumnType = StringTraits::ColumnType;
820
821 static SQLRETURN InputParameter(SQLHSTMT stmt,
822 SQLUSMALLINT column,
823 Utf32StringType const& value,
824 SqlDataBinderCallback& cb) noexcept
825 {
826 // Always go via UTF-16 + SQL_C_WCHAR; the driver handles encoding conversion
827 // for the target server.
828 auto u16String = std::make_shared<std::u16string>(ToUtf16(detail::SqlViewHelper<Utf32StringType>::View(value)));
829 cb.PlanPostExecuteCallback([u16String = u16String]() {}); // Keep the string alive
830 auto const* data = u16String->data();
831 auto const charCount = u16String->size();
832 auto const sizeInBytes = u16String->size() * sizeof(char16_t);
833 auto const CType = SQLSMALLINT { SQL_C_WCHAR };
834 auto const sqlType = static_cast<SQLSMALLINT>(charCount > SqlOptimalMaxColumnSize ? SQL_WLONGVARCHAR : SQL_WVARCHAR);
835
836 SQLLEN* indicator = cb.ProvideInputIndicator();
837 *indicator = static_cast<SQLLEN>(sizeInBytes);
838
839 return SQLBindParameter(stmt,
840 column,
841 SQL_PARAM_INPUT,
842 CType,
843 sqlType,
844 detail::NonZeroColumnSize(charCount),
845 0,
846 (SQLPOINTER) data,
847 static_cast<SQLLEN>(sizeInBytes),
848 indicator);
849 }
850
851 static SQLRETURN OutputColumn(
852 SQLHSTMT stmt, SQLUSMALLINT column, Utf32StringType* result, SQLLEN* indicator, SqlDataBinderCallback& cb) noexcept
853 {
854 return detail::BindOutputColumnNonUtf16Unicode<Utf32StringType>(stmt, column, result, indicator, cb);
855 }
856
857 static SQLRETURN GetColumn(SQLHSTMT stmt,
858 SQLUSMALLINT column,
859 Utf32StringType* result,
860 SQLLEN* indicator,
861 SqlDataBinderCallback const& cb) noexcept
862 {
863 auto u16String = std::u16string {};
864 auto const sqlResult = detail::GetColumnUtf16(stmt, column, &u16String, indicator, cb);
865 if (!SQL_SUCCEEDED(sqlResult))
866 return sqlResult;
867
868 auto const u32String = ToUtf32(u16String);
869 StringTraits::Resize(result, static_cast<SQLLEN>(u32String.size()));
870 std::copy_n((CharType const*) u32String.data(), u32String.size(), StringTraits::Data(result));
871
872 return sqlResult;
873 }
874
875 static LIGHTWEIGHT_FORCE_INLINE std::string Inspect(Utf32StringType const& value) noexcept
876 {
877 auto u8String = ToUtf8(detail::SqlViewHelper<Utf32StringType>::View(value));
878 return std::string(reinterpret_cast<char const*>(u8String.data()), u8String.size());
879 }
880};
881
882// SqlDataBinder<> specialization for UTF-8 strings
883template <typename Utf8StringType>
884 requires SqlBasicStringBinderConcept<Utf8StringType, char8_t>
885struct SqlDataBinder<Utf8StringType>
886{
887 using ValueType = Utf8StringType;
888 using CharType = char8_t;
889 using StringTraits = SqlBasicStringOperations<Utf8StringType>;
890
891 static constexpr auto ColumnType = StringTraits::ColumnType;
892
893 static SQLRETURN InputParameter(SQLHSTMT stmt,
894 SQLUSMALLINT column,
895 Utf8StringType const& value,
896 SqlDataBinderCallback& cb) noexcept
897 {
898 // Always go via UTF-16 + SQL_C_WCHAR; the driver handles encoding conversion
899 // for the target server.
900 auto u16String = std::make_shared<std::u16string>(ToUtf16(detail::SqlViewHelper<Utf8StringType>::View(value)));
901 cb.PlanPostExecuteCallback([u16String = u16String]() {}); // Keep the string alive
902
903 auto const CType = SQL_C_WCHAR;
904 auto const charCount = u16String->size();
905 auto const byteCount = u16String->size() * sizeof(char16_t);
906 auto const sqlType = static_cast<SQLSMALLINT>(charCount > SqlOptimalMaxColumnSize ? SQL_WLONGVARCHAR : SQL_WVARCHAR);
907
908 SQLLEN* indicator = cb.ProvideInputIndicator();
909 *indicator = static_cast<SQLLEN>(byteCount);
910
911 return SQLBindParameter(stmt,
912 column,
913 SQL_PARAM_INPUT,
914 CType,
915 sqlType,
916 detail::NonZeroColumnSize(charCount),
917 0,
918 (SQLPOINTER) u16String->data(),
919 static_cast<SQLLEN>(byteCount),
920 indicator);
921 }
922
923 static SQLRETURN OutputColumn(
924 SQLHSTMT stmt, SQLUSMALLINT column, Utf8StringType* result, SQLLEN* indicator, SqlDataBinderCallback& cb) noexcept
925 {
926 return detail::BindOutputColumnNonUtf16Unicode<Utf8StringType>(stmt, column, result, indicator, cb);
927 }
928
929 static SQLRETURN GetColumn(SQLHSTMT stmt,
930 SQLUSMALLINT column,
931 Utf8StringType* result,
932 SQLLEN* indicator,
933 SqlDataBinderCallback const& cb) noexcept
934 {
935 auto u16String = std::u16string {};
936 u16String.resize(result->size());
937 auto const sqlReturn = detail::GetColumnUtf16(stmt, column, &u16String, indicator, cb);
938 if (SQL_SUCCEEDED(sqlReturn))
939 *result = ToUtf8(u16String);
940 return sqlReturn;
941 }
942
943 static LIGHTWEIGHT_FORCE_INLINE std::string Inspect(Utf8StringType const& value) noexcept
944 {
945 // Pass data as-is
946 return std::string(reinterpret_cast<char const*>(value.data()), value.size());
947 }
948};
949
950} // namespace Lightweight
T ToUtf32(std::u8string_view u8InputString)
LIGHTWEIGHT_API std::u8string ToUtf8(std::u32string_view u32InputString)
std::u16string ToUtf16(std::basic_string_view< T > const u32InputString)