SequelPG

v0.2.1 was about how the app looks. v0.3.0 is about what it lets you do — two substantial additions to the query editor plus a typography rework that makes the whole app feel more like a tool and less like a magazine. The headlines below are also the things in the changelog this release.

EXPLAIN that reads like a sentence

PostgreSQL’s EXPLAIN ANALYZE is the right tool for understanding why a query is slow, but the default plan output is dense — costs, rows, loops, node types like “Bitmap Heap Scan” and “Nested Loop” stacked into a tree. You can read it, but you have to know PostgreSQL well to know what to do about what you read. v0.3.0 ships a visualizer that does the translation for you.

Two new buttons sit next to Run in the query editor toolbar:

  • Explain — safe on any query (including DML); runs EXPLAIN (FORMAT JSON) and shows the planner’s predicted plan without executing the statement.
  • Analyze — runs the query for real with EXPLAIN (FORMAT JSON, ANALYZE, BUFFERS), so you get the actual timing of every step alongside the planner’s estimates.

The plan renders as a vertical list of nodes, root at the top, children indented underneath. Each row is the human-language explanation: “Read the whole orders table”, “Match rows using a hash table”, “Sort by created_at, “Group rows together”. A short type chip on the left tells you what kind of step it is (scan, join, agg, sort, hash, write); a one-sentence summary underneath explains why this step exists; a monospaced metrics row shows how long it took, how many rows it returned, how many times it looped, and the planner cost.

Underneath each row, when something looks wrong, you get a short plain-English finding:

  • Dominant step — “This step alone took 70% of the total time.” Tells you where the time actually went, accounting for loops and subtracting children.
  • Bad row estimate — “Planner expected 25× more rows than actually came out — could be hurting join order.” Surfaces when the planner’s cardinality guess is off by 10× in either direction.
  • Filter waste — “Read 1.2M rows but threw away 95% with a filter — an index on the filter column would help.”
  • Big sequential scan — “Read 250k rows by scanning the whole table — an index on the filter column would skip most of that.”
  • Nested Loop over Seq Scan — “Looped over a full table scan 12k times — usually fixable with an index on the join column.” The classic accidental quadratic.

The right-side detail card carries the raw PostgreSQL fields for readers who already know the language — node type, Sort Method, Hash Cond, buffer hit/read/dirtied/written counters, planner cost range, the table and index names involved. The two surfaces, plain English on the left and raw PostgreSQL on the right, share selection — click any plan node and the detail card refreshes.

Triggers show up as their own “ii. — triggers” section underneath the plan tree when present, with per-trigger time and call count. Trigger time often dominates the execution time of writes, and the standard EXPLAIN output hides it in a flat list at the bottom that’s easy to miss.

Autocomplete that knows where you are in the query

The previous autocomplete worked alphabetically and that was all. v0.3.0 reworks it on two axes. First, ranking: case-insensitive prefix matches always win. Type sele and you get SELECT; type li and you get LIMIT — not SECURITY (which shares se) or public (which contains an l and an i via fuzzy subsequence). Fuzzy is a fallback only, and only fires once you’ve typed at least three characters, so the popup doesn’t surface noise on partials too short to be meaningful.

Second, context. A lightweight pass over the tokens preceding the cursor figures out which clause the cursor is in. After FROM / JOIN / UPDATE / INSERT INTO the popup biases toward tables and views — column names of the currently selected navigator table get pushed down because PostgreSQL won’t accept them in that position. After SELECT / WHERE / ON / GROUP BY / ORDER BY / SET / RETURNING the popup biases toward columns, with tables right behind for qualified tbl.col access. Inside INSERT INTO tbl (...) only column names are surfaced. Type a table name followed by a dot — users. — and column suggestions restrict to that table when its metadata is loaded.

Keywords are always in the candidate pool, regardless of context, so transitions like SELECT * FROM  orders LIMIT 10 work: typing li after a FROM clause still surfaces LIMIT as the top suggestion. Context biases ranking, not the candidate set.

The top match is pre-selected only when it’s a confident prefix hit. For fuzzy fallbacks, no pre-selection — an accidental Tab can’t commit a marginal match. And the popup only auto-fires when the document grows — backspace and delete don’t re-trigger it, so you can correct a wrong insertion without the same suggestion reappearing on every keystroke.

Typography: JetBrains Mono Bold for headlines

v0.2.1 introduced Instrument Serif Italic for “editorial flourish” — object names, section titles, empty-state headlines. It looked good in screenshots and it fit the marketing site’s identity, but inside a tool meant for working with code it kept reading as the wrong register. v0.3.0 replaces the editorial serif with JetBrains Mono Bold at the same sizes.

The same family that renders identifiers, SQL keywords, and column counts now also renders the headline above each tab, the empty-state titles, the inspector section names, and the plan-tree row titles in the new EXPLAIN visualizer. Size and weight do the work that an italic serif used to. The whole UI reads as a single typographic system — closer to VS Code’s settings UI, Linear’s admin views, or JetBrains’ welcome screen than to a magazine layout.

Bare .italic() modifiers are gone everywhere they used to mark soft-secondary text. NULL cells render as ‹NULL› — angle brackets distinguish the meta-value from a literal text cell whose contents happen to be “NULL”, without needing italic to mark the difference. The Instrument Serif font files stay bundled for one more release in case anyone has downstream branches referencing them; the registration call drops them.

Concretely, v0.3.0 means

  • New Models/QueryPlan.swift decoding EXPLAIN (FORMAT JSON) output into a PlanNode tree with derived selfTime, estimateRatio, and hasBadEstimate fields
  • New Utilities/PlanNarrator.swift translating every PostgreSQL node type into a short title and a one-sentence summary; produces the per-row findings
  • New Views/QueryPlanView.swift with the collapsible tree, type-chip rows, dotted-rule dividers, and the right-side detail card
  • New explainQuery(_:analyze:buffers:timeout:) on the PostgresClientProtocol service, wired into the existing query timeout / cancellation machinery
  • New CompletionContext walking the cached SQL tokens to identify clause position and qualifier; biases the candidate pool and ranking in SQLCompletionProvider
  • Prefix-first FuzzyMatcher with a 3-char minimum for fuzzy fallback; preselectTop flag only true for confident prefix matches
  • Editor toolbar buttons forced to their intrinsic horizontal size so adding Explain / Analyze can’t collapse them into one-character columns; connection-status label truncates instead of pushing the row into a layout-feedback loop
  • Theme.serifItalic(size:) Theme.display(size:); appDisplayItalic(_:) view modifier → appDisplay(_:); bare .italic() modifiers removed across InspectorView, QueryHistoryView, and the query-result cell renderer
  • Nine new completion-provider tests covering prefix preference, fuzzy fallback thresholds, context detection from tokens, and the li → public regression

Grab v0.3.0 from the latest release or build from source with Xcode. The EXPLAIN visualizer is the headline feature — if you write SQL that ever touches a production-sized table, paste a query in, hit Analyze, and read down the rows.