diff --git a/crates/dbx-core/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs index 4e030a4f41..707f45250a 100644 --- a/crates/dbx-core/src/db/mysql.rs +++ b/crates/dbx-core/src/db/mysql.rs @@ -285,8 +285,15 @@ fn mysql_bytes_to_json(bytes: Vec, column: &mysql_async::Column) -> serde_js /// Map a MySQL column to a user-facing type name for the result-grid header. /// Returns the bare lowercase type name (no length/precision/signedness), which /// is enough for display; unknown variants fall back to a lowercased debug name. -pub(crate) fn mysql_column_type_name(ty: ColumnType) -> String { +/// +/// MySQL's wire protocol uses the same `MYSQL_TYPE_*BLOB` codes for TEXT and BLOB +/// families. Binary charset (63) means BLOB; any other charset means TEXT. Value +/// decoding already follows that rule — the header type must match, or TEXT +/// columns flash as `blob` until table metadata arrives. +pub(crate) fn mysql_column_type_name(column: &mysql_async::Column) -> String { use mysql_async::consts::ColumnType::*; + let ty = column.column_type(); + let binary = is_mysql_binary_charset(column); match ty { MYSQL_TYPE_TINY => "tinyint", MYSQL_TYPE_SHORT => "smallint", @@ -305,12 +312,48 @@ pub(crate) fn mysql_column_type_name(ty: ColumnType) -> String { MYSQL_TYPE_JSON => "json", MYSQL_TYPE_ENUM => "enum", MYSQL_TYPE_SET => "set", - MYSQL_TYPE_TINY_BLOB => "tinyblob", - MYSQL_TYPE_MEDIUM_BLOB => "mediumblob", - MYSQL_TYPE_LONG_BLOB => "longblob", - MYSQL_TYPE_BLOB => "blob", - MYSQL_TYPE_VARCHAR | MYSQL_TYPE_VAR_STRING => "varchar", - MYSQL_TYPE_STRING => "char", + MYSQL_TYPE_TINY_BLOB => { + if binary { + "tinyblob" + } else { + "tinytext" + } + } + MYSQL_TYPE_MEDIUM_BLOB => { + if binary { + "mediumblob" + } else { + "mediumtext" + } + } + MYSQL_TYPE_LONG_BLOB => { + if binary { + "longblob" + } else { + "longtext" + } + } + MYSQL_TYPE_BLOB => { + if binary { + "blob" + } else { + "text" + } + } + MYSQL_TYPE_VARCHAR | MYSQL_TYPE_VAR_STRING => { + if binary { + "varbinary" + } else { + "varchar" + } + } + MYSQL_TYPE_STRING => { + if binary { + "binary" + } else { + "char" + } + } MYSQL_TYPE_GEOMETRY => "geometry", MYSQL_TYPE_NULL => "null", other => return format!("{:?}", other).to_lowercase(), @@ -2778,8 +2821,7 @@ async fn execute_result_set_with_text_protocol_on_conn( }); } let columns: Vec = result.columns_ref().iter().map(|c| c.name_str().to_string()).collect(); - let column_types: Vec = - result.columns_ref().iter().map(|c| mysql_column_type_name(c.column_type())).collect(); + let column_types: Vec = result.columns_ref().iter().map(mysql_column_type_name).collect(); if should_collect_text_result_set(sql, row_limit, max_rows) { let rows: Vec = result.collect_and_drop().await.map_err(|e| e.to_string())?; @@ -2857,8 +2899,7 @@ async fn execute_result_set_with_prepared_protocol_on_conn( ) -> Result { let mut result = conn.exec_iter(sql, ()).await.map_err(|e| e.to_string())?; let columns: Vec = result.columns_ref().iter().map(|c| c.name_str().to_string()).collect(); - let column_types: Vec = - result.columns_ref().iter().map(|c| mysql_column_type_name(c.column_type())).collect(); + let column_types: Vec = result.columns_ref().iter().map(mysql_column_type_name).collect(); let mut result_rows: Vec> = Vec::new(); let mut stream = result @@ -2964,8 +3005,7 @@ async fn stream_query_result_text( return Ok(0); } let columns: Vec = result.columns_ref().iter().map(|c| c.name_str().to_string()).collect(); - let column_types: Vec = - result.columns_ref().iter().map(|c| mysql_column_type_name(c.column_type())).collect(); + let column_types: Vec = result.columns_ref().iter().map(mysql_column_type_name).collect(); on_item(MySqlQueryStreamItem::Columns { columns, column_types })?; let mut stream = result @@ -3003,8 +3043,7 @@ async fn stream_query_result_prepared( if columns.is_empty() { return Ok(0); } - let column_types: Vec = - result.columns_ref().iter().map(|c| mysql_column_type_name(c.column_type())).collect(); + let column_types: Vec = result.columns_ref().iter().map(mysql_column_type_name).collect(); on_item(MySqlQueryStreamItem::Columns { columns, column_types })?; let mut stream = result @@ -3858,16 +3897,66 @@ mod tests { #[test] fn mysql_column_type_names_map_to_friendly_names() { use mysql_async::consts::ColumnType::*; - assert_eq!(mysql_column_type_name(MYSQL_TYPE_TINY), "tinyint"); - assert_eq!(mysql_column_type_name(MYSQL_TYPE_LONG), "int"); - assert_eq!(mysql_column_type_name(MYSQL_TYPE_LONGLONG), "bigint"); - assert_eq!(mysql_column_type_name(MYSQL_TYPE_NEWDECIMAL), "decimal"); - assert_eq!(mysql_column_type_name(MYSQL_TYPE_VARCHAR), "varchar"); - assert_eq!(mysql_column_type_name(MYSQL_TYPE_VAR_STRING), "varchar"); - assert_eq!(mysql_column_type_name(MYSQL_TYPE_STRING), "char"); - assert_eq!(mysql_column_type_name(MYSQL_TYPE_DATETIME), "datetime"); - assert_eq!(mysql_column_type_name(MYSQL_TYPE_JSON), "json"); - assert_eq!(mysql_column_type_name(MYSQL_TYPE_BLOB), "blob"); + let utf8 = 45u16; + let binary = 63u16; + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_TINY, utf8, ColumnFlags::empty(), 4)), + "tinyint" + ); + assert_eq!(mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_LONG, utf8, ColumnFlags::empty(), 11)), "int"); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_LONGLONG, utf8, ColumnFlags::empty(), 20)), + "bigint" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_NEWDECIMAL, utf8, ColumnFlags::empty(), 10)), + "decimal" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_VARCHAR, utf8, ColumnFlags::empty(), 255)), + "varchar" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_VAR_STRING, utf8, ColumnFlags::empty(), 255)), + "varchar" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_STRING, utf8, ColumnFlags::empty(), 16)), + "char" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_DATETIME, utf8, ColumnFlags::empty(), 19)), + "datetime" + ); + assert_eq!(mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_JSON, utf8, ColumnFlags::empty(), 0)), "json"); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_BLOB, binary, ColumnFlags::BLOB_FLAG, 65_535)), + "blob" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_BLOB, utf8, ColumnFlags::empty(), 65_535)), + "text" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_TINY_BLOB, utf8, ColumnFlags::empty(), 255)), + "tinytext" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_MEDIUM_BLOB, utf8, ColumnFlags::empty(), 16_777_215)), + "mediumtext" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_LONG_BLOB, utf8, ColumnFlags::empty(), 4_294_967_295)), + "longtext" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_VAR_STRING, binary, ColumnFlags::BINARY_FLAG, 16)), + "varbinary" + ); + assert_eq!( + mysql_column_type_name(&mysql_test_column(MYSQL_TYPE_STRING, binary, ColumnFlags::BINARY_FLAG, 16)), + "binary" + ); } #[test] diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index 7b0353455f..a3b3b45492 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -3125,8 +3125,7 @@ async fn execute_manual_txn_mysql_statement( let start = std::time::Instant::now(); let mut result = conn.query_iter(sql).await.map_err(|e| format!("Query failed: {e}"))?; let columns: Vec = result.columns_ref().iter().map(|c| c.name_str().to_string()).collect(); - let column_types: Vec = - result.columns_ref().iter().map(|c| db::mysql::mysql_column_type_name(c.column_type())).collect(); + let column_types: Vec = result.columns_ref().iter().map(db::mysql::mysql_column_type_name).collect(); let mut data: Vec> = Vec::with_capacity(row_limit.min(1024)); let mut stream = result .stream::()