feat(grid): support composite unique reference keys
This commit is contained in:
19 files changed
+219
-56
No files matched your search
@@ -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}`,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1652,8 +1652,8 @@ export default withEnglishFallback({
|
||||
formatterForeignKeyReferenceColumn: "対応列",
|
||||
formatterForeignKeyReferenceColumnPlaceholder: "現在の値に対応する列を選択",
|
||||
formatterForeignKeyReferenceMetadataUnavailable: "対応列の一意性を確認できませんでした:{error}",
|
||||
formatterForeignKeyReferenceInvalid: "この列は存在しないか、有効な単一列の一意キーではありません。別の列を選択してください。",
|
||||
formatterForeignKeyReferenceUnavailable: "対象テーブルに利用可能な単一列の一意キーがありません。",
|
||||
formatterForeignKeyReferenceInvalid: "この列は存在しないか、一意キーと現在の等価条件によって一意性を保証できません。別の列を選択してください。",
|
||||
formatterForeignKeyReferenceUnavailable: "対象テーブルに利用可能な一意キーがありません。複合一意キーでは、等価条件で別の列を固定してください。",
|
||||
formatterForeignKeyDisplayColumn: "表示列",
|
||||
formatterForeignKeyDisplayColumnPlaceholder: "表示列を選択",
|
||||
formatterForeignKeyFilter: "参照行を絞り込む",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1681,8 +1681,8 @@ export default withEnglishFallback({
|
||||
formatterForeignKeyReferenceColumn: "关联字段",
|
||||
formatterForeignKeyReferenceColumnPlaceholder: "选择与当前字段匹配的字段",
|
||||
formatterForeignKeyReferenceMetadataUnavailable: "无法验证关联字段唯一性:{error}",
|
||||
formatterForeignKeyReferenceInvalid: "该字段不存在,或不是有效的单列唯一键。请重新选择。",
|
||||
formatterForeignKeyReferenceUnavailable: "目标表没有可用的单列唯一键。",
|
||||
formatterForeignKeyReferenceInvalid: "该字段不存在,或无法由唯一键和当前等值条件保证唯一。请重新选择。",
|
||||
formatterForeignKeyReferenceUnavailable: "目标表没有可用的唯一键。联合唯一键需使用等值条件固定另一个字段。",
|
||||
formatterForeignKeyDisplayColumn: "显示字段",
|
||||
formatterForeignKeyDisplayColumnPlaceholder: "选择用于显示的字段",
|
||||
formatterForeignKeyFilter: "限定引用数据",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 })}`);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -523,6 +523,10 @@ export interface IndexInfo {
|
||||
key_is_expression?: boolean[] | null;
|
||||
}
|
||||
|
||||
export interface ReferenceKeyInfo {
|
||||
columns: string[];
|
||||
}
|
||||
|
||||
export interface ForeignKeyInfo {
|
||||
name: string;
|
||||
column: string;
|
||||
|
||||
Reference in new issue
Block a user