diff --git a/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts b/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts index 2d6b1dcddb..cd180b353c 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts +++ b/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts @@ -152,10 +152,24 @@ vi.mock("@/components/ui/searchable-select", async () => { SearchableSelect: defineComponent({ name: "SearchableSelect", inheritAttrs: false, + props: { + modelValue: { type: String, default: "" }, + options: { type: Array, default: () => [] }, + allowCustom: { type: Boolean, default: false }, + }, + emits: ["update:modelValue"], setup: - (_props, { attrs }) => + (props, { attrs, emit }) => () => - h("div", attrs), + h("button", { + ...attrs, + type: "button", + "data-searchable-select": "true", + "data-model-value": props.modelValue, + "data-options": JSON.stringify(props.options), + "data-allow-custom": String(props.allowCustom), + onClick: () => emit("update:modelValue", "custom_domain"), + }), }), }; }); @@ -240,19 +254,19 @@ function draft(isPrimaryKey = false) { }; } -async function mountEditor(databaseType: "sqlserver" | "postgres" | "sqlite" | "oracle" | "dameng" | "duckdb" | "informix", isPrimaryKey = false) { +async function mountEditor(databaseType: "sqlserver" | "postgres" | "sqlite" | "oracle" | "dameng" | "duckdb" | "informix", isPrimaryKey = false, options: { database?: string; dynamicTypes?: string[] } = {}) { mocks.connection.db_type = databaseType; mocks.connection.name = databaseType; mocks.connection.driver_label = databaseType; mocks.ensureConnected.mockResolvedValue(undefined); - mocks.listDataTypes.mockResolvedValue([]); + mocks.listDataTypes.mockResolvedValue(options.dynamicTypes ?? []); mocks.buildTableStructureChangeSql.mockResolvedValue({ statements: [], warnings: [] }); const root = document.createElement("div"); document.body.append(root); const app = createApp(TableStructureEditor, { connectionId: mocks.connection.id, - database: "test", + database: options.database ?? "test", schema: "SYSDBA", tableName: "users", draft: draft(isPrimaryKey), @@ -394,6 +408,33 @@ describe("TableStructureEditor primary key editing", () => { }); }); +describe("TableStructureEditor data type options", () => { + it("keeps dynamic Dameng types first and deduplicates fallback types case-insensitively", async () => { + const root = await mountEditor("dameng", false, { + database: "dynamic-types-5275", + dynamicTypes: ["VARCHAR", "CUSTOM_DM_TYPE", "NUMBER"], + }); + const picker = root.querySelector('[data-searchable-select="true"]'); + if (!picker) throw new Error("Missing data type picker"); + + await vi.waitFor(() => expect(JSON.parse(picker.dataset.options ?? "[]").slice(0, 3)).toEqual(["VARCHAR", "CUSTOM_DM_TYPE", "NUMBER"])); + const options = JSON.parse(picker.dataset.options ?? "[]") as string[]; + expect(options.filter((option) => option.toLowerCase() === "varchar")).toEqual(["VARCHAR"]); + expect(options.filter((option) => option.toLowerCase() === "number")).toEqual(["NUMBER"]); + expect(options).toContain("longvarchar"); + }); + + it("continues to accept manually entered Dameng data types", async () => { + const root = await mountEditor("dameng", false, { database: "manual-type-5275" }); + const picker = root.querySelector('[data-searchable-select="true"]'); + if (!picker) throw new Error("Missing data type picker"); + + expect(picker.dataset.allowCustom).toBe("true"); + picker.click(); + await vi.waitFor(() => expect(mocks.buildTableStructureChangeSql).toHaveBeenLastCalledWith(expect.objectContaining({ columns: [expect.objectContaining({ dataType: "custom_domain" })] }))); + }); +}); + describe("TableStructureEditor local column order notice", () => { it.each(["sqlserver", "postgres", "sqlite", "oracle", "dameng", "duckdb", "informix"] as const)("does not show the reorder notice when adding a %s column", async (databaseType) => { const root = await mountEditor(databaseType); diff --git a/apps/desktop/src/lib/table/tableStructureEditorState.ts b/apps/desktop/src/lib/table/tableStructureEditorState.ts index fb0af64801..07941e627a 100644 --- a/apps/desktop/src/lib/table/tableStructureEditorState.ts +++ b/apps/desktop/src/lib/table/tableStructureEditorState.ts @@ -199,6 +199,63 @@ export const DATA_TYPE_OPTIONS: Record = { "xmltype", "sdo_geometry", ], + dameng: [ + "number", + "numeric", + "decimal", + "dec", + "integer", + "int", + "bigint", + "smallint", + "tinyint", + "byte", + "float", + "double", + "real", + "double precision", + "bit", + "binary", + "varbinary", + "raw", + "char", + "character", + "varchar2", + "varchar", + "rowid", + "bool", + "boolean", + "date", + "time", + "timestamp", + "datetime", + "time with time zone", + "timestamp with time zone", + "timestamp with local time zone", + "interval year", + "interval month", + "interval year to month", + "interval day", + "interval hour", + "interval minute", + "interval second", + "interval day to hour", + "interval day to minute", + "interval day to second", + "interval hour to minute", + "interval hour to second", + "interval minute to second", + "text", + "long", + "longvarchar", + "image", + "longvarbinary", + "blob", + "clob", + "bfile", + "json", + "jsonb", + ], clickhouse: [ "Int8", "Int16", @@ -301,7 +358,6 @@ const DATA_TYPE_OPTION_ALIASES: Partial> = { vastbase: "postgres", kingbase: "postgres", firebird: "postgres", - dameng: "oracle", "oceanbase-oracle": "oracle", iris: "oracle", yashandb: "oracle", @@ -1138,7 +1194,11 @@ export function defaultNewColumnDataType(dbType: DatabaseType | undefined, dataT } if (options.length > 0) { - const preferred = options.find((type) => /^(varchar|character varying|nvarchar)$/i.test(type.trim())) ?? options.find((type) => /^(string|clob|lvarchar|text)$/i.test(type.trim())) ?? options.find((type) => /^varchar/i.test(type.trim())); + const preferred = + (dbType === "dameng" ? options.find((type) => /^varchar2$/i.test(type.trim())) : undefined) ?? + options.find((type) => /^(varchar|character varying|nvarchar)$/i.test(type.trim())) ?? + options.find((type) => /^(string|clob|lvarchar|text)$/i.test(type.trim())) ?? + options.find((type) => /^varchar/i.test(type.trim())); if (preferred) { return combineDataTypeForDatabase(dbType, preferred, getDefaultLengthForType(dbType, preferred)); } diff --git a/packages/app-tests/tableStructureEditorState.test.ts b/packages/app-tests/tableStructureEditorState.test.ts index 5c755569a9..e25bf94a57 100644 --- a/packages/app-tests/tableStructureEditorState.test.ts +++ b/packages/app-tests/tableStructureEditorState.test.ts @@ -14,6 +14,7 @@ import { generateUniqueIndexName, getColumnEditorControls, getDataTypeOptions, + defaultNewColumnDataType, isDataTypeLengthDisabled, isProtectedManticoreIdColumn, isDamengIdentityCompatibleDataType, @@ -479,13 +480,90 @@ test("normalizes temporal precision when combining data types", () => { }); test("returns data type options for compatible table structure editors", () => { - assert.deepEqual(getDataTypeOptions("dameng"), getDataTypeOptions("oracle")); assert.deepEqual(getDataTypeOptions("gaussdb"), getDataTypeOptions("postgres")); assert.deepEqual(getDataTypeOptions("doris"), getDataTypeOptions("mysql")); - assert.equal(getDataTypeOptions("dameng").includes("varchar2"), true); assert.equal(getDataTypeOptions("sqlserver").includes("nvarchar"), true); }); +test("returns the complete Dameng fallback data type options", () => { + const options = getDataTypeOptions("dameng"); + const expected = [ + "number", + "numeric", + "decimal", + "dec", + "integer", + "int", + "bigint", + "smallint", + "tinyint", + "byte", + "float", + "double", + "real", + "double precision", + "bit", + "binary", + "varbinary", + "raw", + "char", + "character", + "varchar", + "varchar2", + "rowid", + "bool", + "boolean", + "date", + "time", + "timestamp", + "datetime", + "time with time zone", + "timestamp with time zone", + "timestamp with local time zone", + "interval year", + "interval month", + "interval year to month", + "interval day", + "interval hour", + "interval minute", + "interval second", + "interval day to hour", + "interval day to minute", + "interval day to second", + "interval hour to minute", + "interval hour to second", + "interval minute to second", + "text", + "long", + "longvarchar", + "image", + "longvarbinary", + "blob", + "clob", + "bfile", + "json", + "jsonb", + ]; + + for (const type of expected) assert.equal(options.includes(type), true, `missing Dameng type: ${type}`); + for (const oracleOnly of ["binary_float", "binary_double", "nvarchar2", "nclob", "urowid", "xmltype", "sdo_geometry", "vector"]) { + assert.equal(options.includes(oracleOnly), false, `unexpected Oracle-only type: ${oracleOnly}`); + } +}); + +test("keeps Oracle data type options independent from Dameng", () => { + const oracle = getDataTypeOptions("oracle"); + + assert.equal(oracle.includes("binary_float"), true); + assert.equal(oracle.includes("varchar"), false); + assert.equal(oracle.includes("longvarchar"), false); + assert.equal(oracle.includes("jsonb"), false); +}); + +test("keeps the existing Dameng new-column default", () => { + assert.equal(defaultNewColumnDataType("dameng"), "varchar2(255)"); +}); + test("returns PostgreSQL array type options without serial pseudo-types", () => { const options = getDataTypeOptions("postgres"); assert.equal(options.includes("integer[]"), true);