fix(grid): disambiguate duplicate column names in headers

This commit is contained in:
Xsyl.way
2026-10-17 20:11:29 +08:00
committed by GitHub
parent 7068ffae92
commit 50f4ec9f6b
3 changed files with 50 additions and 1 deletions
@@ -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
<DataGridColumnHeader
v-for="col in renderedGridColumns"
:key="`${col.name}-${col.actualColIdx}`"
:name="col.name"
:name="displayColumnNames[col.actualColIdx] ?? col.name"
:actual-column-index="col.actualColIdx"
:visible-column-index="col.visibleColIdx"
:class="{ 'crosshair-column': !!crosshairTarget?.columnCrosshair && crosshairTarget.visibleColIdx === col.visibleColIdx }"
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { dedupeResultColumnNames } from "@/lib/sql/resultColumnNames";
describe("dedupeResultColumnNames", () => {
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([]);
});
});
@@ -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<string>();
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;
});
}