@@ -234,7 +234,7 @@ crow::json::wvalue QueryResult::convertVectorEntryToJson(const duckdb_vector &ve
234234 case DUCKDB_TYPE_BIGINT :
235235 return convertVectorEntryToJson<std::int64_t >(vector, row_idx);
236236 case DUCKDB_TYPE_HUGEINT :
237- return convertVectorEntryToJson<std:: int64_t > (vector, row_idx);
237+ return convertVectorHugeintToJson (vector, row_idx);
238238 case DUCKDB_TYPE_FLOAT :
239239 return convertVectorEntryToJson<float >(vector, row_idx);
240240 case DUCKDB_TYPE_DOUBLE :
@@ -278,13 +278,13 @@ crow::json::wvalue QueryResult::convertVectorEntryToJson(const duckdb_vector &ve
278278 case DUCKDB_TYPE_UBIGINT :
279279 return convertVectorEntryToJson<std::uint64_t >(vector, row_idx);
280280 case DUCKDB_TYPE_UHUGEINT :
281- return convertVectorEntryToJson<std:: uint64_t > (vector, row_idx); // Treat as uint64
281+ return convertVectorUhugeintToJson (vector, row_idx);
282282 case DUCKDB_TYPE_BLOB :
283- return convertVectorVarcharToJson (vector, row_idx); // Treat as string for JSON
283+ return convertVectorBlobToJson (vector, row_idx);
284284 case DUCKDB_TYPE_UUID :
285- return convertVectorVarcharToJson (vector, row_idx); // Treat as string for JSON
285+ return convertVectorUuidToJson (vector, row_idx);
286286 case DUCKDB_TYPE_BIT :
287- return convertVectorVarcharToJson (vector, row_idx); // Treat as string for JSON
287+ return convertVectorBitToJson (vector, row_idx);
288288 case DUCKDB_TYPE_MAP :
289289 return convertVectorMapToJson (vector, row_idx);
290290 case DUCKDB_TYPE_ARRAY :
@@ -422,6 +422,143 @@ crow::json::wvalue QueryResult::convertVectorEnumToJson(const duckdb_vector &vec
422422 return crow::json::wvalue (str_val);
423423}
424424
425+ crow::json::wvalue QueryResult::convertVectorUuidToJson (const duckdb_vector &vector, const idx_t row_idx) {
426+ auto validity = duckdb_vector_get_validity (vector);
427+ if (!duckdb_validity_row_is_valid (validity, row_idx)) {
428+ return crow::json::wvalue (nullptr );
429+ }
430+
431+ // UUID is physically a 128-bit integer (hugeint), NOT a string — reading
432+ // it via the VARCHAR path dereferences garbage and crashes (issue #89).
433+ // Format it the way DuckDB's UUID::ToString does: flip the stored MSB
434+ // back, then emit the canonical 8-4-4-4-12 lowercase hex form.
435+ auto data = static_cast <duckdb_hugeint *>(duckdb_vector_get_data (vector));
436+ auto value = data[row_idx];
437+ uint64_t upper = static_cast <uint64_t >(value.upper ) ^ (uint64_t (1 ) << 63 );
438+ uint64_t lower = value.lower ;
439+
440+ static const char *HEX = " 0123456789abcdef" ;
441+ char buf[36 ];
442+ idx_t pos = 0 ;
443+ auto byte_to_hex = [&](uint64_t byte_val) {
444+ buf[pos++] = HEX [(byte_val >> 4 ) & 0xf ];
445+ buf[pos++] = HEX [byte_val & 0xf ];
446+ };
447+ byte_to_hex ((upper >> 56 ) & 0xFF );
448+ byte_to_hex ((upper >> 48 ) & 0xFF );
449+ byte_to_hex ((upper >> 40 ) & 0xFF );
450+ byte_to_hex ((upper >> 32 ) & 0xFF );
451+ buf[pos++] = ' -' ;
452+ byte_to_hex ((upper >> 24 ) & 0xFF );
453+ byte_to_hex ((upper >> 16 ) & 0xFF );
454+ buf[pos++] = ' -' ;
455+ byte_to_hex ((upper >> 8 ) & 0xFF );
456+ byte_to_hex (upper & 0xFF );
457+ buf[pos++] = ' -' ;
458+ byte_to_hex ((lower >> 56 ) & 0xFF );
459+ byte_to_hex ((lower >> 48 ) & 0xFF );
460+ buf[pos++] = ' -' ;
461+ byte_to_hex ((lower >> 40 ) & 0xFF );
462+ byte_to_hex ((lower >> 32 ) & 0xFF );
463+ byte_to_hex ((lower >> 24 ) & 0xFF );
464+ byte_to_hex ((lower >> 16 ) & 0xFF );
465+ byte_to_hex ((lower >> 8 ) & 0xFF );
466+ byte_to_hex (lower & 0xFF );
467+
468+ return crow::json::wvalue (std::string (buf, pos));
469+ }
470+
471+ crow::json::wvalue QueryResult::convertVectorHugeintToJson (const duckdb_vector &vector, const idx_t row_idx) {
472+ auto validity = duckdb_vector_get_validity (vector);
473+ if (!duckdb_validity_row_is_valid (validity, row_idx)) {
474+ return crow::json::wvalue (nullptr );
475+ }
476+
477+ // HUGEINT is 128-bit: reading it as int64 truncates and mis-strides for
478+ // multi-row chunks (issue #89). Emit the exact decimal as a JSON string so
479+ // values beyond 2^63 survive (JSON consumers lose precision above 2^53).
480+ auto data = static_cast <duckdb_hugeint *>(duckdb_vector_get_data (vector));
481+ auto value = duckdb_create_hugeint (data[row_idx]);
482+ DuckDBString str (duckdb_value_to_string (value));
483+ duckdb_destroy_value (&value);
484+ return crow::json::wvalue (str.to_string ());
485+ }
486+
487+ crow::json::wvalue QueryResult::convertVectorUhugeintToJson (const duckdb_vector &vector, const idx_t row_idx) {
488+ auto validity = duckdb_vector_get_validity (vector);
489+ if (!duckdb_validity_row_is_valid (validity, row_idx)) {
490+ return crow::json::wvalue (nullptr );
491+ }
492+
493+ // UHUGEINT is unsigned 128-bit; emit as a decimal string for the same
494+ // precision reasons as HUGEINT.
495+ auto data = static_cast <duckdb_uhugeint *>(duckdb_vector_get_data (vector));
496+ auto value = duckdb_create_uhugeint (data[row_idx]);
497+ DuckDBString str (duckdb_value_to_string (value));
498+ duckdb_destroy_value (&value);
499+ return crow::json::wvalue (str.to_string ());
500+ }
501+
502+ crow::json::wvalue QueryResult::convertVectorBlobToJson (const duckdb_vector &vector, const idx_t row_idx) {
503+ auto validity = duckdb_vector_get_validity (vector);
504+ if (!duckdb_validity_row_is_valid (validity, row_idx)) {
505+ return crow::json::wvalue (nullptr );
506+ }
507+
508+ // A BLOB's bytes are arbitrary binary; emitting them raw via the VARCHAR
509+ // path can produce invalid UTF-8 / invalid JSON. Render DuckDB's own blob
510+ // string form (printable bytes as-is, others as \xNN), matching to_json /
511+ // CAST AS VARCHAR. (duckdb_value_to_string would add a '...'::BLOB wrapper.)
512+ auto data = static_cast <duckdb_string_t *>(duckdb_vector_get_data (vector));
513+ auto length = duckdb_string_is_inlined (data[row_idx])
514+ ? data[row_idx].value .inlined .length
515+ : data[row_idx].value .pointer .length ;
516+ auto bytes = duckdb_string_is_inlined (data[row_idx])
517+ ? reinterpret_cast <const uint8_t *>(data[row_idx].value .inlined .inlined )
518+ : reinterpret_cast <const uint8_t *>(data[row_idx].value .pointer .ptr );
519+
520+ static const char *HEX = " 0123456789ABCDEF" ;
521+ std::string out;
522+ out.reserve (length);
523+ for (idx_t i = 0 ; i < length; i++) {
524+ uint8_t c = bytes[i];
525+ bool regular = c >= 32 && c <= 126 && c != ' \\ ' && c != ' \' ' && c != ' "' ;
526+ if (regular) {
527+ out.push_back (static_cast <char >(c));
528+ } else {
529+ out.push_back (' \\ ' );
530+ out.push_back (' x' );
531+ out.push_back (HEX [(c >> 4 ) & 0xF ]);
532+ out.push_back (HEX [c & 0xF ]);
533+ }
534+ }
535+ return crow::json::wvalue (out);
536+ }
537+
538+ crow::json::wvalue QueryResult::convertVectorBitToJson (const duckdb_vector &vector, const idx_t row_idx) {
539+ auto validity = duckdb_vector_get_validity (vector);
540+ if (!duckdb_validity_row_is_valid (validity, row_idx)) {
541+ return crow::json::wvalue (nullptr );
542+ }
543+
544+ // BIT is stored as its internal bitstring blob; the VARCHAR path renders
545+ // that raw storage (padding byte + bytes) as garbage. Round-trip through a
546+ // BIT value so it serializes as a "0101" string, matching to_json.
547+ auto data = static_cast <duckdb_string_t *>(duckdb_vector_get_data (vector));
548+ auto length = duckdb_string_is_inlined (data[row_idx])
549+ ? data[row_idx].value .inlined .length
550+ : data[row_idx].value .pointer .length ;
551+ auto bytes = duckdb_string_is_inlined (data[row_idx])
552+ ? reinterpret_cast <const uint8_t *>(data[row_idx].value .inlined .inlined )
553+ : reinterpret_cast <const uint8_t *>(data[row_idx].value .pointer .ptr );
554+
555+ duckdb_bit bit { const_cast <uint8_t *>(bytes), length };
556+ auto value = duckdb_create_bit (bit);
557+ DuckDBString str (duckdb_value_to_string (value));
558+ duckdb_destroy_value (&value);
559+ return crow::json::wvalue (str.to_string ());
560+ }
561+
425562crow::json::wvalue QueryResult::convertVectorListToJson (const duckdb_vector &vector, const idx_t row_idx) {
426563 auto validity = duckdb_vector_get_validity (vector);
427564 if (!duckdb_validity_row_is_valid (validity, row_idx)) {
0 commit comments