feat(xugu): expand table editor data types

This commit is contained in:
Elias authored and GitHub committed 2026-08-20 01:52:43 +08:00
1 parent 884744df4c
commit 82da9cb6dc
7 files changed
+465 -38

No files matched your search

@@ -7,6 +7,7 @@ import {
createCopiedColumnDrafts,
createColumnDrafts,
createTriggerDrafts,
dataTypeBaseInputValue,
dataTypeLengthInputValue,
dataTypeLengthUnitValue,
DATA_TYPE_OPTIONS,
@@ -410,6 +411,55 @@ describe("tableStructureEditorState", () => {
expect(defaultNewColumnDataType("clickhouse")).toBe("String");
});
it("offers the Xugu types that can be used by the table editor", () => {
for (const dataType of ["TINYINT", "DOUBLE", "DATETIME", "DATETIME WITH TIME ZONE", "TIME WITH TIME ZONE", "TIMESTAMP WITH TIME ZONE", "INTERVAL YEAR", "INTERVAL DAY TO SECOND", "GUID", "ROWID", "JSON", "BIT", "VARBIT", "INTEGER[]", "DOUBLE[]", "CHAR[]", "CLOB[]"]) {
expect(DATA_TYPE_OPTIONS.xugu).toContain(dataType);
}
for (const pseudoType of ["NULL", '"NULL"', "ARRAY", "ROWVERSION", "POINT", "LSEG", "LINE", "BOX", "PATH", "POLYGON", "CIRCLE"]) {
expect(DATA_TYPE_OPTIONS.xugu).not.toContain(pseudoType);
}
});
it("keeps Xugu type parameters within syntax the generic editor can emit", () => {
for (const fixedType of ["GUID", "ROWID", "JSON", "XML", "BLOB", "CLOB", "INTEGER[]", "INTERVAL DAY TO SECOND", "DATETIME WITH TIME ZONE"]) {
expect(isDataTypeLengthDisabled("xugu", fixedType)).toBe(true);
expect(combineDataTypeForDatabase("xugu", fixedType, "12")).toBe(fixedType);
}
for (const parameterizedType of ["VARCHAR", "BINARY", "BIT", "VARBIT", "NUMERIC", "TIME", "TIME WITH TIME ZONE", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE"]) {
expect(isDataTypeLengthDisabled("xugu", parameterizedType)).toBe(false);
}
expect(combineDataTypeForDatabase("xugu", "TIME", "3")).toBe("TIME(3)");
expect(combineDataTypeForDatabase("xugu", "TIME", "4")).toBe("TIME");
expect(combineDataTypeForDatabase("xugu", "TIMESTAMP", "6")).toBe("TIMESTAMP(6)");
expect(combineDataTypeForDatabase("xugu", "TIMESTAMP", "7")).toBe("TIMESTAMP");
expect(combineDataTypeForDatabase("xugu", "TIME WITH TIME ZONE", "3")).toBe("TIME(3) WITH TIME ZONE");
expect(combineDataTypeForDatabase("xugu", "TIMESTAMP WITH TIME ZONE", "6")).toBe("TIMESTAMP(6) WITH TIME ZONE");
expect(combineDataTypeForDatabase("xugu", "TIMESTAMP WITH TIME ZONE", "7")).toBe("TIMESTAMP WITH TIME ZONE");
expect(dataTypeBaseInputValue("xugu", "TIMESTAMP(6) WITH TIME ZONE")).toBe("TIMESTAMP WITH TIME ZONE");
expect(dataTypeLengthInputValue("xugu", "TIMESTAMP(6) WITH TIME ZONE")).toBe("6");
// Xugu-only constraints must not change other database profiles.
expect(isDataTypeLengthDisabled("mysql", "json")).toBe(false);
expect(isDataTypeLengthDisabled("oracle", "clob")).toBe(false);
});
it("round-trips Xugu single-parameter metadata through the editor", () => {
const columns = createColumnDrafts(
[
{ name: "flags", data_type: "BIT", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 8 },
{ name: "bits", data_type: "VARBIT", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 64 },
{ name: "local_time", data_type: "TIME", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 3 },
{ name: "created_at", data_type: "TIMESTAMP", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 6 },
{ name: "created_at_tz", data_type: "TIMESTAMP WITH TIME ZONE", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 6 },
{ name: "local_time_tz", data_type: "TIME WITH TIME ZONE", is_nullable: true, column_default: null, is_primary_key: false, extra: null, numeric_precision: 3 },
],
"xugu",
);
expect(columns.map((column) => column.dataType)).toEqual(["BIT(8)", "VARBIT(64)", "TIME(3)", "TIMESTAMP(6)", "TIMESTAMP(6) WITH TIME ZONE", "TIME(3) WITH TIME ZONE"]);
expect(columns.map((column) => column.original?.data_type)).toEqual(columns.map((column) => column.dataType));
});
it("requires a SQLite rebuild only for a retained existing column type change", () => {
const [column] = createColumnDrafts(
[
@@ -371,7 +371,63 @@ export const DATA_TYPE_OPTIONS: Record<string, string[]> = {
"interval day to second",
],
questdb: ["boolean", "ipv4", "byte", "short", "char", "int", "float", "symbol", "varchar", "string", "long", "date", "timestamp", "timestamp_ns", "double", "uuid", "binary", "long256", "geohash", "array", "interval", "decimal"],
xugu: ["BOOLEAN", "INTEGER", "SMALLINT", "BIGINT", "FLOAT", "NUMERIC", "CHAR", "VARCHAR", "CLOB", "DATE", "TIME", "TIMESTAMP", "BINARY", "VARBINARY", "BLOB", "XML", "BOOL", "INT", "SHORT", "LONGINT", "LONG", "REAL", "DECIMAL", "TEXT", "NCHAR", "NVARCHAR", "NVARCHAR2"],
xugu: [
"BIGINT",
"BINARY",
"BIT",
"BLOB",
"BOOL",
"BOOLEAN",
"CHAR",
"CHAR[]",
"CLOB",
"CLOB[]",
"DATE",
"DATETIME",
"DATETIME WITH TIME ZONE",
"DECIMAL",
"DOUBLE",
"DOUBLE[]",
"FLOAT",
"GUID",
"INT",
"INTEGER",
"INTEGER[]",
"INTERVAL DAY",
"INTERVAL DAY TO HOUR",
"INTERVAL DAY TO MINUTE",
"INTERVAL DAY TO SECOND",
"INTERVAL HOUR",
"INTERVAL HOUR TO MINUTE",
"INTERVAL HOUR TO SECOND",
"INTERVAL MINUTE",
"INTERVAL MINUTE TO SECOND",
"INTERVAL MONTH",
"INTERVAL SECOND",
"INTERVAL YEAR",
"INTERVAL YEAR TO MONTH",
"JSON",
"LONG",
"LONGINT",
"NCHAR",
"NUMERIC",
"NVARCHAR",
"NVARCHAR2",
"REAL",
"ROWID",
"SHORT",
"SMALLINT",
"TEXT",
"TIME",
"TIME WITH TIME ZONE",
"TIMESTAMP",
"TIMESTAMP WITH TIME ZONE",
"TINYINT",
"VARBINARY",
"VARBIT",
"VARCHAR",
"XML",
],
duckdb: ["BOOLEAN", "TINYINT", "SMALLINT", "INTEGER", "BIGINT", "HUGEINT", "UTINYINT", "USMALLINT", "UINTEGER", "UBIGINT", "FLOAT", "DOUBLE", "DECIMAL", "VARCHAR", "TEXT", "BLOB", "DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "INTERVAL", "UUID", "JSON"],
h2: ["BOOLEAN", "TINYINT", "SMALLINT", "INTEGER", "BIGINT", "IDENTITY", "DECIMAL", "NUMERIC", "REAL", "DOUBLE", "FLOAT", "CHAR", "CHARACTER", "VARCHAR", "VARCHAR_IGNORECASE", "CLOB", "BINARY", "VARBINARY", "BLOB", "DATE", "TIME", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "UUID", "ARRAY", "JSON"],
};
@@ -559,6 +615,8 @@ export const POSTGRES_TYPE_LENGTH_DISABLES: string[] = [
export const ORACLE_LIKE_TYPE_LENGTH_DISABLES: string[] = ["binary_double", "binary_float", "bigint", "boolean", "bool", "byte", "date", "double", "double precision", "float", "integer", "int", "long", "long raw", "nclob", "real", "smallint", "text", "tinyint"];
const XUGU_TYPE_LENGTH_DISABLES = new Set([...ORACLE_LIKE_TYPE_LENGTH_DISABLES, "blob", "clob", "datetime", "datetime with time zone", "guid", "json", "longint", "rowid", "short", "xml"]);
export const SQLSERVER_TYPE_LENGTH_DISABLES: string[] = ["bigint", "bit", "date", "datetime", "image", "int", "integer", "money", "ntext", "real", "smalldatetime", "smallint", "smallmoney", "sql_variant", "text", "timestamp", "tinyint", "uniqueidentifier", "xml"];
export function parseExtraToColumnExtra(extra: string | null | undefined, databaseType?: DatabaseType): ColumnExtra {
@@ -753,9 +811,10 @@ function columnDefaultForEditor(column: ColumnInfo, databaseType?: DatabaseType)
const CHARACTER_LENGTH_METADATA_TYPES = new Set(["binary", "char", "character", "character varying", "nchar", "nvarchar", "nvarchar2", "varbinary", "varchar", "varchar2"]);
const NUMERIC_PRECISION_METADATA_TYPES = new Set(["decimal", "number", "numeric"]);
const XUGU_SINGLE_PRECISION_METADATA_TYPES = new Set(["bit", "time", "time with time zone", "timestamp", "timestamp with time zone", "varbit"]);
function columnDataTypeForEditor(column: ColumnInfo, databaseType?: DatabaseType): string {
const parsed = splitDataType(column.data_type);
const parsed = splitDataTypeForDatabase(databaseType, column.data_type);
if (parsed.params) return column.data_type;
const baseType = parsed.baseType.trim().replace(/\s+/g, " ");
@@ -763,6 +822,11 @@ function columnDataTypeForEditor(column: ColumnInfo, databaseType?: DatabaseType
if (CHARACTER_LENGTH_METADATA_TYPES.has(normalized) && Number.isInteger(column.character_maximum_length) && Number(column.character_maximum_length) > 0) {
return combineDataTypeForDatabase(databaseType, baseType, String(column.character_maximum_length));
}
if (databaseType === "xugu" && XUGU_SINGLE_PRECISION_METADATA_TYPES.has(normalized) && Number.isInteger(column.numeric_precision)) {
const precision = Number(column.numeric_precision);
const minimum = normalized === "bit" || normalized === "varbit" ? 1 : 0;
if (precision >= minimum) return combineDataTypeForDatabase(databaseType, baseType, String(precision));
}
if (NUMERIC_PRECISION_METADATA_TYPES.has(normalized) && Number.isInteger(column.numeric_precision) && Number(column.numeric_precision) > 0) {
const scale = Number.isInteger(column.numeric_scale) && Number(column.numeric_scale) >= 0 ? `,${column.numeric_scale}` : "";
return combineDataTypeForDatabase(databaseType, baseType, `${column.numeric_precision}${scale}`);
@@ -1047,6 +1111,23 @@ export function splitDataType(raw: string): { baseType: string; params: string }
return { baseType, params };
}
function splitDataTypeForDatabase(dbType: DatabaseType | undefined, raw: string): { baseType: string; params: string } {
if (dbType === "xugu") {
const match = raw.trim().match(/^(TIME|TIMESTAMP)\s*\(([^()]*)\)\s+WITH\s+TIME\s+ZONE$/i);
if (match) {
return {
baseType: `${match[1]} WITH TIME ZONE`,
params: match[2]!.trim(),
};
}
}
return splitDataType(raw);
}
export function dataTypeBaseInputValue(dbType: DatabaseType | undefined, rawDataType: string): string {
return splitDataTypeForDatabase(dbType, rawDataType).baseType;
}
export type DataTypeLengthUnit = "BYTE" | "CHAR";
const CHARACTER_LENGTH_UNIT_TYPES = new Set(["char", "varchar", "varchar2"]);
@@ -1062,7 +1143,7 @@ export function getDataTypeLengthUnitOptions(dbType: DatabaseType | undefined, r
}
function splitDataTypeLengthParams(dbType: DatabaseType | undefined, rawDataType: string): { length: string; unit: DataTypeLengthUnit | "" } {
const { params } = splitDataType(rawDataType);
const { params } = splitDataTypeForDatabase(dbType, rawDataType);
if (!params || getDataTypeLengthUnitOptions(dbType, rawDataType).length === 0) {
return { length: params, unit: "" };
}
@@ -1158,19 +1239,28 @@ export function combineDataTypeForDatabase(dbType: DatabaseType | undefined, bas
const normalizedParams = normalizeDataTypeParams(dbType, baseType, params);
const mysqlType = combineMysqlNumericAttributeType(dbType, baseType, normalizedParams);
if (mysqlType) return mysqlType;
const xuguTemporalType = combineXuguTemporalType(baseType, normalizedParams, dbType);
if (xuguTemporalType) return xuguTemporalType;
return combineDataType(baseType, normalizedParams);
}
export function dataTypeLengthInputValue(dbType: DatabaseType | undefined, rawDataType: string): string {
const parsed = splitDataType(rawDataType);
const parsed = splitDataTypeForDatabase(dbType, rawDataType);
return isDataTypeLengthDisabled(dbType, parsed.baseType) ? "" : splitDataTypeLengthParams(dbType, rawDataType).length;
}
function combineXuguTemporalType(baseType: string, params: string, dbType: DatabaseType | undefined): string | null {
if (dbType !== "xugu") return null;
const match = baseType.trim().match(/^(TIME|TIMESTAMP)\s+WITH\s+TIME\s+ZONE$/i);
if (!match) return null;
return params ? `${match[1]}(${params}) WITH TIME ZONE` : baseType.trim();
}
export function normalizeDataTypeParams(dbType: DatabaseType | undefined, baseType: string, params: string): string {
const p = params.trim();
if (!p) return "";
if (!isTemporalPrecisionType(dbType, baseType)) return p;
return isValidTemporalPrecision(dbType, p) ? p : "";
return isValidTemporalPrecision(dbType, baseType, p) ? p : "";
}
function isTemporalPrecisionType(dbType: DatabaseType | undefined, baseType: string): boolean {
@@ -1200,6 +1290,8 @@ function isTemporalPrecisionType(dbType: DatabaseType | undefined, baseType: str
return ["timestamp", "timestamp with time zone", "timestamp with local time zone"].includes(normalized);
case "questdb":
return ["timestamp"].includes(normalized);
case "xugu":
return ["time", "time with time zone", "timestamp", "timestamp with time zone"].includes(normalized);
default:
return false;
}
@@ -1224,10 +1316,11 @@ function isOracleLikeStructureType(dbType: DatabaseType | undefined): boolean {
return dbType === "oracle" || dbType === "dameng" || dbType === "oceanbase-oracle" || dbType === "iris" || dbType === "yashandb" || dbType === "xugu";
}
function isValidTemporalPrecision(dbType: DatabaseType | undefined, params: string): boolean {
function isValidTemporalPrecision(dbType: DatabaseType | undefined, baseType: string, params: string): boolean {
if (!/^\d+$/.test(params)) return false;
const value = Number(params);
const max = dbType === "oracle" || dbType === "dameng" || dbType === "oceanbase-oracle" ? 9 : 6;
const normalizedBaseType = baseType.trim().replace(/\s+/g, " ").toLowerCase();
const max = dbType === "xugu" && ["time", "time with time zone"].includes(normalizedBaseType) ? 3 : dbType === "oracle" || dbType === "dameng" || dbType === "oceanbase-oracle" ? 9 : 6;
return Number.isInteger(value) && value >= 0 && value <= max && String(value) === params;
}
@@ -1303,6 +1396,10 @@ export function isDataTypeLengthDisabled(_dbType: DatabaseType | undefined, base
return key !== "bit" && key !== "float_vector";
} else if (_dbType === "postgres" || _dbType === "gaussdb" || _dbType === "kwdb" || _dbType === "opengauss" || _dbType === "highgo" || _dbType === "uxdb" || _dbType === "vastbase" || _dbType === "kingbase") {
return key.endsWith("[]") || POSTGRES_TYPE_LENGTH_DISABLES.includes(key);
} else if (_dbType === "xugu") {
// Xugu array suffixes and interval qualifiers require grammar-aware
// placement; a generic TYPE(length) editor would emit invalid DDL for them.
return key.endsWith("[]") || key.startsWith("interval ") || XUGU_TYPE_LENGTH_DISABLES.has(key);
} else if (isOracleLikeStructureType(_dbType)) {
// Dameng/Oracle integer aliases have fixed precision; MySQL-style display widths generate invalid DDL.
return ORACLE_LIKE_TYPE_LENGTH_DISABLES.includes(key);