Skip to content

feat(datagrid): per-element JSON editing for PostgreSQL jsonb[] columns - #2903

Merged
datlechin merged 2 commits into
mainfrom
feat/postgresql-jsonb-array-editor
Sep 16, 2026
Merged

datlechin merged 2 commits into
mainfrom
feat/postgresql-jsonb-array-editor

Conversation

@datlechin

Copy link
Copy Markdown
Member

PostgreSQL jsonb[] and json[] cells now open on their elements, each element in the same JSON viewer a jsonb cell gets, on the data grid chevron and in the row inspector.

Fixes #2897

Root cause

Two booleans in ColumnType kept an array of JSON out of every structured editor:

  • supportsElementEditing returned false when the array element was .json.
  • isJsonType is true only for a scalar .json, never for .array(element: .json).

So a jsonb[] cell matched no branch in DataGridView+Click.handleChevronAction and fell through to the plain inline text editor, and FieldEditorResolver.resolve fell through to .multiLine. Nothing decoded the value wrongly: the escaping in the report is PostgreSQL's own array quoting, arriving intact.

The exclusion rested on a claim that is false

PR #2110 excluded jsonb[] because "its additional escaping layer cannot round-trip through a per-element list". Measured against a live PostgreSQL 17.11, with a swiftc harness compiled over the shipping PostgresArrayLiteralCodec and fed the exact literals array_out produced: every single-dimension case parses to the right elements and re-serializes byte for byte. That covers nested objects containing commas and braces, escaped quotes, doubled backslashes, \n, non-ASCII, the empty string, {} and [] elements, and the SQL-NULL versus JSON-null distinction. Only [0:1]={…} and {{1,2},{3,4}} return nil, which is the correct degrade and already falls back to raw text editing.

So the codec did not need rewriting, and TableProPluginKit's public API does not move: no currentPluginKitVersion bump, no plugin re-release.

What changed

ColumnType.arrayElementEditor replaces the boolean's overloaded job: it answers which element editor an array gets (.scalar, .json, or none), and supportsElementEditing is now derived from it, so the three existing call sites are unchanged.

ArrayValueEditorView gains a JSON mode. The elements are listed above and the selected one opens in JSONViewerView in its existing live-binding mode, so an element has the Text and Tree modes, the tree search and the invalid-JSON reporting the rest of the app already has. Add, remove and reorder act on the selection from the footer, because a list of documents is a master list rather than a stack of one-line fields.

An element the user only read writes back the bytes the server sent. An element whose JSON actually changed writes back compact, which is what the scalar JSON cell editor does. Without that rule, opening a json[] cell and pressing OK would rewrite every element as the pretty-printed form, and PostgreSQL stores json text verbatim. This is the bug DataGrip shipped in the same feature (YouTrack DBE-26425, where the per-element editor stringified object elements on save).

FieldEditorKind.arrayElements gives the row inspector a route it had for no array type at all: #2110 shipped grid-only, and the inspector is the surface in the report's screenshot. It follows SetPickerView exactly, a summary label and a menu carrying Edit Elements…, Set NULL and Set DEFAULT, opening the same editor in a popover that commits once. That inherits pending NULL and DEFAULT and the multi-row "Multiple values" state instead of reinventing them.

FieldEditorResolver resolves the array editor from the value, not from the declared type alone. jsonb[] and jsonb[][] are one type in PostgreSQL's catalog and any array column may carry an explicit lower bound, so the type cannot rule either out on a given row. Gating on a successful parse mirrors what the grid already does, and it is also why another engine's list literal, such as DuckDB's [a, b], cannot reach this editor. That parse is also what scopes the editor to the engines whose arrays are written this way: a document store's object[] classifies identically, so a field with no literal to read keeps the plain editor rather than being offered a list that would commit PostgreSQL {…} syntax. A stored NULL and a multi-row selection therefore stay on the plain editor, which is where they already were.

ArrayValueEditorView takes the stored literal rather than parsed elements, which makes it total over what a column can hold: a literal the list cannot read opens in its raw text mode over that same text. Without that, the editor kind cached on the field made a real data-loss path reachable, because committing a bounds-prefixed literal through Edit as Text and reopening the popover would have shown an empty list and written {} over it on OK.

ColumnType.withAllowedValues carries a column's declared labels down into an array's element. They arrive separately in TableRows.columnEnumValues and were injected on the scalar enum and set cases alone, so an ENUM[] column would have reached the inspector with no vocabulary and offered free-form text where the grid offers the declared labels.

One collateral fix folded in

