diff --git a/src-tauri/src/database/mysql.rs b/src-tauri/src/database/mysql.rs index 12a9199..9d1019c 100644 --- a/src-tauri/src/database/mysql.rs +++ b/src-tauri/src/database/mysql.rs @@ -127,6 +127,43 @@ impl ConnectionPool for MySQLPool { } } +/// Try to read a column value as String, falling back to UTF-8 decoded bytes +/// if the direct String conversion fails. This handles cases where mysql_async's +/// `FromValue` fails on certain INFORMATION_SCHEMA virtual table columns +/// due to charset/collation differences between the virtual table encoding +/// and the connection's character set. +fn get_str(row: &Row, idx: usize) -> String { + row.get_opt::(idx) + .and_then(|r| r.ok()) + .or_else(|| { + row.get_opt::, _>(idx) + .and_then(|r| r.ok()) + .map(|b| String::from_utf8_lossy(&b).to_string()) + }) + .unwrap_or_default() +} + +fn get_str_by_name(row: &Row, name: &str) -> String { + row.get_opt::(name) + .and_then(|r| r.ok()) + .or_else(|| { + row.get_opt::, _>(name) + .and_then(|r| r.ok()) + .map(|b| String::from_utf8_lossy(&b).to_string()) + }) + .unwrap_or_default() +} + +fn get_opt_str(row: &Row, idx: usize) -> Option { + row.get_opt::(idx) + .and_then(|r| r.ok()) + .or_else(|| { + row.get_opt::, _>(idx) + .and_then(|r| r.ok()) + .map(|b| String::from_utf8_lossy(&b).to_string()) + }) +} + /// MySQL database adapter. pub struct MySQLAdapter { pub(crate) config: ConnectionConfig, @@ -672,21 +709,18 @@ impl DatabaseAdapter for MySQLAdapter { let columns = rows .into_iter() .map(|row| { - let name: String = row.get_opt(0).and_then(|r| r.ok()).unwrap_or_default(); - let data_type: String = row.get_opt(1).and_then(|r| r.ok()).unwrap_or_default(); - let is_nullable: String = row - .get_opt(2) - .and_then(|r| r.ok()) - .unwrap_or_else(|| "YES".to_string()); - let default_value: Option = row.get_opt(3).and_then(|r| r.ok()).flatten(); + let name: String = get_str(&row, 0); + let data_type: String = get_str(&row, 1); + let is_nullable: String = get_str(&row, 2); + let default_value: Option = get_opt_str(&row, 3); let max_length: Option = row.get_opt(4).and_then(|r| r.ok()).flatten(); let precision: Option = row.get_opt(5).and_then(|r| r.ok()).flatten(); let scale: Option = row.get_opt(6).and_then(|r| r.ok()).flatten(); - let column_key: String = row.get_opt(7).and_then(|r| r.ok()).unwrap_or_default(); - let extra: String = row.get_opt(8).and_then(|r| r.ok()).unwrap_or_default(); - let description: Option = row.get_opt(9).and_then(|r| r.ok()).flatten(); + let column_key: String = get_str_by_name(&row, "COLUMN_KEY"); + let extra: String = get_str(&row, 8); + let description: Option = get_opt_str(&row, 9); - let is_primary_key = column_key.to_uppercase().contains("PRI"); + let is_primary_key = column_key.eq_ignore_ascii_case("PRI"); let is_auto_increment = extra.to_uppercase().contains("AUTO_INCREMENT"); ColumnInfo { diff --git a/src/components/database-browser/DataTableView.vue b/src/components/database-browser/DataTableView.vue index 9b2ac80..6dd8ee2 100644 --- a/src/components/database-browser/DataTableView.vue +++ b/src/components/database-browser/DataTableView.vue @@ -183,12 +183,16 @@ const pkColumns = computed(() => ) function extractPkValues(row: Record): Record { - return Object.fromEntries(pkColumns.value.map((col: string) => [col, row[col] ?? null])) + const cols = pkColumns.value.length > 0 ? pkColumns.value : (data.value?.columns ?? []) + return Object.fromEntries(cols.map((col: string) => [col, row[col] ?? null])) } function formatPkSummary(row: Record): string { - return pkColumns.value.length > 0 - ? pkColumns.value.map((col: string) => `${col}: ${formatTableValue(row[col])}`).join(', ') + const cols = pkColumns.value.length > 0 + ? pkColumns.value + : (data.value?.columns ?? []).slice(0, 3) + return cols.length > 0 + ? cols.map((col: string) => `${col}: ${formatTableValue(row[col])}`).join(', ') : Object.entries(row).slice(0, 2).map(([k, v]) => `${k}: ${formatTableValue(v)}`).join(', ') } @@ -323,15 +327,15 @@ async function fetchColumnInfo() { return } try { - columnInfoList.value = await invoke('list_columns', { + const result = await invoke('list_columns', { connectionId: props.connectionId, database: props.database, schema: props.schema ?? null, tableName: props.tableName, }) + columnInfoList.value = result } - catch (err) { - console.error('Failed to fetch column info:', err) + catch { columnInfoList.value = [] } } @@ -512,9 +516,8 @@ async function exportCSV() { function openDeleteDialog(row: Record) { if (pkColumns.value.length === 0) { toast.warning(t('components.dataTableView.notifications.noPrimaryKey'), { - description: t('components.dataTableView.notifications.noPrimaryKeyDesc'), + description: t('components.dataTableView.notifications.fallbackAllColumns'), }) - return } deletingRow.value = row deleteDialogOpen.value = true @@ -603,9 +606,8 @@ function rawValueToString(v: unknown): string { function openEditDialog(row: Record) { if (pkColumns.value.length === 0) { toast.warning(t('components.dataTableView.notifications.noPrimaryKey'), { - description: t('components.dataTableView.notifications.noPrimaryKeyDesc'), + description: t('components.dataTableView.notifications.fallbackAllColumns'), }) - return } editingRow.value = row editErrors.value = {} @@ -946,7 +948,7 @@ watch( @@ -860,8 +1089,8 @@ function closeResultPanel() { @@ -1068,5 +1297,39 @@ function closeResultPanel() { + + + + + + + + + + + +