From 1991e7bfd876791f69c1fe2e2dc10aa4641b829c Mon Sep 17 00:00:00 2001 From: h7x4 Date: Mon, 15 Dec 2025 11:43:59 +0900 Subject: [PATCH] Show more data on `show-db` --- src/core/protocol/commands/list_databases.rs | 30 ++++++-- src/server/sql/database_operations.rs | 78 ++++++++++++++++++-- 2 files changed, 96 insertions(+), 12 deletions(-) diff --git a/src/core/protocol/commands/list_databases.rs b/src/core/protocol/commands/list_databases.rs index 9487008..9e1ca7b 100644 --- a/src/core/protocol/commands/list_databases.rs +++ b/src/core/protocol/commands/list_databases.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; -use prettytable::{Cell, Row, Table}; +use itertools::Itertools; +use prettytable::Table; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -40,10 +41,25 @@ pub fn print_list_databases_output_status(output: &ListDatabasesResponse) { println!("No databases to show."); } else { let mut table = Table::new(); - table.add_row(Row::new(vec![Cell::new("Database")])); + table.add_row(row![ + "Database", + "Tables", + "Users", + "Collation", + "Character Set", + "Size (Bytes)" + ]); for db in final_database_list { - table.add_row(row![db.database]); + table.add_row(row![ + db.database, + db.tables.join("\n"), + db.users.iter().map(|user| user.as_str()).join("\n"), + db.collation.as_deref().unwrap_or("N/A"), + db.character_set.as_deref().unwrap_or("N/A"), + db.size_bytes, + ]); } + table.printstd(); } } @@ -52,11 +68,15 @@ pub fn print_list_databases_output_status_json(output: &ListDatabasesResponse) { let value = output .iter() .map(|(name, result)| match result { - Ok(_row) => ( + Ok(row) => ( name.to_string(), json!({ "status": "success", - // NOTE: there will likely be more data to include here in the future + "tables": row.tables, + "users": row.users, + "collation": row.collation, + "character_set": row.character_set, + "size_bytes": row.size_bytes, }), ), Err(err) => ( diff --git a/src/server/sql/database_operations.rs b/src/server/sql/database_operations.rs index d2c1f84..8a1ba39 100644 --- a/src/server/sql/database_operations.rs +++ b/src/server/sql/database_operations.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::core::protocol::CompleteDatabaseNameResponse; use crate::core::types::MySQLDatabase; +use crate::core::types::MySQLUser; use crate::{ core::{ common::UnixUser, @@ -207,12 +208,42 @@ pub async fn drop_databases( #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DatabaseRow { pub database: MySQLDatabase, + pub tables: Vec, + pub users: Vec, + pub collation: Option, + pub character_set: Option, + pub size_bytes: u64, } impl FromRow<'_, sqlx::mysql::MySqlRow> for DatabaseRow { fn from_row(row: &sqlx::mysql::MySqlRow) -> Result { Ok(DatabaseRow { database: row.try_get::("database")?.into(), + tables: { + let s: Option = row.try_get("tables")?; + s.and_then(|s| { + if s.is_empty() { + None + } else { + Some(s.split(',').map(|s| s.to_owned()).collect()) + } + }) + .unwrap_or_default() + }, + users: { + let s: Option = row.try_get("users")?; + s.and_then(|s| { + if s.is_empty() { + None + } else { + Some(s.split(',').map(|s| s.to_owned().into()).collect()) + } + }) + .unwrap_or_default() + }, + collation: row.try_get::, _>("collation")?, + character_set: row.try_get::, _>("character_set")?, + size_bytes: row.try_get::("size_bytes")?, }) } } @@ -244,10 +275,25 @@ pub async fn list_databases( let result = sqlx::query_as::<_, DatabaseRow>( r#" - SELECT CAST(`SCHEMA_NAME` AS CHAR(64)) AS `database` - FROM `information_schema`.`SCHEMATA` - WHERE `SCHEMA_NAME` = ? - "#, + SELECT + CAST(`information_schema`.`SCHEMATA`.`SCHEMA_NAME` AS CHAR(64)) AS `database`, + GROUP_CONCAT(DISTINCT CAST(`information_schema`.`TABLES`.`TABLE_NAME` AS CHAR(64)) SEPARATOR ',') AS `tables`, + GROUP_CONCAT(DISTINCT CAST(`mysql`.`db`.`User` AS CHAR(64)) SEPARATOR ',') AS `users`, + MAX(`information_schema`.`SCHEMATA`.`DEFAULT_COLLATION_NAME`) AS `collation`, + MAX(`information_schema`.`SCHEMATA`.`DEFAULT_CHARACTER_SET_NAME`) AS `character_set`, + CAST(IFNULL( + SUM(`information_schema`.`TABLES`.`DATA_LENGTH` + `information_schema`.`TABLES`.`INDEX_LENGTH`), + 0 + ) AS UNSIGNED INTEGER) AS `size_bytes` + FROM `information_schema`.`SCHEMATA` + LEFT OUTER JOIN `information_schema`.`TABLES` + ON `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = `TABLES`.`TABLE_SCHEMA` + LEFT OUTER JOIN `mysql`.`db` + ON `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = `mysql`.`db`.`DB` + WHERE `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = ? + GROUP BY `information_schema`.`SCHEMATA`.`SCHEMA_NAME` + "#, + ) .bind(database_name.to_string()) .fetch_optional(&mut *connection) @@ -263,6 +309,8 @@ pub async fn list_databases( tracing::error!("Failed to list database '{}': {:?}", &database_name, err); } + // TODO: should we assert that the users are also owned by the unix_user from the request? + results.insert(database_name, result); } @@ -276,10 +324,24 @@ pub async fn list_all_databases_for_user( ) -> ListAllDatabasesResponse { let result = sqlx::query_as::<_, DatabaseRow>( r#" - SELECT CAST(`SCHEMA_NAME` AS CHAR(64)) AS `database` + SELECT + CAST(`information_schema`.`SCHEMATA`.`SCHEMA_NAME` AS CHAR(64)) AS `database`, + GROUP_CONCAT(DISTINCT CAST(`information_schema`.`TABLES`.`TABLE_NAME` AS CHAR(64)) SEPARATOR ',') AS `tables`, + GROUP_CONCAT(DISTINCT CAST(`mysql`.`db`.`User` AS CHAR(64)) SEPARATOR ',') AS `users`, + MAX(`information_schema`.`SCHEMATA`.`DEFAULT_COLLATION_NAME`) AS `collation`, + MAX(`information_schema`.`SCHEMATA`.`DEFAULT_CHARACTER_SET_NAME`) AS `character_set`, + CAST(IFNULL( + SUM(`information_schema`.`TABLES`.`DATA_LENGTH` + `information_schema`.`TABLES`.`INDEX_LENGTH`), + 0 + ) AS UNSIGNED INTEGER) AS `size_bytes` FROM `information_schema`.`SCHEMATA` - WHERE `SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys') - AND `SCHEMA_NAME` REGEXP ? + LEFT OUTER JOIN `information_schema`.`TABLES` + ON `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = `TABLES`.`TABLE_SCHEMA` + LEFT OUTER JOIN `mysql`.`db` + ON `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = `mysql`.`db`.`DB` + WHERE `information_schema`.`SCHEMATA`.`SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys') + AND `information_schema`.`SCHEMATA`.`SCHEMA_NAME` REGEXP ? + GROUP BY `information_schema`.`SCHEMATA`.`SCHEMA_NAME` "#, ) .bind(create_user_group_matching_regex(unix_user)) @@ -287,6 +349,8 @@ pub async fn list_all_databases_for_user( .await .map_err(|err| ListAllDatabasesError::MySqlError(err.to_string())); + // TODO: should we assert that the users are also owned by the unix_user from the request? + if let Err(err) = &result { tracing::error!( "Failed to list databases for user '{}': {:?}",