fix(datagrid): repair the six defects the data rewind investigation found in the save path - #2442
Merged
Merged
Conversation
…ound in the save path Claude-Session: https://claude.ai/code/session_01KsqHrFwJxUW6eWozYjT8JZ
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
datlechin
added a commit
that referenced
this pull request
Aug 26, 2026
…mmitted (#2440) * feat(datagrid): restore the previous values of a save that already committed Claude-Session: https://claude.ai/code/session_01KsqHrFwJxUW6eWozYjT8JZ * style(datagrid): satisfy swiftlint in the data rewind storage and settings Claude-Session: https://claude.ai/code/session_01KsqHrFwJxUW6eWozYjT8JZ * fix(datagrid): repair the six defects the data rewind investigation found in the save path (#2442) Claude-Session: https://claude.ai/code/session_01KsqHrFwJxUW6eWozYjT8JZ * feat(toolbar): offer Restore Previous Values without a license, and sell it at the click Claude-Session: https://claude.ai/code/session_01KsqHrFwJxUW6eWozYjT8JZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #2440. Review that one first; this branch is based on it, not on
main.These are the six defects the Data Rewind investigation turned up in the grid save path and deliberately left out of that PR. Four were verified there; the other two are verified here and both are real.
1 and 2.
PendingChangesrenumbered a different subset of its state in each of three placesPendingChangeskeys six things by row index:changes[].rowIndex,changeIndex,insertedRowIndices,deletedRowIndices,insertedRowDataandmodifiedCells. Three functions renumbered them, and each handled a different subset:shiftRowIndicesUpshiftRowIndicesDownundoBatchRowInsertionchanges[].rowIndexinsertedRowIndicesdeletedRowIndicesinsertedRowDatamodifiedCellsThe
insertedRowDatagap is the live one. Paste three rows, delete the first, press Save: the survivors' values are still filed under their old indices, sogenerateInsertSQLfinds nothing at the index it is given, falls back tocellChanges(empty for a pasted row) and returns nil. Nothing guards INSERT the way UPDATE and DELETE are guarded, so one row is written with another row's values, the third is dropped, and Save reports success.The same function also rebuilt the returned values from
cellChanges, which holds only the columns the user typed into, compacted. Add a row, type "Bob" into the third column, delete it, Cmd+Z: Bob comes back in the first column and theDEFAULTmarkers are gone.Fixed by collapsing the three renumbering paths into one
reindex(_:)that touches all six, so a collection cannot be added without being wired in, and by reading the values frominsertedRowData, which is whatsavedInsertedValuesalready treats as the source of truth.The
deletedRowIndicesandmodifiedCellsarms are not reachable today, because a new row always lands at the end of the grid and nothing pending is ever below one. They are covered anyway, and the code says why: the alternative is three paths covering three subsets again.3. Undo wrote through a display row as if it were a storage index
PendingChangesis keyed by display position for cell edits and existing-row deletes, and by storage index for inserted rows, which is what lets an inserted row be physically removed.applyUndoResult's.cellEditarm passed its display row straight intoTableRows.edit, which indexes storage. The forward write converts two lines earlier; the undo did not. With a per-column value filter narrowing the grid the two diverge and undo reverts whichever row sits at that storage offset.Fixed by resolving through
DisplayRowMappingin that arm, the same waydeleteFilteredRowsalready does. The row arms are left alone: their index is storage-native from the start, so converting them would break what works.I reported lines 199 and 223 as having the same defect. They do not; they consume storage-native indices. Correcting that here.
4. Statements were grouped by verb, so a delete could not free a value for an insert
generateAttributedStatementsbucketed updates and deletes and flushed them after the walk, emitting every INSERT, then every UPDATE, then every DELETE. On a table withUNIQUE(email), deleting the row holdinga@b.comand adding a new row with that email is a valid end state that came out as a constraint violation.Rewritten as one ordered pass. Deletes still batch, but only across a consecutive run, so "select 500 rows, press Delete" is still one statement while a delete separated from another by an insert stays on its own side of it.
Ordering could not come from the array:
removeChangeAtswap-removes with the last element, so cancelling any earlier change relocates a later one into its slot.RowChangenow carries a monotonicsequence, stamped inappendChange, and the generator walks in that order. Without it the fix would have held only until the first undo.5. A partial commit was reported as a total failure
Verified here.
DataWriteExecutorcollected a result per statement and then dropped them all on the way out. On the thirteen drivers with no transactions, the save's catch marked every statement failed in history including the ones that had committed, kept every row pending, and told the user it failed. Pressing Save again re-ran the committed INSERTs and duplicated the rows.DataWritePartialCommitErrornow carries the committed results out, thrown when there was no transaction to roll back or when the rollback itself failed and the committed extent is unknown. The save records the committed statements as the successes they were, records only the one that failed, and shows Save Incomplete naming how many of how many landed and warning that saving again writes them twice. It does not reduce the change set: aDataWriteStepcarries no row identity to do that safely, and claiming a precision the data does not support is how this went wrong the first time.6. "Ignore foreign key checks" did nothing on three engines
Verified here. SQLite, libSQL and Cloudflare D1 disable foreign keys with
PRAGMA foreign_keys = OFF, which SQLite documents as a no-op inside a transaction, and the executor opened the transaction before step 0. SQLite issupportsForeignKeyDisable: trueandsupportsTransactions: true, so the option was reachable and inert. The re-enable never ran on the success path at all.DataWritePlangained aprologuebeside itsepilogue. The toggles are no longer steps: the prologue runs beforeBEGIN, the epilogue afterCOMMITand afterROLLBACKalike. MySQL'sSET FOREIGN_KEY_CHECKSis session-scoped, so outside the transaction is correct there too, which is why this is a placement fix rather than a new per-engine capability.displayStatementscomposes prologue, steps and epilogue, so Preview SQL and the authorization gate still show the toggles the user is approving, and the executor reports the statements it ran around the transaction so they still reach query history.Also
CellChange.rowIndexis gone. It was set at 14 construction sites and read nowhere, and no renumbering path ever touched it, so a contributor wiring new logic to it would have found it silently stale.Verification
buildtest, 26 suitesswiftlint --strict, app, plugins and testsNew tests:
PendingChangesReindexTestsandPendingChangesSequenceTests,SQLStatementGeneratorOrderingTests(five cases including that order survives the swap-remove, and that an interrupted delete run splits),UndoRowIndexTests, and six more inDataWriteExecutorTestsfor the toggle placement, the rollback path and the partial-commit error.DataWriteExecutorTestsnow has one order-recording driver for every case in the file rather than two ad hoc doubles.Run
swiftlintfrom inside the worktree rather than throughverify.sh lintwith relative paths, which resolves them against the main checkout and reported clean while three violations stood.Refs #2107
https://claude.ai/code/session_01KsqHrFwJxUW6eWozYjT8JZ