Skip to content

feat(sidebar): browse PostgreSQL user-defined types, edit enum labels and offer them in the type picker - #2612

Merged
datlechin merged 1 commit into
mainfrom
feat/user-defined-types
Sep 3, 2026
Merged

feat(sidebar): browse PostgreSQL user-defined types, edit enum labels and offer them in the type picker#2612
datlechin merged 1 commit into
mainfrom
feat/user-defined-types

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #2484

What this adds

A Types section under each PostgreSQL schema beside Functions and Triggers, listing enums, composites, domains and ranges. Opening one shows its CREATE statement rebuilt from pg_type, with the catalog's properties beside it. An enum's labels sit above the definition and can be added, at the end or before or after a neighbour, and renamed in place. The structure editor's type picker gains a User-Defined group of the table's own types, Open Quickly indexes types, and the MCP server gets list_types.

PostgreSQL and PGlite in this PR. CockroachDB and Redshift are sibling driver classes in the same plugin and keep the empty default. DuckDB, Cassandra, MSSQL and Oracle each have a comparable catalog, and the PluginKit surface makes each one a single +Types.swift, but each needs a probe against a live server, so they are follow-ups.

How it is built

PluginKit, additive: PluginUserDefinedTypeInfo, PluginUserDefinedTypeKind, PluginUserDefinedTypeField, PluginEnumLabelPlacement, and five defaulted requirements on PluginDatabaseDriver (fetchUserDefinedTypes, fetchUserDefinedType, createTypeTemplate, generateAddEnumLabelSQL, generateRenameEnumLabelSQL) plus DriverPlugin.supportsUserDefinedTypeBrowse. PluginKit 21 is still unreleased on main (#2607's bump has no tag), so these ride that bump and every registry plugin is rebuilt with it anyway.

PostgreSQL: one catalog query over pg_type gated on typtype IN ('e','c','d','r'). Composites are kept only where the backing relation is a stand-alone type (relkind = 'c'), so every table's row type stays out; extension members are excluded through pg_depend the way routines are; arrays and the multirange 14 creates beside every range are other typtypes. PostgreSQL 17 records a domain's NOT NULL as a pg_constraint row, so the constraint list keeps contype = 'c' alone. There is no pg_get_typedef, so PostgreSQLTypeDefinition rebuilds the statement from the row and is pure so every shape is pinned by a test; it carries a domain's own collation, a composite field's collation, a range's collation, its non-default operator class qualified, and a multirange name the creator chose (PostgreSQL's default replaces a trailing range rather than appending). The listing also carries the server's own quote_ident spelling of each type, which is what the picker offers. Version gates: pg_range at 9.2, json_build_object at 9.4, ADD VALUE BEFORE/AFTER at 9.1, RENAME VALUE at 10. A literal carrying a backslash is written as an E'' string so it reads the same under standard_conforming_strings=off; the oid predicate accepts only an integer.

App: a fourth SidebarObjectCategory, .type, threaded through the same seams routines and triggers use: SchemaService for the flat list, DatabaseTreeMetadataService per database and schema, DatabaseTreeNode.Kind.userType, the row view, menu spec, type-select, double-click, selection, filter buckets and the quick switcher. SidebarObjectListPresentation.resolve takes one hasSideObjects instead of two parallel bools. DatabaseObjectRef gains .userType and an optional typeKind, so a persisted tab from before this change still decodes.

One fix outside the new paths: the tree's empty-container check counted every side fetch as pending, and a fetch an engine never runs stays idle for good. Types made that universal, so sideLoadStates now counts only the kinds the engine declares. The same gap already held triggers on a tree-layout engine without trigger browsing.

Enum editing runs each edit as one statement the moment the field commits, authorised through ExecutionGateProvider as a schema mutation, recorded in history, then the listings reload and the open grids get the same refresh a trigger edit sends. It runs on the metadata route, a dedicated autocommit connection wherever the engine can pool one, not the session driver: that one holds whatever transaction the user opened in a query tab, and a label added inside it is unusable until the commit, gone on a rollback, and invisible to the reload. ADD VALUE is written IF NOT EXISTS from 9.3 on, so the driver's one reconnect-and-resend cannot fail on a label that already landed; rename has no such form and stays the same class as every other DDL the driver sends. ALTER TYPE … ADD VALUE cannot run inside a transaction block before 12 anyway, so there is nothing to stage. PostgreSQL cannot drop or reorder a label; the UI does not offer it and the docs say why.

