import type { ColumnInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo } from "@/types/database.ts"; import type { ColumnExtra, EditableStructureColumn, EditableStructureForeignKey, EditableStructureIndex, EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql.ts"; export function hasExistingColumnTypeChange(columns: readonly EditableStructureColumn[]): boolean { return columns.some((column) => !!column.original && !column.markedForDrop && column.dataType !== column.original.data_type); } export const DATA_TYPE_OPTIONS: Record = { mysql: [ "tinyint", "tinyint unsigned", "smallint", "smallint unsigned", "mediumint", "mediumint unsigned", "int", "int unsigned", "integer", "integer unsigned", "bigint", "bigint unsigned", "float", "double", "double precision", "real", "decimal", "numeric", "bit", "boolean", "bool", "serial", "char", "varchar", "tinytext", "text", "mediumtext", "longtext", "binary", "varbinary", "tinyblob", "blob", "mediumblob", "longblob", "enum", "set", "date", "datetime", "timestamp", "time", "year", "json", "geometry", "point", "linestring", "polygon", "multipoint", "multilinestring", "multipolygon", "geometrycollection", ], postgres: [ "smallint", "int2", "integer", "int", "int4", "bigint", "int8", "smallserial", "serial", "bigserial", "decimal", "numeric", "real", "float", "float4", "double precision", "float8", "money", "boolean", "bool", "char", "character", "varchar", "character varying", "text", "bytea", "date", "time", "time without time zone", "time with time zone", "timetz", "timestamp", "timestamp without time zone", "timestamp with time zone", "timestamptz", "interval", "uuid", "json", "jsonb", "xml", "bit", "bit varying", "varbit", "tsvector", "tsquery", "cidr", "inet", "macaddr", "macaddr8", "point", "line", "lseg", "box", "path", "polygon", "circle", "int4range", "int8range", "numrange", "tsrange", "tstzrange", "daterange", "oid", ], sqlite: ["integer", "real", "text", "blob", "numeric"], rqlite: ["integer", "real", "text", "blob", "numeric"], turso: ["integer", "real", "text", "blob", "numeric"], sqlserver: [ "bit", "tinyint", "smallint", "int", "integer", "bigint", "decimal", "numeric", "float", "real", "money", "smallmoney", "char", "nchar", "varchar", "nvarchar", "text", "ntext", "date", "time", "datetime", "datetime2", "smalldatetime", "datetimeoffset", "timestamp", "binary", "varbinary", "image", "uniqueidentifier", "xml", "sql_variant", "hierarchyid", "geography", "geometry", ], oracle: [ "number", "integer", "float", "binary_float", "binary_double", "char", "nchar", "varchar2", "nvarchar2", "clob", "nclob", "long", "date", "timestamp", "timestamp with time zone", "timestamp with local time zone", "interval year to month", "interval day to second", "raw", "long raw", "blob", "bfile", "boolean", "json", "vector", "rowid", "urowid", "xmltype", "sdo_geometry", ], clickhouse: [ "Int8", "Int16", "Int32", "Int64", "Int128", "Int256", "UInt8", "UInt16", "UInt32", "UInt64", "UInt128", "UInt256", "Float16", "Float32", "Float64", "Decimal", "Decimal32", "Decimal64", "Decimal128", "Decimal256", "Bool", "String", "FixedString", "Date", "Date32", "DateTime", "DateTime64", "UUID", "IPv4", "IPv6", "Enum8", "Enum16", "Array", "Map", "Tuple", "Nested", "Nullable", "LowCardinality", "SimpleAggregateFunction", "AggregateFunction", "Point", "Ring", "Polygon", "MultiPolygon", "JSON", ], manticoresearch: ["text", "string", "int", "bit", "bigint", "bool", "timestamp", "float", "json", "float_vector", "multi", "mva"], informix: [ "smallint", "integer", "int", "bigint", "int8", "serial", "serial8", "bigserial", "decimal", "numeric", "money", "smallfloat", "float", "real", "char", "varchar", "lvarchar", "nchar", "nvarchar", "text", "clob", "byte", "blob", "boolean", "date", "datetime year to second", "datetime year to fraction", "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"], }; const DATA_TYPE_OPTION_ALIASES: Partial> = { doris: "mysql", starrocks: "mysql", goldendb: "mysql", sundb: "mysql", oscar: "oracle", gbase: "mysql", gaussdb: "postgres", kwdb: "postgres", opengauss: "postgres", questdb: "questdb", redshift: "postgres", highgo: "postgres", vastbase: "postgres", kingbase: "postgres", dameng: "oracle", "oceanbase-oracle": "oracle", iris: "oracle", }; export function getDataTypeOptions(dbType: DatabaseType | undefined): string[] { const key = dbType ? (DATA_TYPE_OPTION_ALIASES[dbType] ?? dbType) : ""; return DATA_TYPE_OPTIONS[key] ?? []; } export function isMysqlEnumDataType(dbType: DatabaseType | undefined, dataType: string): boolean { return dbType === "mysql" && splitDataType(dataType).baseType.trim().toLowerCase() === "enum"; } export function mysqlEnumDataType(values: readonly string[]): string { // Match MySQL's canonical ENUM literal escaping, including values returned by SHOW CREATE TABLE. const literals = values.map((value) => `'${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`); return `enum(${literals.join(",")})`; } export interface ColumnEditorControls { length: boolean; nullable: boolean; primaryKey: boolean; defaultValue: boolean; comment: boolean; } const DEFAULT_COLUMN_EDITOR_CONTROLS: ColumnEditorControls = { length: true, nullable: true, primaryKey: true, defaultValue: true, comment: true, }; export function getColumnEditorControls(dbType: DatabaseType | undefined): ColumnEditorControls { if (dbType === "manticoresearch") { return { length: true, nullable: false, primaryKey: false, defaultValue: false, comment: false, }; } return DEFAULT_COLUMN_EDITOR_CONTROLS; } export function isProtectedManticoreIdColumn(dbType: DatabaseType | undefined, columnName: string): boolean { return dbType === "manticoresearch" && columnName.trim().toLowerCase() === "id"; } export function canEditManticoreColumnProperties(dbType: DatabaseType | undefined, hasOriginalColumn: boolean): boolean { return dbType === "manticoresearch" && !hasOriginalColumn; } export const DEFAULT_TYPE_LENGTHS: Record = { tinyint: "4", "tinyint unsigned": "4", smallint: "6", "smallint unsigned": "6", mediumint: "9", "mediumint unsigned": "9", int: "11", "int unsigned": "11", integer: "11", "integer unsigned": "11", int4: "11", bigint: "20", "bigint unsigned": "20", int8: "20", float: "10,2", real: "10,2", "double precision": "10,2", double: "10,2", decimal: "10,0", numeric: "10,0", number: "10,0", char: "1", character: "1", varchar: "255", "character varying": "255", varchar2: "255", nvarchar2: "255", nvarchar: "255", nchar: "1", varbinary: "255", binary: "1", bit: "1", year: "4", }; export const QUESTDB_TYPE_LENGTHS: Record = { geohash: "8c", decimal: "10,2", }; export const SQLSERVER_TYPE_LENGTHS: Record = { decimal: "10,0", numeric: "10,0", float: "53", char: "1", nchar: "1", varchar: "255", nvarchar: "255", binary: "1", varbinary: "255", }; export const DEFAULT_TYPE_LENGTH_DISABLES: string[] = []; export const POSTGRES_TYPE_LENGTH_DISABLES: string[] = [ "bigint", "int8", "bigserial", "serial8", "boolean", "bool", "box", "bytea", "cidr", "circle", "date", "double precision", "float", "float8", "inet", "integer", "int", "int4", "json", "jsonb", "line", "lseg", "macaddr", "macaddr8", "money", "path", "pg_lsn", "pg_snapshot", "point", "polygon", "real", "float4", "smallint", "int2", "smallserial", "serial2", "serial", "serial4", "text", "tsquery", "tsvector", "txid_snapshot", "uuid", "xml", ]; 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"]; 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 { const result: ColumnExtra = {}; if (!extra) return result; const lower = extra.toLowerCase().trim(); if (!lower) return result; if (databaseType === "mysql") { if (lower.includes("auto_increment")) { result.autoIncrement = true; } if (lower.includes("on update current_timestamp")) { result.onUpdateCurrentTimestamp = true; } } else if (databaseType === "postgres" || databaseType === "gaussdb" || databaseType === "kwdb" || databaseType === "opengauss" || databaseType === "questdb" || databaseType === "highgo" || databaseType === "vastbase" || databaseType === "kingbase") { const identityMatch = lower.match(/generated\s+(by\s+default|always)\s+as\s+identity/i); if (identityMatch) { const sequenceMatch = lower.match(/start\s+with\s*(-?\d+)\s+increment\s+by\s*(-?\d+)/i); result.identity = { generation: identityMatch[1].toUpperCase() === "BY DEFAULT" ? "BY DEFAULT" : "ALWAYS", }; if (sequenceMatch) { result.identity.seed = Number(sequenceMatch[1]); result.identity.increment = Number(sequenceMatch[2]); } } else if (databaseType === "kingbase") { // SQLServer compatibility reports IDENTITY(seed, increment) instead of PostgreSQL identity syntax. const sqlServerIdentityMatch = lower.match(/identity\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)/i); if (sqlServerIdentityMatch) { result.autoIncrement = true; result.identity = { seed: Number(sqlServerIdentityMatch[1]), increment: Number(sqlServerIdentityMatch[2]), }; } } } else if (databaseType === "sqlserver") { if (lower.includes("identity")) { result.autoIncrement = true; const identityMatch = lower.match(/identity\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)/i); if (identityMatch) { result.identity = { seed: Number(identityMatch[1]), increment: Number(identityMatch[2]), }; } } } else if (databaseType === "dameng") { if (lower.includes("identity")) { result.autoIncrement = true; const identityMatch = lower.match(/identity\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)/i); if (identityMatch) { result.identity = { seed: Number(identityMatch[1]), increment: Number(identityMatch[2]), }; } } } else if (databaseType === "manticoresearch") { const tokens = new Set(lower.split(/\s+/).filter(Boolean)); if (tokens.has("indexed")) result.manticoreIndexed = true; if (tokens.has("stored")) result.manticoreStored = true; if (tokens.has("attribute")) result.manticoreAttribute = true; if (/secondary_index\s*=\s*['"]?1['"]?/.test(lower)) result.manticoreSecondaryIndex = true; } return result; } const MANTICORE_COLUMN_PROPERTY_TOKENS = new Set(["indexed", "stored", "attribute"]); function splitManticoreDdlColumnLine(line: string): { name: string; dataType: string; extra: string } | null { const trimmed = line.trim().replace(/,$/, "").trim(); if (!trimmed || trimmed.startsWith(")") || trimmed.startsWith("(")) return null; let name = ""; let rest = ""; const quoted = trimmed.match(/^`((?:``|[^`])+)`\s+(.+)$/); if (quoted) { name = quoted[1]!.replace(/``/g, "`"); rest = quoted[2]!.trim(); } else { const plain = trimmed.match(/^([A-Za-z_][\w$]*)\s+(.+)$/); if (!plain) return null; name = plain[1]!; rest = plain[2]!.trim(); } const parts = rest.split(/\s+/).filter(Boolean); const dataType = parts.shift() ?? ""; const properties = parts.filter((part) => { const normalized = part.toLowerCase(); return MANTICORE_COLUMN_PROPERTY_TOKENS.has(normalized) || /^secondary_index\s*=/.test(normalized); }); if (!name || !dataType || properties.length === 0) return null; return { name, dataType, extra: properties.join(" ") }; } export function applyManticoreDdlColumnExtras(columns: ColumnInfo[], ddl: string): ColumnInfo[] { if (!ddl.trim()) return columns; const extrasByColumn = new Map(); for (const line of ddl.split(/\r?\n/)) { const parsed = splitManticoreDdlColumnLine(line); if (parsed) extrasByColumn.set(parsed.name.toLowerCase(), { dataType: parsed.dataType, extra: parsed.extra }); } if (extrasByColumn.size === 0) return columns; return columns.map((column) => { const ddlColumn = extrasByColumn.get(column.name.toLowerCase()); if (!ddlColumn) return column; const existingExtra = column.extra?.trim(); return { ...column, data_type: ddlColumn.dataType || column.data_type, extra: existingExtra ? `${existingExtra} ${ddlColumn.extra}` : ddlColumn.extra, }; }); } function isPostgresTextualType(dataType: string): boolean { const baseType = dataType.split("(")[0]?.trim().replace(/\s+/g, " ").toLowerCase() ?? ""; return ["char", "character", "varchar", "character varying", "text", "bpchar", "name", "json", "jsonb", "xml", "bytea", "uuid"].includes(baseType); } function stripPostgresStringDefaultCast(defaultValue: string, dataType: string): string { if (!isPostgresTextualType(dataType)) return defaultValue; const trimmed = defaultValue.trim(); const match = trimmed.match(/^('(?:''|[^'])*')::\s*((?:character\s+varying)|character|varchar|char|text|bpchar|name|jsonb?|xml|bytea|uuid)(?:\s*\(\s*\d+\s*\))?$/i); return match?.[1] ?? defaultValue; } function isWrappedByOuterParens(value: string): boolean { if (value.length < 2 || value[0] !== "(" || value[value.length - 1] !== ")") return false; let depth = 0; let inString = false; let inBracketIdentifier = false; for (let index = 0; index < value.length; index += 1) { const char = value[index]; if (inString) { if (char === "'" && value[index + 1] === "'") { index += 1; } else if (char === "'") { inString = false; } continue; } if (inBracketIdentifier) { if (char === "]") inBracketIdentifier = false; continue; } if (char === "'") { inString = true; continue; } if (char === "[") { inBracketIdentifier = true; continue; } if (char === "(") { depth += 1; } else if (char === ")") { depth -= 1; if (depth < 0) return false; if (depth === 0 && index < value.length - 1) return false; } } return depth === 0; } function stripSqlServerDefaultOuterParens(defaultValue: string): string { let value = defaultValue.trim(); while (isWrappedByOuterParens(value)) { value = value.slice(1, -1).trim(); } return value; } function columnDefaultForEditor(column: ColumnInfo, databaseType?: DatabaseType): string { if (column.column_default === null) return ""; const defaultValue = column.column_default; if (databaseType === "mysql" && defaultValue === "" && isMysqlCharacterDataType(column.data_type)) { // MySQL metadata uses an empty string for DEFAULT '', so keep it distinct from no default. return "''"; } if (databaseType === "postgres") return stripPostgresStringDefaultCast(defaultValue, column.data_type); if (databaseType === "sqlserver") return stripSqlServerDefaultOuterParens(defaultValue); return defaultValue; } 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"]); function columnDataTypeForEditor(column: ColumnInfo, databaseType?: DatabaseType): string { const parsed = splitDataType(column.data_type); if (parsed.params) return column.data_type; const baseType = parsed.baseType.trim().replace(/\s+/g, " "); const normalized = baseType.toLowerCase(); 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 (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}`); } return column.data_type; } export function createColumnDrafts(columns: ColumnInfo[], databaseType?: DatabaseType): EditableStructureColumn[] { return columns.map((column, index) => { const defaultValue = columnDefaultForEditor(column, databaseType); const enumValues = isMysqlEnumDataType(databaseType, column.data_type) ? [...(column.enum_values ?? [])] : undefined; const dataType = enumValues?.length ? mysqlEnumDataType(enumValues) : columnDataTypeForEditor(column, databaseType); return { id: `existing:${column.name}`, name: column.name, dataType, enumValues, isNullable: column.is_nullable, defaultValue, comment: column.comment ?? "", isPrimaryKey: column.is_primary_key, characterSet: column.character_set ?? "", collation: column.collation ?? "", extra: parseExtraToColumnExtra(column.extra, databaseType), original: { ...column, data_type: dataType, column_default: column.column_default === null ? null : defaultValue }, originalPosition: index, markedForDrop: false, }; }); } function existingColumnIdName(id: string): string | undefined { const prefix = "existing:"; return id.startsWith(prefix) ? id.slice(prefix.length) : undefined; } function isNewColumnDraftId(id: string): boolean { return id.startsWith("new:"); } function uniqueNames(names: Array): string[] { const seen = new Set(); const result: string[] = []; for (const name of names) { if (!name) continue; if (seen.has(name)) continue; seen.add(name); result.push(name); } return result; } function findColumnDraftByName(columns: EditableStructureColumn[], names: string[], usedIndexes: Set): number | undefined { for (const name of names) { const exactIndex = columns.findIndex((column, index) => !usedIndexes.has(index) && column.name === name); if (exactIndex >= 0) return exactIndex; } for (const name of names) { const lowerName = name.toLowerCase(); const matches = columns.map((column, index) => ({ column, index })).filter(({ column, index }) => !usedIndexes.has(index) && column.name.toLowerCase() === lowerName); if (matches.length === 1) return matches[0]!.index; } return undefined; } export function rehydrateColumnDraftsFromMetadata(draftColumns: EditableStructureColumn[], columns: ColumnInfo[], databaseType?: DatabaseType): EditableStructureColumn[] { const metadataDrafts = createColumnDrafts(columns, databaseType); if (!metadataDrafts.length) return draftColumns; if (!draftColumns.length) return metadataDrafts; const usedMetadataIndexes = new Set(); const nextColumns = draftColumns.map((column) => { if (!column.original && isNewColumnDraftId(column.id)) return column; const needsHydration = !column.original || column.originalPosition === undefined; const candidates = uniqueNames([column.original?.name, existingColumnIdName(column.id), column.name]); const metadataIndex = findColumnDraftByName(metadataDrafts, candidates, usedMetadataIndexes); if (metadataIndex === undefined) return column; usedMetadataIndexes.add(metadataIndex); const metadataDraft = metadataDrafts[metadataIndex]!; const shouldHydrateEnum = isMysqlEnumDataType(databaseType, column.dataType) && column.enumValues === undefined && metadataDraft.enumValues !== undefined; if (!needsHydration && !shouldHydrateEnum) return column; return { ...column, dataType: shouldHydrateEnum ? metadataDraft.dataType : column.dataType, enumValues: column.enumValues ?? metadataDraft.enumValues, original: shouldHydrateEnum ? metadataDraft.original : (column.original ?? metadataDraft.original), originalPosition: column.originalPosition ?? metadataDraft.originalPosition, }; }); if (usedMetadataIndexes.size === 0) { return [...metadataDrafts, ...draftColumns]; } const missingMetadataDrafts = metadataDrafts.filter((_, index) => !usedMetadataIndexes.has(index)); return [...nextColumns, ...missingMetadataDrafts]; } export function createIndexDrafts(indexes: IndexInfo[]): EditableStructureIndex[] { return indexes.map((index) => ({ id: `existing:${index.name}`, name: index.name, columns: [...index.columns], nameEdited: true, isUnique: index.is_unique, isPrimary: index.is_primary, filter: index.filter ?? "", indexType: index.index_type ?? "", includedColumns: index.included_columns ? [...index.included_columns] : [], comment: index.comment ?? "", original: index, markedForDrop: false, })); } export function createForeignKeyDrafts(foreignKeys: ForeignKeyInfo[]): EditableStructureForeignKey[] { const groups = new Map(); for (const foreignKey of foreignKeys) { const key = [foreignKey.name, foreignKey.ref_schema ?? "", foreignKey.ref_table, foreignKey.on_update ?? "", foreignKey.on_delete ?? ""].join("\u0000"); groups.set(key, [...(groups.get(key) ?? []), foreignKey]); } return [...groups.values()].map((group, index) => { const first = group[0]!; const original = { ...first, column: group.map((foreignKey) => foreignKey.column).join(", "), ref_column: group.map((foreignKey) => foreignKey.ref_column).join(", "), }; return { id: `existing:${first.name}:${index}`, name: first.name, column: original.column, refSchema: first.ref_schema ?? "", refTable: first.ref_table, refColumn: original.ref_column, onUpdate: first.on_update ?? "", onDelete: first.on_delete ?? "", original, markedForDrop: false, }; }); } export function createTriggerDrafts(triggers: TriggerInfo[]): EditableStructureTrigger[] { return triggers.map((trigger) => ({ id: `existing:${trigger.name}`, name: trigger.name, timing: trigger.timing, event: trigger.event, statement: trigger.statement ?? "", original: trigger, markedForDrop: false, })); } export function toColumnNames(columns: string[]): string { return columns.join(", "); } const AUTO_INDEX_NAME_MAX_LENGTH = 63; function normalizeIndexNamePart(value: string): string { const trimmed = value.trim(); const unquoted = (trimmed.startsWith("[") && trimmed.endsWith("]")) || (trimmed.startsWith("`") && trimmed.endsWith("`")) || (trimmed.startsWith('"') && trimmed.endsWith('"')) ? trimmed.slice(1, -1) : trimmed; return unquoted .trim() .replace(/[^a-zA-Z0-9]+/g, "_") .replace(/_+/g, "_") .replace(/^_+|_+$/g, "") .toUpperCase(); } function truncateIndexName(value: string, maxLength = AUTO_INDEX_NAME_MAX_LENGTH): string { if (value.length <= maxLength) return value; const suffix = "_IDX"; if (!value.endsWith(suffix)) return value.slice(0, maxLength).replace(/_+$/g, ""); return `${value.slice(0, maxLength - suffix.length).replace(/_+$/g, "")}${suffix}`; } export function generateIndexName(tableName: string, columns: string[], maxLength = AUTO_INDEX_NAME_MAX_LENGTH): string { const parts = [tableName, ...columns].map(normalizeIndexNamePart).filter(Boolean); if (parts.length === 0) return ""; return truncateIndexName(`${parts.join("_")}_IDX`, maxLength); } export function generateUniqueIndexName(tableName: string, columns: string[], existingNames: Iterable, maxLength = AUTO_INDEX_NAME_MAX_LENGTH): string { const base = generateIndexName(tableName, columns, maxLength); if (!base) return ""; const taken = new Set([...existingNames].map((name) => name.trim().toLowerCase()).filter(Boolean)); if (!taken.has(base.toLowerCase())) return base; for (let counter = 2; counter < 10_000; counter++) { const suffix = `_${counter}`; const stem = base.length + suffix.length <= maxLength ? base : base.slice(0, maxLength - suffix.length).replace(/_+$/g, ""); const candidate = `${stem}${suffix}`; if (!taken.has(candidate.toLowerCase())) return candidate; } return base; } export function splitDataType(raw: string): { baseType: string; params: string } { const trimmed = raw.trim(); const parenIdx = trimmed.indexOf("("); if (parenIdx === -1) return { baseType: trimmed, params: "" }; const closeIdx = trimmed.lastIndexOf(")"); const baseTypePrefix = trimmed.slice(0, parenIdx).trim(); const params = trimmed.slice(parenIdx + 1, closeIdx).trim(); const suffix = trimmed .slice(closeIdx + 1) .trim() .replace(/\s+/g, " "); const baseType = /^(?:signed|unsigned|zerofill)(?:\s+(?:signed|unsigned|zerofill))*$/i.test(suffix) ? `${baseTypePrefix} ${suffix}`.trim() : baseTypePrefix; 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): 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"]); export function isMysqlCharacterDataType(dataType: string): boolean { const { baseType } = splitDataType(dataType); return MYSQL_CHARACTER_DATA_TYPES.has(baseType.trim().replace(/\s+/g, " ").toLowerCase()); } export function isSqlServerIdentityCompatibleDataType(rawDataType: string): boolean { const { baseType, params } = splitDataType(rawDataType); const normalized = baseType.trim().replace(/\s+/g, " ").toLowerCase(); if (["tinyint", "smallint", "int", "integer", "bigint"].includes(normalized)) return true; if (normalized !== "decimal" && normalized !== "numeric") return false; const normalizedParams = params.replace(/\s+/g, ""); if (!normalizedParams) return true; const parts = normalizedParams.split(","); if (parts.length === 1) return /^\d+$/.test(parts[0] ?? ""); if (parts.length !== 2) return false; const [precision, scale] = parts; return /^\d+$/.test(precision ?? "") && scale === "0"; } export function isDamengIdentityCompatibleDataType(rawDataType: string): boolean { const { baseType, params } = splitDataType(rawDataType); const normalized = baseType.trim().replace(/\s+/g, " ").toLowerCase(); if (["tinyint", "smallint", "int", "integer", "bigint"].includes(normalized)) return true; if (!["number", "numeric", "decimal", "dec"].includes(normalized)) return false; const normalizedParams = params.replace(/\s+/g, ""); if (!normalizedParams) return true; const parts = normalizedParams.split(","); if (parts.length === 1) return /^\d+$/.test(parts[0] ?? ""); if (parts.length !== 2) return false; const [precision, scale] = parts; return /^\d+$/.test(precision ?? "") && scale === "0"; } export function combineDataType(baseType: string, params: string): string { const type = baseType.trim(); const p = params.trim(); if (!type) return ""; if (!p) return type; return `${type}(${p})`; } export function combineDataTypeForDatabase(dbType: DatabaseType | undefined, baseType: string, params: string): string { if (isDataTypeLengthDisabled(dbType, baseType)) { return baseType; } const normalizedParams = normalizeDataTypeParams(dbType, baseType, params); const mysqlType = combineMysqlNumericAttributeType(dbType, baseType, normalizedParams); if (mysqlType) return mysqlType; return combineDataType(baseType, normalizedParams); } export function dataTypeLengthInputValue(dbType: DatabaseType | undefined, rawDataType: string): string { const parsed = splitDataType(rawDataType); return isDataTypeLengthDisabled(dbType, parsed.baseType) ? "" : splitDataTypeLengthParams(dbType, rawDataType).length; } 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 : ""; } function isTemporalPrecisionType(dbType: DatabaseType | undefined, baseType: string): boolean { const normalized = baseType.trim().replace(/\s+/g, " ").toLowerCase(); switch (dbType) { case "mysql": case "doris": case "starrocks": case "goldendb": case "sundb": return ["time", "datetime", "timestamp"].includes(normalized); case "postgres": case "gaussdb": case "kwdb": case "opengauss": case "highgo": case "vastbase": case "kingbase": case "redshift": return ["time", "time without time zone", "time with time zone", "timestamp", "timestamp without time zone", "timestamp with time zone"].includes(normalized); case "sqlserver": return ["time", "datetime2", "datetimeoffset"].includes(normalized); case "oracle": case "dameng": case "oceanbase-oracle": return ["timestamp", "timestamp with time zone", "timestamp with local time zone"].includes(normalized); case "questdb": return ["timestamp"].includes(normalized); default: return false; } } function combineMysqlNumericAttributeType(dbType: DatabaseType | undefined, baseType: string, params: string): string | null { if (!params || !isMysqlLikeStructureType(dbType)) return null; const parts = baseType.trim().replace(/\s+/g, " ").split(" ").filter(Boolean); const typeName = parts[0]?.toLowerCase(); if (!typeName || !["tinyint", "smallint", "mediumint", "int", "integer", "bigint", "real", "double", "float", "decimal", "numeric"].includes(typeName)) return null; const attrIndex = parts.findIndex((part) => ["signed", "unsigned", "zerofill"].includes(part.toLowerCase())); if (attrIndex === -1) return null; if (!parts.slice(attrIndex).every((part) => ["signed", "unsigned", "zerofill"].includes(part.toLowerCase()))) return null; return `${parts.slice(0, attrIndex).join(" ")}(${params}) ${parts.slice(attrIndex).join(" ")}`; } function isMysqlLikeStructureType(dbType: DatabaseType | undefined): boolean { return dbType === "mysql" || dbType === "doris" || dbType === "starrocks" || dbType === "goldendb" || dbType === "sundb" || dbType === "databend"; } 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 { if (!/^\d+$/.test(params)) return false; const value = Number(params); const max = dbType === "oracle" || dbType === "dameng" || dbType === "oceanbase-oracle" ? 9 : 6; return Number.isInteger(value) && value >= 0 && value <= max && String(value) === params; } export interface DataTypeDefaultOptions { /** * Native MySQL profiles use MySQL 8-safe defaults. Compatibility profiles * retain their existing DDL defaults because their server/version is unknown. */ omitMysqlDeprecatedDefaults?: boolean; } export function getDefaultLengthForType(_dbType: DatabaseType | undefined, baseType: string, options: DataTypeDefaultOptions = {}): string { const key = baseType.trim().toLowerCase(); if (_dbType === "mysql" && options.omitMysqlDeprecatedDefaults && isMysqlDeprecatedDefaultParameterType(key)) return ""; if (_dbType === "sqlite" || _dbType === "rqlite" || _dbType === "turso") { return ""; } else if (_dbType === "questdb") { return QUESTDB_TYPE_LENGTHS[key] ?? ""; } else if (_dbType === "sqlserver") { return SQLSERVER_TYPE_LENGTHS[key] ?? ""; } else { return DEFAULT_TYPE_LENGTHS[key] ?? ""; } } /** Default data type for a newly added structure-editor column. */ export function defaultNewColumnDataType(dbType: DatabaseType | undefined, dataTypeOptions: readonly string[] = []): string { if (dbType === "manticoresearch") { const baseType = dataTypeOptions[0] ?? "text"; return combineDataTypeForDatabase(dbType, baseType, getDefaultLengthForType(dbType, baseType)); } return dbType === "sqlite" ? "text" : "varchar(255)"; } /** Index at which to insert a new column (after the selected row, or append when none). */ export function resolveInsertColumnIndex(columns: readonly { id: string; markedForDrop?: boolean }[], selectedColumnId: string | null | undefined): number { if (!selectedColumnId) return columns.length; // Dropped rows are not valid insertion anchors. const index = columns.findIndex((column) => column.id === selectedColumnId && !column.markedForDrop); return index >= 0 ? index + 1 : columns.length; } function isMysqlDeprecatedDefaultParameterType(baseType: string): boolean { const typeName = baseType.split(/\s+/)[0]; return ["tinyint", "smallint", "mediumint", "int", "integer", "bigint", "float", "double", "real"].includes(typeName ?? ""); } export function isDataTypeLengthDisabled(_dbType: DatabaseType | undefined, baseType: string): boolean { const key = baseType.trim().toLowerCase(); if (_dbType === "questdb") { return key !== "geohash" && key !== "decimal"; } else if (_dbType === "manticoresearch") { return key !== "bit" && key !== "float_vector"; } else if (_dbType === "postgres" || _dbType === "gaussdb" || _dbType === "kwdb" || _dbType === "opengauss" || _dbType === "highgo" || _dbType === "vastbase" || _dbType === "kingbase") { return POSTGRES_TYPE_LENGTH_DISABLES.includes(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); } else if (_dbType === "sqlserver") { // SQL Server exact integer and legacy LOB types do not accept MySQL-style display widths. return SQLSERVER_TYPE_LENGTH_DISABLES.includes(key); } else if (isMysqlLikeStructureType(_dbType)) { return key === "enum" || key === "set"; } else { return DEFAULT_TYPE_LENGTH_DISABLES.includes(key); } } export function buildStructureTargetLabel(connectionName: string | undefined, database: string | undefined, schema: string | undefined, tableName: string | undefined): string { const parts = [connectionName, database]; if (schema && schema !== database) parts.push(schema); if (tableName) parts.push(tableName); return parts.filter(Boolean).join(" / "); }