feat(oracle): preserve character length units

Closes #4739
This commit is contained in:
t8y2 committed 2026-08-12 19:44:32 +00:00
1 parent bd83ee189b
commit 3dd034bf06
6 files changed
+130 -28

No files matched your search

@@ -73,7 +73,7 @@ import {
parseExtraToColumnExtra,
rehydrateColumnDraftsFromMetadata,
resolveInsertColumnIndex,
restoreDamengLengthUnitsAfterSave,
restoreCharacterLengthUnitsAfterSave,
sameStructureIndexType,
splitDataType,
toColumnNames,
@@ -583,7 +583,7 @@ watch(localStructureDensity, (density, previousDensity) => {
function onColResize(e: MouseEvent, col: number) {
e.preventDefault();
const widthIndex = columnWidthIndex(col);
const minimumWidth = widthIndex === 3 && databaseType.value === "dameng" ? structureDensityMetric.value.minLengthColumnWidth : structureDensityMetric.value.minColumnWidth;
const minimumWidth = widthIndex === 3 && supportsCharacterLengthUnits.value ? structureDensityMetric.value.minLengthColumnWidth : structureDensityMetric.value.minColumnWidth;
colResizing.value = { col: widthIndex, startX: e.clientX, startW: Math.max(colWidths.value[widthIndex] ?? minimumWidth, minimumWidth) };
const onMove = (ev: MouseEvent) => {
if (!colResizing.value) return;
@@ -622,6 +622,7 @@ function onIndexColResize(e: MouseEvent, col: number) {
const connection = computed(() => (props.connectionId ? store.getConfig(props.connectionId) : undefined));
const databaseType = computed(() => tableStructureDatabaseTypeForConnection(connection.value));
const supportsCharacterLengthUnits = computed(() => databaseType.value === "dameng" || databaseType.value === "oracle");
const usesMysql8SafeDefaults = computed(() => databaseType.value === "mysql" && connection.value?.db_type === "mysql" && connection.value.driver_profile === "mysql");
const structureCapabilities = computed(() => getTableStructureCapabilities(databaseType.value, connection.value?.db_type, connection.value?.database_info?.productVersion));
const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(databaseType.value));
@@ -772,7 +773,7 @@ const visibleColWidths = computed(() =>
colLabels.value.map((column) => {
if (column.key === "actions") return columnActionsWidth.value;
const width = colWidths.value[column.widthIndex] ?? structureDensityMetric.value.minColumnWidth;
return column.key === "length" && databaseType.value === "dameng" ? Math.max(width, structureDensityMetric.value.minLengthColumnWidth) : width;
return column.key === "length" && supportsCharacterLengthUnits.value ? Math.max(width, structureDensityMetric.value.minLengthColumnWidth) : width;
}),
);
@@ -1299,7 +1300,7 @@ async function loadStructure(
silent = false,
scope: TableStructureRefreshScope = visibleTableStructureRefreshScope(activeTab.value),
showErrors = true,
options: { blockSecondaryMetadata?: boolean; preserveDraft?: boolean; damengLengthUnitsAfterSave?: ReadonlyMap<string, string>; forceDdl?: boolean; forceMetadata?: boolean } = {},
options: { blockSecondaryMetadata?: boolean; preserveDraft?: boolean; characterLengthUnitsAfterSave?: ReadonlyMap<string, string>; forceDdl?: boolean; forceMetadata?: boolean } = {},
) {
const connectionId = props.connectionId;
const database = props.database;
@@ -1352,7 +1353,7 @@ async function loadStructure(
// editor shows the correct options for the server version.
void loadCharsetMetadata();
const nextColumnDrafts = createColumnDrafts(nextColumns, databaseType.value);
const hydratedColumnDrafts = databaseType.value === "dameng" && options.damengLengthUnitsAfterSave ? restoreDamengLengthUnitsAfterSave(nextColumnDrafts, options.damengLengthUnitsAfterSave) : nextColumnDrafts;
const hydratedColumnDrafts = supportsCharacterLengthUnits.value && options.characterLengthUnitsAfterSave ? restoreCharacterLengthUnitsAfterSave(databaseType.value, nextColumnDrafts, options.characterLengthUnitsAfterSave) : nextColumnDrafts;
columns.value = applyStoredLocalColumnOrder(hydratedColumnDrafts);
loadedMetadataFacets.add("columns");
if (!options.preserveDraft) selectedColumnId.value = null;
@@ -1411,9 +1412,9 @@ async function loadStructure(
}
}
async function refreshStructureAfterSave(scope: TableStructureRefreshScope, damengLengthUnitsAfterSave: ReadonlyMap<string, string>) {
async function refreshStructureAfterSave(scope: TableStructureRefreshScope, characterLengthUnitsAfterSave: ReadonlyMap<string, string>) {
try {
await loadStructure(true, scope, false, { blockSecondaryMetadata: true, damengLengthUnitsAfterSave });
await loadStructure(true, scope, false, { blockSecondaryMetadata: true, characterLengthUnitsAfterSave });
} catch (e) {
console.warn("[DBX][structure-editor:post-save-refresh-failed]", e);
} finally {
@@ -2325,11 +2326,11 @@ async function applyChanges() {
saving.value = true;
errorMessage.value = "";
const refreshScope = captureStructureRefreshScope();
const damengLengthUnitsAfterSave = new Map<string, string>();
if (databaseType.value === "dameng") {
const characterLengthUnitsAfterSave = new Map<string, string>();
if (supportsCharacterLengthUnits.value) {
for (const column of columns.value) {
if (!column.markedForDrop && dataTypeLengthUnitValue("dameng", column.dataType)) {
damengLengthUnitsAfterSave.set(column.name.trim().toLowerCase(), column.dataType);
if (!column.markedForDrop && dataTypeLengthUnitValue(databaseType.value, column.dataType)) {
characterLengthUnitsAfterSave.set(column.name.trim().toLowerCase(), column.dataType);
}
}
}
@@ -2363,7 +2364,7 @@ async function applyChanges() {
postSaveRefreshing.value = true;
skipNextRefreshVersion = true;
emit("saved", tableComment.value !== originalTableComment.value);
await refreshStructureAfterSave(refreshScope, damengLengthUnitsAfterSave);
await refreshStructureAfterSave(refreshScope, characterLengthUnitsAfterSave);
}
return true;
} catch (e: any) {
@@ -21,7 +21,7 @@ import {
parseExtraToColumnExtra,
rehydrateColumnDraftsFromMetadata,
resolveInsertColumnIndex,
restoreDamengLengthUnitsAfterSave,
restoreCharacterLengthUnitsAfterSave,
splitDataType,
} from "@/lib/table/tableStructureEditorState";
@@ -189,7 +189,7 @@ describe("tableStructureEditorState", () => {
expect(combineDataTypeForDatabase("mysql", "integer", "11")).toBe("integer(11)");
});
it("offers BYTE and CHAR units only for supported Dameng character types", () => {
it("offers BYTE and CHAR units for supported Oracle-family character types", () => {
expect(getDataTypeLengthUnitOptions("dameng", "varchar2(255 CHAR)")).toEqual(["BYTE", "CHAR"]);
expect(getDataTypeLengthUnitOptions("dameng", "varchar(255)")).toEqual(["BYTE", "CHAR"]);
expect(getDataTypeLengthUnitOptions("dameng", "char(10 BYTE)")).toEqual(["BYTE", "CHAR"]);
@@ -197,7 +197,9 @@ describe("tableStructureEditorState", () => {
expect(getDataTypeLengthUnitOptions("dameng", "nchar(10)")).toEqual([]);
expect(getDataTypeLengthUnitOptions("dameng", "nvarchar2(10)")).toEqual([]);
expect(getDataTypeLengthUnitOptions("dameng", "number(10,0)")).toEqual([]);
expect(getDataTypeLengthUnitOptions("oracle", "varchar2(255 CHAR)")).toEqual([]);
expect(getDataTypeLengthUnitOptions("oracle", "varchar2(255 CHAR)")).toEqual(["BYTE", "CHAR"]);
expect(getDataTypeLengthUnitOptions("oracle", "char(10 BYTE)")).toEqual(["BYTE", "CHAR"]);
expect(getDataTypeLengthUnitOptions("oracle", "nvarchar2(10)")).toEqual([]);
expect(getDataTypeLengthUnitOptions("mysql", "varchar(255)")).toEqual([]);
});
@@ -213,6 +215,12 @@ describe("tableStructureEditorState", () => {
expect(combineDataTypeForDatabaseWithLengthUnit("dameng", "varchar2", "255", "")).toBe("varchar2(255)");
});
it("separates and reconstructs Oracle character length units", () => {
expect(dataTypeLengthInputValue("oracle", "VARCHAR2(255 CHAR)")).toBe("255");
expect(dataTypeLengthUnitValue("oracle", "VARCHAR2(255 CHAR)")).toBe("CHAR");
expect(combineDataTypeForDatabaseWithLengthUnit("oracle", "VARCHAR2", "64", "BYTE")).toBe("VARCHAR2(64 BYTE)");
});
it("does not reinterpret unsupported length parameters or dialects", () => {
expect(dataTypeLengthInputValue("dameng", "varchar2(255 WORD)")).toBe("255 WORD");
expect(dataTypeLengthUnitValue("dameng", "varchar2(255 WORD)")).toBe("");
@@ -235,7 +243,7 @@ describe("tableStructureEditorState", () => {
"dameng",
);
const [restored] = restoreDamengLengthUnitsAfterSave([legacyAgentDraft!], new Map([["display_name", "VARCHAR2(255 CHAR)"]]));
const [restored] = restoreCharacterLengthUnitsAfterSave("dameng", [legacyAgentDraft!], new Map([["display_name", "VARCHAR2(255 CHAR)"]]));
expect(restored?.dataType).toBe("VARCHAR2(255 CHAR)");
expect(restored?.original?.data_type).toBe("VARCHAR2(255 CHAR)");
@@ -256,12 +264,24 @@ describe("tableStructureEditorState", () => {
"dameng",
);
const [restored] = restoreDamengLengthUnitsAfterSave([liveDraft!], new Map([["display_name", "VARCHAR2(255 CHAR)"]]));
const [restored] = restoreCharacterLengthUnitsAfterSave("dameng", [liveDraft!], new Map([["display_name", "VARCHAR2(255 CHAR)"]]));
expect(restored?.dataType).toBe("VARCHAR2(255 BYTE)");
expect(restored?.original?.data_type).toBe("VARCHAR2(255 BYTE)");
});
it("keeps a saved Oracle length unit when refreshed metadata omits it", () => {
const [legacyAgentDraft] = createColumnDrafts(
[{ name: "DISPLAY_NAME", data_type: "VARCHAR2(255)", is_nullable: true, column_default: null, is_primary_key: false, extra: null }],
"oracle",
);
const [restored] = restoreCharacterLengthUnitsAfterSave("oracle", [legacyAgentDraft!], new Map([["display_name", "VARCHAR2(255 CHAR)"]]));
expect(restored?.dataType).toBe("VARCHAR2(255 CHAR)");
expect(restored?.original?.data_type).toBe("VARCHAR2(255 CHAR)");
});
it("does not add MySQL display lengths when choosing SQLite-family types", () => {
for (const databaseType of ["sqlite", "rqlite", "turso"] as const) {
expect(getDefaultLengthForType(databaseType, "integer")).toBe("");
@@ -974,16 +974,16 @@ export function splitDataType(raw: string): { baseType: string; params: string }
export type DataTypeLengthUnit = "BYTE" | "CHAR";
const DAMENG_LENGTH_UNIT_TYPES = new Set(["char", "varchar", "varchar2"]);
const DAMENG_LENGTH_UNITS: readonly DataTypeLengthUnit[] = ["BYTE", "CHAR"];
const CHARACTER_LENGTH_UNIT_TYPES = new Set(["char", "varchar", "varchar2"]);
const CHARACTER_LENGTH_UNITS: readonly DataTypeLengthUnit[] = ["BYTE", "CHAR"];
function normalizedDataTypeName(rawDataType: string): string {
return splitDataType(rawDataType).baseType.trim().replace(/\s+/g, " ").toLowerCase();
}
export function getDataTypeLengthUnitOptions(dbType: DatabaseType | undefined, rawDataType: string): readonly DataTypeLengthUnit[] {
if (dbType !== "dameng") return [];
return DAMENG_LENGTH_UNIT_TYPES.has(normalizedDataTypeName(rawDataType)) ? DAMENG_LENGTH_UNITS : [];
if (dbType !== "dameng" && dbType !== "oracle") return [];
return CHARACTER_LENGTH_UNIT_TYPES.has(normalizedDataTypeName(rawDataType)) ? CHARACTER_LENGTH_UNITS : [];
}
function splitDataTypeLengthParams(dbType: DatabaseType | undefined, rawDataType: string): { length: string; unit: DataTypeLengthUnit | "" } {
@@ -1015,13 +1015,13 @@ export function combineDataTypeForDatabaseWithLengthUnit(dbType: DatabaseType |
return combineDataTypeForDatabase(dbType, baseType, params);
}
export function restoreDamengLengthUnitsAfterSave(columns: EditableStructureColumn[], savedDataTypesByColumn: ReadonlyMap<string, string>): EditableStructureColumn[] {
export function restoreCharacterLengthUnitsAfterSave(dbType: DatabaseType | undefined, columns: EditableStructureColumn[], savedDataTypesByColumn: ReadonlyMap<string, string>): EditableStructureColumn[] {
if (savedDataTypesByColumn.size === 0) return columns;
return columns.map((column) => {
if (dataTypeLengthUnitValue("dameng", column.dataType)) return column;
if (dataTypeLengthUnitValue(dbType, column.dataType)) return column;
const savedDataType = savedDataTypesByColumn.get(column.name.trim().toLowerCase());
if (!savedDataType || !dataTypeLengthUnitValue("dameng", savedDataType)) return column;
if (!savedDataType || !dataTypeLengthUnitValue(dbType, savedDataType)) return column;
if (normalizedDataTypeName(savedDataType) !== normalizedDataTypeName(column.dataType)) return column;
return {