From 50f4ec9f6b23ddb0290232b0834a38c53848e439 Mon Sep 17 00:00:00 2001 From: "Xsyl.way" <98275209+Xcylway@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:11:29 +0800 Subject: [PATCH] fix(grid): disambiguate duplicate column names in headers --- apps/desktop/src/components/grid/DataGrid.vue | 7 ++++- .../__tests__/sql/resultColumnNames.spec.ts | 17 ++++++++++++ apps/desktop/src/lib/sql/resultColumnNames.ts | 27 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/lib/__tests__/sql/resultColumnNames.spec.ts create mode 100644 apps/desktop/src/lib/sql/resultColumnNames.ts diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index a2d59bb67f..4820001b13 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -119,6 +119,7 @@ import { uuid } from "@/lib/common/utils"; import { generateCellValues, type CellValueGenerationKind, type CellValueGenerationOptions } from "@/lib/dataGrid/cellValueGeneration"; import { MONGO_DOCUMENT_GRID_NULL, mongoDocumentGridClipboardText, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridExternalValue, mongoDocumentGridInputValue, mongoDocumentGridNumericValue, mongoDocumentRelaxedExtendedJson } from "@/lib/mongo/mongoDocumentValues"; import { compactHeaderColumnType, formatMetadataColumnTypeLabel, isNumericColumnType, resolveDataGridTypeVisualKind, resolveHeaderColumnType, resolveResultColumnType, shortHeaderTypeName } from "@/lib/dataGrid/dataGridColumnType"; +import { dedupeResultColumnNames } from "@/lib/sql/resultColumnNames"; import { dataGridCellTextClass, dataGridTypeVisualClass } from "@/lib/dataGrid/dataGridCellTextVisual"; import { DATA_GRID_TYPE_COLOR_KEYS, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme"; import { @@ -2419,6 +2420,10 @@ const filteredGoToColumns = computed(() => { return filterDataGridColumnLookupItems(goToColumnItems.value, goToColumnSearch.value, goToColumnMatchMode.value); }); const visibleColumns = computed(() => visibleColumnIndexes.value.map((index) => props.result.columns[index])); +// Header labels only: SQL generation, sorting, transfer and export keep the raw +// result columns so duplicate-label handling (#12214) and INSERT targets stay +// correct. Duplicate labels are disambiguated for display only (#12452). +const displayColumnNames = computed(() => dedupeResultColumnNames(props.result.columns)); const visibleColumnIndexIndicators = computed(() => visibleColumns.value.map((column) => { if (!showIndexIndicatorsInHeader.value) return false; @@ -14823,7 +14828,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val { + it("renames repeated labels with numeric suffixes", () => { + expect(dedupeResultColumnNames(["id", "name", "name", "name"])).toEqual(["id", "name", "name_1", "name_2"]); + }); + + it("never collides with a label that already uses the suffix", () => { + expect(dedupeResultColumnNames(["name", "name_1", "name", "name"])).toEqual(["name", "name_1", "name_2", "name_3"]); + }); + + it("keeps unique and empty labels untouched", () => { + expect(dedupeResultColumnNames(["", "a", "b"])).toEqual(["", "a", "b"]); + expect(dedupeResultColumnNames([])).toEqual([]); + }); +}); diff --git a/apps/desktop/src/lib/sql/resultColumnNames.ts b/apps/desktop/src/lib/sql/resultColumnNames.ts new file mode 100644 index 0000000000..88d7c4fda3 --- /dev/null +++ b/apps/desktop/src/lib/sql/resultColumnNames.ts @@ -0,0 +1,27 @@ +/** + * Query results can contain duplicate column labels, e.g. + * `SELECT a.name, b.name, c.name ...` (#12452). The grid, sorting, filtering + * and copy paths all look columns up by name, so duplicates make them address + * the first match only. + * + * Rename duplicates once at the result boundary, Navicat-style: + * `name, name, name` -> `name, name_1, name_2`. Existing names are never + * reused, so a result that already contains `name_1` stays unambiguous. + */ +export function dedupeResultColumnNames(columns: readonly string[]): string[] { + const used = new Set(); + return columns.map((column) => { + if (!used.has(column)) { + used.add(column); + return column; + } + let suffix = 1; + let candidate = `${column}_${suffix}`; + while (used.has(candidate)) { + suffix += 1; + candidate = `${column}_${suffix}`; + } + used.add(candidate); + return candidate; + }); +}