Type picker: fetched for the table's own database on popover open through DatabaseManager.withMetadataDriver, across every non-system schema, because the tree service's cache is expansion-driven and empty in the default flat layout. Every entry is schema-qualified, the table's own schema included, because PostgreSQL searches pg_catalog ahead of the search path and a bare text names the built-in even where the schema holds a domain called text. The spelling is the server's quote_ident, so reserved and mixed-case names arrive quoted; the client-side fallback quotes anything that is not a plain lower-case identifier.

Verified

  • Catalog query probed with psql against PostgreSQL 17.11 on a throwaway cluster holding an enum, a composite, two domains, a range, a table, hstore and tablefunc: only the user types came back, with labels, fields, constraints and the range's subtype diff parsed as expected.
  • Debug build driven against that cluster: the Types section lists the schema's types, filters by name, Open Quickly opens the definition tab, and adding a label through the + control ran ALTER TYPE … ADD VALUE (confirmed in psql) and refreshed the labels and the statement.
  • verify.sh generate, build, test (the suites owning every touched type), build PostgreSQLDriver, lint, docs: see the verdicts in the thread. verify.sh plugins is inconclusive on this machine for the known oracle-nio @TaskLocal macro issue; CI runs the aggregate.
  • Codex review (working tree) returned nine findings; all four P1s and three P2s are fixed here. The remaining P2, keying the flat-list type fetch by scope rather than connection, is the same shape routines and triggers already use and is listed below as a follow-up.
  • Codex adversarial-review returned six findings. Fixed here: enum edits joining the user's open transaction (now the metadata route), the non-idempotent ADD VALUE under the driver's reconnect resend (now IF NOT EXISTS), a same-schema type resolving to a built-in and reserved names left bare (server-side spelling, always qualified), the catalog state the rebuilt definition dropped (collations, opclass namespace, multirange name), and the identity lookup that widened a malformed oid into a schema listing and took the first row (fails closed, verifies the oid, no schema predicate). Dismissed: the PluginKit version bump. Adding defaulted requirements does break a plugin rebuilt against them under an older host, but PluginKit 21 has not shipped: v0.71.0 carries 20 and the registry holds 19 and 20 only, so every registry plugin is rebuilt for 21 with the release that feat(plugins)!: order SQL exports by foreign key dependency and report what the order cannot fix (#2517) #2607 already requires.
  • The extended catalog query, both the schema listing and the by-oid reload, was compiled in a standalone harness and run verbatim against the cluster after these changes.
  • Not driven live: the type picker popover itself, because the drawn grid cell does not take an accessibility click; its entries are pinned by unit tests.
  • No TableProUITests case: the flow needs a live PostgreSQL, which CI does not have.

Before / After

Before: a column of type order_status shows the type name and nothing else; nothing lists the schema's types.

After: the docs screenshot pair committed at docs/images/user-defined-types-sidebar(-dark).png shows the Types section, the definition tab and the label list. They were captured on a 1x display, so they are 1512x861 rather than the 3024x1722 the other docs images carry.

Follow-ups noticed, not changed here

  • PostgresColumnTypeResolver.swift:53: any USER-DEFINED column type without enum labels is labelled ENUM(name), so a composite or range column reads as an enum in the structure grid.
  • SchemaService.swift:36: the routine, trigger and type fetches are deduplicated per connection rather than per scope, so two windows loading different databases at once can share one result.

https://claude.ai/code/session_014THhVdsUjbCboxNLnQB1dR

@mintlify

mintlify Bot commented Sep 3, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Sep 3, 2026, 2:51 AM

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

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin
datlechin merged commit 210f647 into main Sep 3, 2026
14 checks passed
@datlechin
datlechin deleted the feat/user-defined-types branch September 3, 2026 03:31
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.

PostgreSQL user-defined types in the structure editor and sidebar

1 participant