Skip to content

fix(datagrid): open materialized views read-only in the data grid - #3072

Merged
datlechin merged 4 commits into
mainfrom
fix/matview-read-only-rows
Sep 23, 2026
Merged

datlechin merged 4 commits into
mainfrom
fix/matview-read-only-rows

Conversation

@datlechin

Copy link
Copy Markdown
Member

Stacked on #3063

Summary

A materialized view opened from the sidebar was an editable grid. Cell edits, Add Row, Duplicate, Paste and Delete all queued, and Save then failed, because PostgreSQL refuses every row write on a materialized view. It now opens read-only from the sidebar, from Open Quickly and from a restored tab, on every engine that lists the kind, ClickHouse included.

Root cause

TableInfo.TableType.allowsRowEditing returned true for .materializedView, on the assumption that an object holding rows accepts row DML. The sidebar derives the tab's isView from that rule, while Open Quickly hard-codes isView: true for anything it groups with views, so the same matview was writable from one route and read-only from the other. The two gates (canEditActiveResult, canAddRow) read !isView and ignored the objectType the tab already carries, so a tab saved with isView == false stayed writable after any rule fix: persisted tabs, back/forward history, recently closed tabs and window payloads all carry the old Bool.

Two more leaks sat behind the gates. RowEditingCoordinator's add, delete, duplicate and paste guards checked isEditable alone, which a table-tab load sets true for every object, views included, so a delete that reached it on a view tab staged a DELETE. And the grid's empty-space Add Row read a closure cached on four onChange hooks that never watched the object kind, the result mode or the schema.

What changed

  • allowsRowEditing is false for .materializedView, matching the iOS model (TableKind.allowsRowEditing was already false).
  • TabTableContext.allowsRowEditing is !isView && resolvedObjectKind().allowsRowEditing. A kind can refuse rows the Bool allows; it can never re-enable a tab the Bool marks read-only. canEditActiveResult reads it.
  • canAddRow now narrows canEditActiveResult rather than repeating half of it, so the toolbar never offers Add Row on a query result the grid refuses.
  • RowEditingCoordinator.addNewRow, deleteSelectedRows, duplicateSelectedRow and pasteRows guard on canEditActiveResult, the gate the Edit menu validates on. canAddRow is not used there because it is Data-mode only, and the Edit menu's Add Row and Duplicate Row run in JSON mode too (pinned by RowEditingCoordinatorJsonModeTests).
  • The grid's empty-space menu reads coordinator.canAddRow live. The cached onAddRow nil-ing in MainEditorContentView and its four onChange hooks are gone; onAddRow is wired once with the other stable refs.
  • Comments that the change made false are corrected: TabTableContext.objectType, EditorTabPayload.objectType, StructureEditEligibility, ForeignKeyEditPolicy, TableStructureView.objectKind, the canAddRow doc, and the TabObjectKindTests header.
  • Docs: databases/clickhouse.mdx (Limitations), databases/postgresql.mdx (next to the matview Structure note), features/change-tracking.mdx.
  • CHANGELOG: a Changed line for ClickHouse, so it reads as deliberate there, and a Fixed line for the Save failure.

Scope is row writes only. Row counts, Cmd+R, export, TRUNCATE eligibility (TableOperationEligibility, kind-based) and Structure gating (resolvedObjectKind()) read neither isView nor allowsRowEditing and are unchanged. The one visible side effect: MCP list_recent_tables reports is_view: true for a matview opened from the sidebar, as it already did for one opened from Open Quickly.

Measured

PostgreSQL 17.11 (Homebrew, aarch64), schema matview_rows, dropped after:

Statement on a matview Result
UPDATE, INSERT, DELETE ERROR: cannot change materialized view "mv"
TRUNCATE ERROR: "mv" is not a table
MERGE ERROR: cannot execute MERGE on relation "mv", not supported for materialized views
COPY mv FROM STDIN ERROR: cannot copy to materialized view "mv"
pg_relation_is_updatable('mv', false) 0 (a table and a simple view: 28)

INSTEAD OF / BEFORE triggers and rules are refused on a matview too, so there is no way to make one writable.

ClickHouse 26.10.1.488 (clickhouse local):

