fix(plugins): refuse a table transfer that maps two source columns to one destination column - #3187
Merged
datlechin merged 6 commits intoSep 30, 2026
Conversation
… one destination column
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
…pping' into fix/table-transfer-duplicate-column-target # Conflicts: # CHANGELOG.md # TablePro/Core/Plugins/ImportDataSinkAdapter.swift # TablePro/Views/Import/RowImportSheet.swift # TableProTests/Core/Plugins/ImportDataSinkAdapterMappingTests.swift
datlechin
changed the base branch from
main
to
feat/import-remembered-column-mapping
September 29, 2026 16:54
datlechin
added this pull request to stack #3209
September 29, 2026 20:15
…pping' into fix/table-transfer-duplicate-column-target # Conflicts: # TableProTests/Core/Plugins/ImportDataSinkAdapterMappingTests.swift
…pping' into fix/table-transfer-duplicate-column-target # Conflicts: # CHANGELOG.md
datlechin
removed this pull request from stack #3209
September 30, 2026 10:12
datlechin
added this pull request to stack #3221
September 30, 2026 10:14
datlechin
removed this pull request from stack #3221
September 30, 2026 10:15
datlechin
merged commit Sep 30, 2026
3ada102
into
feat/import-remembered-column-mapping
1 check passed
This branch was successfully deployed
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 #3183 (
feat/import-remembered-column-mapping). Review and merge #3183 first; this PR's diff is only the Table Transfer work on top of it.Root cause
Table Transfer could send two source columns into one destination column, and nothing stopped it before a row was written.
TableColumnMatcher.applying(overrides:to:destination:)wrotemapping[source] = targetwithout asking whether another source column already heldtarget. The mapping editor offered every destination column in every row, andTableTransferSheet.canTransferchecked only running, selection and destination.TableTransferServicepassed the mapping through untouched.TableColumnMatcher.matchdid the same on its own. It looked destinations up by lowercased name with the last spelling winning and no claim, so source columnsNameandnameboth matched a lonename.Every path ends the same way: the sink builds
INSERT INTO t (name, name), the server refuses the first batch, and with Delete existing rows first on and Wrap each table in a transaction off, theDELETEhas already committed. The destination table is left empty.Fix
ImportColumnMatcheruses for the row import sheet.Match.contestedDestinationsnames any destination column more than one source column is mapped to. An override onto a held column is kept and reported, not resolved by quietly unmapping the other column.match(source:destination:overrides:)is the one entry point the sheet and the editor share. It replacesapplying(overrides:to:destination:), and both paths build their result through one resolver, so the unmatched columns stay in each table's own column order. The override path used to return them sorted by name.TableTransferMappingEditorholds its overrides in@Stateseeded ininitand reports each edit throughonChange, per the.popoverinvariant in CLAUDE.md (Select fields not working when trying to add highlighting rule #3015). Through the old customBindinga pick changed only the sheet's state, which the popover does not redraw from, so the conflict message and the existing "Not written" line would have stayed stale.transfer()refuses a contested mapping for every table before the first table is touched, so noDELETEruns. The sheet already blocks this; the check covers any other caller. It also passes the source table's columns to the sink assourceFields, the input feat(plugins): remember row import column mappings per table and add Match by Name and Match by Position #3183 added, so a source column the mapping skips is never folded onto its case twin's destination column.docs/features/import-export.mdxgains a "Column mapping" subsection. The Delete existing rows first warning stays above it.Dropped as duplicate of #3183
This branch was built off
mainin parallel with #3183 and carried its own copy of the same sink change. It now merges #3183 in and keeps none of it:ImportDataSinkAdapter: feat(plugins): remember row import column mappings per table and add Match by Name and Match by Position #3183's version is kept as is (exact names first; the case-insensitive fallback only for a field the sheet never listed, and only when one listed field, one mapping key and one row field share the spelling). This branch's copy had the same rule with a different ambiguity count. Checked that Table Transfer needs nothing feat(plugins): remember row import column mappings per table and add Match by Name and Match by Position #3183's sink lacks: a skipped source twin is insourceFieldsso it never folds, and a driver that labels result columns in another case thanfetchColumnsstill folds, because that spelling is not insourceFields.ImportService: was byte-identical to feat(plugins): remember row import column mappings per table and add Match by Name and Match by Position #3183's, so nothing to drop.RowImportSheet: feat(plugins): remember row import column mappings per table and add Match by Name and Match by Position #3183's version is kept. It already passes every detected field (Set(plan.fields)) assourceFields.newSpellingOfOneListedFieldFolds), and a skipped twin never folds (skippedFieldNeverFolds; the dropped copy's extrainsertRowrefusal is whatunmappedRowIsRefusedcovers). Those two were dropped. One distinct case is kept: a row carrying the skipped field and an unknown spelling of it together reaches no column. The "Field matching ignores case" doc comment keeps this branch's wording, because feat(plugins): remember row import column mappings per table and add Match by Name and Match by Position #3183's says the mapping is matched case-insensitively, which its own sink no longer does for a listed field.Tests
Built and tested in a worktree through
verify.sh, after the merge:build: PASStest TableColumnMatcherTests TableTransferServiceTests ImportDataSinkAdapterMappingTests SQLServerImportBatchTests NativeDumpBatchTests RowImportMappingTests ImportColumnMatcherTests: PASS, 89 executed, 89 passed, 0 failed (12, 12, 13, 9, 6, 14, 23)linton the seven Swift files this PR changes over feat(plugins): remember row import column mappings per table and add Match by Name and Match by Position #3183: 0 violations. The step reports FAIL only for a stale/Applications/Xcode-beta.apppath in.claude/skills/fix-issue/references/verification.md, which this branch does not touch.verify.sh docs(house style, source claims, links): PASS.python3 scripts/localization.py verify: ok.Cases this PR adds:
TableColumnMatcherTests(new, 12): exact spelling wins over a case twin, first twin wins when neither is exact, twins on both sides reach their own columns, an override onto a held column is contested, skipping one side clears it. "No overrides gives the automatic match" uses leftovers out of name order on both sides, and "An override leaves the unmatched source columns in the source table's order" checks the override path.TableTransferServiceTests(+4): a contested mapping is refused, and with delete-first on and no transaction the destination sees no statement at all; a source withNameandnameinto a lonenamewritesnameonce, with the exact twin's value; twins mapped to their own columns each reach their own column. Both end-to-end cases run through the realExportDataSourceAdapter,ImportDataSinkAdapter(feat(plugins): remember row import column mappings per table and add Match by Name and Match by Position #3183's) andPluginDriverAdapterover a stub plugin driver.ImportDataSinkAdapterMappingTests(+1 over feat(plugins): remember row import column mappings per table and add Match by Name and Match by Position #3183): an unknown spelling beside a skipped field of the same name reaches no column.Review
Codex reviewed the merged branch against
feat/import-remembered-column-mappingand reported no findings. Earlier rounds againstmainare unchanged in substance: one independent review found the sink fallback writing a skipped field into its twin's column (now fixed by #3183's sink plussourceFieldsfrom Table Transfer), a matcher test that could not fail (fixed with out-of-order leftovers), and the delete warning under the wrong heading (moved).Not verified
@Stateshape follows the measured.popoverinvariant; it was not measured on this popover.fetchColumnsreturns. If a driver differs by case, Table Transfer still reaches the column through the sink's fallback, because the header spelling is not insourceFields.Found while investigating #3172 (#3183)