fix(mysql): normalize unsigned column types

This commit is contained in:
t8y2 2026-06-21 09:02:16 +08:00
parent 4ef94eb3a9
commit 9781b0f562
4 changed files with 106 additions and 3 deletions

View File

@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { combineDataTypeForDatabase, splitDataType } from "../tableStructureEditorState";
describe("tableStructureEditorState", () => {
it("keeps mysql unsigned attributes in the editable base type", () => {
expect(splitDataType("int(11) unsigned")).toEqual({ baseType: "int unsigned", params: "11" });
expect(splitDataType("bigint(20) unsigned zerofill")).toEqual({
baseType: "bigint unsigned zerofill",
params: "20",
});
});
it("combines mysql unsigned type choices with the length field", () => {
expect(combineDataTypeForDatabase("mysql", "int unsigned", "11")).toBe("int(11) unsigned");
expect(combineDataTypeForDatabase("mysql", "bigint unsigned zerofill", "20")).toBe("bigint(20) unsigned zerofill");
});
});

View File

@ -643,8 +643,14 @@ 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 baseType = trimmed.slice(0, parenIdx).trim();
const params = trimmed.slice(parenIdx + 1, trimmed.lastIndexOf(")")).trim();
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 };
}
@ -660,7 +666,10 @@ export function combineDataTypeForDatabase(dbType: DatabaseType | undefined, bas
if (isDataTypeLengthDisabled(dbType, baseType)) {
return baseType;
}
return combineDataType(baseType, normalizeDataTypeParams(dbType, baseType, params));
const normalizedParams = normalizeDataTypeParams(dbType, baseType, params);
const mysqlType = combineMysqlNumericAttributeType(dbType, baseType, normalizedParams);
if (mysqlType) return mysqlType;
return combineDataType(baseType, normalizedParams);
}
export function normalizeDataTypeParams(dbType: DatabaseType | undefined, baseType: string, params: string): string {
@ -701,6 +710,21 @@ function isTemporalPrecisionType(dbType: DatabaseType | undefined, baseType: str
}
}
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 isValidTemporalPrecision(dbType: DatabaseType | undefined, params: string): boolean {
if (!/^\d+$/.test(params)) return false;
const value = Number(params);

View File

@ -132,6 +132,12 @@ pub(super) fn normalize_column_data_type(dialect: StructureDialect, data_type: &
return trimmed.to_string();
}
if dialect == StructureDialect::Mysql {
if let Some(normalized) = normalize_mysql_numeric_attribute_type(base_type, params) {
return normalized;
}
}
if is_temporal_precision_type(dialect, base_type) {
return if is_valid_temporal_precision(params, dialect) {
format!("{base_type}({params})")
@ -143,6 +149,41 @@ pub(super) fn normalize_column_data_type(dialect: StructureDialect, data_type: &
trimmed.to_string()
}
fn normalize_mysql_numeric_attribute_type(base_type: &str, params: &str) -> Option<String> {
let mut parts: Vec<&str> = base_type.split_whitespace().collect();
let type_name = parts.first().copied()?.to_ascii_lowercase();
if !matches!(
type_name.as_str(),
"tinyint"
| "smallint"
| "mediumint"
| "int"
| "integer"
| "bigint"
| "real"
| "double"
| "float"
| "decimal"
| "numeric"
) {
return None;
}
let split_index = parts.iter().position(|part| {
let normalized = part.to_ascii_lowercase();
matches!(normalized.as_str(), "signed" | "unsigned" | "zerofill")
})?;
if !parts[split_index..].iter().all(|part| {
let normalized = part.to_ascii_lowercase();
matches!(normalized.as_str(), "signed" | "unsigned" | "zerofill")
}) {
return None;
}
let attrs = parts.split_off(split_index).join(" ");
let base = parts.join(" ");
Some(format!("{base}({params}) {attrs}"))
}
pub(super) fn is_temporal_precision_type(dialect: StructureDialect, base_type: &str) -> bool {
let normalized = base_type.split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_lowercase();
match dialect {

View File

@ -117,6 +117,27 @@ fn builds_mysql_column_and_index_changes() {
);
}
#[test]
fn builds_mysql_unsigned_integer_column_with_length_before_attribute() {
let mut score = column("score");
score.data_type = "int unsigned(11)".to_string();
let result = build_table_structure_change_sql(TableStructureSqlOptions {
database_type: Some(DatabaseType::Mysql),
schema: None,
table_name: "users".to_string(),
columns: vec![score],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
});
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(result.statements, vec!["ALTER TABLE `users` ADD COLUMN `score` int(11) unsigned;"]);
}
#[test]
fn builds_highgo_foreign_key_changes_with_postgres_syntax() {
let mut old_fk = foreign_key("orders_user_id_fkey", "user_id", "users", "id");