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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Traditional Chinese (繁體中文) language in Settings > General with full UI translation

### Fixed

- DuckDB VARIANT columns now show their value as text instead of an empty cell.

## [0.51.1] - 2026-06-16

### Added
Expand Down
41 changes: 26 additions & 15 deletions Plugins/DuckDBDriverPlugin/DuckDBConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ actor DuckDBConnectionActor {
}

var raw = Self.extractResult(from: &result, startTime: startTime)
Self.patchTzColumns(&raw, query: query, connection: conn)
Self.patchCastedColumns(&raw, query: query, connection: conn)
return raw
}

Expand Down Expand Up @@ -163,7 +163,7 @@ actor DuckDBConnectionActor {
}

var raw = Self.extractResult(from: &result, startTime: startTime)
Self.patchTzColumns(&raw, query: query, connection: conn)
Self.patchCastedColumns(&raw, query: query, connection: conn)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve bindings when patching prepared VARIANT results

When executeParameterized is used for a query that returns a VARIANT column and also has placeholders, this call sends only the original SQL into patchCastedColumns, not the bound parameter values. The patch path builds a wrapped query and runs it with duckdb_query, so a query such as SELECT v FROM variants WHERE id = ? fails on the unbound ?; the guard in patchCastedColumns then returns silently and the original VARIANT cell remains the nil value produced by duckdb_value_varchar, so parameterized VARIANT reads still display empty cells.

Useful? React with 👍 / 👎.

return raw
}

Expand Down Expand Up @@ -204,7 +204,7 @@ actor DuckDBConnectionActor {
columnTypeNames.append(Self.typeName(for: colType))
}

