From 1bba6fa09e814bac12421e13fbbcd89c46c25e28 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 11 Aug 2026 18:20:26 +0000 Subject: [PATCH] fix(mysql): list logical ShardingSphere Proxy tables Closes #5775 --- crates/dbx-core/src/db/mysql.rs | 71 +++++++++++++++++++++----- crates/dbx-core/src/schema.rs | 88 ++++++++++++++++++++++++++++++--- 2 files changed, 140 insertions(+), 19 deletions(-) diff --git a/crates/dbx-core/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs index 948b8c0611..03a15ae7f3 100644 --- a/crates/dbx-core/src/db/mysql.rs +++ b/crates/dbx-core/src/db/mysql.rs @@ -2589,6 +2589,40 @@ async fn list_table_names_show(pool: &MySqlPool, database: &str) -> Result String { + if database.trim().is_empty() { + "SHOW FULL TABLES".to_string() + } else { + format!("SHOW FULL TABLES FROM {}", quote_identifier(database)) + } +} + +fn table_infos_from_show_rows(rows: &[mysql_async::Row]) -> Vec { + let mut tables = rows + .iter() + .filter_map(|row| { + let name = get_str(row, 0).trim().to_string(); + if name.is_empty() { + return None; + } + let table_type = normalize_mysql_table_type(&get_str(row, 1)); + Some(TableInfo { name, table_type, comment: None, parent_schema: None, parent_name: None }) + }) + .collect::>(); + tables.sort_by(|a, b| a.name.cmp(&b.name)); + tables +} + +pub async fn list_shardingsphere_tables(pool: &MySqlPool, database: &str) -> Result, String> { + // ShardingSphere's logical names are authoritative here. Do not add SHOW TABLE STATUS: + // this hot path must replace the information_schema lookup with one metadata request. + let sql = shardingsphere_show_full_tables_sql(database); + let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?; + let result = conn.query_iter(&sql).await.map_err(|error| error.to_string())?; + let rows = result.collect_and_drop::().await.map_err(|error| error.to_string())?; + Ok(table_infos_from_show_rows(&rows)) +} + async fn list_table_names_show_filtered( pool: &MySqlPool, database: &str, @@ -2618,19 +2652,7 @@ async fn list_table_names_show_filtered( } } let rows = rows.ok_or_else(|| last_error.unwrap_or_else(|| "SHOW TABLES returned no result".to_string()))?; - let mut tables: Vec = rows - .iter() - .filter_map(|row| { - let name = get_str(row, 0).trim().to_string(); - if name.is_empty() { - return None; - } - let table_type = normalize_mysql_table_type(&get_str(row, 1)); - Some(TableInfo { name, table_type, comment: None, parent_schema: None, parent_name: None }) - }) - .collect(); - tables.sort_by(|a, b| a.name.cmp(&b.name)); - Ok(tables) + Ok(table_infos_from_show_rows(&rows)) } struct ShowTablesQueryAttempt { @@ -5861,6 +5883,29 @@ mod tests { assert_eq!(normalize_mysql_table_type(""), "TABLE"); } + #[test] + fn shardingsphere_show_full_tables_is_one_exact_statement() { + assert_eq!(shardingsphere_show_full_tables_sql("app"), "SHOW FULL TABLES FROM `app`"); + assert_eq!(shardingsphere_show_full_tables_sql(""), "SHOW FULL TABLES"); + } + + #[test] + fn shardingsphere_show_rows_keep_logical_names_and_types_without_comments() { + let rows = vec![ + mysql_test_row(vec![Value::Bytes(b"normal_table".to_vec()), Value::Bytes(b"BASE TABLE".to_vec())]), + mysql_test_row(vec![Value::Bytes(b"t_order".to_vec()), Value::Bytes(b"BASE TABLE".to_vec())]), + mysql_test_row(vec![Value::Bytes(b"active_users".to_vec()), Value::Bytes(b"VIEW".to_vec())]), + ]; + + let tables = table_infos_from_show_rows(&rows); + + assert_eq!( + tables.iter().map(|table| (table.name.as_str(), table.table_type.as_str())).collect::>(), + vec![("active_users", "VIEW"), ("normal_table", "BASE TABLE"), ("t_order", "BASE TABLE")] + ); + assert!(tables.iter().all(|table| table.comment.is_none())); + } + #[test] fn mysql_filtered_show_fallback_is_server_bounded() { let tables_sql = show_tables_filtered_sql("app", true, Some("missing_%"), &[]); diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index 4e00a26c45..26c57014e4 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -2163,6 +2163,10 @@ async fn list_tables_once( if *mode == MysqlMode::OceanBaseOracle { let tables = db::ob_oracle::list_tables(p, schema).await?; Ok(filter_table_infos(tables, filter, limit, offset, object_types, table_name_filter)) + } else if mysql_table_list_source_for_config(db_config.as_ref()) == MysqlTableListSource::ShowFullTables { + db::mysql::list_shardingsphere_tables(p, mysql_table_metadata_catalog(database, schema)) + .await + .map(|tables| filter_table_infos(tables, filter, limit, offset, object_types, table_name_filter)) } else { db::mysql::list_tables_filtered( p, @@ -2605,6 +2609,29 @@ fn mysql_table_metadata_catalog<'a>(database: &'a str, schema: &'a str) -> &'a s } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum MysqlTableListSource { + InformationSchema, + ShowFullTables, +} + +fn is_shardingsphere_proxy_version(version: &str) -> bool { + const MARKER: &[u8] = b"shardingsphere-proxy"; + version.as_bytes().windows(MARKER.len()).any(|window| window.eq_ignore_ascii_case(MARKER)) +} + +fn mysql_table_list_source_for_config(config: Option<&ConnectionConfig>) -> MysqlTableListSource { + if config + .and_then(|config| config.database_info.as_ref()) + .and_then(|info| info.product_version.as_deref()) + .is_some_and(is_shardingsphere_proxy_version) + { + MysqlTableListSource::ShowFullTables + } else { + MysqlTableListSource::InformationSchema + } +} + fn quote_presto_like_identifier(identifier: &str) -> String { format!("\"{}\"", identifier.replace('"', "\"\"")) } @@ -2628,17 +2655,17 @@ mod tests { gbase8a_object_statistics_sql, is_agent_postgres_metadata_fallback_config, is_mysql_external_driver_config, is_retryable_metadata_error, metadata_error_action, metadata_name_or_comment_matches, mysql_external_driver_ddl_from_query_result, mysql_external_driver_ddl_sql, - mysql_object_source_ddl_column_index, mysql_object_source_sql, mysql_table_metadata_catalog, - normalize_information_schema_table_type, oracle_columns_from_query_result, oracle_columns_sql, - oracle_object_statistics_dba_segments_sql, oracle_object_statistics_from_query_result, + mysql_object_source_ddl_column_index, mysql_object_source_sql, mysql_table_list_source_for_config, + mysql_table_metadata_catalog, normalize_information_schema_table_type, oracle_columns_from_query_result, + oracle_columns_sql, oracle_object_statistics_dba_segments_sql, oracle_object_statistics_from_query_result, oracle_object_statistics_rows_only_sql, oracle_object_statistics_sql, oracle_object_statistics_user_segments_sql, oracle_table_comment_from_query_result, oracle_table_comment_sql, oracle_table_comments_sql, presto_like_columns_from_query_result, presto_like_information_schema_columns_sql, presto_like_information_schema_tables_sql, presto_like_tables_from_query_result, replace_metadata_runtime, should_query_oracle_columns_via_sql_first, table_comments_from_query_result, table_name_filter_matches, tdengine_table_comment_like_pattern, tdengine_table_comment_sql, tdengine_table_comments_sql, - uses_mongodb_agent_collection_listing, visible_schema_filter, MetadataErrorAction, TableNameFilter, - TDENGINE_COMMENT_SEARCH_TIMEOUT, TDENGINE_LIKE_PATTERN_MAX_BYTES, + uses_mongodb_agent_collection_listing, visible_schema_filter, MetadataErrorAction, MysqlTableListSource, + TableNameFilter, TDENGINE_COMMENT_SEARCH_TIMEOUT, TDENGINE_LIKE_PATTERN_MAX_BYTES, }; use super::{list_databases_core, list_tables_core}; use super::{ @@ -2646,7 +2673,7 @@ mod tests { object_types_only_custom_types, supports_custom_type_details, supports_pg_custom_type_objects, }; use crate::connection::{AppState, PoolKind}; - use crate::models::connection::{ConnectionConfig, DatabaseType}; + use crate::models::connection::{ConnectionConfig, DatabaseConnectionInfo, DatabaseType}; use crate::storage::Storage; use std::collections::HashMap; use std::time::Duration; @@ -3447,6 +3474,55 @@ for line in sys.stdin: assert_eq!(filtered[0].name, "audit_record"); } + #[test] + fn shardingsphere_proxy_marker_is_ascii_case_insensitive_and_exact() { + assert!(super::is_shardingsphere_proxy_version("5.7.22-ShardingSphere-Proxy 5.5.2")); + assert!(super::is_shardingsphere_proxy_version("8.0.36-SHARDINGSPHERE-PROXY 5.5.2")); + assert!(!super::is_shardingsphere_proxy_version("8.0.36-ShardingSphere Proxy 5.5.2")); + assert!(!super::is_shardingsphere_proxy_version("8.0.36-MySQL Community Server")); + } + + #[test] + fn mysql_table_list_source_uses_only_saved_shardingsphere_version() { + let mut config = test_connection_config(DatabaseType::Mysql); + assert_eq!(mysql_table_list_source_for_config(Some(&config)), MysqlTableListSource::InformationSchema); + + config.database_info = Some(DatabaseConnectionInfo { + product_version: Some("5.7.22-ShardingSphere-Proxy 5.5.2".to_string()), + ..DatabaseConnectionInfo::default() + }); + assert_eq!(mysql_table_list_source_for_config(Some(&config)), MysqlTableListSource::ShowFullTables); + + config.database_info.as_mut().unwrap().product_version = Some("8.0.36-MySQL Community Server".to_string()); + assert_eq!(mysql_table_list_source_for_config(Some(&config)), MysqlTableListSource::InformationSchema); + assert_eq!(mysql_table_list_source_for_config(None), MysqlTableListSource::InformationSchema); + } + + #[test] + fn shardingsphere_logical_tables_keep_local_constraints() { + let tables = vec![ + test_table_info("normal_table"), + test_table_info("t_order"), + test_table_info("t_order_archive"), + super::db::TableInfo { + name: "t_order_view".to_string(), + table_type: "VIEW".to_string(), + comment: None, + parent_schema: None, + parent_name: None, + }, + test_table_info("t_user"), + ]; + let table_types = vec!["TABLE".to_string()]; + let name_filter = + TableNameFilter { include_patterns: vec!["t_%".to_string()], exclude_patterns: vec!["%user%".to_string()] }; + + let filtered = + filter_table_infos(tables, Some("order"), Some(1), Some(1), Some(&table_types), Some(&name_filter)); + + assert_eq!(filtered.into_iter().map(|table| table.name).collect::>(), vec!["t_order_archive"]); + } + #[test] fn mongodb_agent_collection_listing_only_applies_to_mongodb() { let mongodb = test_connection_config(DatabaseType::MongoDb);