Materialized view INSERT ALTER TABLE … UPDATE / DELETE
Inner storage (ENGINE = MergeTree … POPULATE) accepted accepted
TO tgt accepted Code: 80. MATERIALIZED VIEW targets existing table matview_rows.tgt. Execute the statement directly on it. (INCORRECT_QUERY)

Both list as engine MaterializedView in system.tables, so the object list cannot tell them apart, and an editable grid over a TO view is this bug again. Editing tgt shows through mv_to at once.

Tests

New and changed cases:

  • MaterializedViewRowWriteTests (new, 11 cases): the sidebar route, sidebar and Open Quickly parity, a restored tab that kept isView == false, a restored tab with no kind, Add/Duplicate/Paste/Delete through RowEditingCoordinator on a matview and on a loaded view, and the grid's empty-space menu. Every canAddRow assertion seeds authoritative rows in Data mode, and each refusal has a table positive control.
  • TabObjectKindTests: the matview case asserts !allowsRowEditing with isView == false; three new cases pin the gate in both directions (a refusing kind wins over the Bool, the Bool wins over a writable kind, writable kinds and no kind stay writable).
  • TableInfoTests.materializedViewDisallowsRowEditing (new); matview removed from localRelationsAllowRowEditing.
  • QuickSwitcherObjectKindTests: the matview fixture is isReadOnly: true, and the cross-connection builder is asserted to produce [true, false] for a matview and a partitioned table.
  • MainContentCoordinatorAddRowTests.queryTabFollowsTheResultRefusal (new): a query result with resolved keys offers a row, one with unresolved keys does not.

Run through verify.sh, 24 suites (MaterializedViewRowWriteTests TableInfoTests TabObjectKindTests QuickSwitcherObjectKindTests QuickSwitcherItemIdentityTests QuickSwitcherViewModelTests MainContentCoordinatorAddRowTests MainContentCommandActionsResultViewTests RowEditingCoordinatorJsonModeTests RowEditingCoordinatorValueFilterTests RowEditingCoordinatorCopyTests SaveCompletionTests ValueFilterEditedRowTests CommandActionsDispatchTests PluginDriverAdapterTableTypeMappingTests EditorTabPayloadTests MultiConnectionNavigationTests TabPersistenceCoordinatorTests MainWindowToolbarValidationTests ToolbarContextResolverTests MenuContentModeParityTests MainContentCoordinatorGridSelectionTests ResultSwitchIdentityTests ResultEditabilityTests): 376 executed, 366 passed, 10 failed. All 10 are QuickSwitcherViewModelTests cases that sleep 200 ms for a search debounce and miss it under the machine's load (load average 130 to 230 from parallel builds); the count moved between runs (17, then 10), and the suite run alone passes 57 of 57. An unrelated branch on the same machine hit 18 of the same failures.

Each new test was turned red by reverting the edit it guards (measured):

Mutation Red
allowsRowEditing back to true for .materializedView TableInfoTests.materializedViewDisallowsRowEditing, QuickSwitcherObjectKindTests.crossConnectionItemsCarryTheType, TabObjectKindTests.materializedViewKeepsItsKind, .refusingKindDecidesRowEditing, MaterializedViewRowWriteTests.sidebarMaterializedViewIsReadOnly, .sidebarAndOpenQuicklyAgree, .restoredStaleTabIsReadOnly, .rowCommandsRefuseAMaterializedView, .emptySpaceMenuFollowsTheGate (9)
TabTableContext.allowsRowEditing back to !isView restoredStaleTabIsReadOnly, rowCommandsRefuseAMaterializedView, emptySpaceMenuFollowsTheGate, TabObjectKindTests.refusingKindDecidesRowEditing, .materializedViewKeepsItsKind (5)
TabTableContext.allowsRowEditing as the kind alone, without !isView TabObjectKindTests.readOnlyMarkOutranksTheKind
RowEditingCoordinator guards back to isEditable, empty-space menu back to the closure alone, canAddRow not built on canEditActiveResult rowCommandsRefuseAMaterializedView, deleteRefusesALoadedView, emptySpaceMenuFollowsTheGate, MainContentCoordinatorAddRowTests.queryTabFollowsTheResultRefusal

