SequelPG v0.1.12: AppKit results grid, type-aware cells, and a test-connection probe
v0.1.11 was about reach — database-wide tools, query history, a curated function library, and parameterized catalog queries. v0.1.12 is about the grid. Most of the time you spend inside SequelPG is staring at a 20-column-wide table of rows, and the SwiftUI Table we shipped through v0.1.7 was starting to hold us back: first-click selection was unreliable, multi-select didn’t really work, a stray modifier could freeze the row when editing a jsonb cell, and a handful of common PostgreSQL types rendered as nil because the binary wire decoder didn’t know about them. This release rebuilds the grid on AppKit, decodes every type that landed in our test database, wires up a working Test Connection button on the server form, and ships a dockerized PostgreSQL 18 instance so you can reproduce all of it locally.
Rebuilding the results grid on AppKit
The new DataGridView is an NSViewRepresentable wrapping an NSScrollView + NSTableView. The SwiftUI Table we replaced is wonderful for a static list of structured rows, but a database client needs three things SwiftUI fights with: deterministic single-click row selection, true multi-select, and a cell that distinguishes between “clicked to select” and “clicked to enter edit mode”. AppKit’s table view has handled those semantics for twenty years; we just had to step out of SwiftUI’s way.
A few details worth calling out:
- Single click selects the row, double click edits the cell. We started with a SwiftUI
TapGesture(count: 2)overlay but it raced the underlying selection gesture and produced visible flicker. The AppKit table’s native double-click handler runs after the row is already selected, so the first click always lands. - Multi-select works.
Shift+clickextends the range andCmd+clicktoggles individual rows. The inspector reflects the most recently selected row; row-mutation actions (delete, insert) disable themselves when more than one row is selected, since the SQL builders only have single-row variants for now. - Vertical cell dividers are drawn one pixel wide on the trailing edge of each cell, with a careful inset so the divider sits flush with the column boundary and doesn’t absorb the click that targets the next column. Without this, clicking near a column boundary would silently fail to select the cell you aimed for.
- Header rules stay aligned with cell dividers. The header row uses the same divider geometry as the body, so the visual grid is continuous from header to last visible row.
The migration was incremental: the Content tab moved first, then the Query results tab, with the SwiftUI Table kept around as a fallback until the AppKit version handled every cell type and editing surface the old grid did. The legacy renderer is now gone.
Type-aware cell rendering
The other half of the grid rework is decoding. PostgresNIO returns cells as a PostgresCell with a typed payload, but for a long time we only knew how to decode the “easy” types (text, int4/int8, bool, float8). Anything else — numeric, money, inet, cidr, macaddr, jsonb, composite types — came across the wire in binary, fell through the decoder, and rendered as an empty cell. The new release adds a binary-format decoder for every type that turned up in our PostgreSQL 18 test database:
- numeric / decimal / money — the binary format is a little-known sign+weight+digit-group encoding. We decode it back to a normalized decimal string so
0.10doesn’t round-trip as0.1and money keeps its locale-formatted currency symbol. - inet / cidr / macaddr / macaddr8 — addresses are now decoded to their canonical text form, with the IPv6 zone-ID preserved.
- jsonb — jsonb sends a version byte followed by the document. We strip the version and parse the body, so jsonb cells now render with the same pretty-printed preview as their
jsonsiblings. - Composite types — row values from
CREATE TYPE ... AS (...)tuples and from explicitROW(...)expressions are decoded recursively and shown as(a, b, c)with the inner fields rendered through the same per-type formatters. - float4 precision — previously we coerced
float4through a Float-shaped path that quietly lost precision on the round-trip. It’s now decoded asDoubleso the displayed value matches what the server stored.
The decoder is dictionary-driven ([PostgresDataType: (PostgresCell) -> CellValue?]), which is the structure we landed on in v0.1.11’s refactor. Adding a new type is one entry in the map and one renderer function. Header cells also got a small richer treatment — column data type is shown beside the column name, so you can tell at a glance whether the column you’re looking at is text, varchar(64), or jsonb.
The “keep the grid mounted” fix
A subtle but persistent annoyance was that when you triggered a reload — sort, filter, page change — the content area would unmount and remount, flashing a blank panel for a frame before the new rows arrived. The fix is straightforward in retrospect: keep the grid view mounted across reloads, update its row data in place, and let the AppKit table view itself drive the transition. No more blink on every Cmd+R.
Test Connection
The connection form has had a disabled Test button since v0.1.0 with a “coming soon” tooltip. v0.1.12 finally wires it up. When you press Test, SequelPG runs the form validation, spawns a throwaway DatabaseClient — not the one the active tab is attached to — calls connect (which already does a SELECT 1 as its readiness check), then tears it down. While the probe is running, the button shows a small spinner; the result is rendered inline as either a green Connection successful banner or a red error with the PostgreSQL message verbatim. The error message is textSelection(.enabled) so you can copy it.
Two design choices worth flagging:
- The active session is untouched. If you’re already connected to production and you want to test a new staging profile, Test does not disconnect, switch databases, or churn your navigator tree. It opens a fresh client, probes, and discards it.
- The throwaway client is injected.
ConnectionListViewModelnow takes atestClientFactory: () -> any PostgresClientProtocolwith a default of{ DatabaseClient() }. Tests can substitute a mock that returns scripted errors without touching the network. The same Test button is available on both the inline detail editor on the start page and the modal add/edit sheet, so whichever entry point you came in through, the feature is there.
Dockerized PostgreSQL 18 demo database
Developing a Postgres client without a Postgres instance to point it at is awkward. The repo now ships a dev-db/ directory with a docker-compose.yml that brings up PostgreSQL 18 and seeds it with a small schema covering every type the new decoder claims to handle — numeric, money, inet, cidr, macaddr, macaddr8, jsonb, arrays, composite types, ranges, and a few partitioned tables for good measure. make up brings it up, make down tears it back down. If you’re contributing a fix and want to verify your patch against the same fixtures the maintainers use, this is the easier path.
Stability fixes worth noting
- Function navigator names round-trip. Functions with overloads were sometimes shown by their bare name in the navigator, which broke drop and show definition on the wrong overload. We now resolve names through
regprocedureso the qualified signature comes back. - Selecting a type no longer raises an error toast. Selecting a non-relation object — a composite type, a sequence, a function — used to issue a defensive
SELECT * FROM <object>in the Content tab and display the resulting “cannot open relation” error. The Content tab now skips that lookup entirely for object types it can’t paginate. - Aggregate DDL is no longer truncated.
pg_get_functiondefon aggregates returned an empty string in some cases; the Definition tab now uses a hand-rolledCREATE AGGREGATEreconstruction so you see the full definition. - PSQL error messages surface their actual cause. The previous error wrapper hid the underlying server message behind a generic Swift error description; the structured PostgresNIO
serverInfoAPI now feeds the toast and inline error banners directly. - jsonb edits no longer freeze the row. A modifier on the SwiftUI cell intercepted the gesture that unselected the row after edit, leaving the row stuck in “editing” state. The AppKit grid sidesteps the problem entirely.
What’s next
The roadmap from the v0.1.11 post still stands. With the grid rebuilt and binary decoding caught up, the next priorities lean on both:
- Foreign-key cell navigation — click an FK value in the new grid, jump to the referenced row. The AppKit renderer makes the click target unambiguous, so this becomes tractable.
- Live activity panels —
pg_stat_activity,pg_locks, andpg_stat_user_tables, polled and rendered through the same grid. - Enum value browser — the types list shows the type, but not the labels; we want one click to see them.
- Inline signature hints in the SQL editor, reusing the curated Function Library data.
- Row-Level Security policy management and Publications / Subscriptions for logical replication.
SequelPG is open source on GitHub. Grab v0.1.12 from the releases page, file issues, or send a PR. Feedback is very welcome.