fix(data-compare): avoid unsafe key inference
This commit is contained in:
1 parent
665e5920c9
commit
fab5c75fd2
4 files changed
+109
-6
No files matched your search
@@ -14,6 +14,7 @@ import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection } from
|
||||
import { isSchemaAware } from "@/lib/database/databaseCapabilities";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import type { DataCompareCellValue, DataCompareModifiedRow, DataCompareResult, DataCompareRow, DataCompareSyncPlan, DataCompareSyncPlanTableOptions } from "@/lib/dataGrid/dataCompare";
|
||||
import { inferCompareKeyColumns } from "@/lib/dataGrid/dataCompare";
|
||||
import type { ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
@@ -394,9 +395,7 @@ async function loadColumnsWithCache(cache: Map<string, CompareColumn[]>, connect
|
||||
async function inferKeyColumnsForTable(table: string, sourceColumnCache?: Map<string, CompareColumn[]>): Promise<string[]> {
|
||||
if (!sourceConnectionId.value || !sourceDatabase.value || !sourceSchema.value || !table) return [];
|
||||
const columns = sourceColumnCache ? await loadColumnsWithCache(sourceColumnCache, sourceConnectionId.value, sourceDatabase.value, sourceSchema.value, table) : (((await api.getColumns(sourceConnectionId.value, sourceDatabase.value, sourceSchema.value, table)) as CompareColumn[]) ?? []);
|
||||
const primaryKeys = columns.filter((column) => column.is_primary_key).map((column) => column.name);
|
||||
if (primaryKeys.length > 0) return primaryKeys;
|
||||
return columns.slice(0, 1).map((column) => column.name);
|
||||
return inferCompareKeyColumns(columns);
|
||||
}
|
||||
|
||||
async function inferKeyColumns() {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { inferCompareKeyColumns } from "../dataCompare";
|
||||
|
||||
describe("inferCompareKeyColumns", () => {
|
||||
it("returns all primary-key columns when a primary key exists", () => {
|
||||
expect(
|
||||
inferCompareKeyColumns([
|
||||
{ name: "id", is_primary_key: true },
|
||||
{ name: "name", is_primary_key: false },
|
||||
]),
|
||||
).toEqual(["id"]);
|
||||
});
|
||||
|
||||
it("returns a composite primary key as-is", () => {
|
||||
expect(
|
||||
inferCompareKeyColumns([
|
||||
{ name: "tenant_id", is_primary_key: true },
|
||||
{ name: "user_id", is_primary_key: true },
|
||||
{ name: "name", is_primary_key: false },
|
||||
]),
|
||||
).toEqual(["tenant_id", "user_id"]);
|
||||
});
|
||||
|
||||
it("returns an empty array instead of falling back to the first column when there is no primary key", () => {
|
||||
expect(
|
||||
inferCompareKeyColumns([
|
||||
{ name: "category", is_primary_key: false },
|
||||
{ name: "name", is_primary_key: false },
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array for an empty column list", () => {
|
||||
expect(inferCompareKeyColumns([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -118,3 +118,24 @@ export interface DataCompareSyncPlan {
|
||||
syncStatements: string[];
|
||||
syncSql: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely infers the columns to use as comparison keys for a table.
|
||||
*
|
||||
* Only primary-key columns are trusted: a primary key is unique by
|
||||
* definition, so comparing on it can never produce duplicate-key failures.
|
||||
* Composite primary keys are returned as-is.
|
||||
*
|
||||
* When no primary key exists an empty array is returned so the caller can ask
|
||||
* the user to pick matching columns explicitly. Falling back to the first
|
||||
* column would silently select an arbitrary, possibly non-unique field
|
||||
* (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[] {
|
||||
const primaryKeys: string[] = [];
|
||||
for (const column of columns) {
|
||||
if (column.is_primary_key) primaryKeys.push(column.name);
|
||||
}
|
||||
return primaryKeys;
|
||||
}
|
||||
Reference in new issue
Block a user