Skip to content

refactor(datagrid): drop DataGridIdentity + version-counter bridge#938

Merged
datlechin merged 2 commits intomainfrom
refactor/datagrid-phase-d-b-drop-identity
Apr 29, 2026
Merged

refactor(datagrid): drop DataGridIdentity + version-counter bridge#938
datlechin merged 2 commits intomainfrom
refactor/datagrid-phase-d-b-drop-identity

Conversation

@datlechin
Copy link
Copy Markdown
Member

Summary

Completes the Phase D arc: removes the entire SwiftUI counter-driven reload bridge from the data grid. The metadataChanged (FK column reload) and paginationChanged (scroll-to-top) branches in reloadAndSyncSelection were the last consumers of DataGridIdentity, and they're now explicit dispatch calls on TableViewCoordinating. With both gone, DataGridIdentity has no consumers and the early-return short-circuit goes with it.

Net −165 LOC including the deleted DataGridIdentityTests (no longer applicable).

What changes

New TableViewCoordinating methods

  • refreshForeignKeyColumns() — reloads visible FK column cells. Logic lifted verbatim from the old metadataChanged branch.
  • scrollToTop()tableView.scrollRowToVisible(0) if any rows exist.

Dispatch wiring

  • FK refresh: MainContentCoordinator+QueryHelpers.swift:495 (the columnEnumValues enrichment that bumps metadataVersion) now also calls refreshForeignKeyColumns() if the affected tab is active. The other metadataVersion bump (line 290 in setupResultsForExecutedQuery) is followed immediately by setActiveTableRows, which already dispatches applyFullReplace and renders FK columns fresh — no extra dispatch needed there.
  • Scroll to top: pagination is trickier. paginateAfterConfirmation bumps paginationVersion BEFORE runQuery fires; the scroll has to wait until the new data lands. Added pendingScrollToTopAfterReplace flag on MainContentCoordinator. paginateAfterConfirmation sets it; notifyFullReplaceIfActive checks and clears it after applyFullReplace fires.

Deletions

  • struct DataGridIdentity (25 LOC)
  • var lastIdentity on TableViewCoordinator
  • The currentIdentity == coordinator.lastIdentity early-return short-circuit in updateNSView
  • previousIdentity capture, metadataChanged / paginationChanged variables
  • metadataVersion, paginationVersion, schemaVersion props on DataGridView (no remaining readers)
  • The matching call-site args in MainEditorContentView.dataGridView and TableStructureView
  • lastIdentity = nil writes in applyInsertedRows / applyRemovedRows / applyFullReplace
  • DataGridIdentityTests.swift (entire file, 105 LOC)

Subtle preserved behavior

The "initial pre-warm display cache" check (was previousIdentity == nil || previousIdentity?.rowCount == 0) becomes oldRowCount == 0, rowDisplayCount > 0 — same intent, derived from the existing cachedRowCount instead of the dropped identity.

Performance note

Without the early-return, every SwiftUI body re-eval runs updateNSView's full body. After the previous Phase D + cachedTableRows collapse + sort-cache merge + FK paste-routing PRs, that body is mostly cheap idempotent assignments and Equatable guards (syncDisplayFormats, rebuildVisualStateCache is version-guarded, updateColumns only runs if columnsChanged, etc.). For typical tables this is microseconds per re-eval — cost is negligible compared to what the early-return was protecting against.

Test plan

  • Build clean
  • All affected tests pass (TableRowsMutationTests, DataTabGridDelegateTests, SortCacheInvalidationTests, CellPasteRoutingTests, EvictionTests)
  • Manual: open table with FK columns → arrows render
  • Manual: pagination Next/Prev → scrolls back to row 0
  • Manual: cell edit → yellow modified marker → undo clears
  • Manual: add row, delete row, undo each
  • Manual: switch tables → fresh data renders
  • Manual: sort by column header → sorted correctly

Phase D recap

This closes out the multi-PR arc that started with PR #933:

The data grid no longer has any SwiftUI counter machinery for cell edits, row inserts/removals, FK metadata refreshes, or pagination scrolls. All four are explicit dispatches on TableViewCoordinating. Tab switches still dispatch through setActiveTableRowsapplyFullReplace. Sort changes dispatch through dataGridDidReplaceAllRows.

…ter bridge (Phase D-b)

Completes Phase D. The remaining metadataChanged (FK column reload) and paginationChanged (scroll-to-top) branches in updateNSView were the last consumers of DataGridIdentity. Both convert to explicit dispatch on TableViewCoordinating:

- refreshForeignKeyColumns(): reloads visible FK column cells (lifted verbatim from the metadataChanged branch). Called from MainContentCoordinator+QueryHelpers after the columnEnumValues mutation that bumps metadataVersion. The other metadataVersion bump (in setupResultsForExecutedQuery) is followed immediately by setActiveTableRows, which already dispatches applyFullReplace and renders FK columns fresh — no extra dispatch needed there.

- scrollToTop(): scrolls to row 0 if any rows exist. Trickier than refreshFK because pagination bumps the version BEFORE runQuery is fired; the scroll has to wait until the new data lands. Added pendingScrollToTopAfterReplace flag on MainContentCoordinator. paginateAfterConfirmation sets it; notifyFullReplaceIfActive checks and clears it after applyFullReplace fires.

With both branches gone, DataGridIdentity has no consumers. Drops:
- struct DataGridIdentity (entire type, 25 LOC)
- var lastIdentity on TableViewCoordinator
- The early-return short-circuit in updateNSView
- previousIdentity capture and the metadataChanged / paginationChanged variables
- metadataVersion / paginationVersion / schemaVersion props on DataGridView (no remaining readers)
- The matching call-site args in MainEditorContentView.dataGridView and TableStructureView
- DataGridIdentityTests entirely (-105 LOC test file)

