fix(compare): match data compare columns case-insensitively

This commit is contained in:
dienaso authored and GitHub committed 2026-09-06 10:29:48 +08:00
1 parent ab35a5b1da
commit eea3faa224
4 files changed
+165 -12

No files matched your search

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { inferCompareKeyColumns } from "../dataCompare";
import { inferCompareKeyColumns, intersectCompareColumns, matchColumnNameIgnoreCase } from "../dataCompare";
describe("inferCompareKeyColumns", () => {
it("returns all primary-key columns when a primary key exists", () => {
@@ -34,3 +34,40 @@ describe("inferCompareKeyColumns", () => {
expect(inferCompareKeyColumns([])).toEqual([]);
});
});
describe("intersectCompareColumns", () => {
it("matches lower-case source columns against upper-case target columns", () => {
const intersection = intersectCompareColumns([{ name: "snid" }, { name: "username" }, { name: "amount" }], [{ name: "SNID" }, { name: "USERNAME" }, { name: "AMOUNT" }]);
expect(intersection.columns).toEqual(["SNID", "USERNAME", "AMOUNT"]);
expect(intersection.sourceColumns).toEqual(["snid", "username", "amount"]);
});
it("keeps source order and omits columns missing on either side", () => {
const intersection = intersectCompareColumns([{ name: "Amount" }, { name: "snid" }, { name: "extra" }], [{ name: "SNID" }, { name: "AMOUNT" }]);
expect(intersection.columns).toEqual(["AMOUNT", "SNID"]);
expect(intersection.sourceColumns).toEqual(["Amount", "snid"]);
});
it("returns an empty intersection when no column names match ignoring case", () => {
const intersection = intersectCompareColumns([{ name: "snid" }], [{ name: "id" }]);
expect(intersection.columns).toEqual([]);
expect(intersection.sourceColumns).toEqual([]);
});
it("deduplicates duplicated source columns", () => {
const intersection = intersectCompareColumns([{ name: "snid" }, { name: "SNID" }], [{ name: "SNID" }]);
expect(intersection.columns).toEqual(["SNID"]);
expect(intersection.sourceColumns).toEqual(["snid"]);
});
});
describe("matchColumnNameIgnoreCase", () => {
it("resolves a name to the canonical entry ignoring case", () => {
expect(matchColumnNameIgnoreCase("SNID", ["snid", "name"])).toBe("snid");
expect(matchColumnNameIgnoreCase("Name", ["SNID", "NAME"])).toBe("NAME");
});
it("returns undefined when no column matches", () => {
expect(matchColumnNameIgnoreCase("missing", ["snid"])).toBeUndefined();
});
});
@@ -65,6 +65,8 @@ export interface DataCompareFromTablesOptions {
targetTable: string;
columns: string[];
keyColumns: string[];
/** Source-side column names aligned positionally with `columns`; needed when the two databases store identifier case differently (e.g. SQL Server vs Oracle). */
sourceColumns?: string[];
fetchBatchSize?: number;
degradationThreshold?: DegradationThreshold;
samplingStrategy?: SamplingStrategy;
@@ -139,3 +141,48 @@ export function inferCompareKeyColumns(columns: Pick<ColumnInfo, "name" | "is_pr
}
return primaryKeys;
}
export interface CompareColumnIntersection {
/** Target-side column names, kept in source order. */
columns: string[];
/** Source-side names aligned positionally with `columns`. */
sourceColumns: string[];
}
/**
* Intersects source and target columns case-insensitively.
*
* Databases store identifier case differently (SQL Server keeps the created
* case, Oracle and Dameng store upper case, PostgreSQL lower case), so a
* migrated table's columns only match across engines when compared without
* case. Target names are canonical because sync SQL runs against the target.
*/
export function intersectCompareColumns(sourceColumns: Pick<ColumnInfo, "name">[], targetColumns: Pick<ColumnInfo, "name">[]): CompareColumnIntersection {
const targetNamesByLowerName = new Map<string, string>();
for (const column of targetColumns) {
const lowerName = column.name.toLowerCase();
if (!targetNamesByLowerName.has(lowerName)) targetNamesByLowerName.set(lowerName, column.name);
}
const columns: string[] = [];
const sourceNames: string[] = [];
const matched = new Set<string>();
for (const column of sourceColumns) {
const lowerName = column.name.toLowerCase();
const targetName = targetNamesByLowerName.get(lowerName);
if (targetName === undefined || matched.has(lowerName)) continue;
matched.add(lowerName);
columns.push(targetName);
sourceNames.push(column.name);
}
return { columns, sourceColumns: sourceNames };
}
/**
* Resolves a column name to the matching entry of `columns`, ignoring case.
* Returns undefined when no column matches.
*/
export function matchColumnNameIgnoreCase(name: string, columns: string[]): string | undefined {
const lowerName = name.toLowerCase();
return columns.find((column) => column.toLowerCase() === lowerName);
}