feat(dameng): preserve character length units

This commit is contained in:
onenewcode
2026-07-14 01:28:19 +08:00
committed by GitHub
parent 8020fe5886
commit 32b4acee3a
13 changed files with 382 additions and 24 deletions
@@ -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();
@@ -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<ColumnInfo> 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<List<Object>> independentIndexes,
List<String> sqls,
String dbmsMetadataDdl
) {
return metadataConnection(
allColumnComment,
fallbackColumnComment,
includeMaterializedView,
independentIndexes,
sqls,
dbmsMetadataDdl,
defaultColumnMetadataRows(allColumnComment)
);
}
private static Connection metadataConnectionForColumns(List<List<Object>> 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<List<Object>> independentIndexes,
List<String> sqls,
String dbmsMetadataDdl,
List<List<Object>> 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<List<Object>> 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<Object> 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;
@@ -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<string, string> } = {}) {
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<string, string>) {
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<string, string>();
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]) => {
</div>
</PopoverContent>
</Popover>
<Input v-else :model-value="dataTypeLengthInputValue(databaseType, column.dataType)" :class="structureMonoControlClass" :disabled="isColumnLengthDisabled(column)" @update:model-value="updateColumnDataTypeLength(column, $event)" />
<div v-else class="flex min-w-0 items-center gap-1">
<Input :model-value="dataTypeLengthInputValue(databaseType, column.dataType)" :class="[structureMonoControlClass, 'min-w-0 flex-1']" :disabled="isColumnLengthDisabled(column)" @update:model-value="updateColumnDataTypeLength(column, $event)" />
<Select v-if="columnLengthUnitOptions(column).length" :model-value="dataTypeLengthUnitValue(databaseType, column.dataType) || '__default'" :disabled="isColumnLengthUnitDisabled(column)" @update:model-value="updateColumnDataTypeLengthUnit(column, $event)">
<SelectTrigger
:aria-label="t('structureEditor.lengthUnit')"
:title="t('structureEditor.lengthUnit')"
class="h-[var(--structure-control-height)] w-16 shrink-0 rounded-[6px] px-[var(--structure-control-px)] font-mono text-[length:var(--structure-font-size)] focus-visible:border-ring/50 focus-visible:ring-1 focus-visible:ring-ring/25"
>
<SelectValue :placeholder="t('structureEditor.unitPlaceholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__default">{{ t("structureEditor.defaultAction") }}</SelectItem>
<SelectItem v-for="unit in columnLengthUnitOptions(column)" :key="unit" :value="unit">{{ unit }}</SelectItem>
</SelectContent>
</Select>
</div>
</td>
<td v-if="columnEditorControls.nullable" :class="structureCellClass">
<label class="flex items-center gap-1.5">
+2
View File
@@ -2025,6 +2025,8 @@ export default {
numeric: "NUMERIC affinity attempts to convert well-formed numeric text to INTEGER first, then REAL; it is not a separate SQLite storage class. DATE, DATETIME, and TIME declarations also have NUMERIC affinity; store ISO-8601 TEXT, Julian-day REAL, or Unix-time INTEGER consistently.",
},
length: "Length",
lengthUnit: "Length unit",
unitPlaceholder: "Unit",
enumValues: "Enum values",
enumValueCount: "{count} values",
addEnumValue: "Add value",
+2
View File
@@ -1909,6 +1909,8 @@ export default withEnglishFallback({
typePlaceholder: "Seleccionar o escribir...",
noMatchingType: "Sin coincidencias",
length: "Longitud",
lengthUnit: "Unidad de longitud",
unitPlaceholder: "Unidad",
nullable: "Admite nulos",
primaryKey: "Clave primaria",
defaultValue: "Valor por defecto",
+2
View File
@@ -1907,6 +1907,8 @@ export default withEnglishFallback({
typePlaceholder: "Seleziona o digita...",
noMatchingType: "Nessun tipo corrispondente",
length: "Lunghezza",
lengthUnit: "Unità di lunghezza",
unitPlaceholder: "Unità",
nullable: "Nullabile",
primaryKey: "Chiave Primaria",
defaultValue: "Valore Predefinito",
+2
View File
@@ -1942,6 +1942,8 @@ export default withEnglishFallback({
typePlaceholder: "選択または入力...",
noMatchingType: "一致する型がありません",
length: "長さ",
lengthUnit: "長さの単位",
unitPlaceholder: "単位",
nullable: "NULL許容",
primaryKey: "主キー",
defaultValue: "デフォルト",
+2
View File
@@ -1909,6 +1909,8 @@ export default withEnglishFallback({
typePlaceholder: "Selecione ou digite...",
noMatchingType: "Nenhum tipo correspondente",
length: "Comprimento",
lengthUnit: "Unidade de comprimento",
unitPlaceholder: "Unidade",
nullable: "Permite nulo",
primaryKey: "Chave primária",
defaultValue: "Padrão",
+2
View File
@@ -1921,6 +1921,8 @@ export default withEnglishFallback({
columnName: "字段名",
dataType: "类型",
length: "长度",
lengthUnit: "长度单位",
unitPlaceholder: "单位",
enumValues: "枚举值",
enumValueCount: "{count} 个值",
addEnumValue: "添加值",
+2
View File
@@ -1815,6 +1815,8 @@ export default withEnglishFallback({
columnName: "欄位",
dataType: "類型",
length: "長度",
lengthUnit: "長度單位",
unitPlaceholder: "單位",
typePlaceholder: "選擇或輸入……",
noMatchingType: "沒有相符類型",
nullable: "可為空",
@@ -1,10 +1,13 @@
import { describe, expect, it } from "vitest";
import {
combineDataTypeForDatabase,
combineDataTypeForDatabaseWithLengthUnit,
createColumnDrafts,
dataTypeLengthInputValue,
dataTypeLengthUnitValue,
DATA_TYPE_OPTIONS,
defaultNewColumnDataType,
getDataTypeLengthUnitOptions,
getDefaultLengthForType,
hasExistingColumnTypeChange,
isDataTypeLengthDisabled,
@@ -14,6 +17,7 @@ import {
isSqlServerIdentityCompatibleDataType,
mysqlEnumDataType,
rehydrateColumnDraftsFromMetadata,
restoreDamengLengthUnitsAfterSave,
splitDataType,
} from "@/lib/table/tableStructureEditorState";
@@ -101,6 +105,79 @@ describe("tableStructureEditorState", () => {
expect(combineDataTypeForDatabase("mysql", "integer", "11")).toBe("integer(11)");
});
it("offers BYTE and CHAR units only for supported Dameng 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"]);
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("mysql", "varchar(255)")).toEqual([]);
});
it("separates and reconstructs Dameng character length units", () => {
expect(dataTypeLengthInputValue("dameng", "varchar2(255 char)")).toBe("255");
expect(dataTypeLengthUnitValue("dameng", "varchar2(255 char)")).toBe("CHAR");
expect(dataTypeLengthInputValue("dameng", "char(10 BYTE)")).toBe("10");
expect(dataTypeLengthUnitValue("dameng", "char(10 BYTE)")).toBe("BYTE");
expect(combineDataTypeForDatabaseWithLengthUnit("dameng", "varchar2", "255", "CHAR")).toBe("varchar2(255 CHAR)");
expect(combineDataTypeForDatabaseWithLengthUnit("dameng", "varchar", "64", "byte")).toBe("varchar(64 BYTE)");
expect(combineDataTypeForDatabaseWithLengthUnit("dameng", "char", "", "CHAR")).toBe("char");
expect(combineDataTypeForDatabaseWithLengthUnit("dameng", "varchar2", "255", "")).toBe("varchar2(255)");
});
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("");
expect(combineDataTypeForDatabaseWithLengthUnit("mysql", "varchar", "255", "CHAR")).toBe("varchar(255)");
expect(combineDataTypeForDatabaseWithLengthUnit("dameng", "nvarchar2", "20", "BYTE")).toBe("nvarchar2(20)");
});
it("keeps a saved Dameng length unit when an older agent omits it during post-save refresh", () => {
const [legacyAgentDraft] = createColumnDrafts(
[
{
name: "DISPLAY_NAME",
data_type: "VARCHAR2(255)",
is_nullable: true,
column_default: null,
is_primary_key: false,
extra: null,
},
],
"dameng",
);
const [restored] = restoreDamengLengthUnitsAfterSave([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("prefers live Dameng metadata when the agent returns an explicit length unit", () => {
const [liveDraft] = createColumnDrafts(
[
{
name: "DISPLAY_NAME",
data_type: "VARCHAR2(255 BYTE)",
is_nullable: true,
column_default: null,
is_primary_key: false,
extra: null,
},
],
"dameng",
);
const [restored] = restoreDamengLengthUnitsAfterSave([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("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("");
@@ -842,6 +842,66 @@ export function splitDataType(raw: string): { baseType: string; params: string }
return { baseType, params };
}
export type DataTypeLengthUnit = "BYTE" | "CHAR";
const DAMENG_LENGTH_UNIT_TYPES = new Set(["char", "varchar", "varchar2"]);
const DAMENG_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 : [];
}
function splitDataTypeLengthParams(dbType: DatabaseType | undefined, rawDataType: string): { length: string; unit: DataTypeLengthUnit | "" } {
const { params } = splitDataType(rawDataType);
if (!params || getDataTypeLengthUnitOptions(dbType, rawDataType).length === 0) {
return { length: params, unit: "" };
}
const match = params.match(/^(.*\S)\s+(BYTE|CHAR)$/i);
if (!match) return { length: params, unit: "" };
return {
length: match[1]!.trim(),
unit: match[2]!.toUpperCase() as DataTypeLengthUnit,
};
}
export function dataTypeLengthUnitValue(dbType: DatabaseType | undefined, rawDataType: string): DataTypeLengthUnit | "" {
return splitDataTypeLengthParams(dbType, rawDataType).unit;
}
export function combineDataTypeForDatabaseWithLengthUnit(dbType: DatabaseType | undefined, baseType: string, length: string, unit: string | undefined): string {
const normalizedLength = length.trim();
if (!normalizedLength) return combineDataTypeForDatabase(dbType, baseType, "");
const normalizedUnit = unit?.trim().toUpperCase();
const validUnit = normalizedUnit === "BYTE" || normalizedUnit === "CHAR" ? normalizedUnit : "";
const allowedUnits = getDataTypeLengthUnitOptions(dbType, baseType);
const params = validUnit && allowedUnits.includes(validUnit) ? `${normalizedLength} ${validUnit}` : normalizedLength;
return combineDataTypeForDatabase(dbType, baseType, params);
}
export function restoreDamengLengthUnitsAfterSave(columns: EditableStructureColumn[], savedDataTypesByColumn: ReadonlyMap<string, string>): EditableStructureColumn[] {
if (savedDataTypesByColumn.size === 0) return columns;
return columns.map((column) => {
if (dataTypeLengthUnitValue("dameng", column.dataType)) return column;
const savedDataType = savedDataTypesByColumn.get(column.name.trim().toLowerCase());
if (!savedDataType || !dataTypeLengthUnitValue("dameng", savedDataType)) return column;
if (normalizedDataTypeName(savedDataType) !== normalizedDataTypeName(column.dataType)) return column;
return {
...column,
dataType: savedDataType,
original: column.original ? { ...column.original, data_type: savedDataType } : column.original,
};
});
}
/** MySQL character/text types that accept `CHARACTER SET` and `COLLATE`. */
const MYSQL_CHARACTER_DATA_TYPES = new Set(["char", "varchar", "tinytext", "text", "mediumtext", "longtext", "enum", "set"]);
@@ -898,7 +958,7 @@ export function combineDataTypeForDatabase(dbType: DatabaseType | undefined, bas
export function dataTypeLengthInputValue(dbType: DatabaseType | undefined, rawDataType: string): string {
const parsed = splitDataType(rawDataType);
return isDataTypeLengthDisabled(dbType, parsed.baseType) ? "" : parsed.params;
return isDataTypeLengthDisabled(dbType, parsed.baseType) ? "" : splitDataTypeLengthParams(dbType, rawDataType).length;
}
export function normalizeDataTypeParams(dbType: DatabaseType | undefined, baseType: string, params: string): string {
@@ -2269,6 +2269,61 @@ fn dameng_create_table_with_identity() {
assert!(result.statements[0].contains("PRIMARY KEY (\"ID\")"), "ddl: {}", result.statements[0]);
}
#[test]
fn dameng_create_table_preserves_character_length_units() {
let mut name = column("NAME");
name.data_type = "VARCHAR2(255 CHAR)".to_string();
let mut code = column("CODE");
code.data_type = "VARCHAR(64 BYTE)".to_string();
let result = build_create_table_sql(TableStructureSqlOptions {
database_type: Some(DatabaseType::Dameng),
schema: Some("SYSDBA".to_string()),
table_name: "USERS".to_string(),
columns: vec![name, code],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
});
assert_eq!(result.warnings, Vec::<String>::new());
assert!(result.statements[0].contains("\"NAME\" VARCHAR2(255 CHAR)"), "ddl: {}", result.statements[0]);
assert!(result.statements[0].contains("\"CODE\" VARCHAR(64 BYTE)"), "ddl: {}", result.statements[0]);
}
#[test]
fn dameng_alter_column_preserves_character_length_unit() {
let mut name = column("NAME");
name.data_type = "VARCHAR2(64 BYTE)".to_string();
name.original = Some(ColumnInfo {
name: "NAME".to_string(),
data_type: "VARCHAR2(64 CHAR)".to_string(),
is_nullable: true,
column_default: None,
is_primary_key: false,
extra: None,
comment: None,
..Default::default()
});
let result = build_table_structure_change_sql(TableStructureSqlOptions {
database_type: Some(DatabaseType::Dameng),
schema: Some("SYSDBA".to_string()),
table_name: "USERS".to_string(),
columns: vec![name],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
});
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(result.statements, vec!["ALTER TABLE \"SYSDBA\".\"USERS\" MODIFY (\"NAME\" VARCHAR2(64 BYTE));"]);
}
#[test]
fn dameng_rejects_multiple_identity_columns() {
let mut first = column("ID");