feat(xugu): expand table editor data types
This commit is contained in:
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);
|
||||
|
||||
Reference in new issue
Block a user