diff --git a/src-tauri/src/commands/schema.rs b/src-tauri/src/commands/schema.rs index 9e4fc22145..d87c44671a 100644 --- a/src-tauri/src/commands/schema.rs +++ b/src-tauri/src/commands/schema.rs @@ -48,7 +48,7 @@ fn duckdb_query_columns(con: &duckdb::Connection, table: &str) -> Result(1)?, is_nullable: row.get::<_, String>(2).unwrap_or_default() == "YES", column_default: row.get::<_, Option>(3)?, - extra: None, + extra: None, comment: None, }) }).map_err(|e| e.to_string())?; Ok(rows.filter_map(|r| r.ok()).collect()) diff --git a/src-tauri/src/db/clickhouse_driver.rs b/src-tauri/src/db/clickhouse_driver.rs index af04364e40..a3f8646388 100644 --- a/src-tauri/src/db/clickhouse_driver.rs +++ b/src-tauri/src/db/clickhouse_driver.rs @@ -111,7 +111,7 @@ pub async fn get_columns(client: &ChClient, database: &str, table: &str) -> Resu is_nullable, column_default, is_primary_key: is_pk, - extra: None, + extra: None, comment: None, } }).collect()) } diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs index 5c04ac8f34..7e6b95102a 100644 --- a/src-tauri/src/db/mod.rs +++ b/src-tauri/src/db/mod.rs @@ -29,6 +29,7 @@ pub struct ColumnInfo { pub column_default: Option, pub is_primary_key: bool, pub extra: Option, + pub comment: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/db/mysql.rs b/src-tauri/src/db/mysql.rs index bf1eeb7a00..853d1c39e8 100644 --- a/src-tauri/src/db/mysql.rs +++ b/src-tauri/src/db/mysql.rs @@ -149,7 +149,7 @@ pub async fn get_columns( table: &str, ) -> Result, String> { let rows: Vec = sqlx::query( - "SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.EXTRA, \ + "SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.EXTRA, c.COLUMN_COMMENT, \ CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK \ FROM information_schema.COLUMNS c \ LEFT JOIN information_schema.KEY_COLUMN_USAGE kcu \ @@ -175,6 +175,7 @@ pub async fn get_columns( column_default: get_opt_str(row, "COLUMN_DEFAULT"), is_primary_key: row.get::("IS_PK") == 1, extra: get_opt_str(row, "EXTRA"), + comment: get_opt_str(row, "COLUMN_COMMENT").filter(|s| !s.is_empty()), }) .collect()) } diff --git a/src-tauri/src/db/oracle_driver.rs b/src-tauri/src/db/oracle_driver.rs index c459b903df..35a5b7e4a2 100644 --- a/src-tauri/src/db/oracle_driver.rs +++ b/src-tauri/src/db/oracle_driver.rs @@ -97,7 +97,7 @@ pub async fn get_columns(conn: &OracleClient, schema: &str, table: &str) -> Resu data_type: row.get_string(1).unwrap_or("").to_string(), is_nullable: row.get_string(2).unwrap_or("N") == "Y", column_default: None, - extra: None, + extra: None, comment: None, } }).collect()) } diff --git a/src-tauri/src/db/postgres.rs b/src-tauri/src/db/postgres.rs index 2e4ccae9ec..bb346080b1 100644 --- a/src-tauri/src/db/postgres.rs +++ b/src-tauri/src/db/postgres.rs @@ -71,7 +71,8 @@ pub async fn get_columns( ) -> Result, String> { let rows: Vec = sqlx::query( "SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, \ - CASE WHEN tc.constraint_type = 'PRIMARY KEY' THEN true ELSE false END AS is_pk \ + CASE WHEN tc.constraint_type = 'PRIMARY KEY' THEN true ELSE false END AS is_pk, \ + col_description((c.table_schema || '.' || c.table_name)::regclass, c.ordinal_position) AS column_comment \ FROM information_schema.columns c \ LEFT JOIN information_schema.key_column_usage kcu \ ON c.table_schema = kcu.table_schema \ @@ -99,6 +100,7 @@ pub async fn get_columns( column_default: row.get::, _>("column_default"), is_primary_key: row.get::("is_pk"), extra: None, + comment: row.get::, _>("column_comment"), }) .collect()) } diff --git a/src-tauri/src/db/sqlite.rs b/src-tauri/src/db/sqlite.rs index 1b39890dc4..c8a0585b1e 100644 --- a/src-tauri/src/db/sqlite.rs +++ b/src-tauri/src/db/sqlite.rs @@ -52,7 +52,7 @@ pub async fn get_columns(pool: &SqlitePool, _schema: &str, table: &str) -> Resul is_nullable: row.get::("notnull") == 0, column_default: row.get::, _>("dflt_value"), is_primary_key: row.get::("pk") > 0, - extra: None, + extra: None, comment: None, }) .collect()) } diff --git a/src-tauri/src/db/sqlserver.rs b/src-tauri/src/db/sqlserver.rs index 501fc7ff1d..e22fb74f67 100644 --- a/src-tauri/src/db/sqlserver.rs +++ b/src-tauri/src/db/sqlserver.rs @@ -111,7 +111,7 @@ pub async fn get_columns(client: &mut SqlServerClient, schema: &str, table: &str is_nullable: row.get::<&str, _>(2).unwrap_or("NO") == "YES", column_default: row.get::<&str, _>(3).map(|s| s.to_string()), is_primary_key: row.get::(4).unwrap_or(0) == 1, - extra: None, + extra: None, comment: None, } }).collect()) } diff --git a/src-tauri/src/models/connection.rs b/src-tauri/src/models/connection.rs index f45a613fd5..e07bde928a 100644 --- a/src-tauri/src/models/connection.rs +++ b/src-tauri/src/models/connection.rs @@ -177,12 +177,15 @@ impl ConnectionConfig { let value = self.url_params.as_deref().unwrap_or("").trim(); match self.db_type { DatabaseType::Mysql => { + let base = "ssl-mode=preferred&charset=utf8mb4"; if value.is_empty() { - "ssl-mode=preferred".to_string() + base.to_string() } else if value.contains("ssl-mode=") { - value.trim_start_matches('?').to_string() + let v = value.trim_start_matches('?'); + if v.contains("charset=") { v.to_string() } else { format!("{v}&charset=utf8mb4") } } else { - format!("ssl-mode=preferred&{}", value.trim_start_matches('?')) + let v = value.trim_start_matches('?'); + if v.contains("charset=") { format!("ssl-mode=preferred&{v}") } else { format!("{base}&{v}") } } } DatabaseType::Postgres => value.trim_start_matches('?').to_string(), @@ -230,7 +233,7 @@ mod tests { assert_eq!( config.connection_url(), - "mysql://user%40tenant%23cluster:secret@10.1.2.3:2883?ssl-mode=preferred" + "mysql://user%40tenant%23cluster:secret@10.1.2.3:2883?ssl-mode=preferred&charset=utf8mb4" ); } @@ -240,7 +243,7 @@ mod tests { assert_eq!( config.connection_url(), - "mysql://root:p%40ss%3Aword%231@10.1.2.3:2883/db%2Fname?ssl-mode=preferred" + "mysql://root:p%40ss%3Aword%231@10.1.2.3:2883/db%2Fname?ssl-mode=preferred&charset=utf8mb4" ); } @@ -273,7 +276,7 @@ mod tests { let url = config.redacted_connection_url(); - assert_eq!(url, "mysql://10.1.2.3:2883/db%2Fname?ssl-mode=preferred"); + assert_eq!(url, "mysql://10.1.2.3:2883/db%2Fname?ssl-mode=preferred&charset=utf8mb4"); assert!(!url.contains("user")); assert!(!url.contains("p%40ss")); assert!(!url.contains("p@ss")); diff --git a/src/components/grid/DataGrid.vue b/src/components/grid/DataGrid.vue index 7cb07c2d8f..3d0f9da3f0 100644 --- a/src/components/grid/DataGrid.vue +++ b/src/components/grid/DataGrid.vue @@ -63,6 +63,16 @@ const columnTypeMap = computed(() => { return map; }); +const columnCommentMap = computed(() => { + const map = new Map(); + if (props.tableMeta?.columns) { + for (const col of props.tableMeta.columns) { + if (col.comment) map.set(col.name, col.comment); + } + } + return map; +}); + function shortTypeName(t: string): string { const s = t.toLowerCase(); if (s === "character varying") return "varchar"; @@ -778,6 +788,7 @@ function escapeAndHighlightKeywords(s: string): string { :key="col" class="shrink-0 px-3 py-1.5 border-r border-border whitespace-nowrap cursor-pointer hover:bg-accent/50 select-none relative overflow-hidden" :style="{ width: `var(--col-w-${colIdx})` }" + :title="columnCommentMap.get(col)" @click="toggleSort(col)" > diff --git a/src/components/sidebar/TreeItem.vue b/src/components/sidebar/TreeItem.vue index e66a656bf3..31300d7056 100644 --- a/src/components/sidebar/TreeItem.vue +++ b/src/components/sidebar/TreeItem.vue @@ -240,6 +240,7 @@ function disconnectConnection() { const canExpand = !leafTypes.has(props.node.type); const canPin = computed(() => pinnableTypes.has(props.node.type)); const isPinned = computed(() => props.node.pinned || connectionStore.isTreeNodePinned(props.node.id)); +const columnComment = computed(() => props.node.type === "column" && props.node.meta && "comment" in props.node.meta ? (props.node.meta as any).comment : null); const paddingLeft = `${props.depth * 16 + 8}px`; const isConnected = computed(() => props.node.type === "connection" && !!props.node.connectionId && connectionStore.connectedIds.has(props.node.connectionId) @@ -299,6 +300,7 @@ async function showMore() { {{ isGroupLabel(node) ? t(node.label) : node.label }} + {{ columnComment }}