From 32b4acee3a334aa17064042bc91e6833fe8c63d1 Mon Sep 17 00:00:00 2001 From: onenewcode Date: Tue, 14 Jul 2026 01:28:19 +0800 Subject: [PATCH] feat(dameng): preserve character length units --- .../com/dbx/agent/dameng/DamengAgent.java | 38 ++++++++- .../agent/dameng/DamengAgentMetadataTest.java | 83 ++++++++++++++++--- .../structure/TableStructureEditor.vue | 77 ++++++++++++++--- apps/desktop/src/i18n/locales/en.ts | 2 + apps/desktop/src/i18n/locales/es.ts | 2 + apps/desktop/src/i18n/locales/it.ts | 2 + apps/desktop/src/i18n/locales/ja.ts | 2 + apps/desktop/src/i18n/locales/pt-BR.ts | 2 + apps/desktop/src/i18n/locales/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + .../table/tableStructureEditorState.spec.ts | 77 +++++++++++++++++ .../lib/table/tableStructureEditorState.ts | 62 +++++++++++++- .../dbx-core/src/table_structure_sql/tests.rs | 55 ++++++++++++ 13 files changed, 382 insertions(+), 24 deletions(-) diff --git a/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java b/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java index 8ac4a03d81..fc73ec26f7 100644 --- a/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java +++ b/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java @@ -666,6 +666,7 @@ public final class DamengAgent extends BaseDatabaseAgent { c.DATA_SCALE, c.DATA_LENGTH, c.CHAR_LENGTH, + c.CHAR_USED, cc.COMMENTS FROM ALL_TAB_COLUMNS c LEFT JOIN ALL_COL_COMMENTS cc @@ -688,7 +689,8 @@ public final class DamengAgent extends BaseDatabaseAgent { Integer numScale = intObject(rs, "DATA_SCALE"); Integer dataLen = intObject(rs, "DATA_LENGTH"); Integer charLen = intObject(rs, "CHAR_LENGTH"); - String dataType = formatDataType(baseType, numPrec, numScale, dataLen, charLen); + String charUsed = rs.getString("CHAR_USED"); + String dataType = formatDataType(baseType, numPrec, numScale, dataLen, charLen, charUsed); result.add(new ColumnInfo( name, @@ -965,9 +967,20 @@ public final class DamengAgent extends BaseDatabaseAgent { return "jdbc:dm://" + params.getHost() + ":" + params.getPort() + suffix; } - private static String formatDataType(String base, Integer numPrec, Integer numScale, Integer dataLen, Integer charLen) { + private static String formatDataType( + String base, + Integer numPrec, + Integer numScale, + Integer dataLen, + Integer charLen, + String charUsed + ) { return switch (base.toUpperCase(Locale.ROOT)) { - case "VARCHAR2", "NVARCHAR2", "VARCHAR", "CHAR", "NCHAR" -> { + case "VARCHAR2", "VARCHAR", "CHAR" -> { + Integer length = characterLength(dataLen, charLen, charUsed); + yield length != null ? base + "(" + length + characterLengthUnit(charUsed) + ")" : base; + } + case "NVARCHAR2", "NCHAR" -> { Integer length = charLen != null ? charLen : dataLen; yield length != null ? base + "(" + length + ")" : base; } @@ -982,6 +995,25 @@ public final class DamengAgent extends BaseDatabaseAgent { }; } + private static Integer characterLength(Integer dataLen, Integer charLen, String charUsed) { + String normalized = charUsed == null ? "" : charUsed.trim().toUpperCase(Locale.ROOT); + if ("B".equals(normalized) || "BYTE".equals(normalized)) { + return dataLen != null ? dataLen : charLen; + } + return charLen != null ? charLen : dataLen; + } + + private static String characterLengthUnit(String charUsed) { + if (charUsed == null) { + return ""; + } + return switch (charUsed.trim().toUpperCase(Locale.ROOT)) { + case "B", "BYTE" -> " BYTE"; + case "C", "CHAR" -> " CHAR"; + default -> ""; + }; + } + private static Integer intObject(ResultSet rs, String column) throws Exception { Object value = rs.getObject(column); return value == null ? null : ((Number) value).intValue(); diff --git a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java index 61c9356e9b..ebf109daa7 100644 --- a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java +++ b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java @@ -34,6 +34,7 @@ class DamengAgentMetadataTest { .findFirst() .orElseThrow(); Assertions.assertTrue(columnsSql.contains("LEFT JOIN ALL_COL_COMMENTS"), columnsSql); + Assertions.assertTrue(columnsSql.contains("c.CHAR_USED"), columnsSql); Assertions.assertTrue(columnsSql.startsWith("SELECT /*+ PARALLEL(1) */"), columnsSql); } @@ -206,6 +207,30 @@ class DamengAgentMetadataTest { Assertions.assertEquals("identity", columns.get(0).getExtra()); } + @Test + void preservesCharacterLengthUnitsFromMetadata() { + DamengAgent agent = new DamengAgent(); + TestSupport.setPrivateConnection(agent, metadataConnectionForColumns(List.of( + Arrays.asList("BYTE_VALUE", "VARCHAR2", "Y", null, null, 256, 64, "B", "byte length"), + Arrays.asList("CHAR_VALUE", "VARCHAR", "Y", null, null, 256, 64, "C", "character length"), + Arrays.asList("FIXED_VALUE", "CHAR", "Y", null, null, 40, 10, "C", "fixed length"), + Arrays.asList("DEFAULT_VALUE", "VARCHAR2", "Y", null, null, 80, 20, null, "default length"), + Arrays.asList("WORD_VALUE", "VARCHAR2", "Y", null, null, 128, 32, "BYTE", "word byte length"), + Arrays.asList("NATIONAL_VALUE", "NVARCHAR2", "Y", null, null, 80, 20, "C", "national length"), + Arrays.asList("NATIONAL_FIXED", "NCHAR", "Y", null, null, 40, 10, "B", "national fixed length") + ))); + + List columns = agent.getColumns("APP", "USERS"); + + Assertions.assertEquals("VARCHAR2(256 BYTE)", columns.get(0).getData_type()); + Assertions.assertEquals("VARCHAR(64 CHAR)", columns.get(1).getData_type()); + Assertions.assertEquals("CHAR(10 CHAR)", columns.get(2).getData_type()); + Assertions.assertEquals("VARCHAR2(20)", columns.get(3).getData_type()); + Assertions.assertEquals("VARCHAR2(128 BYTE)", columns.get(4).getData_type()); + Assertions.assertEquals("NVARCHAR2(20)", columns.get(5).getData_type()); + Assertions.assertEquals("NCHAR(10)", columns.get(6).getData_type()); + } + @Test void appendsTableAndColumnCommentsToTableDdl() { DamengAgent agent = new DamengAgent(); @@ -367,6 +392,38 @@ class DamengAgentMetadataTest { List> independentIndexes, List sqls, String dbmsMetadataDdl + ) { + return metadataConnection( + allColumnComment, + fallbackColumnComment, + includeMaterializedView, + independentIndexes, + sqls, + dbmsMetadataDdl, + defaultColumnMetadataRows(allColumnComment) + ); + } + + private static Connection metadataConnectionForColumns(List> columnRows) { + return metadataConnection( + "id comment", + null, + false, + List.of(), + null, + "CREATE TABLE \"APP\".\"USERS\" (\n \"ID\" NUMBER\n);", + columnRows + ); + } + + private static Connection metadataConnection( + String allColumnComment, + String fallbackColumnComment, + boolean includeMaterializedView, + List> independentIndexes, + List sqls, + String dbmsMetadataDdl, + List> columnRows ) { boolean[] dbmsMetadataResultOpen = {false}; return proxy(Connection.class, (method, args) -> { @@ -440,16 +497,7 @@ class DamengAgentMetadataTest { return metadataStatement(List.of()); } if (sql.contains("ALL_TAB_COLUMNS")) { - return metadataStatement(List.of(Arrays.asList( - "ID", - "NUMBER", - "N", - Integer.valueOf(10), - Integer.valueOf(0), - Integer.valueOf(22), - Integer.valueOf(10), - allColumnComment - ))); + return metadataStatement(columnRows); } } if ("close".equals(name)) { @@ -462,6 +510,20 @@ class DamengAgentMetadataTest { }); } + private static List> defaultColumnMetadataRows(String allColumnComment) { + return List.of(Arrays.asList( + "ID", + "NUMBER", + "N", + Integer.valueOf(10), + Integer.valueOf(0), + Integer.valueOf(22), + Integer.valueOf(10), + null, + allColumnComment + )); + } + private static List indexRow(String name, String columns, String uniqueness, String indexType) { return List.of(name, columns, uniqueness, indexType); } @@ -570,6 +632,7 @@ class DamengAgentMetadataTest { case "DATA_TYPE" -> string(rows, index[0], 1); case "NULLABLE" -> string(rows, index[0], 2); case "DATA_DEFAULT" -> null; + case "CHAR_USED" -> string(rows, index[0], 7); case "COMMENTS", "COMMENT$" -> string(rows, index[0], rows.get(index[0]).size() - 1); case "COLNAME" -> string(rows, index[0], 0); default -> null; diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 64055bbeae..cb83b23fe1 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -41,16 +41,19 @@ import { buildStructureTargetLabel, canEditManticoreColumnProperties, combineDataTypeForDatabase, + combineDataTypeForDatabaseWithLengthUnit, createColumnDrafts, createForeignKeyDrafts, createIndexDrafts, createTriggerDrafts, dataTypeLengthInputValue, + dataTypeLengthUnitValue, defaultNewColumnDataType, generateIndexName, generateUniqueIndexName, getColumnEditorControls, getDataTypeOptions, + getDataTypeLengthUnitOptions, getDefaultLengthForType, hasExistingColumnTypeChange, isDataTypeLengthDisabled, @@ -62,6 +65,7 @@ import { mysqlEnumDataType, parseExtraToColumnExtra, rehydrateColumnDraftsFromMetadata, + restoreDamengLengthUnitsAfterSave, splitDataType, toColumnNames, } from "@/lib/table/tableStructureEditorState"; @@ -273,6 +277,7 @@ const structureDensityMetrics: Record< columns: number[]; indexes: number[]; minColumnWidth: number; + minLengthColumnWidth: number; minIndexColumnWidth: number; actionButtonWidth: number; fontSize: number; @@ -291,6 +296,7 @@ const structureDensityMetrics: Record< columns: [28, 168, 136, 82, 60, 52, 108, 220, 80, 120, 144, 108], indexes: [120, 180, 60, 88, 124, 144, 120, 70], minColumnWidth: 24, + minLengthColumnWidth: 140, minIndexColumnWidth: 48, actionButtonWidth: 24, fontSize: 11, @@ -308,6 +314,7 @@ const structureDensityMetrics: Record< columns: [32, 200, 160, 104, 72, 64, 128, 260, 90, 140, 160, 136], indexes: [148, 224, 72, 108, 148, 180, 148, 84], minColumnWidth: 28, + minLengthColumnWidth: 156, minIndexColumnWidth: 60, actionButtonWidth: 28, fontSize: 12, @@ -325,6 +332,7 @@ const structureDensityMetrics: Record< columns: [36, 232, 188, 116, 84, 76, 152, 300, 100, 160, 188, 148], indexes: [176, 260, 84, 124, 176, 216, 176, 104], minColumnWidth: 32, + minLengthColumnWidth: 176, minIndexColumnWidth: 64, actionButtonWidth: 32, fontSize: 13, @@ -565,11 +573,12 @@ watch(localStructureDensity, (density, previousDensity) => { function onColResize(e: MouseEvent, col: number) { e.preventDefault(); const widthIndex = columnWidthIndex(col); - colResizing.value = { col: widthIndex, startX: e.clientX, startW: colWidths.value[widthIndex] }; + const minimumWidth = widthIndex === 3 && databaseType.value === "dameng" ? 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; const delta = ev.clientX - colResizing.value.startX; - colWidths.value[widthIndex] = Math.max(structureDensityMetric.value.minColumnWidth, colResizing.value.startW + delta); + colWidths.value[widthIndex] = Math.max(minimumWidth, colResizing.value.startW + delta); }; const onUp = () => { colResizing.value = null; @@ -749,7 +758,13 @@ const columnActionsWidth = computed(() => { return metric.actionButtonWidth * count + actionButtonGap * Math.max(0, count - 1) + metric.cellPaddingX * 2; }); const visibleColumnIndexes = computed(() => colLabels.value.map((column) => column.widthIndex)); -const visibleColWidths = computed(() => colLabels.value.map((column) => (column.key === "actions" ? columnActionsWidth.value : (colWidths.value[column.widthIndex] ?? structureDensityMetric.value.minColumnWidth)))); +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; + }), +); function columnWidthIndex(visibleIndex: number) { return visibleColumnIndexes.value[visibleIndex] ?? visibleIndex; @@ -1215,7 +1230,7 @@ async function fetchTableCommentValue(connectionId: string, database: string, sc } } -async function loadStructure(silent = false, scope: StructureRefreshScope = FULL_STRUCTURE_REFRESH_SCOPE, showErrors = true, options: { blockSecondaryMetadata?: boolean; preserveDraft?: boolean } = {}) { +async function loadStructure(silent = false, scope: StructureRefreshScope = FULL_STRUCTURE_REFRESH_SCOPE, showErrors = true, options: { blockSecondaryMetadata?: boolean; preserveDraft?: boolean; damengLengthUnitsAfterSave?: ReadonlyMap } = {}) { const connectionId = props.connectionId; const database = props.database; const schema = metadataSchema.value; @@ -1251,7 +1266,9 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL // Load live charset/collation metadata from the MySQL server so the column // editor shows the correct options for the server version. void loadCharsetMetadata(); - columns.value = applyStoredLocalColumnOrder(createColumnDrafts(nextColumns, databaseType.value)); + const nextColumnDrafts = createColumnDrafts(nextColumns, databaseType.value); + const hydratedColumnDrafts = databaseType.value === "dameng" && options.damengLengthUnitsAfterSave ? restoreDamengLengthUnitsAfterSave(nextColumnDrafts, options.damengLengthUnitsAfterSave) : nextColumnDrafts; + columns.value = applyStoredLocalColumnOrder(hydratedColumnDrafts); } const nextTableComment = await tableCommentPromise; @@ -1296,9 +1313,9 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL } } -async function refreshStructureAfterSave(scope: StructureRefreshScope) { +async function refreshStructureAfterSave(scope: StructureRefreshScope, damengLengthUnitsAfterSave: ReadonlyMap) { try { - await loadStructure(true, scope, false, { blockSecondaryMetadata: true }); + await loadStructure(true, scope, false, { blockSecondaryMetadata: true, damengLengthUnitsAfterSave }); } catch (e) { console.warn("[DBX][structure-editor:post-save-refresh-failed]", e); } finally { @@ -1579,7 +1596,16 @@ function removeMysqlEnumValue(column: EditableStructureColumn, index: number) { } function updateColumnDataTypeLength(column: EditableStructureColumn, value: string | number) { - column.dataType = combineDataTypeForDatabase(databaseType.value, splitDataType(column.dataType).baseType, String(value)); + const baseType = splitDataType(column.dataType).baseType; + column.dataType = combineDataTypeForDatabaseWithLengthUnit(databaseType.value, baseType, String(value), dataTypeLengthUnitValue(databaseType.value, column.dataType)); + syncSqlServerIdentityForDataType(column); + syncDamengIdentityForDataType(column); +} + +function updateColumnDataTypeLengthUnit(column: EditableStructureColumn, value: unknown) { + const baseType = splitDataType(column.dataType).baseType; + const unit = value === "__default" ? "" : String(value ?? ""); + column.dataType = combineDataTypeForDatabaseWithLengthUnit(databaseType.value, baseType, dataTypeLengthInputValue(databaseType.value, column.dataType), unit); syncSqlServerIdentityForDataType(column); syncDamengIdentityForDataType(column); } @@ -1885,6 +1911,14 @@ function isColumnLengthDisabled(column: EditableStructureColumn): boolean { return isDataTypeLengthDisabled(databaseType.value, baseType); } +function columnLengthUnitOptions(column: EditableStructureColumn) { + return getDataTypeLengthUnitOptions(databaseType.value, column.dataType); +} + +function isColumnLengthUnitDisabled(column: EditableStructureColumn): boolean { + return isColumnLengthDisabled(column) || !dataTypeLengthInputValue(databaseType.value, column.dataType).trim(); +} + function isColumnNullableDisabled(column: EditableStructureColumn): boolean { return column.markedForDrop || column.isPrimaryKey || (!!column.original && !structureCapabilities.value.alterNullability); } @@ -2173,6 +2207,14 @@ async function applyChanges() { saving.value = true; errorMessage.value = ""; const refreshScope = captureStructureRefreshScope(); + const damengLengthUnitsAfterSave = new Map(); + if (databaseType.value === "dameng") { + for (const column of columns.value) { + if (!column.markedForDrop && dataTypeLengthUnitValue("dameng", column.dataType)) { + damengLengthUnitsAfterSave.set(column.name.trim().toLowerCase(), column.dataType); + } + } + } const startedAt = Date.now(); try { const result = hasSqliteTypeChange.value @@ -2196,7 +2238,7 @@ async function applyChanges() { postSaveRefreshing.value = true; skipNextRefreshVersion = true; emit("saved", tableComment.value !== originalTableComment.value); - await refreshStructureAfterSave(refreshScope); + await refreshStructureAfterSave(refreshScope, damengLengthUnitsAfterSave); } return true; } catch (e: any) { @@ -2645,7 +2687,22 @@ watch([activeTab, ddlLoading], ([tab, loading]) => { - +
+ + +