fix(diff): default missing VARCHAR length for MySQL-family targets

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-09-05 22:15:59 +08:00
1 parent 781bb46d3a
commit eab1f6344b
3 files changed
+335 -14

No files matched your search

@@ -103,6 +103,11 @@ pub struct DdlDialectProfile {
pub auto_inc: AutoIncSyntax,
/// When false, display widths like `INT(11)` are stripped if no type_map hit.
pub supports_display_width: bool,
/// Whether this target's VARCHAR/CHARACTER VARYING/NVARCHAR syntax rejects a
/// missing length (e.g. `VARCHAR` with no length is a MySQL syntax error).
/// Distinct from `supports_display_width`: that flag is about whether the
/// target *prints* MySQL-style `INT(11)` widths, not whether VARCHAR needs one.
pub requires_explicit_varchar_length: bool,
/// Cap for length params when applying `{}` templates (e.g. Access TEXT max 255).
pub max_varchar_len: Option<u32>,
/// MySQL-style inline `COMMENT '...'` on columns.
@@ -314,6 +319,7 @@ fn mysql_family(db: DatabaseType) -> DdlDialectProfile {
quote: QuoteStyle::Backtick,
auto_inc: AutoIncSyntax::Suffix(" AUTO_INCREMENT"),
supports_display_width: true,
requires_explicit_varchar_length: true,
max_varchar_len: Some(65_535),
inline_column_comment: true,
foreign_keys_inline_in_create: false,
@@ -359,6 +365,7 @@ fn postgres_family(db: DatabaseType) -> DdlDialectProfile {
quote: QuoteStyle::DoubleQuote,
auto_inc: AutoIncSyntax::PostgresSequence,
supports_display_width: false,
requires_explicit_varchar_length: false,
max_varchar_len: None,
inline_column_comment: false,
foreign_keys_inline_in_create: false,
@@ -404,6 +411,10 @@ fn oracle_family(db: DatabaseType) -> DdlDialectProfile {
quote: QuoteStyle::UnquotedUpper,
auto_inc: AutoIncSyntax::Suffix(" GENERATED BY DEFAULT AS IDENTITY"),
supports_display_width: false,
// Oracle's VARCHAR2 also rejects a missing length in real DDL, but this
// is left false (unverified, no Oracle instance in scope for this
// change) rather than asserting untested cross-dialect behavior.
requires_explicit_varchar_length: false,
max_varchar_len: Some(4000),
inline_column_comment: false,
foreign_keys_inline_in_create: false,
@@ -450,6 +461,7 @@ fn sqlserver_family(db: DatabaseType) -> DdlDialectProfile {
quote: QuoteStyle::Brackets,
auto_inc: AutoIncSyntax::Suffix(" IDENTITY(1,1)"),
supports_display_width: false,
requires_explicit_varchar_length: false,
max_varchar_len: None,
inline_column_comment: false,
foreign_keys_inline_in_create: false,
@@ -495,6 +507,7 @@ fn sqlite_family(db: DatabaseType) -> DdlDialectProfile {
quote: QuoteStyle::DoubleQuote,
auto_inc: AutoIncSyntax::None,
supports_display_width: false,
requires_explicit_varchar_length: false,
max_varchar_len: None,
inline_column_comment: false,
foreign_keys_inline_in_create: true,
@@ -540,6 +553,7 @@ fn access_profile() -> DdlDialectProfile {
quote: QuoteStyle::DoubleQuote,
auto_inc: AutoIncSyntax::ReplaceTypeWith("COUNTER"),
supports_display_width: false,
requires_explicit_varchar_length: false,
max_varchar_len: Some(255),
inline_column_comment: false,
foreign_keys_inline_in_create: false,
@@ -585,6 +599,7 @@ fn h2_profile() -> DdlDialectProfile {
quote: QuoteStyle::DoubleQuote,
auto_inc: AutoIncSyntax::None,
supports_display_width: false,
requires_explicit_varchar_length: false,
max_varchar_len: None,
inline_column_comment: false,
foreign_keys_inline_in_create: false,
@@ -630,6 +645,12 @@ fn conservative_ansi(db: DatabaseType) -> DdlDialectProfile {
quote: QuoteStyle::DoubleQuote,
auto_inc: AutoIncSyntax::None,
supports_display_width: false,
// This fallback also covers DB2 and Informix, which reject a bare
// VARCHAR with no length — but it's shared with dialects that don't
// (Snowflake, BigQuery, DuckDB, Trino, ...), so it can't be flipped
// true here without a dedicated DB2/Informix profile and DB
// verification. Tracked as a known gap, not fixed by this flag.
requires_explicit_varchar_length: false,
max_varchar_len: None,
inline_column_comment: false,
foreign_keys_inline_in_create: false,
@@ -343,10 +343,33 @@ pub fn rewrite_column_type(source_type: &str, target: DatabaseType, source_diale
// Dialect-kind matrix
if let Some(src) = source_dialect {
let target_kind = DialectKind::from_database_type(target);
let matrix = TypeMappingMatrix::for_dialects(src, target_kind);
let (converted, _) = matrix.convert_type(source_type);
if converted != source_type {
return strip_display_width_if_needed(&converted, &profile);
if src != target_kind {
let matrix = TypeMappingMatrix::for_dialects(src, target_kind);
let (converted, _) = matrix.convert_type(source_type);
if converted != source_type {
return strip_display_width_if_needed(&converted, &profile);
}
// No matrix rule for this cross-dialect pair, and it's a
// *varying*-length string type reported without a length — e.g.
// Postgres/Kingbase's unbounded `character varying` via
// format_type(). A bare passthrough would be invalid DDL for a
// target that requires an explicit VARCHAR length (MySQL and its
// family: VARCHAR/CHARACTER VARYING with no length is a syntax
// error there). Default to 255, matching the same fallback
// apply_template() already uses when a mapped type's length is
// missing. CHAR/CHARACTER are deliberately excluded: MySQL
// accepts a bare CHAR as CHAR(1), so defaulting it to 255 would
// silently change valid output into a different, wrong length.
// VARCHAR2/NVARCHAR2 are also excluded: they aren't valid MySQL
// type names at all, with or without a length, so defaulting
// their length fixes nothing.
if params.is_none()
&& profile.requires_explicit_varchar_length
&& profile.max_varchar_len.is_some()
&& matches!(base.as_str(), "VARCHAR" | "CHARACTER VARYING" | "NVARCHAR")
{
return format!("{}(255)", source_type.trim());
}
}
}
@@ -509,6 +532,45 @@ mod tests {
assert_eq!(t, "int(11)");
}
#[test]
fn mysql_defaults_missing_length_on_cross_dialect_varchar() {
// Postgres/Kingbase report an unbounded varchar column as a bare
// `character varying` (no length) via format_type(). MySQL's
// VARCHAR/CHARACTER VARYING requires an explicit length, so passing
// that through verbatim is a syntax error (issue #8011).
let source = Some(DialectKind::Postgres);
assert_eq!(rewrite_column_type("character varying", DatabaseType::Mysql, source), "character varying(255)");
assert_eq!(rewrite_column_type("varchar", DatabaseType::Mysql, source), "varchar(255)");
}
#[test]
fn mysql_leaves_bare_char_untouched() {
// Unlike VARCHAR, a bare CHAR/CHARACTER is already valid MySQL
// syntax (implicitly CHAR(1)); defaulting it to 255 would silently
// change correct output into a different, wrong length.
let source = Some(DialectKind::Postgres);
assert_eq!(rewrite_column_type("character", DatabaseType::Mysql, source), "character");
assert_eq!(rewrite_column_type("char", DatabaseType::Mysql, source), "char");
}
#[test]
fn mysql_keeps_explicit_length_on_cross_dialect_varchar() {
let source = Some(DialectKind::Postgres);
assert_eq!(
rewrite_column_type("character varying(120)", DatabaseType::Mysql, source),
"character varying(120)"
);
}
#[test]
fn mysql_same_dialect_varchar_is_untouched_regardless_of_length() {
// Same-dialect metadata (or unknown source dialect) must never be
// rewritten — only a genuine cross-dialect gap should trigger the
// missing-length default.
assert_eq!(rewrite_column_type("varchar(255)", DatabaseType::Mysql, Some(DialectKind::Mysql)), "varchar(255)");
assert_eq!(rewrite_column_type("varchar(255)", DatabaseType::Mysql, None), "varchar(255)");
}
#[test]
fn normalize_len_params_strips_oracle_char_unit() {
assert_eq!(normalize_len_params("(6 CHAR)"), "(6)");