Skip to content

fix(plugin-duckdb): render VARIANT columns as text#1702

Merged
datlechin merged 2 commits into
mainfrom
worktree-fix-3872-duckdb-variant
Jun 17, 2026
Merged

fix(plugin-duckdb): render VARIANT columns as text#1702
datlechin merged 2 commits into
mainfrom
worktree-fix-3872-duckdb-variant

Conversation

@datlechin

@datlechin datlechin commented Jun 17, 2026

Copy link
Copy Markdown
Member

Problem

DuckDB VARIANT columns showed as empty cells in the macOS DuckDB plugin. Reported upstream at TablePlus/TablePlus#3872 ("Support for DuckDB VARIANT type").

Root cause

The bundled DuckDB 1.5.6 lib's deprecated duckdb_column_type C API returns INVALID (0) for a VARIANT column, not 41, and duckdb_value_varchar returns NULL for it. The plugin's isUnrenderable denylist only covered TIMESTAMP_TZ/TIME_TZ/GEOMETRY, so the INVALID-typed variant column fell through to duckdb_value_varchar and rendered as null. (The upstream header defines DUCKDB_TYPE_VARIANT = 41 and the issue reported "id: (41)", but our lib's C enum API never surfaces 41, it collapses unrecognized types to INVALID. Confirmed with a C probe against Libs/libduckdb_arm64.a.)

Fix

Treat DUCKDB_TYPE_INVALID as unrenderable and CAST(col AS VARCHAR) it through the existing wrapped-query path. This renders VARIANT as text and is forward compatible: any type the deprecated C enum does not recognize now shows its value instead of null. Plain SELECT NULL/SELECT 1 report INTEGER, not INVALID, so the change is targeted.

The iOS DuckDBDriver already uses an allowlist that treated INVALID as needing a cast, so it rendered VARIANT correctly already and needs no change.

Tests

  • Added testVariantRendersAsText to DuckDBDriverTests. The full 14-test DuckDB suite passes on the simulator, including testNullRendersAsNil (INVALID-casting does not disturb plain NULLs).
  • The macOS plugin has no test target (DuckDB behavior is only covered via TableProMobileTests), so the plugin's changed lines are verified by the C probe and a local build rather than an automated plugin test.

Shipping

DuckDB is a registry-only plugin with no PluginKit ABI change, so this reaches users when the DuckDB plugin is re-released to the registry.


Update (refactor): Reworked the detection from a denylist (isUnrenderable) to a render allowlist (isNativelyRenderable / requiresTextCast), matching the iOS DuckDBDriver's design. Any type not known to render through the value API (VARIANT/INVALID, TZ types, GEOMETRY, and future unknown types) is cast to VARCHAR in SQL. The allowlist is exactly the previously-rendered type set, so behavior is unchanged for every known type; only unknown/exotic types now render as text instead of null. patchTzColumns renamed to patchCastedColumns since it no longer handles only timezone columns. Verified the wrapped projection end-to-end with a C probe (native columns pass through, variant casts, NULL stays NULL).

@mintlify

mintlify Bot commented Jun 17, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Jun 17, 2026, 8:05 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7cfa85f3b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

static func isUnrenderable(_ type: duckdb_type) -> Bool {
switch type {
case DUCKDB_TYPE_TIMESTAMP_TZ, DUCKDB_TYPE_TIME_TZ, DUCKDB_TYPE_GEOMETRY:
case DUCKDB_TYPE_INVALID, DUCKDB_TYPE_TIMESTAMP_TZ, DUCKDB_TYPE_TIME_TZ, DUCKDB_TYPE_GEOMETRY:

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 Avoid patching VARIANT columns from a second execution

Adding DUCKDB_TYPE_INVALID here routes VARIANT results through patchTzColumns, which re-executes the original query and then copies only the patched columns from that second result back into the first result by row index. For order-unstable or volatile queries, e.g. SELECT i, {'i': i}::VARIANT AS v FROM range(100) ORDER BY random(), the displayed VARIANT text can come from a different execution than the rest of the row, corrupting the result grid. Cast before decoding or replace the entire result from one wrapped execution so all columns come from the same run.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: adbda4d2ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


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 👍 / 👎.

@datlechin datlechin merged commit 4f417b4 into main Jun 17, 2026
5 checks passed
@datlechin datlechin deleted the worktree-fix-3872-duckdb-variant branch June 17, 2026 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant