fix(sidebar): paginate table list by page size

This commit is contained in:
zipg authored and GitHub committed 2026-06-25 23:54:09 +08:00
1 parent 8ac0038ad7
commit 0f312084ff
5 files changed
+346 -38

No files matched your search

+160 -6
View File
@@ -1045,10 +1045,18 @@ fn database_infos_from_names(
}
pub async fn list_tables(pool: &MySqlPool, database: &str) -> Result<Vec<TableInfo>, String> {
let sql = format!(
"SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = {} ORDER BY TABLE_NAME",
quote_value(database),
);
list_tables_filtered(pool, database, None, None, None, None).await
}
pub async fn list_tables_filtered(
pool: &MySqlPool,
database: &str,
filter: Option<&str>,
limit: Option<usize>,
offset: Option<usize>,
object_types: Option<&[String]>,
) -> Result<Vec<TableInfo>, String> {
let sql = list_tables_sql(database, filter, limit, offset, object_types);
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
let result = match conn.query_iter(&sql).await {
Ok(result) => result,
@@ -1056,7 +1064,9 @@ pub async fn list_tables(pool: &MySqlPool, database: &str) -> Result<Vec<TableIn
log::debug!(
"Falling back to SHOW TABLES for database `{database}` after information_schema.TABLES failed: {err}"
);
return list_tables_show(pool, database).await;
return list_tables_show(pool, database)
.await
.map(|tables| filter_list_tables_fallback(tables, filter, limit, offset, object_types));
}
};
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|e| e.to_string())?;
@@ -1077,7 +1087,7 @@ pub async fn list_tables(pool: &MySqlPool, database: &str) -> Result<Vec<TableIn
})
.collect();
if tables.is_empty() {
if tables.is_empty() && should_fallback_empty_list_tables(filter, limit, offset, object_types) {
log::debug!("Falling back to SHOW TABLES for database `{database}` after information_schema.TABLES returned no named tables");
return list_tables_show(pool, database).await;
}
@@ -1085,6 +1095,86 @@ pub async fn list_tables(pool: &MySqlPool, database: &str) -> Result<Vec<TableIn
Ok(tables)
}
fn should_fallback_empty_list_tables(
filter: Option<&str>,
limit: Option<usize>,
offset: Option<usize>,
object_types: Option<&[String]>,
) -> bool {
let has_filter = filter.is_some_and(|filter| !filter.trim().is_empty());
let has_object_types = object_types.is_some_and(|object_types| !object_types.is_empty());
!has_filter && limit.is_none() && offset.unwrap_or(0) == 0 && !has_object_types
}
fn filter_list_tables_fallback(
tables: Vec<TableInfo>,
filter: Option<&str>,
limit: Option<usize>,
offset: Option<usize>,
object_types: Option<&[String]>,
) -> Vec<TableInfo> {
let filter = filter.unwrap_or("").trim().to_ascii_lowercase();
let normalized_object_types: Vec<String> = object_types
.unwrap_or(&[])
.iter()
.map(|object_type| object_type.to_ascii_uppercase().replace(' ', "_"))
.collect();
let wants_table =
normalized_object_types.is_empty() || normalized_object_types.iter().any(|object_type| object_type == "TABLE");
let wants_view =
normalized_object_types.is_empty() || normalized_object_types.iter().any(|object_type| object_type == "VIEW");
tables
.into_iter()
.filter(|table| filter.is_empty() || table.name.to_ascii_lowercase().contains(&filter))
.filter(|table| if table.table_type.eq_ignore_ascii_case("VIEW") { wants_view } else { wants_table })
.skip(offset.unwrap_or(0))
.take(limit.unwrap_or(usize::MAX))
.collect()
}
fn list_tables_sql(
database: &str,
filter: Option<&str>,
limit: Option<usize>,
offset: Option<usize>,
object_types: Option<&[String]>,
) -> String {
let mut sql = format!(
"SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = {}",
quote_value(database),
);
if let Some(object_types) = object_types.filter(|object_types| !object_types.is_empty()) {
let wants_table = object_types
.iter()
.map(|object_type| object_type.to_ascii_uppercase().replace(' ', "_"))
.any(|object_type| object_type == "TABLE");
let wants_view = object_types
.iter()
.map(|object_type| object_type.to_ascii_uppercase().replace(' ', "_"))
.any(|object_type| object_type == "VIEW");
match (wants_table, wants_view) {
(true, false) => sql.push_str(" AND TABLE_TYPE <> 'VIEW'"),
(false, true) => sql.push_str(" AND TABLE_TYPE = 'VIEW'"),
(false, false) => sql.push_str(" AND 1 = 0"),
(true, true) => {}
}
}
if let Some(filter) = filter.map(str::trim).filter(|filter| !filter.is_empty()) {
let escaped = filter.to_ascii_lowercase().replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_");
let pattern = format!("%{}%", escaped);
sql.push_str(&format!(" AND LOWER(TABLE_NAME) LIKE {} ESCAPE '\\\\'", quote_value(&pattern)));
}
sql.push_str(" ORDER BY TABLE_NAME");
if let Some(limit) = limit {
sql.push_str(&format!(" LIMIT {}", limit));
}
if let Some(offset) = offset.filter(|offset| *offset > 0) {
sql.push_str(&format!(" OFFSET {}", offset));
}
sql
}
pub async fn completion_assistant_search(
pool: &MySqlPool,
request: &CompletionAssistantRequest,
@@ -2483,6 +2573,70 @@ mod tests {
assert!(sql.contains("UPDATE_TIME"));
}
#[test]
fn mysql_list_tables_sql_applies_filter_limit_and_offset() {
let sql = list_tables_sql("app", Some("user_%"), Some(101), Some(200), None);
assert!(sql.contains("FROM information_schema.TABLES"));
assert!(sql.contains("TABLE_SCHEMA = 'app'"));
assert!(sql.contains("LOWER(TABLE_NAME) LIKE '%user\\\\_\\\\%%' ESCAPE '\\\\'"));
assert!(sql.contains("ORDER BY TABLE_NAME"));
assert!(sql.contains("LIMIT 101"));
assert!(sql.contains("OFFSET 200"));
}
#[test]
fn mysql_list_tables_sql_filters_table_type_before_pagination() {
let tables = vec!["TABLE".to_string()];
let table_sql = list_tables_sql("app", None, Some(1000), None, Some(&tables));
assert!(table_sql.contains("TABLE_TYPE <> 'VIEW'"));
assert!(table_sql.find("TABLE_TYPE <> 'VIEW'") < table_sql.find("ORDER BY TABLE_NAME"));
assert!(table_sql.find("ORDER BY TABLE_NAME") < table_sql.find("LIMIT 1000"));
let views = vec!["VIEW".to_string()];
let view_sql = list_tables_sql("app", None, Some(1000), None, Some(&views));
assert!(view_sql.contains("TABLE_TYPE = 'VIEW'"));
}
#[test]
fn mysql_empty_list_tables_fallback_only_for_unfiltered_query() {
assert!(should_fallback_empty_list_tables(None, None, None, None));
assert!(!should_fallback_empty_list_tables(Some("missing"), None, None, None));
assert!(!should_fallback_empty_list_tables(None, Some(1000), None, None));
assert!(!should_fallback_empty_list_tables(None, None, Some(1000), None));
assert!(!should_fallback_empty_list_tables(None, None, None, Some(&["VIEW".to_string()])));
}
#[test]
fn mysql_show_tables_fallback_applies_filter_type_limit_and_offset() {
let rows = vec![
TableInfo {
name: "audit_2024".to_string(),
table_type: "BASE TABLE".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
TableInfo {
name: "audit_view".to_string(),
table_type: "VIEW".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
TableInfo {
name: "audit_2025".to_string(),
table_type: "BASE TABLE".to_string(),
comment: None,
parent_schema: None,
parent_name: None,
},
];
let filtered = filter_list_tables_fallback(rows, Some("audit"), Some(1), Some(1), Some(&["TABLE".to_string()]));
assert_eq!(filtered.iter().map(|table| table.name.as_str()).collect::<Vec<_>>(), vec!["audit_2025"]);
}
#[test]
fn mysql_table_comment_sql_targets_single_table() {
let sql = table_comment_sql("app", "users");
+14 -5
View File
@@ -1011,12 +1011,21 @@ async fn list_tables_once(
.map(|tables| filter_table_infos(tables, filter, limit, offset, object_types))
}
PoolKind::Mysql(p, mode) => {
let tables = if *mode == MysqlMode::OceanBaseOracle {
db::ob_oracle::list_tables(p, schema).await
if *mode == MysqlMode::OceanBaseOracle {
let tables = db::ob_oracle::list_tables(p, schema).await?;
Ok(filter_table_infos(tables, filter, limit, offset, object_types))
} else {
db::mysql::list_tables(p, mysql_table_metadata_catalog(database, schema)).await
}?;
Ok(filter_table_infos(tables, filter, limit, offset, object_types))
db::mysql::list_tables_filtered(
p,
mysql_table_metadata_catalog(database, schema),
filter,
limit,
offset,
object_types,
)
.await
.map(|tables| filter_table_infos(tables, None, None, None, object_types))
}
}
PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_questdb_config) => {
db::questdb::list_tables(p, schema)