SequelPG

Every release so far has had a “No CSV export” line sitting in the README’s Known Limitations — the kind of gap you notice the moment a query result needs to end up in a spreadsheet, a ticket, or a script. v0.3.5 closes it, and pairs it with the other thing a query tool owes you: a way to stop a statement that’s taking too long, on your terms rather than a hard-coded timer’s.

Export the rows you’re looking at

There’s a new Export ▾ menu in two places: the query-results footer and the content tab’s pagination bar. It writes exactly the grid you’re looking at — including the client-side sort order you’ve applied — to a file:

  • CSV — RFC 4180 quoting: fields containing commas, quotes, or newlines are quoted, embedded quotes are doubled, and NULL becomes an empty field. Opens cleanly in Numbers, Excel, and anything that reads CSV.
  • JSON — an array of objects keyed by column name. NULL encodes as null (not the string “NULL”), and when a join selects the same column name twice, the duplicates get _2, _3… suffixes so no cell is silently dropped.

The serializer is a small pure utility with its own test suite — quoting, escaping, NULL handling, and duplicate-column behaviour are all pinned down — and the file write goes through the view model layer, keeping the app’s no-I/O-in-Views rule intact.

Stop. And a timeout you choose

Until now every query ran with a hard-coded 10-second cap. Fine for browsing; useless the moment you run a real analytical query. Settings → SQL Editor now has a Query timeout picker — 5 seconds to 5 minutes, or No limit — applied to Run, Explain/Analyze, and content-page loads.

With the cap loosened, you need the other half: while a statement is executing, the Run button becomes Stop (⌘., the macOS-standard cancel chord). Stopping cancels the in-flight task — and the result pane reads Cancelled, not a scary red error, because a query you chose to stop isn’t a failure.

The bug hiding under the timeout

Implementing the timeout setting surfaced a real bug in how the old one worked. SequelPG guards queries server-side with SET statement_timeout — but statement_timeout is session state, and the driver’s PostgresClient is a connection pool. Issue the SET through the pool and it can land on one connection while the query it was meant to guard runs on another — leaving one connection silently capped and the query unguarded.

v0.3.5 leases a single connection for the whole sequence — SET statement_timeout → query → RESET statement_timeout — so the guard always applies to the statement it belongs to. The reset also switched from SET statement_timeout = 0 to RESET: if your role or database configures its own default timeout, the app now restores that instead of stomping it to “unlimited.”

No more stale pages from rapid clicks

Filter, sort, and pagination changes used to fire independent fire-and-forget loads. Click fast enough — apply a filter, then clear it; flip two pages quickly — and an older fetch could finish after a newer one and overwrite it, showing rows that didn’t match the controls on screen. Content loads now cancel the previous in-flight fetch and carry a generation token checked before any state write, so the newest request always wins. The same guard covers switching object tabs mid-load: a late result can no longer write the old tab’s rows into the one you just opened.

Quoted identifiers, quoted everywhere

Functions living in schemas that need quoting — "CamelCase", names with spaces — failed to resolve in the Run sheet and the Definition tab. The lookup pins down overloads by casting a schema.name(args) string to regprocedure, and that cast parses its input like SQL source: unquoted CamelCase folds to lowercase and misses. Both lookup paths now quote the identifier parts. While in there, the two near-identical 80-line metadata queries became one shared definition.

Smaller fixes

  • SSH host-key messages got smarter. ssh now runs with LC_ALL=C so error detection keeps working on localized systems — and a changed host key (“REMOTE HOST IDENTIFICATION HAS CHANGED”, the potential-MITM case) gets its own guidance about verifying the new fingerprint, instead of the same “just add it to known_hosts” advice as a first connection.
  • Orphaned sort indicators. Re-running a query with a different SELECT list used to keep the previous sort column even if it no longer existed in the result; the sort now clears itself.
  • Export/Import sheet spinners can no longer get stuck if the underlying task is cancelled mid-stream.

Concretely, v0.3.5 means

  • Export ▾ in the query-results footer and content pagination bar — CSV (RFC 4180, NULL-as-empty) and JSON (objects keyed by column, NULL as null, duplicate columns suffixed)
  • A Query timeout picker in Settings → SQL Editor — 5 s to 5 min or no limit — for Run, Explain/Analyze, and content pages
  • Run becomes Stop (⌘.) while a statement executes; a stopped query reads as Cancelled
  • statement_timeout set, used, and reset on the same pooled connection, with RESET preserving role/database defaults
  • Filter / sort / pagination loads cancel their predecessor and can’t write stale rows over newer ones — including across tab switches
  • Functions in schemas needing quoting resolve correctly in the Run sheet and Definition tab
  • Locale-proof SSH error detection with dedicated changed-host-key guidance, self-clearing orphaned sort indicators, and un-stickable export/import spinners
  • 31 new unit tests — the CSV/JSON serializer, the cascade-delete SQL builder, and the export/import sheet guards

Grab v0.3.5 from the latest release or build from source with Xcode. Run a query, hit Export ▾ under the grid, and your rows are a CSV away — and if a query ever runs longer than you meant it to, ⌘. now actually means stop.