mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 03:46:22 +00:00
feat: display column comments in sidebar and data grid header (#37)
- Add comment field to ColumnInfo (MySQL COLUMN_COMMENT, PostgreSQL col_description) - Show column comments in sidebar tree nodes - Show column comments on hover in data grid header - Default MySQL charset to utf8mb4 for proper CJK support Closes #14
This commit is contained in:
@@ -48,7 +48,7 @@ fn duckdb_query_columns(con: &duckdb::Connection, table: &str) -> Result<Vec<db:
|
||||
data_type: row.get::<_, String>(1)?,
|
||||
is_nullable: row.get::<_, String>(2).unwrap_or_default() == "YES",
|
||||
column_default: row.get::<_, Option<String>>(3)?,
|
||||
extra: None,
|
||||
extra: None, comment: None,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub struct ColumnInfo {
|
||||
pub column_default: Option<String>,
|
||||
pub is_primary_key: bool,
|
||||
pub extra: Option<String>,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -149,7 +149,7 @@ pub async fn get_columns(
|
||||
table: &str,
|
||||
) -> Result<Vec<ColumnInfo>, String> {
|
||||
let rows: Vec<MySqlRow> = 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::<i32, _>("IS_PK") == 1,
|
||||
extra: get_opt_str(row, "EXTRA"),
|
||||
comment: get_opt_str(row, "COLUMN_COMMENT").filter(|s| !s.is_empty()),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ pub async fn get_columns(
|
||||
) -> Result<Vec<ColumnInfo>, String> {
|
||||
let rows: Vec<PgRow> = 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::<Option<String>, _>("column_default"),
|
||||
is_primary_key: row.get::<bool, _>("is_pk"),
|
||||
extra: None,
|
||||
comment: row.get::<Option<String>, _>("column_comment"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ pub async fn get_columns(pool: &SqlitePool, _schema: &str, table: &str) -> Resul
|
||||
is_nullable: row.get::<i32, _>("notnull") == 0,
|
||||
column_default: row.get::<Option<String>, _>("dflt_value"),
|
||||
is_primary_key: row.get::<i32, _>("pk") > 0,
|
||||
extra: None,
|
||||
extra: None, comment: None,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -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::<i32, _>(4).unwrap_or(0) == 1,
|
||||
extra: None,
|
||||
extra: None, comment: None,
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -63,6 +63,16 @@ const columnTypeMap = computed(() => {
|
||||
return map;
|
||||
});
|
||||
|
||||
const columnCommentMap = computed(() => {
|
||||
const map = new Map<string, string>();
|
||||
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)"
|
||||
>
|
||||
<span class="flex min-w-0 items-center gap-1 overflow-hidden">
|
||||
|
||||
@@ -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() {
|
||||
<component v-else :is="getIconInfo(node)?.icon || Database" class="w-3.5 h-3.5 shrink-0" :class="getIconInfo(node)?.colorClass" />
|
||||
<span v-if="node.type === 'connection' && connectionColor" class="h-3 w-1.5 rounded-full shrink-0" :style="{ backgroundColor: connectionColor }" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ isGroupLabel(node) ? t(node.label) : node.label }}</span>
|
||||
<span v-if="columnComment" class="truncate text-muted-foreground/60 text-[10px] max-w-[40%]">{{ columnComment }}</span>
|
||||
<span v-if="node.type === 'connection' && node.connectionId && connectionStore.connectedIds.has(node.connectionId)" class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" />
|
||||
<button
|
||||
v-if="canPin"
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface ColumnInfo {
|
||||
column_default: string | null;
|
||||
is_primary_key: boolean;
|
||||
extra: string | null;
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
export interface IndexInfo {
|
||||
|
||||
Reference in New Issue
Block a user