import type { ColumnInfo, DatabaseConnectionInfo, 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 function resolveColumnSelectionActiveId(columns: readonly Pick[], selectedIds: ReadonlySet, preferredId: string): string | null { if (selectedIds.has(preferredId)) return preferredId; for (let index = columns.length - 1; index >= 0; index -= 1) { const column = columns[index]; if (column && !column.markedForDrop && selectedIds.has(column.id)) return column.id; } return null; } export function isSyntheticContextMenuClick(contextMenuButton: number | null, contextMenuCtrlKey: boolean, clickButton: number): boolean { return contextMenuButton === 2 && contextMenuCtrlKey && clickButton === 0; } type TableStructureIdentifierCaseInfo = Pick; const LOWER_UNQUOTED_MIXED_QUOTED_DATABASES = new Set(["postgres", "redshift", "opengauss", "gaussdb", "highgo", "uxdb"]); const UPPER_UNQUOTED_MIXED_QUOTED_DATABASES = new Set(["oracle", "oceanbase-oracle", "dameng", "kingbase", "vastbase", "goldendb", "yashandb"]); function defaultTableStructureIdentifierCaseInfo(databaseType?: DatabaseType): Required { if (databaseType && LOWER_UNQUOTED_MIXED_QUOTED_DATABASES.has(databaseType)) { return { unquotedIdentifierCase: "lower", quotedIdentifierCase: "mixed" }; } if (databaseType && UPPER_UNQUOTED_MIXED_QUOTED_DATABASES.has(databaseType)) { return { unquotedIdentifierCase: "upper", quotedIdentifierCase: "mixed" }; } if (databaseType === "clickhouse") { return { unquotedIdentifierCase: "mixed", quotedIdentifierCase: "mixed" }; } return { unquotedIdentifierCase: "lower", quotedIdentifierCase: "lower" }; } function applyIdentifierCase(value: string, identifierCase: NonNullable): string { if (identifierCase === "lower") return value.toLowerCase(); if (identifierCase === "upper") return value.toUpperCase(); return value; } export function tableStructureIdentifierComparisonKey(name: string, databaseType?: DatabaseType, databaseInfo?: TableStructureIdentifierCaseInfo): string { const value = name.trim(); const defaults = defaultTableStructureIdentifierCaseInfo(databaseType); const unquotedIdentifierCase = databaseInfo?.unquotedIdentifierCase ?? defaults.unquotedIdentifierCase; const quotedIdentifierCase = databaseInfo?.quotedIdentifierCase ?? defaults.quotedIdentifierCase; const normalizedUnquoted = applyIdentifierCase(value, unquotedIdentifierCase); if (unquotedIdentifierCase === "mixed") return `exact:${value}`; if (quotedIdentifierCase === "mixed" && value !== normalizedUnquoted) return `quoted:${value}`; return `unquoted:${normalizedUnquoted}`; } const POSTGRES_SERIAL_PSEUDO_TYPES = new Set(["smallserial", "serial", "bigserial"]); function withPostgresArrayTypes(types: readonly string[]): string[] { return [...types, ...types.filter((type) => !POSTGRES_SERIAL_PSEUDO_TYPES.has(type)).map((type) => `${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: withPostgresArrayTypes([ "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", ], dameng: [ "number", "numeric", "decimal", "dec", "integer", "int", "bigint", "smallint", "tinyint", "byte", "float", "double", "real", "double precision", "bit", "binary", "varbinary", "raw", "char", "character", "varchar2", "varchar", "rowid", "bool", "boolean", "date", "time", "timestamp", "datetime", "time with time zone", "timestamp with time zone", "timestamp with local time zone", "interval year", "interval month", "interval year to month", "interval day", "interval hour", "interval minute", "interval second", "interval day to hour", "interval day to minute", "interval day to second", "interval hour to minute", "interval hour to second", "interval minute to second", "text", "long", "longvarchar", "image", "longvarbinary", "blob", "clob", "bfile", "json", "jsonb", ], 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: [ "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"], }; 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", vertica: "postgres", highgo: "postgres", uxdb: "postgres", vastbase: "postgres", kingbase: "postgres", firebird: "postgres", "oceanbase-oracle": "oracle", iris: "oracle", yashandb: "oracle", rqlite: "sqlite", turso: "sqlite", "cloudflare-d1": "sqlite", access: "h2", }; 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"]; 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 { 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 === "uxdb" || 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"]); 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 = splitDataTypeForDatabase(databaseType, 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 (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}`); } 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, }; }); } /** * Turns another table's metadata into columns that will be added to the table * currently being edited. Unlike createColumnDrafts(), these must not retain * original metadata: the SQL builder uses that metadata to identify existing * columns that should be altered. */ export function createCopiedColumnDrafts(columns: ColumnInfo[], databaseType: DatabaseType | undefined, createId: () => string): EditableStructureColumn[] { return createColumnDrafts(columns, databaseType).map(({ original: _original, originalPosition: _originalPosition, isPrimaryKey: _isPrimaryKey, extra, ...column }) => ({ ...column, id: `new:${createId()}`, isPrimaryKey: false, extra: copyableColumnExtra(extra), })); } /** Copy only field-local extras; keys and generated-value state are table-level concerns. */ function copyableColumnExtra(extra: ColumnExtra): ColumnExtra { const { autoIncrement: _autoIncrement, identity: _identity, ...copyableExtra } = extra; return copyableExtra; } /** Clone an editable field as a new column, without linking it to persisted metadata or key state. */ export function cloneColumnDraftAsNew(column: EditableStructureColumn, createId: () => string): EditableStructureColumn { return { id: `new:${createId()}`, name: column.name, dataType: column.dataType, enumValues: column.enumValues ? [...column.enumValues] : undefined, isNullable: column.isNullable, defaultValue: column.defaultValue, comment: column.comment, isPrimaryKey: false, characterSet: column.characterSet, collation: column.collation, extra: copyableColumnExtra(column.extra), 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]; } /** Canonicalize index method for structure editor options (e.g. Postgres `btree` → `BTREE`). */ export function normalizeStructureIndexType(indexType: string | null | undefined): string { return (indexType ?? "").trim().toUpperCase(); } /** Case-insensitive index-type equality (draft is uppercased; API may still return lowercase amname). */ export function sameStructureIndexType(left: string | null | undefined, right: string | null | undefined): boolean { return normalizeStructureIndexType(left) === normalizeStructureIndexType(right); } /** Keep selected fields removable even after they are no longer available on the table. */ export function filterStructureIndexColumnOptions(availableColumns: readonly string[], selectedColumns: readonly string[], search = ""): string[] { const availableSet = new Set(availableColumns); const unavailableSelected = selectedColumns.filter((column) => column.trim() && !availableSet.has(column)); const options = [...new Set([...unavailableSelected, ...availableColumns])]; const query = search.trim().toLowerCase(); return query ? options.filter((column) => column.toLowerCase().includes(query)) : options; } 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 ?? "", // Match Select options (BTREE/GIN/…); Postgres pg_am.amname is lowercase. indexType: normalizeStructureIndexType(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 canEditStructuredTriggerDraft(databaseType: DatabaseType | undefined, trigger: EditableStructureTrigger): boolean { return !trigger.original || (databaseType !== undefined && databaseType !== "oracle"); } 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 }; } 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"]); const CHARACTER_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" && dbType !== "oracle") return []; return CHARACTER_LENGTH_UNIT_TYPES.has(normalizedDataTypeName(rawDataType)) ? CHARACTER_LENGTH_UNITS : []; } function splitDataTypeLengthParams(dbType: DatabaseType | undefined, rawDataType: string): { length: string; unit: DataTypeLengthUnit | "" } { const { params } = splitDataTypeForDatabase(dbType, 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 restoreCharacterLengthUnitsAfterSave(dbType: DatabaseType | undefined, columns: EditableStructureColumn[], savedDataTypesByColumn: ReadonlyMap): EditableStructureColumn[] { if (savedDataTypesByColumn.size === 0) return columns; return columns.map((column) => { if (dataTypeLengthUnitValue(dbType, column.dataType)) return column; const savedDataType = savedDataTypesByColumn.get(column.name.trim().toLowerCase()); if (!savedDataType || !dataTypeLengthUnitValue(dbType, 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; 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 = 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, baseType, 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 "uxdb": 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); case "xugu": return ["time", "time with time zone", "timestamp", "timestamp with time zone"].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, baseType: string, params: string): boolean { if (!/^\d+$/.test(params)) return false; const value = Number(params); 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; } 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)); } const options = dataTypeOptions.length > 0 ? dataTypeOptions : getDataTypeOptions(dbType); const dialectKey = dbType ? (DATA_TYPE_OPTION_ALIASES[dbType] ?? dbType) : ""; if (dialectKey === "sqlite" || dialectKey === "duckdb") { const textType = options.find((type) => /^text$/i.test(type.trim())); return textType ?? "text"; } if (options.length > 0) { const preferred = (dbType === "dameng" ? options.find((type) => /^varchar2$/i.test(type.trim())) : undefined) ?? options.find((type) => /^(varchar|character varying|nvarchar)$/i.test(type.trim())) ?? options.find((type) => /^(string|clob|lvarchar|text)$/i.test(type.trim())) ?? options.find((type) => /^varchar/i.test(type.trim())); if (preferred) { return combineDataTypeForDatabase(dbType, preferred, getDefaultLengthForType(dbType, preferred)); } } 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; } /** * Compute the contiguous column-id range for a shift-click in the structure * editor, mirroring the object browser's range-select behavior * (objectBrowserSelection.ts): the range spans every row between the anchor * and the clicked row in visible order, but rows marked for drop are not * selectable and are dropped from the result. */ export function structureColumnSelectionRange(columns: readonly { id: string; markedForDrop?: boolean }[], anchorId: string, currentId: string): string[] { const anchorIndex = columns.findIndex((column) => column.id === anchorId); const currentIndex = columns.findIndex((column) => column.id === currentId); if (anchorIndex < 0 || currentIndex < 0) { return columns.some((column) => column.id === currentId && !column.markedForDrop) ? [currentId] : []; } const start = Math.min(anchorIndex, currentIndex); const end = Math.max(anchorIndex, currentIndex); return columns .slice(start, end + 1) .filter((column) => !column.markedForDrop) .map((column) => column.id); } 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 === "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); } 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(" / "); }