if columnTypes.contains(where: Self.isUnrenderable) {
if columnTypes.contains(where: Self.requiresTextCast) {
duckdb_destroy_result(&result)
try Self.streamWrappedQuery(
query: query,
Expand Down Expand Up @@ -235,7 +235,7 @@ actor DuckDBConnectionActor {
continuation: AsyncThrowingStream<PluginStreamElement, Error>.Continuation
) throws {
let castExprs = columns.enumerated().map { i, name in
castExpression(for: columnTypes[i], column: name)
projection(for: columnTypes[i], column: name)
}
let wrappedQuery = buildWrappedQuery(originalQuery: query, castExprs: castExprs)

Expand Down Expand Up @@ -484,16 +484,16 @@ actor DuckDBConnectionActor {
}
}

static func patchTzColumns(
static func patchCastedColumns(
_ raw: inout DuckDBRawResult, query: String, connection: duckdb_connection
) {
let patchedColIndices = raw.columnTypes.enumerated().compactMap { idx, type in
isUnrenderable(type) ? idx : nil
requiresTextCast(type) ? idx : nil
}
guard !patchedColIndices.isEmpty, !raw.rows.isEmpty else { return }

let castExprs = raw.columns.enumerated().map { i, name in
castExpression(for: raw.columnTypes[i], column: name)
projection(for: raw.columnTypes[i], column: name)
}
let wrappedQuery = buildWrappedQuery(originalQuery: query, castExprs: castExprs)

Expand All @@ -514,25 +514,36 @@ actor DuckDBConnectionActor {
}
}

static func isUnrenderable(_ type: duckdb_type) -> Bool {
static func isNativelyRenderable(_ type: duckdb_type) -> Bool {
switch type {
case DUCKDB_TYPE_TIMESTAMP_TZ, DUCKDB_TYPE_TIME_TZ, DUCKDB_TYPE_GEOMETRY:
case DUCKDB_TYPE_BOOLEAN,
DUCKDB_TYPE_TINYINT, DUCKDB_TYPE_SMALLINT, DUCKDB_TYPE_INTEGER, DUCKDB_TYPE_BIGINT, DUCKDB_TYPE_HUGEINT,
DUCKDB_TYPE_UTINYINT, DUCKDB_TYPE_USMALLINT, DUCKDB_TYPE_UINTEGER, DUCKDB_TYPE_UBIGINT, DUCKDB_TYPE_UHUGEINT,
DUCKDB_TYPE_FLOAT, DUCKDB_TYPE_DOUBLE, DUCKDB_TYPE_DECIMAL,
DUCKDB_TYPE_VARCHAR, DUCKDB_TYPE_BLOB, DUCKDB_TYPE_UUID, DUCKDB_TYPE_BIT, DUCKDB_TYPE_ENUM,
DUCKDB_TYPE_DATE, DUCKDB_TYPE_TIME, DUCKDB_TYPE_TIME_NS, DUCKDB_TYPE_INTERVAL,
DUCKDB_TYPE_TIMESTAMP, DUCKDB_TYPE_TIMESTAMP_S, DUCKDB_TYPE_TIMESTAMP_MS, DUCKDB_TYPE_TIMESTAMP_NS,
DUCKDB_TYPE_LIST, DUCKDB_TYPE_STRUCT, DUCKDB_TYPE_MAP, DUCKDB_TYPE_ARRAY, DUCKDB_TYPE_UNION:
return true
default:
return false
}
}

static func requiresTextCast(_ type: duckdb_type) -> Bool {
!isNativelyRenderable(type)
}

static func castExpression(for type: duckdb_type, column: String) -> String {
let quoted = quoteIdentifier(column)
switch type {
case DUCKDB_TYPE_GEOMETRY:
if type == DUCKDB_TYPE_GEOMETRY {
return "CASE WHEN \(quoted) IS NULL THEN NULL ELSE ST_AsText(\(quoted)) END AS \(quoted)"
case DUCKDB_TYPE_TIMESTAMP_TZ, DUCKDB_TYPE_TIME_TZ:
return "CASE WHEN \(quoted) IS NULL THEN NULL ELSE CAST(\(quoted) AS VARCHAR) END AS \(quoted)"
default:
return quoted
}
return "CASE WHEN \(quoted) IS NULL THEN NULL ELSE CAST(\(quoted) AS VARCHAR) END AS \(quoted)"
}

static func projection(for type: duckdb_type, column: String) -> String {
requiresTextCast(type) ? castExpression(for: type, column: column) : quoteIdentifier(column)
}

static func buildWrappedQuery(originalQuery: String, castExprs: [String]) -> String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ final class DuckDBDriverTests: XCTestCase {
XCTAssertTrue(value.contains("2024-01-02"), "expected rendered timestamptz, got \(value)")
}

func testVariantRendersAsText() async throws {
let driver = try XCTUnwrap(driver)
let result = try await driver.execute(query: "SELECT {'a': 42, 'b': [1, 2, 3]}::VARIANT AS v")
XCTAssertEqual(result.rows.count, 1)
let value = try XCTUnwrap(result.rows[0][0])
XCTAssertTrue(value.contains("42"), "expected rendered variant text, got \(value)")
}

func testCancelWithoutRunningQueryIsSafe() async throws {
let driver = try XCTUnwrap(driver)
try await driver.cancelCurrentQuery()
Expand Down
2 changes: 1 addition & 1 deletion docs/databases/duckdb.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ DuckDB supports a wide range of data types:
| Numeric | `INTEGER`, `BIGINT`, `HUGEINT`, `DOUBLE`, `FLOAT`, `DECIMAL` |
| String | `VARCHAR`, `TEXT`, `CHAR` |
| Date/Time | `DATE`, `TIME`, `TIMESTAMP`, `INTERVAL` |
| Complex | `LIST`, `MAP`, `STRUCT`, `UNION`, `ENUM` |
| Complex | `LIST`, `MAP`, `STRUCT`, `UNION`, `ENUM`, `VARIANT` |
| Other | `BOOLEAN`, `BLOB`, `UUID`, `JSON`, `BIT` |

## Limitations
Expand Down
Loading