SequelPG v0.1.11: database tools, query history, and a catalog-query security pass
v0.1.9 shipped the first wave of object lifecycle management: drop any object, create views/functions/sequences/types/domains, and view raw DDL in the Definition tab. v0.1.11 broadens the scope from objects-in-a-schema to the whole database — extensions, roles, indexes, constraints, triggers — and fills in the two quality-of-life features users asked for most: a visible query log and a searchable catalog of built-in functions. Underneath all that sits a large refactor: every introspection query now uses PostgresNIO parameter bindings, not hand-escaped string interpolation.
Database tools menu
A new toolbar button (the server-rack icon) opens a compact “Database tools” menu with three entries that reach beyond the current schema:
Extensions
Lists every installed extension from pg_extension alongside the installable set from pg_available_extensions, with the extension’s comment, installed version, and default version visible inline. Type into the search box to narrow down by name or description — useful when you know you want something geo-related but can’t remember if it’s postgis or postgis_topology. Installing runs CREATE EXTENSION; dropping prompts for confirmation before issuing DROP EXTENSION.
Roles & Privileges
A read-only browser over pg_roles, showing each role’s capabilities as colored badges: SUPERUSER, NOLOGIN, CREATEDB, CREATEROLE, REPLICATION. When a role is a member of other roles (via pg_auth_members), the membership chain is listed directly under the name. Login roles get a person-icon so you can scan for human accounts at a glance. Below the list is a small GRANT/REVOKE builder for the common privileges (SELECT, INSERT, UPDATE, DELETE, USAGE, EXECUTE, etc.). Complex scenarios (RESOURCE LIMITS, row-level grants) still belong in the SQL editor.
Function Library
A curated catalog of ~50 built-in PostgreSQL functions that come up in day-to-day work — string manipulation, numeric/rounding, date/time arithmetic, JSON/JSONB operators, aggregates, window functions, and array helpers. Each entry shows the signature and a one-line summary; filter by category or search by name. Double-clicking inserts the signature at your cursor in the SQL editor so you can fill in the arguments without retyping the shape. This is not a mirror of pg_proc — it’s a hand-picked shortcut for the functions you actually reach for.
Structure tab: indexes, constraints, triggers, partitions
The Structure tab used to show only the column list. For tables it now gains four additional sections, rendered as collapsible groups so a wide table still keeps its column view front and center:
- Indexes — each index shows its name, method (btree / hash / gin / gist / brin / spgist), column list, and flags for UNIQUE / PRIMARY / PARTIAL. Each row has a drop button with confirmation.
- Constraints — primary keys, foreign keys, unique, check, and exclusion constraints, pulled from
pg_constraint. Foreign keys display the referenced schema, table, and columns inline. The fullpg_get_constraintdefoutput is shown for copy-paste. - Triggers — per-trigger row collapses the per-event representation in
information_schema.triggersdown to one row, soINSERT OR UPDATEtriggers appear as a single entry with the events joined. Timing (BEFORE / AFTER / INSTEAD OF), event mask, action statement, and enabled/disabled state are all visible. - Partitions — for partitioned parent tables, the child partitions (
pg_inherits→pg_classwithrelkind = ‘p’) are listed with their fully qualified names. Non-partitioned tables skip this section entirely.
Create Index sheet
A new Create Index sheet (from the Structure tab toolbar) lets you build non-partial indexes without typing DDL: pick the columns in order, choose UNIQUE, and pick the access method (btree / hash / gin / gist / brin / spgist). Expression indexes and WHERE predicates still drop you into the SQL editor — the sheet stays minimal on purpose.
Query History panel
Press Cmd+Shift+Y to reveal a bottom-docked Query History panel that lives alongside the active tab in a VSplitView. Every query is logged with its source (Manual = you typed it; System = SequelPG issued it on your behalf for edits, inserts, deletes, cascade deletes, schema changes), execution time, row count, and error message if it failed. Filter by source with the segmented picker, clear the log with the trash icon, or click an entry to jump back to it in the SQL editor.
The subtle-but-important detail is literal redaction: by default, string literals in system-issued INSERT / UPDATE / DELETE queries are replaced with *** before they are stored in memory. This is toggled by an eye icon in the panel header. The motivation is straightforward — if you’re doing a screen share or posting a screenshot and your history includes a row you just edited that contained PII, the literal values shouldn’t be sitting there in plaintext. Flip the toggle off when you need to see exactly what was sent for debugging.
History is capped at 500 entries per tab — the most recent on top — and lives in memory only, never on disk.
Parameterized catalog queries
The headline refactor in this release is a security and correctness pass over every introspection query in DatabaseClient. Previously, each query built its SQL by running user-supplied schema and table names through replacingOccurrences(of: “'”, with: “''”) and interpolating them directly into the query string. This is the classic “manual quote doubling” defense — it works, but every new query is one missed replacement away from a bug.
Every catalog query — listTables, listViews, listMaterializedViews, listFunctions, listSequences, listTypes, listAllSchemaObjects (13 inline queries), getColumns, getPrimaryKeys, getApproximateRowCount, listIndexes, listConstraints, listTriggers, listPartitions, getObjectDDL (one query per object type, 12 total) — now uses PostgresQuery string interpolation with bound parameters. Schema and table names are passed via $1, $2, etc., which lets the PostgresNIO driver handle typing and escaping at the protocol level. Version-dependent filter fragments (e.g. p.prokind = ‘f’ vs NOT p.proisagg for PG < 11) are spliced via the explicit \(unescaped: ...) interpolation so the intent is obvious at the call site.
The net effect is the same catalog behavior with a smaller risk surface, clearer queries, and — as a bonus — slightly better query plans on older servers, because prepared statements can now be cached by the driver.
Refactor: 12 smaller cleanups
Alongside the parameterization pass, twelve targeted code-quality improvements landed. None of them change behavior, but they’re worth listing for anyone reading the diff:
CellValue.isNulluses an exhaustiveswitchrather thanif case, so adding a new case is a compiler error until it’s handled.ConnectionProfiledrops its manualinit(from:)in favor of a tiny@DecodableDefaultproperty wrapper that supplies defaults for SSH fields missing from older stored profiles. The struct now uses synthesizedCodableagain.defaultInsertValuematches exactinformation_schema.data_typevalues via aTemporalColumnKindenum (“timestamp with time zone”, “time without time zone”, etc.) instead of substring heuristics liketype.contains(“tz”).- Query table-ref regex helper — the quoted/unquoted group-selection logic in
QueryViewModel.parseTableFromQueryis now a singlefirstCaptureGroup(in:match:groups:)helper. - Inspector null branches — the three repeated
if value.isNull { Text(“NULL”)}branches in JSON / array / boolean previews fold into one sharednullOr(_:_:)@ViewBuilder. decodeCellValueis now table-driven — a[PostgresDataType: (PostgresCell) -> CellValue?]dictionary replaces the longswitch/try?chain. Adding a new type is one dictionary entry.- Filter validation dedup —
applyContentFiltersandpreviewFilterSQLshared their filter-validation logic via copy-paste; it now lives in a singlevalidFilterConditions()helper so both see the exact same rules. buildFilterConditioninverts to aguardclause and extracts the “Any Column” branch intobuildAnyColumnCondition, turning a 40-line function with nested ifs into two focused ones.CascadeDeleteBuilderpulls the 100+ line WHERE/CTE-construction block out ofexecuteCascadeDeleteinto a testable struct with a handful of focused private methods. The DB execution, error handling, and refresh logic stay in the view model.ConnectionFormModelconsolidates the 14 parallel@Statefields that existed in bothConnectionFormViewandStartPageView. The two views now share one struct, oneload(from:)method, and onebuildProfile(id:fallbackPort:)builder.RowDeleteConfirmingprotocol formalizes the shared delete-row-confirmation concern betweenTableViewModelandQueryViewModel. Both conform with avar deleteConfirmationRowIndex: Int?requirement.
Under the hood
- Five new model types:
ExtensionInfo,RoleInfo,IndexInfo,ConstraintInfo,TriggerInfo. PostgresClientProtocolgainedlistExtensions,listAvailableExtensions,listRoles,listIndexes,listConstraints,listTriggers, andlistPartitions.QueryHistoryViewModel(new) owns the rolling 500-entry log;AppViewModelgates its visibility withshowQueryHistory.- The SwiftUI command group adds
Cmd+Shift+Yas the “Toggle Query History” shortcut, sent viaNotificationCenterso per-tab AppViewModels can react independently.
What’s next
With database-wide tools and the introspection refactor in, the next priorities are:
- Live monitoring panels —
pg_stat_activity,pg_locks, andpg_stat_user_tables. - Foreign-key cell navigation — click an FK value, jump to the referenced row.
- Enum value browser (types list shows the type, but not the labels yet).
- Inline signature hints in the SQL editor, reusing the Function Library data.
- Row-Level Security policy management and Publications / Subscriptions for logical replication.
SequelPG is open source on GitHub. Grab v0.1.11 from the releases page, file issues, or send a PR. Feedback is very welcome.