Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/include/query_executor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class QueryResult {
static crow::json::wvalue convertVectorUnionToJson(const duckdb_vector &vector, const idx_t row_idx);
static crow::json::wvalue convertVectorMapToJson(const duckdb_vector &vector, const idx_t row_idx);
static std::string vectorEntryToMapKey(const duckdb_vector &vector, const idx_t row_idx);
// Universal C++-API fallback for types without a dedicated C-API reader.
static crow::json::wvalue convertVectorValueToJson(const duckdb_vector &vector, const idx_t row_idx);

template<typename T>
static crow::json::wvalue convertVectorEntryToJson(const duckdb_vector &vector, const idx_t row_idx) {
Expand Down
48 changes: 45 additions & 3 deletions src/query_executor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
#include <sstream>
#include <stdexcept>

// The C++ API is used as a universal fallback for exotic column types
// (BIGNUM/VARINT, GEOMETRY, VARIANT, ...) that have no dedicated C-API
// reader. A duckdb_vector handle is just a reinterpret_cast of a
// duckdb::Vector*, so dropping to Vector::GetValue() is a safe round-trip.
#include "duckdb.hpp"

namespace flapi {

QueryExecutor::QueryExecutor(duckdb_database db) : has_result(false) {
Expand Down Expand Up @@ -291,9 +297,12 @@ crow::json::wvalue QueryResult::convertVectorEntryToJson(const duckdb_vector &ve
return convertVectorArrayToJson(vector, row_idx);
case DUCKDB_TYPE_UNION:
return convertVectorUnionToJson(vector, row_idx);
default:
CROW_LOG_WARNING << "Unknown type: " << type_id;
return crow::json::wvalue(nullptr);
case DUCKDB_TYPE_BIGNUM: // arbitrary-precision integer (VARINT)
case DUCKDB_TYPE_VARIANT: // dynamically-typed value
default: // GEOMETRY and any future/unknown type
// No dedicated C-API reader; use DuckDB's own value
// stringification via the C++ fallback.
return convertVectorValueToJson(vector, row_idx);
}
}

Expand Down Expand Up @@ -731,4 +740,37 @@ crow::json::wvalue QueryResult::convertVectorMapToJson(const duckdb_vector &vect
return result;
}

crow::json::wvalue QueryResult::convertVectorValueToJson(const duckdb_vector &vector, const idx_t row_idx) {
// duckdb_vector is a reinterpret_cast of duckdb::Vector* (see the C API's
// data_chunk-c.cpp), so we can borrow the C++ Vector to let DuckDB itself
// produce the value's canonical string — covering BIGNUM/VARINT, GEOMETRY,
// VARIANT and any type lacking a dedicated reader. Wrapped in try/catch so
// an unconvertible extension value degrades to null instead of crashing.
try {
auto *vec = reinterpret_cast<duckdb::Vector *>(vector);
duckdb::Value value = vec->GetValue(row_idx);
if (value.IsNull()) {
return crow::json::wvalue(nullptr);
}

// A VARIANT whose string form is already JSON (the common case for
// JSON-derived values) is embedded as nested JSON so callers don't have
// to parse twice. Other VARIANT renderings (DuckDB's SQL-ish form, e.g.
// {'a': 1}) and every other fallback type are emitted as a string.
if (value.type().id() == duckdb::LogicalTypeId::VARIANT) {
auto str = value.ToString();
auto parsed = crow::json::load(str);
if (parsed) {
return crow::json::wvalue(parsed);
}
return crow::json::wvalue(str);
}

return crow::json::wvalue(value.ToString());
} catch (const std::exception &e) {
CROW_LOG_WARNING << "Unable to serialize value at row " << row_idx << ": " << e.what();
return crow::json::wvalue(nullptr);
}
}

} // namespace flapi
66 changes: 66 additions & 0 deletions test/cpp/query_executor_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,72 @@ TEST_CASE("QueryExecutor type coverage", "[query_executor]") {
duckdb_close(&database);
}

TEST_CASE("QueryExecutor exotic type coverage", "[query_executor][exotic_types]") {
// Regression for the issue #89 follow-up: VARINT/BIGNUM and VARIANT used
// to serialize as null (no dedicated reader). They now round-trip via the
// C++ Value fallback. GEOMETRY uses the same path (verified manually; not
// tested here as it needs the spatial extension download).
duckdb_database database;
REQUIRE(duckdb_open(NULL, &database) == DuckDBSuccess);

SECTION("VARINT emits exact decimal string (lossless)") {
QueryExecutor executor(database);
executor.execute(R"SQL(
SELECT * FROM (VALUES
(1, '123456789012345678901234567890'::VARINT),
(2, (-5)::VARINT),
(3, '0'::VARINT)
) t(id, v) ORDER BY id
)SQL");
auto doc = crow::json::load(executor.toJson().dump());
REQUIRE(doc.size() == 3);
REQUIRE(doc[0]["v"].t() == crow::json::type::String);
REQUIRE(doc[0]["v"].s() == "123456789012345678901234567890");
REQUIRE(doc[1]["v"].s() == "-5");
REQUIRE(doc[2]["v"].s() == "0");
}

SECTION("VARIANT object is embedded as nested JSON") {
QueryExecutor executor(database);
executor.execute(R"SQL(SELECT '{"a":1,"b":[2,3]}'::VARIANT AS v)SQL");
auto doc = crow::json::load(executor.toJson().dump());
REQUIRE(doc.size() == 1);
REQUIRE(doc[0]["v"].t() == crow::json::type::Object);
REQUIRE(doc[0]["v"]["a"].i() == 1);
REQUIRE(doc[0]["v"]["b"].size() == 2);
REQUIRE(doc[0]["v"]["b"][1].i() == 3);
}

SECTION("struct-origin VARIANT serializes (as DuckDB's string form)") {
// A VARIANT built from a struct stringifies as {'k': v} (single
// quotes), which isn't JSON — so it degrades to a string rather than
// null. (JSON-derived VARIANTs embed as nested JSON, see above.)
QueryExecutor executor(database);
executor.execute("SELECT {'test': 123, 'arr': [1, 2]}::VARIANT AS v");
auto doc = crow::json::load(executor.toJson().dump());
REQUIRE(doc.size() == 1);
REQUIRE(doc[0]["v"].t() == crow::json::type::String);
REQUIRE(std::string(doc[0]["v"].s()).find("test") != std::string::npos);
}

SECTION("VARIANT scalar is embedded as its JSON value") {
QueryExecutor executor(database);
executor.execute("SELECT 42::VARIANT AS v");
auto doc = crow::json::load(executor.toJson().dump());
REQUIRE(doc[0]["v"].i() == 42);
}

SECTION("NULL VARINT / VARIANT stay null") {
QueryExecutor executor(database);
executor.execute("SELECT CAST(NULL AS VARINT) AS a, CAST(NULL AS VARIANT) AS b");
auto doc = crow::json::load(executor.toJson().dump());
REQUIRE(doc[0]["a"].t() == crow::json::type::Null);
REQUIRE(doc[0]["b"].t() == crow::json::type::Null);
}

duckdb_close(&database);
}

TEST_CASE("QueryExecutor scalar type coverage", "[query_executor][scalar_types]") {
// Regression for issue #89 type audit: UUID previously crashed (read as a
// string pointer), HUGEINT/UHUGEINT silently truncated, BLOB/BIT emitted
Expand Down
Loading