feat(data-compare): support per-table match column configuration
This commit is contained in:
1 parent
aa4e0a30fd
commit
d10216e302
18 files changed
+1359
-120
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { inferCompareKeyColumns, intersectCompareColumns, matchColumnNameIgnoreCase } from "../dataCompare";
|
||||
import { inferCompareKeyColumns, intersectCompareColumns, matchColumnNameIgnoreCase, normalizeKeyColumns, sameKeyColumns } from "../dataCompare";
|
||||
|
||||
describe("inferCompareKeyColumns", () => {
|
||||
it("returns all primary-key columns when a primary key exists", () => {
|
||||
@@ -71,3 +71,36 @@ describe("matchColumnNameIgnoreCase", () => {
|
||||
expect(matchColumnNameIgnoreCase("missing", ["snid"])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeKeyColumns", () => {
|
||||
it("keeps the given order so a composite key stays stable", () => {
|
||||
expect(normalizeKeyColumns(["tenant_id", "order_id"])).toEqual(["tenant_id", "order_id"]);
|
||||
});
|
||||
|
||||
it("trims names and drops blanks", () => {
|
||||
expect(normalizeKeyColumns([" id ", "", " ", "name"])).toEqual(["id", "name"]);
|
||||
});
|
||||
|
||||
it("drops case-insensitive duplicates but keeps the first spelling", () => {
|
||||
expect(normalizeKeyColumns(["ID", "id", "Id"])).toEqual(["ID"]);
|
||||
});
|
||||
|
||||
it("returns an empty array for an empty selection", () => {
|
||||
expect(normalizeKeyColumns([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sameKeyColumns", () => {
|
||||
it("ignores order and identifier case", () => {
|
||||
expect(sameKeyColumns(["tenant_id", "order_id"], ["ORDER_ID", "TENANT_ID"])).toBe(true);
|
||||
});
|
||||
|
||||
it("detects different selections and different lengths", () => {
|
||||
expect(sameKeyColumns(["id"], ["ID", "name"])).toBe(false);
|
||||
expect(sameKeyColumns(["id"], ["name"])).toBe(false);
|
||||
});
|
||||
|
||||
it("treats two empty selections as equal", () => {
|
||||
expect(sameKeyColumns([], [])).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -121,6 +121,9 @@ export interface DataCompareSyncPlan {
|
||||
syncSql: string;
|
||||
}
|
||||
|
||||
/** Minimal column shape a match-column picker needs, so it can be fed straight from `getColumns`. */
|
||||
export type CompareKeyColumnOption = Pick<ColumnInfo, "name" | "is_primary_key">;
|
||||
|
||||
/**
|
||||
* Safely infers the columns to use as comparison keys for a table.
|
||||
*
|
||||
@@ -134,7 +137,7 @@ export interface DataCompareSyncPlan {
|
||||
* (category, status, date, ...) and make the whole table comparison fail with
|
||||
* an unhelpful duplicate-key error.
|
||||
*/
|
||||
export function inferCompareKeyColumns(columns: Pick<ColumnInfo, "name" | "is_primary_key">[]): string[] {
|
||||
export function inferCompareKeyColumns(columns: CompareKeyColumnOption[]): string[] {
|
||||
const primaryKeys: string[] = [];
|
||||
for (const column of columns) {
|
||||
if (column.is_primary_key) primaryKeys.push(column.name);
|
||||
@@ -142,6 +145,41 @@ export function inferCompareKeyColumns(columns: Pick<ColumnInfo, "name" | "is_pr
|
||||
return primaryKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a match-column selection: trims entries, drops blanks and removes
|
||||
* case-insensitive duplicates while keeping the caller's order.
|
||||
*
|
||||
* Order is preserved because the backend builds each row key from `keyColumns`
|
||||
* in order; a stable order keeps diff keys identical between runs, and it lets a
|
||||
* composite primary key keep the column order the table declares.
|
||||
*/
|
||||
export function normalizeKeyColumns(columns: readonly string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const normalized: string[] = [];
|
||||
for (const column of columns) {
|
||||
const name = column.trim();
|
||||
if (!name) continue;
|
||||
const lowerName = name.toLowerCase();
|
||||
if (seen.has(lowerName)) continue;
|
||||
seen.add(lowerName);
|
||||
normalized.push(name);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two match-column selections ignoring order and identifier case.
|
||||
* Used to decide whether a manual selection still equals the inferred primary
|
||||
* key, in which case it carries no override.
|
||||
*/
|
||||
export function sameKeyColumns(a: readonly string[], b: readonly string[]): boolean {
|
||||
const normalizedA = normalizeKeyColumns(a);
|
||||
const normalizedB = normalizeKeyColumns(b);
|
||||
if (normalizedA.length !== normalizedB.length) return false;
|
||||
const lowerB = new Set(normalizedB.map((column) => column.toLowerCase()));
|
||||
return normalizedA.every((column) => lowerB.has(column.toLowerCase()));
|
||||
}
|
||||
|
||||
export interface CompareColumnIntersection {
|
||||
/** Target-side column names, kept in source order. */
|
||||
columns: string[];
|
||||
|
||||
Reference in new issue
Block a user