The 'initial pre-warm display cache' check (was `previousIdentity == nil || previousIdentity?.rowCount == 0`) becomes `oldRowCount == 0, rowDisplayCount > 0` — same intent, derived from the existing cachedRowCount instead of the dropped identity.

Without the early-return, every SwiftUI body re-eval runs updateNSView's full body. After previous Phase D + the cachedTableRows / sort-cache / FK-routing PRs, that body is mostly cheap idempotent assignments and Equatable guards. Smoke-tested cell edit + undo, add row + undo, table switch, FK column display, pagination scroll, sort. Net -165 LOC including the deleted test file.
@datlechin datlechin changed the title refactor(datagrid): drop DataGridIdentity + version-counter bridge (Phase D-b) refactor(datagrid): drop DataGridIdentity + version-counter bridge Apr 29, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e59834b42

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


mutate(&self.tabManager.tabs[idx].pagination)
self.tabManager.tabs[idx].paginationVersion += 1
self.pendingScrollToTopAfterReplace = true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Track pending pagination scroll per tab

pendingScrollToTopAfterReplace is a single global flag set during pagination, but it is consumed only when notifyFullReplaceIfActive runs for whichever tab is currently selected. If the user paginates tab A and switches to tab B before A’s query result applies, tab A’s setActiveTableRows path returns early (tabManager.tabs[idx].id == tabId guard fails), so A never gets its scroll-to-top; later, an unrelated full replace on tab B can consume the flag and scroll B unexpectedly. This is a real behavior regression from the old paginationVersion-driven logic for cross-tab timing.

Useful? React with 👍 / 👎.

…spatch tests

The original Bool flag could strand if pagination's runQuery failed before setActiveTableRows fired — the flag would stay set, and the next setActiveTableRows from any other code path (undo, FK navigation, multi-statement query) would unexpectedly scroll to top.

Changing to Set<UUID> scopes the intent per-tab. Pagination inserts the tabId; notifyFullReplaceIfActive checks via remove() and only dispatches scrollToTop on a matching hit. A stranded entry stays scoped to that one tab and only fires when that tab next receives a full replace — narrow blast radius.

Adds three regression tests:
- scrollToTopFiresOnPendingFlag: setActiveTableRows on the active tab WITH the flag set dispatches scrollToTop and clears the flag
- scrollToTopFlagIsScopedPerTab: a flag set for tab A doesn't fire when tab B is replaced; tab A's entry stays
- scrollToTopSkippedWhenFlagAbsent: setActiveTableRows without the flag doesn't scroll
@datlechin datlechin merged commit b87be8a into main Apr 29, 2026
2 checks passed
@datlechin datlechin deleted the refactor/datagrid-phase-d-b-drop-identity branch April 29, 2026 05:30
datlechin added a commit that referenced this pull request Apr 29, 2026
…+ fix delete-row visual regression (#940)

* refactor(coordinator): migrate RowOperations to selectedTabAndIndex + fix invalidateCachesForUndoRedo regression

Two changes:

1. RowOperations migration. Six methods (addNewRow, deleteSelectedRows, duplicateSelectedRow, handleUndoResult, pasteRows, updateCellInTab) used the selectedTabIndex + bounds-check + tabs[index] pattern; now use selectedTabAndIndex which returns (tab, index) atomically. Three read-only methods (copySelectedRowsToClipboard, copySelectedRowsWithHeaders, copySelectedRowsAsJson) drop the index entirely and use selectedTab. undoInsertRow only needs the tabId so it uses selectedTab?.id. Behavior unchanged.

2. Bug fix discovered during RowOperations smoke test: pressing Delete on an existing row didn't show the red background until the user clicked another row. Phase D-b (PR #938) dropped the versionChanged branch in reloadAndSyncSelection, which used to reload visible rows on any changeManager.reloadVersion bump. invalidateCachesForUndoRedo cleared the displayCache and rebuilt the visualStateCache but never told NSTableView to re-query, so visible cell views kept rendering the pre-change state. Adding a reloadData(forRowIndexes: visibleRows, columnIndexes: allCols) call after the cache invalidation restores the lost behavior. Same root cause covers undo/redo of cell edits, deletes, and inserts.

* test+refactor: lock invalidateCachesForUndoRedo dispatch, tighten copy/undo paths

Two follow-ups to the review:

1. Read-only copy paths (copySelectedRowsToClipboard, copySelectedRowsWithHeaders, copySelectedRowsAsJson) and undoInsertRow now use selectedTabAndIndex instead of selectedTab. selectedTab has a fallback to tabs.first when selectedTabId is nil — the old selectedTabIndex pattern did not. Switching to selectedTabAndIndex (with _ for unused index) restores strict equivalence with the pre-refactor behavior. Closes the subtle init-transient divergence the review flagged.

2. New RowOperationsDispatchTests covers the dispatch wiring restored by this PR's bug fix:
   - softDeleteDispatchesInvalidate: deleteSelectedRows on existing rows fires invalidateCachesForUndoRedo (the path that triggers the visible-rows reload). Without this, a future refactor could silently re-drop the reload (as Phase D-b accidentally did).
   - physicalDeleteDispatchesDelta: deleting an inserted row fires applyDelta and bypasses invalidate — guards the branching in deleteSelectedRows so the two paths don't get tangled.
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