3 files changed
+557
-4
No files matched your search
@@ -34,6 +34,173 @@ import {
|
||||
} from "@/lib/table/tableStructureEditorState";
|
||||
|
||||
describe("tableStructureEditorState", () => {
|
||||
describe("DuckDB type parameters", () => {
|
||||
it.each([
|
||||
"TINYINT",
|
||||
"INT1",
|
||||
"SMALLINT",
|
||||
"INT2",
|
||||
"SHORT",
|
||||
"INT16",
|
||||
"INTEGER",
|
||||
"INT",
|
||||
"INT4",
|
||||
"SIGNED",
|
||||
"INTEGRAL",
|
||||
"INT32",
|
||||
"BIGINT",
|
||||
"INT8",
|
||||
"LONG",
|
||||
"OID",
|
||||
"INT64",
|
||||
"HUGEINT",
|
||||
"INT128",
|
||||
"UTINYINT",
|
||||
"UINT8",
|
||||
"USMALLINT",
|
||||
"UINT16",
|
||||
"UINTEGER",
|
||||
"UINT32",
|
||||
"UBIGINT",
|
||||
"UINT64",
|
||||
"UHUGEINT",
|
||||
"UINT128",
|
||||
"REAL",
|
||||
"FLOAT4",
|
||||
"DOUBLE",
|
||||
"DOUBLE PRECISION",
|
||||
"FLOAT8",
|
||||
"BOOLEAN",
|
||||
"BOOL",
|
||||
"LOGICAL",
|
||||
"BLOB",
|
||||
"BYTEA",
|
||||
"BINARY",
|
||||
"VARBINARY",
|
||||
"BIT",
|
||||
"BITSTRING",
|
||||
"VARINT",
|
||||
"BIGNUM",
|
||||
"DATE",
|
||||
"TIME",
|
||||
"TIME WITHOUT TIME ZONE",
|
||||
"TIME WITH TIME ZONE",
|
||||
"TIMETZ",
|
||||
"TIMESTAMPTZ",
|
||||
"TIMESTAMP WITH TIME ZONE",
|
||||
"TIMESTAMP_S",
|
||||
"TIMESTAMP_MS",
|
||||
"TIMESTAMP_NS",
|
||||
"UUID",
|
||||
"GUID",
|
||||
"JSON",
|
||||
"INTERVAL",
|
||||
])("does not populate, expose, or save modifiers for %s", (baseType) => {
|
||||
expect(getDefaultLengthForType("duckdb", baseType)).toBe("");
|
||||
expect(isDataTypeLengthDisabled("duckdb", baseType)).toBe(true);
|
||||
expect(dataTypeLengthInputValue("duckdb", `${baseType}(11)`)).toBe("");
|
||||
expect(combineDataTypeForDatabase("duckdb", baseType, "11")).toBe(baseType);
|
||||
expect(combineDataTypeForDatabase("duckdb", baseType.toLowerCase(), "10,2")).toBe(baseType.toLowerCase());
|
||||
});
|
||||
|
||||
it("uses FLOAT mantissa precision instead of MySQL precision and scale", () => {
|
||||
expect(getDefaultLengthForType("duckdb", "FLOAT")).toBe("");
|
||||
expect(isDataTypeLengthDisabled("duckdb", "FLOAT")).toBe(false);
|
||||
for (const precision of ["1", "24", "25", "53"]) {
|
||||
expect(combineDataTypeForDatabase("duckdb", "FLOAT", precision)).toBe(`FLOAT(${precision})`);
|
||||
expect(dataTypeLengthInputValue("duckdb", `FLOAT(${precision})`)).toBe(precision);
|
||||
}
|
||||
for (const invalid of ["0", "54", "-1", "1.5", "10,2", "abc", "999999999999999999999999"]) {
|
||||
expect(combineDataTypeForDatabase("duckdb", "FLOAT", invalid)).toBe("FLOAT");
|
||||
expect(dataTypeLengthInputValue("duckdb", `FLOAT(${invalid})`)).toBe("");
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["DECIMAL", "10,2"],
|
||||
["NUMERIC", "38,0"],
|
||||
["DEC", "12,3"],
|
||||
["VARCHAR", "255"],
|
||||
["CHAR", "1"],
|
||||
["CHARACTER", "20"],
|
||||
["CHARACTER VARYING", "20"],
|
||||
["BPCHAR", "20"],
|
||||
["STRING", "20"],
|
||||
["TEXT", "20"],
|
||||
["NVARCHAR", "20"],
|
||||
["TIMESTAMP", "9"],
|
||||
["DATETIME", "3"],
|
||||
["TIMESTAMP_US", "3"],
|
||||
])("preserves supported %s parameters", (baseType, params) => {
|
||||
expect(isDataTypeLengthDisabled("duckdb", baseType)).toBe(false);
|
||||
expect(combineDataTypeForDatabase("duckdb", baseType, params)).toBe(`${baseType}(${params})`);
|
||||
expect(dataTypeLengthInputValue("duckdb", `${baseType}(${params})`)).toBe(params);
|
||||
});
|
||||
|
||||
it("normalizes stale scalar metadata without rewriting compound or user-defined types", () => {
|
||||
const cases = [
|
||||
["INTEGER(11)", "INTEGER"],
|
||||
["FLOAT(10,2)", "FLOAT"],
|
||||
["FLOAT(53)", "FLOAT(53)"],
|
||||
["DECIMAL(12,3)", "DECIMAL(12,3)"],
|
||||
["VARCHAR(42)", "VARCHAR(42)"],
|
||||
["DECIMAL(10,2)[]", "DECIMAL(10,2)[]"],
|
||||
["INTEGER[3]", "INTEGER[3]"],
|
||||
["STRUCT(id INTEGER, price DECIMAL(10,2))", "STRUCT(id INTEGER, price DECIMAL(10,2))"],
|
||||
["MAP(VARCHAR, DECIMAL(10,2))", "MAP(VARCHAR, DECIMAL(10,2))"],
|
||||
["UNION(id INTEGER, name VARCHAR)", "UNION(id INTEGER, name VARCHAR)"],
|
||||
["ENUM('a', 'b')", "ENUM('a', 'b')"],
|
||||
["custom_type(10)", "custom_type(10)"],
|
||||
["main.INTEGER(11)", "main.INTEGER(11)"],
|
||||
['"INTEGER"(11)', '"INTEGER"(11)'],
|
||||
];
|
||||
for (const [dataType, expected] of cases) {
|
||||
const [draft] = createColumnDrafts([{ name: "value", data_type: dataType!, is_nullable: true, column_default: null, is_primary_key: false }], "duckdb");
|
||||
expect(draft?.dataType).toBe(expected);
|
||||
expect(draft?.original?.data_type).toBe(expected);
|
||||
expect(combineDataTypeForDatabase("duckdb", dataType!, "")).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("retains temporal qualifiers instead of confusing them with length", () => {
|
||||
expect(dataTypeBaseInputValue("duckdb", "TIMESTAMP(6) WITH TIME ZONE")).toBe("TIMESTAMP WITH TIME ZONE");
|
||||
expect(dataTypeLengthInputValue("duckdb", "TIMESTAMP(6) WITH TIME ZONE")).toBe("");
|
||||
expect(combineDataTypeForDatabase("duckdb", "TIMESTAMP(6) WITH TIME ZONE", "")).toBe("TIMESTAMP WITH TIME ZONE");
|
||||
expect(combineDataTypeForDatabase("duckdb", "TIME(6) WITHOUT TIME ZONE", "")).toBe("TIME WITHOUT TIME ZONE");
|
||||
expect(dataTypeBaseInputValue("duckdb", "TIMESTAMP(3) WITHOUT TIME ZONE")).toBe("TIMESTAMP WITHOUT TIME ZONE");
|
||||
expect(dataTypeLengthInputValue("duckdb", "TIMESTAMP(3) WITHOUT TIME ZONE")).toBe("3");
|
||||
expect(combineDataTypeForDatabase("duckdb", "TIMESTAMP WITHOUT TIME ZONE", "3")).toBe("TIMESTAMP(3) WITHOUT TIME ZONE");
|
||||
});
|
||||
|
||||
it("hydrates supported metadata precision without restoring fixed-type lengths", () => {
|
||||
const columns = [
|
||||
{ data_type: "INTEGER", numeric_precision: 32 },
|
||||
{ data_type: "BINARY", character_maximum_length: 255 },
|
||||
{ data_type: "DECIMAL", numeric_precision: 12, numeric_scale: 3 },
|
||||
{ data_type: "VARCHAR", character_maximum_length: 64 },
|
||||
].map((column, index) => ({ name: `value_${index}`, is_nullable: true, column_default: null, is_primary_key: false, ...column }));
|
||||
expect(createColumnDrafts(columns, "duckdb").map((column) => column.dataType)).toEqual(["INTEGER", "BINARY", "DECIMAL(12,3)", "VARCHAR(64)"]);
|
||||
});
|
||||
|
||||
it("keeps defaults and other dialects unchanged", () => {
|
||||
expect(defaultNewColumnDataType("duckdb")).toBe("TEXT");
|
||||
expect(getDefaultLengthForType("duckdb", "DECIMAL")).toBe("10,0");
|
||||
expect(getDefaultLengthForType("duckdb", "VARCHAR")).toBe("255");
|
||||
expect(getDefaultLengthForType("mysql", "INTEGER")).toBe("11");
|
||||
expect(getDefaultLengthForType("mysql", "FLOAT")).toBe("10,2");
|
||||
expect(combineDataTypeForDatabase("mysql", "INTEGER", "11")).toBe("INTEGER(11)");
|
||||
expect(combineDataTypeForDatabase("mysql", "FLOAT", "10,2")).toBe("FLOAT(10,2)");
|
||||
expect(combineDataTypeForDatabase("postgres", "INTEGER", "11")).toBe("INTEGER");
|
||||
expect(getDefaultLengthForType("postgres", "INTEGER")).toBe("11");
|
||||
expect(getDefaultLengthForType("sqlite", "INTEGER")).toBe("");
|
||||
expect(combineDataTypeForDatabase("sqlite", "INTEGER", "11")).toBe("INTEGER(11)");
|
||||
expect(isDataTypeLengthDisabled("duckdb", "custom_type")).toBe(false);
|
||||
expect(combineDataTypeForDatabase("duckdb", "custom_type", "10")).toBe("custom_type(10)");
|
||||
expect(combineDataTypeForDatabase("duckdb", "DECIMAL", "39,0")).toBe("DECIMAL(39,0)");
|
||||
expect(combineDataTypeForDatabase("duckdb", "VARCHAR", "-1")).toBe("VARCHAR(-1)");
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps quoted mixed-case identifiers distinct when detecting copied-column duplicates", () => {
|
||||
const postgresNames = new Set([tableStructureIdentifierComparisonKey("Foo", "postgres")]);
|
||||
expect(postgresNames.has(tableStructureIdentifierComparisonKey("foo", "postgres"))).toBe(false);
|
||||
|
||||
@@ -630,6 +630,68 @@ export const SQLSERVER_TYPE_LENGTHS: Record<string, string> = {
|
||||
|
||||
export const DEFAULT_TYPE_LENGTH_DISABLES: string[] = [];
|
||||
|
||||
const DUCKDB_TYPE_LENGTH_DISABLES = new Set([
|
||||
"tinyint",
|
||||
"int1",
|
||||
"smallint",
|
||||
"int2",
|
||||
"short",
|
||||
"int16",
|
||||
"integer",
|
||||
"int",
|
||||
"int4",
|
||||
"signed",
|
||||
"integral",
|
||||
"int32",
|
||||
"bigint",
|
||||
"int8",
|
||||
"long",
|
||||
"oid",
|
||||
"int64",
|
||||
"hugeint",
|
||||
"int128",
|
||||
"utinyint",
|
||||
"uint8",
|
||||
"usmallint",
|
||||
"uint16",
|
||||
"uinteger",
|
||||
"uint32",
|
||||
"ubigint",
|
||||
"uint64",
|
||||
"uhugeint",
|
||||
"uint128",
|
||||
"real",
|
||||
"float4",
|
||||
"double",
|
||||
"double precision",
|
||||
"float8",
|
||||
"boolean",
|
||||
"bool",
|
||||
"logical",
|
||||
"blob",
|
||||
"bytea",
|
||||
"binary",
|
||||
"varbinary",
|
||||
"bit",
|
||||
"bitstring",
|
||||
"varint",
|
||||
"bignum",
|
||||
"date",
|
||||
"time",
|
||||
"time without time zone",
|
||||
"time with time zone",
|
||||
"timetz",
|
||||
"timestamptz",
|
||||
"timestamp with time zone",
|
||||
"timestamp_s",
|
||||
"timestamp_ms",
|
||||
"timestamp_ns",
|
||||
"uuid",
|
||||
"guid",
|
||||
"json",
|
||||
"interval",
|
||||
]);
|
||||
|
||||
export const POSTGRES_TYPE_LENGTH_DISABLES: string[] = [
|
||||
"bigint",
|
||||
"int8",
|
||||
@@ -895,6 +957,10 @@ 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 {
|
||||
if (databaseType === "duckdb") {
|
||||
const normalized = normalizeDuckdbDataType(column.data_type);
|
||||
if (normalized !== column.data_type) return normalized;
|
||||
}
|
||||
const parsed = splitDataTypeForDatabase(databaseType, column.data_type);
|
||||
if (parsed.params) return column.data_type;
|
||||
|
||||
@@ -1194,6 +1260,10 @@ export function splitDataType(raw: string): { baseType: string; params: string }
|
||||
}
|
||||
|
||||
function splitDataTypeForDatabase(dbType: DatabaseType | undefined, raw: string): { baseType: string; params: string } {
|
||||
if (dbType === "duckdb") {
|
||||
const parsed = splitDuckdbScalarDataType(raw);
|
||||
if (parsed) return parsed;
|
||||
}
|
||||
if (dbType === "xugu") {
|
||||
const match = raw.trim().match(/^(TIME|TIMESTAMP)\s*\(([^()]*)\)\s+WITH\s+TIME\s+ZONE$/i);
|
||||
if (match) {
|
||||
@@ -1315,23 +1385,49 @@ export function combineDataType(baseType: string, params: string): string {
|
||||
}
|
||||
|
||||
export function combineDataTypeForDatabase(dbType: DatabaseType | undefined, baseType: string, params: string): string {
|
||||
if (dbType === "duckdb") baseType = normalizeDuckdbDataType(baseType);
|
||||
if (isDataTypeLengthDisabled(dbType, baseType)) {
|
||||
return baseType;
|
||||
}
|
||||
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;
|
||||
const qualifiedTemporalType = combineQualifiedTemporalType(baseType, normalizedParams, dbType);
|
||||
if (qualifiedTemporalType) return qualifiedTemporalType;
|
||||
return combineDataType(baseType, normalizedParams);
|
||||
}
|
||||
|
||||
export function dataTypeLengthInputValue(dbType: DatabaseType | undefined, rawDataType: string): string {
|
||||
const parsed = splitDataTypeForDatabase(dbType, rawDataType);
|
||||
if (dbType === "duckdb") {
|
||||
return isDataTypeLengthDisabled(dbType, parsed.baseType) ? "" : normalizeDataTypeParams(dbType, parsed.baseType, parsed.params);
|
||||
}
|
||||
return isDataTypeLengthDisabled(dbType, parsed.baseType) ? "" : splitDataTypeLengthParams(dbType, rawDataType).length;
|
||||
}
|
||||
|
||||
function combineXuguTemporalType(baseType: string, params: string, dbType: DatabaseType | undefined): string | null {
|
||||
function splitDuckdbScalarDataType(raw: string): { baseType: string; params: string } | null {
|
||||
const match = raw.trim().match(/^([a-z][a-z0-9_\s]*?)\s*\(([^()]*)\)(?:\s+(WITH(?:OUT)?\s+TIME\s+ZONE))?$/i);
|
||||
if (!match) return null;
|
||||
const baseType = match[1]!.trim();
|
||||
if (match[3] && !/^(time|timestamp)$/i.test(baseType)) return null;
|
||||
return { baseType: match[3] ? `${baseType} ${match[3]}` : baseType, params: match[2]!.trim() };
|
||||
}
|
||||
|
||||
function normalizeDuckdbDataType(raw: string): string {
|
||||
const parsed = splitDuckdbScalarDataType(raw);
|
||||
if (!parsed?.params) return raw;
|
||||
if (isDataTypeLengthDisabled("duckdb", parsed.baseType)) return parsed.baseType;
|
||||
if (parsed.baseType.toLowerCase() === "float") {
|
||||
return combineDataType(parsed.baseType, normalizeDataTypeParams("duckdb", parsed.baseType, parsed.params));
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function combineQualifiedTemporalType(baseType: string, params: string, dbType: DatabaseType | undefined): string | null {
|
||||
if (dbType === "duckdb") {
|
||||
const match = baseType.trim().match(/^(TIMESTAMP)\s+WITHOUT\s+TIME\s+ZONE$/i);
|
||||
return match ? (params ? `${match[1]}(${params}) WITHOUT TIME ZONE` : baseType.trim()) : null;
|
||||
}
|
||||
if (dbType !== "xugu") return null;
|
||||
const match = baseType.trim().match(/^(TIME|TIMESTAMP)\s+WITH\s+TIME\s+ZONE$/i);
|
||||
if (!match) return null;
|
||||
@@ -1341,6 +1437,9 @@ function combineXuguTemporalType(baseType: string, params: string, dbType: Datab
|
||||
export function normalizeDataTypeParams(dbType: DatabaseType | undefined, baseType: string, params: string): string {
|
||||
const p = params.trim();
|
||||
if (!p) return "";
|
||||
if (dbType === "duckdb" && baseType.trim().toLowerCase() === "float") {
|
||||
return /^\d+$/.test(p) && Number(p) >= 1 && Number(p) <= 53 ? p : "";
|
||||
}
|
||||
if (!isTemporalPrecisionType(dbType, baseType)) return p;
|
||||
return isValidTemporalPrecision(dbType, baseType, p) ? p : "";
|
||||
}
|
||||
@@ -1416,6 +1515,7 @@ export interface DataTypeDefaultOptions {
|
||||
|
||||
export function getDefaultLengthForType(_dbType: DatabaseType | undefined, baseType: string, options: DataTypeDefaultOptions = {}): string {
|
||||
const key = baseType.trim().toLowerCase();
|
||||
if (_dbType === "duckdb" && (key === "float" || isDataTypeLengthDisabled(_dbType, baseType))) return "";
|
||||
if (_dbType === "mysql" && options.omitMysqlDeprecatedDefaults && isMysqlDeprecatedDefaultParameterType(key)) return "";
|
||||
if (_dbType === "sqlite" || _dbType === "rqlite" || _dbType === "turso") {
|
||||
return "";
|
||||
@@ -1493,7 +1593,9 @@ function isMysqlDeprecatedDefaultParameterType(baseType: string): boolean {
|
||||
|
||||
export function isDataTypeLengthDisabled(_dbType: DatabaseType | undefined, baseType: string): boolean {
|
||||
const key = baseType.trim().toLowerCase();
|
||||
if (_dbType === "questdb") {
|
||||
if (_dbType === "duckdb") {
|
||||
return DUCKDB_TYPE_LENGTH_DISABLES.has(key.replace(/\s+/g, " "));
|
||||
} else if (_dbType === "questdb") {
|
||||
return key !== "geohash" && key !== "decimal";
|
||||
} else if (_dbType === "manticoresearch") {
|
||||
return key !== "bit" && key !== "float_vector";
|
||||
|
||||
Reference in new issue
Block a user