fix(settings): keep cross-db same-name preset fields
This commit is contained in:
parent
a5e46fd659
commit
8fb7559310
|
|
@ -99,7 +99,7 @@ import { normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/
|
|||
import { validateConfigName, generateId, type AiConfigItem, type ConfigNameValidationResult } from "@/lib/ai/aiConfigList";
|
||||
import { currentExecutableStatementRange, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache";
|
||||
import { EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE, parseTableColumnTemplateFields, TABLE_COLUMN_TEMPLATE_DATABASE_TYPES } from "@/lib/table/tableColumnTemplates";
|
||||
import { EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE, parseTableColumnTemplateFields, TABLE_COLUMN_TEMPLATE_DATABASE_TYPES, tableColumnTemplateRowsToSettings } from "@/lib/table/tableColumnTemplates";
|
||||
import { DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES, normalizeSqlVariableSyntaxOverrides, SQL_VARIABLE_SYNTAX_DATABASE_TYPES, SQL_VARIABLE_SYNTAX_KEYS, SQL_VARIABLE_SYNTAX_TOKENS, type SqlVariableSyntaxOverrides, type SqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax";
|
||||
import { buildMcpCherryStudioConfig, buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpTraeConfig, buildMcpVsCodeConfig, mcpWebBackendUrl, type McpLaunchConfig } from "@/lib/mcp/mcpConfigTemplates";
|
||||
import { beginMcpStatusRequest, mcpUpdateAvailability } from "@/lib/mcp/mcpUpdateStatus";
|
||||
|
|
@ -249,35 +249,6 @@ function tableColumnTemplateRowsFromSettings(lines: readonly string[]): TableCol
|
|||
}));
|
||||
}
|
||||
|
||||
function tableColumnTemplateRowsToSettings(rows: readonly TableColumnTemplateGridRow[]): string[] {
|
||||
const seenNames = new Set<string>();
|
||||
const settings: string[] = [];
|
||||
for (const row of rows) {
|
||||
const name = row.name.trim();
|
||||
if (!name) continue;
|
||||
const key = name.toLowerCase();
|
||||
if (seenNames.has(key)) continue;
|
||||
seenNames.add(key);
|
||||
|
||||
const parts = [name];
|
||||
|
||||
const seenDatabaseTypes = new Set<DatabaseType>();
|
||||
for (const override of row.overrides) {
|
||||
const dataType = override.dataType.trim();
|
||||
if (seenDatabaseTypes.has(override.databaseType)) continue;
|
||||
seenDatabaseTypes.add(override.databaseType);
|
||||
parts.push(`${override.databaseType}:${dataType || EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE}`);
|
||||
}
|
||||
if (!row.required) parts.push("required:false");
|
||||
const defaultValue = row.defaultValue.trim();
|
||||
if (defaultValue) parts.push(`default:${defaultValue}`);
|
||||
const comment = row.comment.trim();
|
||||
if (comment) parts.push(`comment:${comment}`);
|
||||
settings.push(parts.join(" | "));
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
function createEmptyTableColumnTemplateRow(): TableColumnTemplateGridRow {
|
||||
return {
|
||||
id: uuid(),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ export interface TableColumnTemplateField {
|
|||
comment?: string;
|
||||
}
|
||||
|
||||
export interface TableColumnTemplateGridRowInput {
|
||||
name: string;
|
||||
required: boolean;
|
||||
defaultValue: string;
|
||||
comment: string;
|
||||
overrides: readonly { databaseType: DatabaseType; dataType: string }[];
|
||||
}
|
||||
|
||||
export const PRESET_FIELDS_TEMPLATE_ID = "preset-fields";
|
||||
export const EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE = "<empty>";
|
||||
export const TABLE_COLUMN_TEMPLATE_DATABASE_TYPES: DatabaseType[] = manifestDatabaseTypes().filter(isTableColumnTemplateDatabaseType);
|
||||
|
|
@ -24,21 +32,27 @@ export const DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS: string[] = [];
|
|||
|
||||
export function normalizeTableColumnTemplateFields(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [...DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS];
|
||||
const fields: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of value) {
|
||||
if (typeof item !== "string") continue;
|
||||
const field = item.trim();
|
||||
const name = field.split("|")[0]?.trim();
|
||||
if (!field || !name || seen.has(name.toLowerCase())) continue;
|
||||
seen.add(name.toLowerCase());
|
||||
fields.push(field);
|
||||
}
|
||||
return fields;
|
||||
return parseTableColumnTemplateFieldLines(value).map(serializeTableColumnTemplateField);
|
||||
}
|
||||
|
||||
export function parseTableColumnTemplateFields(value: unknown): TableColumnTemplateField[] {
|
||||
return normalizeTableColumnTemplateFields(value).map(parseTableColumnTemplateField);
|
||||
if (!Array.isArray(value)) return parseTableColumnTemplateFieldLines(DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS);
|
||||
return parseTableColumnTemplateFieldLines(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize settings-grid rows.
|
||||
* UI is filtered per database, so the same field name can appear as separate rows (one per
|
||||
* engine). Merge those into one stored field with per-DB types — dropping later names made
|
||||
* Apply look unchanged (#5579).
|
||||
*/
|
||||
export function tableColumnTemplateRowsToSettings(rows: readonly TableColumnTemplateGridRowInput[]): string[] {
|
||||
const fields: TableColumnTemplateField[] = [];
|
||||
for (const row of rows) {
|
||||
const field = fieldFromGridRow(row);
|
||||
if (field) fields.push(field);
|
||||
}
|
||||
return mergeTableColumnTemplateFields(fields).map(serializeTableColumnTemplateField);
|
||||
}
|
||||
|
||||
export function tableColumnTemplates(columnNames: readonly string[] = DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS): TableColumnTemplate[] {
|
||||
|
|
@ -58,6 +72,80 @@ export function createTableColumnTemplateDrafts(options: { templateId: string; d
|
|||
return presetFieldColumns(options.databaseType, parseTableColumnTemplateFields([...(options.columnNames ?? DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS)]), options.createId).filter((column) => !existingNames.has(column.name.toLowerCase()));
|
||||
}
|
||||
|
||||
/** Case-insensitive name merge: first wins for field props; per-DB types are unioned. */
|
||||
function mergeTableColumnTemplateFields(fields: readonly TableColumnTemplateField[]): TableColumnTemplateField[] {
|
||||
const order: string[] = [];
|
||||
const byName = new Map<string, TableColumnTemplateField>();
|
||||
|
||||
for (const field of fields) {
|
||||
const name = field.name.trim();
|
||||
if (!name) continue;
|
||||
const key = name.toLowerCase();
|
||||
const existing = byName.get(key);
|
||||
if (!existing) {
|
||||
byName.set(key, {
|
||||
name,
|
||||
dataTypesByDatabase: { ...field.dataTypesByDatabase },
|
||||
isNullable: field.isNullable,
|
||||
defaultValue: field.defaultValue,
|
||||
comment: field.comment,
|
||||
});
|
||||
order.push(key);
|
||||
continue;
|
||||
}
|
||||
if (!existing.defaultValue && field.defaultValue) existing.defaultValue = field.defaultValue;
|
||||
if (!existing.comment && field.comment) existing.comment = field.comment;
|
||||
for (const [databaseType, dataType] of Object.entries(field.dataTypesByDatabase) as [DatabaseType, string][]) {
|
||||
if (existing.dataTypesByDatabase[databaseType] !== undefined) continue;
|
||||
existing.dataTypesByDatabase[databaseType] = dataType;
|
||||
}
|
||||
}
|
||||
|
||||
return order.map((key) => byName.get(key)!);
|
||||
}
|
||||
|
||||
function serializeTableColumnTemplateField(field: TableColumnTemplateField): string {
|
||||
const parts = [field.name];
|
||||
for (const [databaseType, dataType] of Object.entries(field.dataTypesByDatabase)) {
|
||||
if (!dataType) continue;
|
||||
parts.push(`${databaseType}:${dataType}`);
|
||||
}
|
||||
if (field.isNullable === true) parts.push("required:false");
|
||||
if (field.defaultValue) parts.push(`default:${field.defaultValue}`);
|
||||
if (field.comment) parts.push(`comment:${field.comment}`);
|
||||
return parts.join(" | ");
|
||||
}
|
||||
|
||||
function fieldFromGridRow(row: TableColumnTemplateGridRowInput): TableColumnTemplateField | null {
|
||||
const name = row.name.trim();
|
||||
if (!name) return null;
|
||||
|
||||
const dataTypesByDatabase: Partial<Record<DatabaseType, string>> = {};
|
||||
for (const override of row.overrides) {
|
||||
if (dataTypesByDatabase[override.databaseType] !== undefined) continue;
|
||||
dataTypesByDatabase[override.databaseType] = override.dataType.trim() || EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
dataTypesByDatabase,
|
||||
isNullable: row.required ? undefined : true,
|
||||
defaultValue: row.defaultValue.trim() || undefined,
|
||||
comment: row.comment.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function parseTableColumnTemplateFieldLines(value: readonly unknown[]): TableColumnTemplateField[] {
|
||||
const parsed: TableColumnTemplateField[] = [];
|
||||
for (const item of value) {
|
||||
if (typeof item !== "string") continue;
|
||||
const trimmed = item.trim();
|
||||
if (!trimmed || !trimmed.split("|")[0]?.trim()) continue;
|
||||
parsed.push(parseTableColumnTemplateField(trimmed));
|
||||
}
|
||||
return mergeTableColumnTemplateFields(parsed);
|
||||
}
|
||||
|
||||
function presetFieldColumns(databaseType: DatabaseType | undefined, fields: readonly TableColumnTemplateField[], createId: () => string): EditableStructureColumn[] {
|
||||
return fields
|
||||
.filter((field) => isTableColumnTemplateFieldApplicable(field, databaseType))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { createTableColumnTemplateDrafts, DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields, parseTableColumnTemplateFields, PRESET_FIELDS_TEMPLATE_ID, tableColumnTemplates, TABLE_COLUMN_TEMPLATE_DATABASE_TYPES } from "../../apps/desktop/src/lib/table/tableColumnTemplates.ts";
|
||||
import {
|
||||
createTableColumnTemplateDrafts,
|
||||
DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS,
|
||||
normalizeTableColumnTemplateFields,
|
||||
parseTableColumnTemplateFields,
|
||||
PRESET_FIELDS_TEMPLATE_ID,
|
||||
tableColumnTemplateRowsToSettings,
|
||||
tableColumnTemplates,
|
||||
TABLE_COLUMN_TEMPLATE_DATABASE_TYPES,
|
||||
} from "../../apps/desktop/src/lib/table/tableColumnTemplates.ts";
|
||||
|
||||
const sixCustomFields = [
|
||||
"tenant_id | mysql:bigint | postgres:uuid | default:0 | comment:Tenant",
|
||||
|
|
@ -115,3 +124,46 @@ test("skips configured preset fields that already exist", () => {
|
|||
["request_id", "created_time", "modified_time", "creator_id"],
|
||||
);
|
||||
});
|
||||
|
||||
test("merges same-name fields across database types (#5579)", () => {
|
||||
const normalized = normalizeTableColumnTemplateFields(["id | sqlite:INTEGER", "id | mysql:BIGINT", "create_time | sqlite:TEXT | default:CURRENT_TIMESTAMP", "create_time | mysql:DATETIME"]);
|
||||
assert.deepEqual(normalized, ["id | sqlite:INTEGER | mysql:BIGINT", "create_time | sqlite:TEXT | mysql:DATETIME | default:CURRENT_TIMESTAMP"]);
|
||||
|
||||
assert.deepEqual(
|
||||
createTableColumnTemplateDrafts({
|
||||
templateId: PRESET_FIELDS_TEMPLATE_ID,
|
||||
databaseType: "mysql",
|
||||
columnNames: normalized,
|
||||
createId: () => "x",
|
||||
}).map((column) => ({ name: column.name, dataType: column.dataType })),
|
||||
[
|
||||
{ name: "id", dataType: "BIGINT" },
|
||||
{ name: "create_time", dataType: "DATETIME" },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("tableColumnTemplateRowsToSettings serializes multi-db rows and merges duplicate names", () => {
|
||||
assert.deepEqual(
|
||||
tableColumnTemplateRowsToSettings([
|
||||
{
|
||||
name: "id",
|
||||
required: true,
|
||||
defaultValue: "",
|
||||
comment: "",
|
||||
overrides: [
|
||||
{ databaseType: "sqlite", dataType: "INTEGER" },
|
||||
{ databaseType: "mysql", dataType: "BIGINT" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "id",
|
||||
required: false,
|
||||
defaultValue: "0",
|
||||
comment: "pk",
|
||||
overrides: [{ databaseType: "postgres", dataType: "bigint" }],
|
||||
},
|
||||
]),
|
||||
["id | sqlite:INTEGER | mysql:BIGINT | postgres:bigint | default:0 | comment:pk"],
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue