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