mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 05:20:02 +00:00
feat(dameng): preserve character length units
This commit is contained in:
@@ -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();
|
||||
|
||||
+73
-10
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1942,6 +1942,8 @@ export default withEnglishFallback({
|
||||
typePlaceholder: "選択または入力...",
|
||||
noMatchingType: "一致する型がありません",
|
||||
length: "長さ",
|
||||
lengthUnit: "長さの単位",
|
||||
unitPlaceholder: "単位",
|
||||
nullable: "NULL許容",
|
||||
primaryKey: "主キー",
|
||||
defaultValue: "デフォルト",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1921,6 +1921,8 @@ export default withEnglishFallback({
|
||||
columnName: "字段名",
|
||||
dataType: "类型",
|
||||
length: "长度",
|
||||
lengthUnit: "长度单位",
|
||||
unitPlaceholder: "单位",
|
||||
enumValues: "枚举值",
|
||||
enumValueCount: "{count} 个值",
|
||||
addEnumValue: "添加值",
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user