Skip to content

feat(results): render STRUCT columns as nested sub-columns - #19

Open
HernandoR wants to merge 1 commit into
ChuckJonas:mainfrom
HernandoR:feat/nested-struct-columns
Open

HernandoR wants to merge 1 commit into
ChuckJonas:mainfrom
HernandoR:feat/nested-struct-columns

Conversation

@HernandoR

Copy link
Copy Markdown

What

STRUCT columns previously rendered as a single JSON blob per cell. They now expand into grouped sub-columns with a multi-row header.

before after
s → {"x":10,"y":{"z":"a"}} s group header spanning x | y.z leaf columns

Depth is configurable via duckdb.nestedColumnMaxDepth (default 2). Setting it to 0 restores the previous flat JSON rendering, so this is opt-out.

Nested leaves are first-class — sorting, filtering, column stats, distinct values, selection, copy, and column resize all work on them, not just on top-level columns.

The SQL quoting fix underneath

Making nested columns sortable/filterable surfaced a latent issue: every call site built column references as `"${column}"`. For a nested path that emits "s.x" — a request for a column literally named s.x — instead of the struct-field reference "s"."x".

A new shared ColumnRef type (string | string[]) plus quoteColumnRef() quotes segment-by-segment and replaces the hand-rolled quoting in fetchPage, exportCache, getCopyData, getCacheColumnStats, getColumnDistinctValues, getColumnCardinality, and filterToSql. Top-level columns still pass plain strings, so existing behavior is unchanged.

This also handles field names containing dots or quotes correctly.

SUMMARIZE

DuckDB's SUMMARIZE only reports top-level columns, so nested leaves would be missing from the columns panel. They're summarized separately via a projection aliasing each leaf to its dotted label:

SUMMARIZE (SELECT "s"."x" AS "s.x", "s"."y"."z" AS "s.y.z" FROM cache)

That query is best-effort — if it fails, the flat summaries still render.

Scope / limitations

  • Cell editing stays top-level only. Write-back targets a whole column, so editing a struct field would require rebuilding the struct value. Nested cells remain viewable but read-only (no Save affordance).
  • STRUCT(...)[] (list-of-struct), MAP, and UNION are not expanded and render as before.
  • Sorting/filtering a nested leaf is server-side like any other column; no client-side fallback was added.

Testing

21 new tests, all passing (185 total in the suite):

  • src/test/nestedColumns.test.ts (13) — pure functions: STRUCT type-string parsing (quoted identifiers, embedded commas/parens/escaped quotes, DECIMAL(18,3), rejection of list-of-struct/MAP), header colSpan/rowSpan construction, depth limiting, path-based value extraction.
  • src/test/nestedColumnSql.test.ts (8) — integration against a real DuckDB connection, verifying ORDER BY / WHERE / aggregates / distinct+cardinality / the SUMMARIZE projection all execute correctly on nested fields. One test asserts the naive "s.we.ird" form fails, pinning the dotted-field-name bug so it can't regress.

tsc --noEmit clean, esbuild passes, eslint src reports 0 errors (warning count is below the pre-existing baseline on main).

STRUCT values previously rendered as a single JSON blob per cell. They
now expand into grouped sub-columns with a multi-row header, up to a
configurable depth (`duckdb.nestedColumnMaxDepth`, default 2; 0 restores
the old flat JSON rendering).

Nested leaves are first-class: sorting, filtering, column stats, distinct
values, selection, copy and column resize all work on them, not just on
top-level columns.

Making that work required fixing how column names reach SQL. Every call
site built references as `"${column}"`, so a nested path would emit
`"s.x"` — a request for a column literally named `s.x` — instead of the
struct-field reference `"s"."x"`. A new shared `ColumnRef` type (name or
path) plus `quoteColumnRef()` quotes segment-by-segment and replaces the
hand-rolled quoting in fetchPage, exportCache, getCopyData,
getCacheColumnStats, getColumnDistinctValues, getColumnCardinality and
filterToSql.

DuckDB's SUMMARIZE only reports top-level columns, so nested leaves are
summarized separately via a projection that aliases each leaf to its
dotted label; that query is best-effort and never blocks the flat
summaries.

Cell editing stays top-level only: write-back targets a whole column, so
editing a struct field would require rebuilding the struct value. Nested
cells remain viewable but read-only.

Tests: 21 new (13 pure-function, 8 integration against a real DuckDB
connection covering ORDER BY / WHERE / aggregates / distinct+cardinality
/ SUMMARIZE on nested fields). One asserts the naive `"s.we.ird"` form
fails, pinning the dotted-field-name bug.
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