PostgresArrayLiteralCodec trimmed whitespace with Swift's Character.isWhitespace, the whole Unicode set, where PostgreSQL's array_in trims six characters. Measured on the same server: array_out writes U+00A0 and U+3000 into a text[] literal unquoted and array_in reads them back as part of the value, while the codec parsed {<U+00A0>abc,def} to abc and re-serialized to {abc,def}. Editing any other element in the same cell then deleted that character silently, because committing re-serializes every row.

Fixing that exposed the general form of the bug, so the codec now scans Unicode scalars rather than Characters, which is what the server does. A Swift grapheme can carry a structural scalar and a combining mark together, and Character comparison then misses it: , followed by U+0301 is one Character that is not ",", so a grapheme scan keeps an element the server splits in two, and a space followed by U+0301 is one Character that is not whitespace, so it goes out unquoted for the server to trim. CR LF is the same shape, one grapheme the server reads as two of its six.

It ships here because the new inspector route gives text[] a surface it did not have before, which makes an existing silent-data-loss path materially more likely to bite. The public API does not move: parse and serialize keep their signatures and their Character delimiter.

Not in scope

  • The grid cell text stays the raw PostgreSQL literal. Copy and Find operate on it, so a prettified summary would desynchronise the cell from the value.
  • Multi-dimensional values and dimension-prefixed literals such as [0:2]={a,b,c} keep degrading to raw text editing.
  • bytea[] and composite arrays stay excluded.
  • The json[] badge the report noticed is ColumnType.badgeLabel, a semantic vocabulary (string, number, bool, json, date) in which scalar jsonb also badges as json. The raw type name is intact throughout, so nothing is losing a b.

Verification

Step Result
verify.sh build PASS
verify.sh test over the five touched suites PASS, 217 executed, 217 passed
verify.sh lint over 19 changed Swift files PASS, 0 violations
verify.sh docs PASS
verify.sh plugins (AllPlugins) Pre-existing failure, unrelated: unknown attribute 'usableFromInlinenonisolated' from a @TaskLocal macro expansion in oracle-nio. Same error on unrelated branches.

Suites: PostgresArrayLiteralCodecTests, ArrayValueEditorModelTests, FieldEditorResolverTests, InspectorFieldLayoutTests, ColumnTypeClassifierTests. New coverage pins the measured PostgreSQL 17.11 literals as byte-identical round trips, the SQL-NULL versus JSON-null distinction, the six characters PostgreSQL does trim against the two it does not, the write-back rule in all four of its cases, and the resolver's parse gate including the multi-dimensional and foreign-literal fallbacks. ColumnTypeClassifierTests previously asserted !classify("jsonb[]").supportsElementEditing; that assertion is now the opposite, which is the point of the change.

A second-model review (Codex) read the diff over two rounds and raised ten defects. Fixed here: the grapheme-versus-scalar scan, the discarded raw edit on reopen, the missing enum-array labels, unbounded JSON parsing in the element list's render path, engine scoping for a NULL object[] cell, and six icon-only buttons plus an invalid-JSON warning that VoiceOver read as generic symbols. One suggestion could not be taken: ForEach(rows.enumerated(), id:) is the project's own rule in .claude/skills/swiftui/references/api.md, and EnumeratedSequence's RandomAccessCollection conformance is macOS 26 against a macOS 14 target, so the array copy stays with the reason recorded beside it.

End to end against a live PostgreSQL 17.11: the column opens on its elements in the row inspector with the json[] badge, and text[] gets the same list.

No TableProUITests coverage. The flow needs a live PostgreSQL server, which CI does not have, and the data grid takes no synthetic input since #2381 replaced its cells with CoreText drawing.

The Arrays of JSON docs section ships without a screenshot and needs one. The editor opens from a SwiftUI Menu, which does not respond to System Events synthetic clicks, so the popover could not be driven from a script. A placeholder was written and then removed rather than shipped: docs/STYLE.md requires alt text to describe what is actually in the image, and a title card under "Array element list above a JSON document editor" would not. The surrounding inspector field was captured and is below.

After

The row inspector's items field, with the json[] badge and the element editor on it, captured against a live PostgreSQL 17.11. The before is the plain text field over the escaped literal in the report's own screenshot.

Row inspector showing a jsonb array field with the json[] badge and the element editor control

@mintlify

mintlify Bot commented Sep 15, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 15, 2026, 6:44 PM

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

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
@datlechin
datlechin merged commit 32b83f8 into main Sep 16, 2026
5 checks passed
@datlechin
datlechin deleted the feat/postgresql-jsonb-array-editor branch September 16, 2026 03:22
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.

Add formatted JSON inspection for PostgreSQL jsonb[] columns

1 participant