Skip to content

feat(datagrid): show whether a materialized view can be refreshed concurrently, and gate its structure edits by kind - #3063

Merged
datlechin merged 5 commits into
mainfrom
feat/2522-matview-indexes
Sep 23, 2026
Merged

datlechin merged 5 commits into
mainfrom
feat/2522-matview-indexes

Conversation

@datlechin

Copy link
Copy Markdown
Member

Summary

A materialized view's Indexes tab now says whether Refresh Materialized View can run concurrently, and the Structure tab stops offering edits a materialized view refuses on the routes that still offered them.

Asks 1 and 2 of #2522 (list a matview's indexes, add and drop them) already worked on PostgreSQL when the view was opened from the sidebar, since #2772 and #2778. What was left:

  • Ask 3 had a data source and no consumer. concurrentRefreshAvailability (since feat(plugins): refresh materialized views, show view DDL and edit object comments (#2726) #2753) was read by the Refresh prompt only.
  • Open Quickly dropped the object's kind. It maps a materialized view to its .view row kind for the icon and opened the tab with isView: true and no objectType, so the Structure tab gated it as a plain view and refused Add Index with "A view cannot have indexes."
  • PGlite runs the PostgreSQL driver and PostgreSQL 17, and was left on the tables-only edit matrix, so a PGlite matview's index add and drop were refused the same way.
  • Three surfaces beside the grid skipped the edit gate: the inspector let you change a matview column's Type, Nullable and Default (the server refuses all three), the row menu offered Delete and Duplicate where they did nothing, and the Triggers tab offered New Trigger on a matview.

What changed

  • StructureEditingSession holds the answer as a MetadataLoadState, asked of the server after every index fetch (first load, Cmd+R, after Save, after Refresh Materialized View) and on remount. It is never derived from the index grid: an index left INVALID by a failed CREATE UNIQUE INDEX CONCURRENTLY lists exactly like a valid one. A failed recheck shows "Couldn't check…" rather than the previous answer, because the index list beside it has already been refetched, and only the latest request may settle the state, so a remount's check that finishes after a post-save check cannot overwrite it.
  • ConcurrentRefreshNoteView shows it as one secondary line in a bottom safeAreaInset of the Indexes tab, mirroring the Filter field's top inset. Nothing shows where the engine has no concurrent refresh.
  • QuickSwitcherItem.tableType carries the TableInfo.TableType the row was built from, through both same-window arms and the cross-window route (LaunchIntent.openTable(objectType:) into EditorTabPayload).
  • PGlite gets structureEdits: .postgreSQL.
  • StructureEditGate.locksField(at:on:orderedFields:) is the one lock rule. The grid's per-heading lock, the inspector (new InspectorRowField.isEditable) and the commit path all read it, so no surface takes a keystroke another refuses. A list whose add the object or engine refuses (Indexes on a view, Foreign Keys on a matview, Indexes on an engine without CREATE INDEX) is locked whole instead of taking input and dropping it.
  • Row menu: canDuplicate / canDelete from the gate replace one engine-wide Bool.
  • TriggerEditEligibility withholds triggers only from the kinds measured to refuse them. It is deliberately not a cell of the edit matrix, which offers an uncurated engine nothing but tables: SQLite, SQL Server and Oracle take INSTEAD OF triggers on views.
  • "Available once the view holds rows" was the wrong condition in the Refresh prompt too. PostgreSQL checks relispopulated, and a view refreshed over an empty result is populated with zero rows. Both strings now say "populated".

No PluginKit change, so no kit bump.

Measured on PostgreSQL 17.11

  • The driver's indexList returns a matview's indexes, expression keys included (no relkind filter).
  • CREATE TRIGGER: accepted on a table, partitioned table, view (INSTEAD OF) and foreign table; refused on a matview (relation "mv" cannot have triggers) and a system catalog.
  • ALTER COLUMN … TYPE, SET DEFAULT, SET NOT NULL on a matview: all refused.
  • REFRESH MATERIALIZED VIEW CONCURRENTLY on a matview created WITH NO DATA over an empty query: refused until one plain refresh, then accepted with relispopulated = t and 0 rows.
  • End to end in a sandboxed Debug build: daily_totals (unique index on day, region) shows "This view can be refreshed concurrently."; region_totals (non-unique index only) shows the unique-index requirement; after CREATE UNIQUE INDEX on the server and a refresh, the line switched to available; the matview's Triggers tab showed no action bar.

Before / After

Before: the same tab with no line under the list, which is what 0.75.0 shows. After:

Indexes tab of daily_totals with a unique index on day and region, saying it can be refreshed concurrently

Indexes tab of region_totals with only a non-unique index, naming the unique index it needs

The same tab in dark appearance

Tests

  • New: MaterializedViewConcurrentRefreshNoteTests, MaterializedViewConcurrentRefreshCheckTests (name, schema and scope reach the driver; other kinds never ask; recheck replaces; a failed recheck reports the failure; an older request finishing last cannot overwrite a newer answer), QuickSwitcherObjectKindTests.
  • Extended: StructureEditGateTests (PGlite matrix, trigger rule, the per-tab lock), StructureGridDelegateInspectorTests (a matview's locked fields refuse inspector commits, unlocked ones take them, row menu flags), StructureRowMenuRouteTests, MultiRowEditStateTests.
  • 434 cases across the 31 suites that own the changed types pass locally.
  • No UI test: the flow needs a live PostgreSQL server, and the UI test sandbox has none it can reach deterministically. It was checked by hand as above.

Deliberately not fixed here

A tab opened by name alone (a deeplink, AppleScript, the MCP open_table_tab tool, or a tab file from before tabs stored their kind) still guesses its kind from isView, as it did before this change. A first version resolved it from SchemaService's loaded object list, and two review rounds found five defects in that path: the per-schema cache has no database identity, the $states subscription can fire before loadedScope is committed, adopting a kind left isView/isEditable on the guess, dropping a guessed session could lose a half-typed inline edit, and the lookup used the raw tab schema instead of the resolved one. Doing it properly needs a scope-atomic catalog revision first, so it was cut to what the Open Quickly route can carry today. For the same reason, choosing a materialized view in Open Quickly while a kind-less tab on it is already open reuses that tab as it is.

Open Quickly still opens a materialized view with isView: true, which keeps its rows read-only, as before this change. That is the right answer on PostgreSQL, where UPDATE on a materialized view fails with cannot change materialized view; the sidebar route, which makes those rows editable, is the one that is wrong, and it is reported separately rather than copied here.

Fixes #2522

@mintlify

mintlify Bot commented Sep 23, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 23, 2026, 7:18 PM

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

…exes

# Conflicts:
#	CHANGELOG.md
#	TablePro/Resources/Localizable.xcstrings
#	TablePro/ViewModels/QuickSwitcherViewModel.swift
@datlechin
datlechin merged commit 7d7499a into main Sep 23, 2026
5 checks passed
@datlechin
datlechin deleted the feat/2522-matview-indexes branch September 23, 2026 19:19
@datlechin
datlechin restored the feat/2522-matview-indexes branch September 23, 2026 19:20

This branch was successfully deployed

1 active deployment
staging - docs 3460bdf7 Deployed Sep 23, 2026 by mintlify[bot]
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.

Show indexes on materialized views in the structure tab

1 participant