From ec486b29a7455e32124394ec87c5285d27450026 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 11 Jul 2026 09:10:36 +0800 Subject: [PATCH] fix(mysql): avoid foreign key metadata joins --- crates/dbx-core/src/db/mysql.rs | 69 ++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/crates/dbx-core/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs index e686bea03..9f3fc1a31 100644 --- a/crates/dbx-core/src/db/mysql.rs +++ b/crates/dbx-core/src/db/mysql.rs @@ -3717,35 +3717,58 @@ fn mysql_quoted_string_argument(input: &str, keyword: &str) -> Option { } pub async fn list_foreign_keys(pool: &MySqlPool, database: &str, table: &str) -> Result, String> { - let sql = format!( - "SELECT kcu.CONSTRAINT_NAME, kcu.COLUMN_NAME, \ - kcu.REFERENCED_TABLE_SCHEMA, kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME, \ - rc.UPDATE_RULE, rc.DELETE_RULE \ - FROM information_schema.KEY_COLUMN_USAGE kcu \ - LEFT JOIN information_schema.REFERENTIAL_CONSTRAINTS rc \ - ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA \ - AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME \ - AND rc.TABLE_NAME = kcu.TABLE_NAME \ - WHERE kcu.TABLE_SCHEMA = {} AND kcu.TABLE_NAME = {} \ - AND kcu.REFERENCED_TABLE_NAME IS NOT NULL \ - ORDER BY kcu.CONSTRAINT_NAME, kcu.ORDINAL_POSITION", + let column_sql = format!( + "SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_SCHEMA, \ + REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME \ + FROM information_schema.KEY_COLUMN_USAGE \ + WHERE TABLE_SCHEMA = {} AND TABLE_NAME = {} \ + AND REFERENCED_TABLE_NAME IS NOT NULL \ + ORDER BY CONSTRAINT_NAME, ORDINAL_POSITION", quote_value(database), quote_value(table), ); let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?; - let result = conn.query_iter(&sql).await.map_err(|e| e.to_string())?; - let rows: Vec = result.collect_and_drop().await.map_err(|e| e.to_string())?; + let column_result = conn.query_iter(&column_sql).await.map_err(|e| e.to_string())?; + let column_rows: Vec = column_result.collect_and_drop().await.map_err(|e| e.to_string())?; + if column_rows.is_empty() { + return Ok(Vec::new()); + } - Ok(rows + // MySQL 5.7 materializes information_schema tables without normal indexes. + // Avoid joining two metadata tables because the join can scan the entire catalog. + let rule_sql = format!( + "SELECT CONSTRAINT_NAME, UPDATE_RULE, DELETE_RULE \ + FROM information_schema.REFERENTIAL_CONSTRAINTS \ + WHERE CONSTRAINT_SCHEMA = {} AND TABLE_NAME = {}", + quote_value(database), + quote_value(table), + ); + let rule_result = conn.query_iter(&rule_sql).await.map_err(|e| e.to_string())?; + let rule_rows: Vec = rule_result.collect_and_drop().await.map_err(|e| e.to_string())?; + let rules = rule_rows .iter() - .map(|row| ForeignKeyInfo { - name: get_str_by_name(row, "CONSTRAINT_NAME"), - column: get_str_by_name(row, "COLUMN_NAME"), - ref_schema: Some(get_str_by_name(row, "REFERENCED_TABLE_SCHEMA")), - ref_table: get_str_by_name(row, "REFERENCED_TABLE_NAME"), - ref_column: get_str_by_name(row, "REFERENCED_COLUMN_NAME"), - on_update: Some(get_str_by_name(row, "UPDATE_RULE")).filter(|value| !value.is_empty()), - on_delete: Some(get_str_by_name(row, "DELETE_RULE")).filter(|value| !value.is_empty()), + .map(|row| { + ( + get_str_by_name(row, "CONSTRAINT_NAME"), + (get_str_by_name(row, "UPDATE_RULE"), get_str_by_name(row, "DELETE_RULE")), + ) + }) + .collect::>(); + + Ok(column_rows + .iter() + .map(|row| { + let name = get_str_by_name(row, "CONSTRAINT_NAME"); + let (on_update, on_delete) = rules.get(&name).cloned().unwrap_or_default(); + ForeignKeyInfo { + name, + column: get_str_by_name(row, "COLUMN_NAME"), + ref_schema: Some(get_str_by_name(row, "REFERENCED_TABLE_SCHEMA")), + ref_table: get_str_by_name(row, "REFERENCED_TABLE_NAME"), + ref_column: get_str_by_name(row, "REFERENCED_COLUMN_NAME"), + on_update: Some(on_update).filter(|value| !value.is_empty()), + on_delete: Some(on_delete).filter(|value| !value.is_empty()), + } }) .collect()) }