fix(mysql): prefer text protocol for result sets

This commit is contained in:
t8y2 2026-06-15 12:07:47 +08:00
parent 6f480ca047
commit aab30d7c44
4 changed files with 49 additions and 5 deletions

View File

@ -725,7 +725,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
@sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string) => emit('sort', column, columnIndex, direction, whereInput)"
>
<template v-if="activeTab.result?.columns.includes('Error')" #error-actions="{ errorMessage }">
<Button variant="outline" size="sm" class="mt-2 h-7 gap-1.5 border-destructive/30 bg-background px-2.5 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive" @click="emit('fixWithAi', String(errorMessage))">
<Button variant="outline" size="sm" class="h-7 gap-1.5 px-2.5 text-xs" @click="emit('fixWithAi', String(errorMessage))">
<Bot class="h-3.5 w-3.5" />
{{ t("ai.fixWithAi") }}
</Button>

View File

@ -42,13 +42,13 @@ async function copy() {
</div>
<!-- centered: 居中占满 -->
<div v-else class="flex-1 flex flex-col items-center justify-center gap-2 px-6 text-center text-destructive">
<div v-else class="flex-1 flex flex-col items-center justify-center gap-2 px-6 text-center">
<TriangleAlert class="h-8 w-8 text-destructive/50" aria-hidden="true" />
<div class="space-y-1 select-text" @mousedown.stop @click.stop>
<div class="space-y-1 select-text text-destructive" @mousedown.stop @click.stop>
<div class="text-sm font-medium">{{ displayTitle }}</div>
<div class="text-xs max-w-lg break-all cursor-text text-destructive/80 select-text">{{ message }}</div>
</div>
<div class="flex flex-wrap items-center justify-center gap-2">
<div class="flex flex-wrap items-center justify-center gap-2 text-foreground">
<Button variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click.stop="copy">
<Copy class="h-3.5 w-3.5" />
{{ t("grid.copy") }}

View File

@ -724,6 +724,8 @@ fn mysql_error_should_retry_with_text_protocol(error: &str) -> bool {
(lower.contains("1105") && lower.contains("hy000"))
|| (lower.contains("1615") && lower.contains("re-prepared"))
|| lower.contains("com_stmt_prepare")
|| lower.contains("can't parse")
|| lower.contains("buf doesn't have enough data")
|| lower.contains("prepared statement protocol")
|| lower.contains("this command is not supported in the prepared statement protocol yet")
}
@ -1556,7 +1558,7 @@ pub async fn execute_query_on_conn_with_max_rows(
let row_limit = query_result_row_limit(max_rows);
if is_result_set_query(sql, dialect) {
if bare || requires_text_protocol_query(sql, dialect) {
if bare || prefers_text_protocol_query(sql, dialect) {
execute_result_set_with_text_protocol_on_conn(conn, sql, row_limit, start).await
} else {
match execute_result_set_with_prepared_protocol_on_conn(conn, sql, row_limit, start).await {
@ -1595,6 +1597,12 @@ pub async fn execute_query_on_conn_with_max_rows(
}
}
fn prefers_text_protocol_query(sql: &str, dialect: MySqlQueryDialect) -> bool {
// User-entered result-set queries are not parameterized in DBX. Text protocol
// avoids binary result decoding bugs in MySQL-compatible servers and proxies.
is_result_set_query(sql, dialect) || requires_text_protocol_query(sql, dialect)
}
fn is_result_set_query(sql: &str, dialect: MySqlQueryDialect) -> bool {
starts_with_executable_sql_keyword(sql, &["SELECT", "SHOW", "DESCRIBE", "EXPLAIN", "WITH"])
|| dialect.supports_admin_show_results && is_admin_show_query(sql)
@ -2018,6 +2026,23 @@ mod tests {
assert!(!requires_text_protocol_query("SELECT * FROM users", MySqlQueryDialect::default()));
}
#[test]
fn mysql_user_result_sets_prefer_text_protocol() {
let dialect = MySqlQueryDialect::default();
assert!(prefers_text_protocol_query("SELECT * FROM users", dialect));
assert!(prefers_text_protocol_query("WITH recent AS (SELECT 1 AS id) SELECT id FROM recent", dialect));
assert!(prefers_text_protocol_query("SHOW TABLES", dialect));
assert!(!prefers_text_protocol_query("UPDATE users SET name = 'Ada' WHERE id = 1", dialect));
}
#[test]
fn mysql_binary_decode_parse_errors_retry_with_text_protocol() {
assert!(mysql_error_should_retry_with_text_protocol(
"Input/output error: can't parse: buf doesn't have enough data"
));
}
#[test]
fn mysql_timestamp_default_null_ddl_enables_explicit_defaults() {
let create_sql = r#"

View File

@ -0,0 +1,19 @@
#[tokio::test]
#[ignore = "requires the remote DBX MySQL 5.7 smoke-test container"]
async fn live_mysql57_text_protocol_select_succeeds() {
let url = std::env::var("DBX_LIVE_MYSQL57_URL").expect("DBX_LIVE_MYSQL57_URL");
let pool = dbx_core::db::mysql::connect(&url, std::time::Duration::from_secs(5)).await.unwrap();
let result = dbx_core::db::mysql::execute_query_with_max_rows(
&pool,
"SELECT 1 AS id, CAST('mysql57' AS CHAR) AS label",
false,
Some(10),
Default::default(),
)
.await
.unwrap();
assert_eq!(result.columns, vec!["id", "label"]);
assert_eq!(result.rows, vec![vec![serde_json::json!("1"), serde_json::json!("mysql57")]]);
}