The last two ran together (5 red, each tied to one mutation). Positive controls stayed green throughout: sidebarTableStaysWritable, restoredTableTabStaysWritable, restoredTabWithoutAKindFollowsTheBool, rowCommandsStillWorkOnATable, deleteStillStagesOnATable, writableKindsKeepRowEditing.

Other checks: verify.sh generate, build and docs pass. verify.sh lint over the 16 touched Swift files reports one violation, sorted_imports at TableProTests/Models/Query/TabObjectKindTests.swift:9, on an import line this change does not touch (the file sits outside SwiftLint's included:).

Before / After

Screenshots to be added. States to capture, on PostgreSQL with a materialized view daily_totals in public:

  1. Before: daily_totals opened from the sidebar, a cell double-clicked into edit mode, toolbar Add Row enabled.
  2. Before: the Save error after editing a cell (cannot change materialized view).
  3. After: the same tab, cell double-click does not edit, toolbar Add Row dimmed, right-click on the grid's empty space shows no Add Row.
  4. After: Edit menu with Add Row, Duplicate Row and Delete dimmed over a selected row.
  5. After, ClickHouse: a materialized view tab, same dimmed state.

Critique points not taken

None. Every objection in the critique is applied:

  • ClickHouse and PostgreSQL docs pages carry the change, and the CHANGELOG has a ClickHouse Changed line.
  • RowEditingCoordinator guards read the gate, and the empty-space menu reads canAddRow live (the cache is removed rather than taught one more key).
  • The gate is !isView && resolvedObjectKind().allowsRowEditing, tested in both directions.
  • Every canAddRow assertion seeds authoritative rows in Data mode and has a positive control.
  • QuickSwitcherObjectKindTests uses isReadOnly: true for the matview item and asserts the production builder produces it.
  • The file_fdw case is listed below.

Deliberately not fixed here

  • Foreign tables that refuse writes. TableInfo.TableType.allowsRowEditing is true for .foreignTable (TablePro/Models/Query/QueryResult.swift:171). Measured on PostgreSQL 17.11: a file_fdw foreign table refuses UPDATE, INSERT and DELETE and pg_relation_is_updatable answers 0, while a postgres_fdw one answers 28. Writability there belongs to the object, not its kind, so it needs a driver-sourced per-object flag from pg_relation_is_updatable. The same flag would let ClickHouse report which materialized views store their own rows and give inner-storage MVs back their grid editing.
  • Name-only routes carry no kind. MCP open_table_tab, AppleScript, deeplinks, a URL table= and a query tab running SELECT * FROM mv open with objectType == nil and isView == false (openTableTab(_:schema:database:showStructure:isView:objectType:…) defaults, MainContentCoordinator+Navigation.swift:48; resolveTableEditability, MainContentCoordinator.swift:1483). Fixing them needs a catalog kind lookup, which the Structure gate needs too.
  • Oracle and Dameng materialized views list as TABLE (the container table), so they stay editable and fail with ORA-01732 on a read-only MV. That is a listing defect in those plugins.
  • No reason shown. A table tab over a view or matview refuses edits silently, the way views always have; there is no tooltip or banner saying why.
  • No UI test. The UI test sandbox has no PostgreSQL materialized view; the gates are covered by coordinator tests instead.

@datlechin
datlechin deleted the branch main September 23, 2026 19:19
@datlechin datlechin closed this Sep 23, 2026
@datlechin
datlechin deleted the fix/matview-read-only-rows branch September 23, 2026 19:19
@datlechin
datlechin restored the fix/matview-read-only-rows branch September 23, 2026 19:20
@datlechin datlechin reopened this Sep 23, 2026
@datlechin
datlechin changed the base branch from feat/2522-matview-indexes to main September 23, 2026 19:21
@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:24 PM

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

@datlechin
datlechin merged commit c504331 into main Sep 23, 2026
8 of 9 checks passed
@datlechin
datlechin deleted the fix/matview-read-only-rows branch September 23, 2026 19:26

This branch was successfully deployed

1 active deployment
staging - docs 45118ae8 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.

1 participant