feat(grid): support composite unique reference keys

This commit is contained in:
zipg authored and GitHub committed 2026-08-27 16:00:00 +08:00
1 parent cf03de122a
commit eb0994e980
19 files changed
+219 -56

No files matched your search

+27 -16
View File
@@ -77,7 +77,7 @@ import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
import EnumCellEditor from "@/components/grid/EnumCellEditor.vue";
import DataGridReadonlyTextSelection from "@/components/grid/DataGridReadonlyTextSelection.vue";
import GridSnapshotDialog from "@/components/grid/GridSnapshotDialog.vue";
import type { QueryResult, ColumnInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo, TableInfoTab, QueryResultSourceColumnRef } from "@/types/database";
import type { QueryResult, ColumnInfo, DatabaseType, ForeignKeyInfo, IndexInfo, ReferenceKeyInfo, TriggerInfo, TableInfoTab, QueryResultSourceColumnRef } from "@/types/database";
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import { tableObjectSourceKind } from "@/lib/table/tableObjectSourceKind";
import { tableColumnDefaultDisplayValue } from "@/lib/table/tableColumnDefaultPresentation";
@@ -180,6 +180,7 @@ import {
resolveColumnFormatter,
type ColumnFormatterConfig,
type DateTimeFormatterUnit,
type ForeignKeyDisplayFilterConfig,
DateTimePatterns,
} from "@/lib/dataGrid/columnFormatter";
import { temporalCellEditorConfig, type TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor";
@@ -1163,7 +1164,7 @@ const formatterForeignKeyManual = ref(false);
const formatterForeignKeySchemas = ref<string[]>([]);
const formatterForeignKeyTables = ref<string[]>([]);
const formatterForeignKeyColumns = ref<ColumnInfo[]>([]);
const formatterForeignKeyReferenceKeyColumns = ref<string[]>([]);
const formatterForeignKeyReferenceKeys = ref<ReferenceKeyInfo[]>([]);
const formatterForeignKeyReferenceMetadataStatus = ref<ManualReferenceMetadataStatus>("loading");
const formatterForeignKeySchemasLoading = ref(false);
const formatterForeignKeyTablesLoading = ref(false);
@@ -1183,8 +1184,18 @@ interface ForeignKeyDisplayLabelState {
labels: Map<string, string>;
}
const formatterForeignKeyReferenceColumns = computed(() => (formatterForeignKeyManual.value ? manualReferenceKeyColumns(formatterForeignKeyColumns.value, formatterForeignKeyReferenceKeyColumns.value) : formatterForeignKeyColumns.value));
const formatterForeignKeyReferenceValidation = computed(() => manualReferenceColumnValidation(formatterForeignKeyColumns.value, formatterForeignKeyReferenceKeyColumns.value, formatterForeignKeyRefColumn.value, formatterForeignKeyReferenceMetadataStatus.value));
const formatterForeignKeyReferenceFilter = computed<ForeignKeyDisplayFilterConfig | undefined>(() =>
formatterForeignKeyFilterEnabled.value
? {
column: formatterForeignKeyFilterColumn.value,
mode: formatterForeignKeyFilterMode.value,
value: formatterForeignKeyFilterValue.value,
endValue: formatterForeignKeyFilterEndValue.value,
}
: undefined,
);
const formatterForeignKeyReferenceColumns = computed(() => (formatterForeignKeyManual.value ? manualReferenceKeyColumns(formatterForeignKeyColumns.value, formatterForeignKeyReferenceKeys.value, formatterForeignKeyReferenceFilter.value) : formatterForeignKeyColumns.value));
const formatterForeignKeyReferenceValidation = computed(() => manualReferenceColumnValidation(formatterForeignKeyColumns.value, formatterForeignKeyReferenceKeys.value, formatterForeignKeyRefColumn.value, formatterForeignKeyReferenceMetadataStatus.value, formatterForeignKeyReferenceFilter.value));
const foreignKeyDisplayLabels = shallowRef(new Map<number, ForeignKeyDisplayLabelState>());
const foreignKeyDisplayRequests = createForeignKeyDisplayRequestCoordinator();
@@ -1606,7 +1617,7 @@ function loadFormatterDraft(formatter: ColumnFormatterConfig | undefined) {
formatterForeignKeySchemas.value = [];
formatterForeignKeyTables.value = [];
formatterForeignKeyColumns.value = [];
formatterForeignKeyReferenceKeyColumns.value = [];
formatterForeignKeyReferenceKeys.value = [];
formatterForeignKeyReferenceMetadataStatus.value = "loading";
formatterForeignKeyTargetError.value = "";
formatterForeignKeyColumnsError.value = "";
@@ -1668,7 +1679,7 @@ function formatterForeignKeyForColumn(columnIndex: number): ForeignKeyInfo | und
async function loadFormatterForeignKeyColumns(columnIndex: number) {
const request = ++formatterForeignKeyColumnsRequest;
formatterForeignKeyColumns.value = [];
formatterForeignKeyReferenceKeyColumns.value = [];
formatterForeignKeyReferenceKeys.value = [];
formatterForeignKeyReferenceMetadataStatus.value = "loading";
formatterForeignKeyColumnsError.value = "";
formatterForeignKeyReferenceMetadataError.value = "";
@@ -1679,7 +1690,7 @@ async function loadFormatterForeignKeyColumns(columnIndex: number) {
const manual = formatterForeignKeyManual.value;
const [columnsResult, referenceKeysResult] = await Promise.allSettled([
api.getColumns(props.connectionId, props.database || "", schema, formatterForeignKeyRefTable.value, props.tableMeta?.catalog),
manual ? api.listReferenceKeyColumns(props.connectionId, props.database || "", schema, formatterForeignKeyRefTable.value, props.tableMeta?.catalog) : Promise.resolve([] as string[]),
manual ? api.listReferenceKeys(props.connectionId, props.database || "", schema, formatterForeignKeyRefTable.value, props.tableMeta?.catalog) : Promise.resolve([] as ReferenceKeyInfo[]),
]);
if (request !== formatterForeignKeyColumnsRequest || formatterOpenColumn.value !== columnIndex) return;
if (columnsResult.status === "rejected") throw columnsResult.reason;
@@ -1689,10 +1700,10 @@ async function loadFormatterForeignKeyColumns(columnIndex: number) {
formatterForeignKeyReferenceMetadataStatus.value = "unavailable";
formatterForeignKeyReferenceMetadataError.value = String(referenceKeysResult.reason?.message || referenceKeysResult.reason);
} else {
formatterForeignKeyReferenceKeyColumns.value = referenceKeysResult.status === "fulfilled" ? referenceKeysResult.value : [];
formatterForeignKeyReferenceKeys.value = referenceKeysResult.status === "fulfilled" ? referenceKeysResult.value : [];
formatterForeignKeyReferenceMetadataStatus.value = "available";
}
const referenceColumns = manualReferenceKeyColumns(columns, formatterForeignKeyReferenceKeyColumns.value);
const referenceColumns = manualReferenceKeyColumns(columns, formatterForeignKeyReferenceKeys.value, formatterForeignKeyReferenceFilter.value);
const foreignKey = formatterForeignKeyForColumn(columnIndex);
if (manual) {
formatterForeignKeyRefColumn.value = reconcileManualReferenceColumn(formatterForeignKeyRefColumn.value, referenceColumns, formatterForeignKeyReferenceMetadataStatus.value);
@@ -1763,7 +1774,7 @@ function selectFormatterForeignKeySchema(value: string, columnIndex: number) {
formatterForeignKeyDisplayColumn.value = "";
formatterForeignKeyFilterColumn.value = "";
formatterForeignKeyColumns.value = [];
formatterForeignKeyReferenceKeyColumns.value = [];
formatterForeignKeyReferenceKeys.value = [];
formatterForeignKeyReferenceMetadataStatus.value = "loading";
formatterForeignKeyReferenceMetadataError.value = "";
void loadFormatterManualReferenceTables(columnIndex);
@@ -1775,7 +1786,7 @@ function selectFormatterForeignKeyTable(value: string, columnIndex: number) {
formatterForeignKeyRefColumn.value = "";
formatterForeignKeyDisplayColumn.value = "";
formatterForeignKeyFilterColumn.value = "";
formatterForeignKeyReferenceKeyColumns.value = [];
formatterForeignKeyReferenceKeys.value = [];
formatterForeignKeyReferenceMetadataStatus.value = "loading";
formatterForeignKeyReferenceMetadataError.value = "";
void loadFormatterForeignKeyColumns(columnIndex);
@@ -10925,12 +10936,12 @@ async function loadForeignKeyDisplayLabels() {
const refColumnInfo = columns.find((column) => column.name === config.refColumn);
if (!refColumnInfo) return;
if (config.referenceMode === "manual") {
const referenceKeyColumns = await foreignKeyDisplayRequests.request(requestGeneration, JSON.stringify(["reference-key-columns", props.connectionId, props.database || "", props.tableMeta?.catalog || "", schema, config.refTable]), () =>
api.listReferenceKeyColumns(props.connectionId!, props.database || "", schema, config.refTable, props.tableMeta?.catalog),
const referenceKeys = await foreignKeyDisplayRequests.request(requestGeneration, JSON.stringify(["reference-keys", props.connectionId, props.database || "", props.tableMeta?.catalog || "", schema, config.refTable]), () =>
api.listReferenceKeys(props.connectionId!, props.database || "", schema, config.refTable, props.tableMeta?.catalog),
);
if (!referenceKeyColumns || !foreignKeyDisplayRequests.isCurrent(requestGeneration)) return;
if (!manualReferenceKeyColumnIsUnique(columns, referenceKeyColumns, config.refColumn)) {
appendDebugLog("warn", "[DBX][DataGrid:foreign-key-display:non-unique-manual-reference]", {
if (!referenceKeys || !foreignKeyDisplayRequests.isCurrent(requestGeneration)) return;
if (!manualReferenceKeyColumnIsUnique(columns, referenceKeys, config.refColumn, config.filter)) {
appendDebugLog("warn", "[DBX][DataGrid:foreign-key-display:unsafe-manual-reference]", {
column: props.result.columns[columnIndex],
reference: `${schema}.${config.refTable}.${config.refColumn}`,
});
+2 -2
View File
@@ -1756,8 +1756,8 @@ export default {
formatterForeignKeyReferenceColumn: "Matching column",
formatterForeignKeyReferenceColumnPlaceholder: "Select the column matching this value",
formatterForeignKeyReferenceMetadataUnavailable: "Could not verify matching-column uniqueness: {error}",
formatterForeignKeyReferenceInvalid: "This column is missing or is not a valid single-column unique key. Select another column.",
formatterForeignKeyReferenceUnavailable: "The target table has no usable single-column unique key.",
formatterForeignKeyReferenceInvalid: "This column is missing or is not guaranteed unique by a unique key and the current equality filter. Select another column.",
formatterForeignKeyReferenceUnavailable: "The target table has no usable unique key. For a composite unique key, fix the other column with an equality filter.",
formatterForeignKeyDisplayColumn: "Display column",
formatterForeignKeyDisplayColumnPlaceholder: "Select a display column",
formatterForeignKeyFilter: "Limit referenced rows",
+2 -2
View File
@@ -1636,8 +1636,8 @@ export default withEnglishFallback({
formatterForeignKeyReferenceColumn: "Columna relacionada",
formatterForeignKeyReferenceColumnPlaceholder: "Seleccionar la columna que coincide con este valor",
formatterForeignKeyReferenceMetadataUnavailable: "No se pudo verificar la unicidad de la columna relacionada: {error}",
formatterForeignKeyReferenceInvalid: "La columna no existe o no es una clave única válida de una sola columna. Seleccione otra columna.",
formatterForeignKeyReferenceUnavailable: "La tabla de destino no tiene ninguna clave única utilizable de una sola columna.",
formatterForeignKeyReferenceInvalid: "La columna no existe o una clave única y el filtro de igualdad actual no garantizan que sea única. Seleccione otra columna.",
formatterForeignKeyReferenceUnavailable: "La tabla de destino no tiene ninguna clave única utilizable. Para una clave única compuesta, fije la otra columna con un filtro de igualdad.",
formatterForeignKeyDisplayColumn: "Columna de visualización",
formatterForeignKeyDisplayColumnPlaceholder: "Seleccionar una columna",
formatterForeignKeyFilter: "Limitar filas referenciadas",
+2 -2
View File
@@ -1634,8 +1634,8 @@ export default withEnglishFallback({
formatterForeignKeyReferenceColumn: "Colonna correlata",
formatterForeignKeyReferenceColumnPlaceholder: "Seleziona la colonna corrispondente a questo valore",
formatterForeignKeyReferenceMetadataUnavailable: "Impossibile verificare l'unicità della colonna correlata: {error}",
formatterForeignKeyReferenceInvalid: "La colonna non esiste o non è una chiave univoca valida a colonna singola. Seleziona un'altra colonna.",
formatterForeignKeyReferenceUnavailable: "La tabella di destinazione non ha chiavi univoche utilizzabili a colonna singola.",
formatterForeignKeyReferenceInvalid: "La colonna non esiste o non è garantita come univoca da una chiave univoca e dal filtro di uguaglianza corrente. Seleziona un'altra colonna.",
formatterForeignKeyReferenceUnavailable: "La tabella di destinazione non ha chiavi univoche utilizzabili. Per una chiave univoca composta, fissa l'altra colonna con un filtro di uguaglianza.",
formatterForeignKeyDisplayColumn: "Colonna visualizzata",
formatterForeignKeyDisplayColumnPlaceholder: "Seleziona una colonna",
formatterForeignKeyFilter: "Limita le righe referenziate",
+2 -2
View File
@@ -1652,8 +1652,8 @@ export default withEnglishFallback({
formatterForeignKeyReferenceColumn: "対応列",
formatterForeignKeyReferenceColumnPlaceholder: "現在の値に対応する列を選択",
formatterForeignKeyReferenceMetadataUnavailable: "対応列の一意性を確認できませんでした:{error}",
formatterForeignKeyReferenceInvalid: "この列は存在しないか、有効な単一列の一意キーではありません。別の列を選択してください。",
formatterForeignKeyReferenceUnavailable: "対象テーブルに利用可能な単一列の一意キーがありません。",
formatterForeignKeyReferenceInvalid: "この列は存在しないか、一意キーと現在の等価条件によって一意性を保証できません。別の列を選択してください。",
formatterForeignKeyReferenceUnavailable: "対象テーブルに利用可能な一意キーがありません。複合一意キーでは、等価条件で別の列を固定してください。",
formatterForeignKeyDisplayColumn: "表示列",
formatterForeignKeyDisplayColumnPlaceholder: "表示列を選択",
formatterForeignKeyFilter: "参照行を絞り込む",
+2 -2
View File
@@ -1636,8 +1636,8 @@ export default withEnglishFallback({
formatterForeignKeyReferenceColumn: "Coluna relacionada",
formatterForeignKeyReferenceColumnPlaceholder: "Selecione a coluna correspondente a este valor",
formatterForeignKeyReferenceMetadataUnavailable: "Não foi possível verificar a exclusividade da coluna relacionada: {error}",
formatterForeignKeyReferenceInvalid: "A coluna não existe ou não é uma chave exclusiva válida de uma única coluna. Selecione outra coluna.",
formatterForeignKeyReferenceUnavailable: "A tabela de destino não tem uma chave exclusiva utilizável de uma única coluna.",
formatterForeignKeyReferenceInvalid: "A coluna não existe ou não tem unicidade garantida por uma chave exclusiva e pelo filtro de igualdade atual. Selecione outra coluna.",
formatterForeignKeyReferenceUnavailable: "A tabela de destino não tem uma chave exclusiva utilizável. Para uma chave exclusiva composta, fixe a outra coluna com um filtro de igualdade.",
formatterForeignKeyDisplayColumn: "Coluna de exibição",
formatterForeignKeyDisplayColumnPlaceholder: "Selecione uma coluna",
formatterForeignKeyFilter: "Limitar linhas referenciadas",
+2 -2
View File
@@ -1681,8 +1681,8 @@ export default withEnglishFallback({
formatterForeignKeyReferenceColumn: "关联字段",
formatterForeignKeyReferenceColumnPlaceholder: "选择与当前字段匹配的字段",
formatterForeignKeyReferenceMetadataUnavailable: "无法验证关联字段唯一性:{error}",
formatterForeignKeyReferenceInvalid: "该字段不存在,或不是有效的单列唯一键。请重新选择。",
formatterForeignKeyReferenceUnavailable: "目标表没有可用的单列唯一键。",
formatterForeignKeyReferenceInvalid: "该字段不存在,或无法由唯一键和当前等值条件保证唯一。请重新选择。",
formatterForeignKeyReferenceUnavailable: "目标表没有可用的唯一键。联合唯一键需使用等值条件固定另一个字段。",
formatterForeignKeyDisplayColumn: "显示字段",
formatterForeignKeyDisplayColumnPlaceholder: "选择用于显示的字段",
formatterForeignKeyFilter: "限定引用数据",
+2 -2
View File
@@ -1635,8 +1635,8 @@ export default withEnglishFallback({
formatterForeignKeyReferenceColumn: "關聯欄位",
formatterForeignKeyReferenceColumnPlaceholder: "選擇與目前欄位匹配的欄位",
formatterForeignKeyReferenceMetadataUnavailable: "無法驗證關聯欄位的唯一性:{error}",
formatterForeignKeyReferenceInvalid: "此欄位不存在,或不是有效的單欄唯一鍵。請重新選擇。",
formatterForeignKeyReferenceUnavailable: "目標資料表沒有可用的單欄唯一鍵。",
formatterForeignKeyReferenceInvalid: "此欄位不存在,或無法由唯一鍵和目前的等值條件保證唯一。請重新選擇。",
formatterForeignKeyReferenceUnavailable: "目標資料表沒有可用的唯一鍵。複合唯一鍵需使用等值條件固定另一個欄位。",
formatterForeignKeyDisplayColumn: "顯示欄位",
formatterForeignKeyDisplayColumnPlaceholder: "選擇用於顯示的欄位",
formatterForeignKeyFilter: "限定引用資料",
@@ -10,6 +10,7 @@ import {
manualReferenceColumnValidation,
manualReferenceKeyColumnIsUnique,
manualReferenceKeyColumns,
manualReferenceKeySupportsColumn,
reconcileManualReferenceColumn,
singleColumnForeignKey,
splitForeignKeyDisplayValues,
@@ -69,12 +70,48 @@ describe("dataGridForeignKeyDisplay", () => {
{ name: "Code", is_primary_key: false },
].map((column) => ({ data_type: "varchar", is_nullable: false, column_default: null, extra: null, ...column })) as ColumnInfo[];
expect(manualReferenceKeyColumns(columns, ["code"]).map((column) => column.name)).toEqual(["code"]);
expect(manualReferenceKeyColumnIsUnique(columns, ["code"], "code")).toBe(true);
expect(manualReferenceKeyColumnIsUnique(columns, ["code"], "Code")).toBe(false);
const referenceKeys = [{ columns: ["code"] }];
expect(manualReferenceKeyColumns(columns, referenceKeys).map((column) => column.name)).toEqual(["code"]);
expect(manualReferenceKeyColumnIsUnique(columns, referenceKeys, "code")).toBe(true);
expect(manualReferenceKeyColumnIsUnique(columns, referenceKeys, "Code")).toBe(false);
expect(manualReferenceKeyColumnIsUnique(columns, [], "tenant_id")).toBe(false);
});
it("supports one column of a two-column unique key only when the other column is fixed", () => {
const columns = ["dict_key", "dict_value", "label"].map((name) => ({ name, data_type: "varchar", is_nullable: false, column_default: null, is_primary_key: false, extra: null })) as ColumnInfo[];
const referenceKeys = [{ columns: ["dict_key", "dict_value"] }];
const statusFilter = { column: "dict_key", mode: "equals" as const, value: "status" };
expect(manualReferenceKeyColumns(columns, referenceKeys, statusFilter).map((column) => column.name)).toEqual(["dict_value"]);
expect(manualReferenceKeyColumnIsUnique(columns, referenceKeys, "dict_value", statusFilter)).toBe(true);
expect(manualReferenceKeyColumnIsUnique(columns, referenceKeys, "dict_key", statusFilter)).toBe(false);
});
it("requires an exact non-empty equals filter for composite reference keys", () => {
const referenceKeys = [{ columns: ["DictKey", "dict_value"] }];
expect(manualReferenceKeySupportsColumn(referenceKeys, "dict_value", { column: "DictKey", mode: "equals", value: "status" })).toBe(true);
expect(manualReferenceKeySupportsColumn(referenceKeys, "dict_value", { column: "dictkey", mode: "equals", value: "status" })).toBe(false);
expect(manualReferenceKeySupportsColumn(referenceKeys, "dict_value", { column: "DictKey", mode: "equals", value: " " })).toBe(false);
expect(manualReferenceKeySupportsColumn(referenceKeys, "dict_value", { column: "DictKey", mode: "like", value: "status" })).toBe(false);
expect(manualReferenceKeySupportsColumn(referenceKeys, "dict_value", { column: "DictKey", mode: "in", value: "status,gender" })).toBe(false);
expect(manualReferenceKeySupportsColumn(referenceKeys, "dict_value", { column: "DictKey", mode: "between", value: "a", endValue: "z" })).toBe(false);
});
it("does not infer uniqueness from a partially fixed three-column key", () => {
const threeColumnKey = [{ columns: ["tenant_id", "dict_key", "dict_value"] }];
const filter = { column: "dict_key", mode: "equals" as const, value: "status" };
expect(manualReferenceKeySupportsColumn(threeColumnKey, "dict_value", filter)).toBe(false);
});
it("accepts a column when any current key proves it unique", () => {
const referenceKeys = [{ columns: ["tenant_id", "code"] }, { columns: ["external_id"] }];
expect(manualReferenceKeySupportsColumn(referenceKeys, "code", { column: "tenant_id", mode: "equals", value: "acme" })).toBe(true);
expect(manualReferenceKeySupportsColumn(referenceKeys, "external_id")).toBe(true);
});
it("preserves saved reference columns when metadata is invalid or unavailable", () => {
const columns = [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }] as ColumnInfo[];
@@ -84,6 +121,7 @@ describe("dataGridForeignKeyDisplay", () => {
expect(reconcileManualReferenceColumn("", columns, "unavailable")).toBe("");
expect(manualReferenceColumnValidation(columns, [], "legacy_code", "available")).toBe("invalid");
expect(manualReferenceColumnValidation(columns, [], "legacy_code", "unavailable")).toBe("unavailable");
expect(manualReferenceColumnValidation(columns, [{ columns: ["tenant_id", "legacy_code"] }], "legacy_code", "available", { column: "tenant_id", mode: "equals", value: "" })).toBe("invalid");
});
it("deduplicates current-page keys with type-safe identities and bounded batches", () => {
+1
View File
@@ -175,6 +175,7 @@ export const getSqlServerColumnMetadata = forward("getSqlServerColumnMetadata");
export const listDataTypes = forward("listDataTypes");
export const listIndexes = forward("listIndexes");
export const listReferenceKeyColumns = forward("listReferenceKeyColumns");
export const listReferenceKeys = forward("listReferenceKeys");
export const listForeignKeys = forward("listForeignKeys");
export const listTriggers = forward("listTriggers");
export const listConstraints = forward("listConstraints");
+5
View File
@@ -21,6 +21,7 @@ import type {
ColumnInfo,
SqlServerColumnMetadata,
IndexInfo,
ReferenceKeyInfo,
ForeignKeyInfo,
TriggerInfo,
ConstraintInfo,
@@ -906,6 +907,10 @@ export async function listReferenceKeyColumns(connectionId: string, database: st
return get(`/api/schema/reference-key-columns?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
}
export async function listReferenceKeys(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ReferenceKeyInfo[]> {
return get(`/api/schema/reference-keys?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
}
export async function listForeignKeys(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ForeignKeyInfo[]> {
return get(`/api/schema/foreign-keys?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
}
+11
View File
@@ -37,6 +37,7 @@ import type {
ColumnInfo,
SqlServerColumnMetadata,
IndexInfo,
ReferenceKeyInfo,
ForeignKeyInfo,
TriggerInfo,
ConstraintInfo,
@@ -1807,6 +1808,16 @@ export async function listReferenceKeyColumns(connectionId: string, database: st
});
}
export async function listReferenceKeys(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ReferenceKeyInfo[]> {
return invoke("list_reference_keys", {
connectionId,
database,
schema,
table,
catalog,
});
}
export async function listForeignKeys(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ForeignKeyInfo[]> {
return invoke("list_foreign_keys", {
connectionId,
@@ -2,7 +2,7 @@ import { displayCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
import type { ColumnFormatterConfig, ForeignKeyDisplayFilterConfig } from "@/lib/dataGrid/columnFormatter";
import { isNumericColumnType } from "@/lib/dataGrid/dataGridColumnType";
import type { ForeignKeyAssociation } from "@/lib/dataGrid/dataGridForeignKeyNavigation";
import type { ColumnInfo, ForeignKeyInfo, QueryResult } from "@/types/database";
import type { ColumnInfo, ForeignKeyInfo, QueryResult, ReferenceKeyInfo } from "@/types/database";
export type ForeignKeyDisplayConfig = Extract<ColumnFormatterConfig, { kind: "foreign-key-display" }>;
@@ -169,19 +169,29 @@ export function foreignKeyDisplayConfigIsUsable(config: ForeignKeyDisplayConfig,
export type ManualReferenceMetadataStatus = "loading" | "available" | "unavailable";
export type ManualReferenceColumnValidation = "pending" | "valid" | "invalid" | "unavailable";
export function manualReferenceKeyColumns(columns: readonly ColumnInfo[], referenceKeyColumnNames: readonly string[]): ColumnInfo[] {
const uniqueColumnNames = new Set(referenceKeyColumnNames);
return columns.filter((column) => uniqueColumnNames.has(column.name));
export function manualReferenceKeySupportsColumn(referenceKeys: readonly ReferenceKeyInfo[], columnName: string, filter?: ForeignKeyDisplayFilterConfig): boolean {
if (!columnName) return false;
return referenceKeys.some((key) => {
if (!key.columns.includes(columnName)) return false;
if (key.columns.length === 1) return true;
if (key.columns.length !== 2 || filter?.mode !== "equals" || !filter.value?.trim()) return false;
const otherColumn = key.columns.find((column) => column !== columnName);
return !!otherColumn && filter.column === otherColumn;
});
}
export function manualReferenceKeyColumnIsUnique(columns: readonly ColumnInfo[], referenceKeyColumnNames: readonly string[], columnName: string): boolean {
return !!columnName && manualReferenceKeyColumns(columns, referenceKeyColumnNames).some((column) => column.name === columnName);
export function manualReferenceKeyColumns(columns: readonly ColumnInfo[], referenceKeys: readonly ReferenceKeyInfo[], filter?: ForeignKeyDisplayFilterConfig): ColumnInfo[] {
return columns.filter((column) => manualReferenceKeySupportsColumn(referenceKeys, column.name, filter));
}
export function manualReferenceColumnValidation(columns: readonly ColumnInfo[], referenceKeyColumnNames: readonly string[], columnName: string, metadataStatus: ManualReferenceMetadataStatus): ManualReferenceColumnValidation {
export function manualReferenceKeyColumnIsUnique(columns: readonly ColumnInfo[], referenceKeys: readonly ReferenceKeyInfo[], columnName: string, filter?: ForeignKeyDisplayFilterConfig): boolean {
return !!columns.some((column) => column.name === columnName) && manualReferenceKeySupportsColumn(referenceKeys, columnName, filter);
}
export function manualReferenceColumnValidation(columns: readonly ColumnInfo[], referenceKeys: readonly ReferenceKeyInfo[], columnName: string, metadataStatus: ManualReferenceMetadataStatus, filter?: ForeignKeyDisplayFilterConfig): ManualReferenceColumnValidation {
if (metadataStatus === "loading") return "pending";
if (metadataStatus === "unavailable") return "unavailable";
return manualReferenceKeyColumnIsUnique(columns, referenceKeyColumnNames, columnName) ? "valid" : "invalid";
return manualReferenceKeyColumnIsUnique(columns, referenceKeys, columnName, filter) ? "valid" : "invalid";
}
export function reconcileManualReferenceColumn(currentColumn: string, referenceColumns: readonly ColumnInfo[], metadataStatus: ManualReferenceMetadataStatus): string {
+4
View File
@@ -523,6 +523,10 @@ export interface IndexInfo {
key_is_expression?: boolean[] | null;
}
export interface ReferenceKeyInfo {
columns: string[];
}
export interface ForeignKeyInfo {
name: string;
column: string;