diff --git a/src-tauri/src/db/mysql.rs b/src-tauri/src/db/mysql.rs index 2dff2c73e5..8691046632 100644 --- a/src-tauri/src/db/mysql.rs +++ b/src-tauri/src/db/mysql.rs @@ -1,5 +1,5 @@ use sqlx::mysql::{MySqlPool, MySqlPoolOptions, MySqlRow}; -use sqlx::{Column, Executor, Row}; +use sqlx::{Column, Executor, Row, TypeInfo, ValueRef}; use std::time::{Duration, Instant}; use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; @@ -17,8 +17,64 @@ fn get_str_by_name(row: &MySqlRow, name: &str) -> String { } fn get_opt_str(row: &MySqlRow, name: &str) -> Option { - row.try_get::, _>(name).ok().flatten() - .or_else(|| row.try_get::>, _>(name).ok().flatten().map(|b| String::from_utf8_lossy(&b).to_string())) + row.try_get::, _>(name) + .ok() + .flatten() + .or_else(|| { + row.try_get::>, _>(name) + .ok() + .flatten() + .map(|b| String::from_utf8_lossy(&b).to_string()) + }) +} + +fn mysql_value_to_json(row: &MySqlRow, idx: usize, type_name: &str) -> serde_json::Value { + if row.try_get_raw(idx).map(|v| v.is_null()).unwrap_or(true) { + return serde_json::Value::Null; + } + + let upper_type = type_name.to_uppercase(); + + if upper_type == "BOOLEAN" { + return row + .try_get::(idx) + .map(serde_json::Value::Bool) + .unwrap_or(serde_json::Value::Null); + } + + if upper_type.contains("BIGINT") { + return row + .try_get::(idx) + .map(|v| serde_json::Value::String(v.to_string())) + .or_else(|_| { + row.try_get::(idx) + .map(|v| serde_json::Value::String(v.to_string())) + }) + .unwrap_or(serde_json::Value::Null); + } + + if upper_type == "DECIMAL" { + return row + .try_get_unchecked::(idx) + .map(serde_json::Value::String) + .unwrap_or(serde_json::Value::Null); + } + + row.try_get::(idx) + .map(serde_json::Value::String) + .or_else(|_| row.try_get::(idx).map(|v| serde_json::Value::Number(v.into()))) + .or_else(|_| row.try_get::(idx).map(|v| serde_json::Value::Number(v.into()))) + .or_else(|_| row.try_get::(idx).map(|v| { + serde_json::Number::from_f64(v) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null) + })) + .or_else(|_| row.try_get::(idx).map(serde_json::Value::Bool)) + .or_else(|_| { + row.try_get::, _>(idx) + .map(|b| serde_json::Value::String(String::from_utf8_lossy(&b).to_string())) + }) + .unwrap_or(serde_json::Value::Null) } pub async fn connect(url: &str) -> Result { @@ -101,6 +157,11 @@ pub async fn execute_query(pool: &MySqlPool, sql: &str) -> Result = desc.columns().iter().map(|c| c.name().to_string()).collect(); + let column_types: Vec = desc + .columns() + .iter() + .map(|c| c.type_info().name().to_string()) + .collect(); let rows: Vec = sqlx::query(sql) .fetch_all(pool) @@ -111,18 +172,7 @@ pub async fn execute_query(pool: &MySqlPool, sql: &str) -> Result(i) - .map(serde_json::Value::String) - .or_else(|_| row.try_get::(i).map(|v| serde_json::Value::Number(v.into()))) - .or_else(|_| row.try_get::(i).map(|v| { - serde_json::Number::from_f64(v) - .map(serde_json::Value::Number) - .unwrap_or(serde_json::Value::Null) - })) - .or_else(|_| row.try_get::(i).map(serde_json::Value::Bool)) - .unwrap_or(serde_json::Value::Null) - }) + .map(|i| mysql_value_to_json(row, i, column_types.get(i).map(String::as_str).unwrap_or(""))) .collect() }) .collect(); diff --git a/src/App.vue b/src/App.vue index 3eb31ab0f3..d36e1a7f08 100644 --- a/src/App.vue +++ b/src/App.vue @@ -634,6 +634,7 @@ async function setupFileDrop() { :result="activeTab.result" :sql="activeTab.sql" :editable="!!activeTab.tableMeta?.primaryKeys?.length" + :database-type="activeConnection?.db_type" :connection-id="activeTab.connectionId" :database="activeTab.database" :table-meta="activeTab.tableMeta" diff --git a/src/components/grid/DataGrid.vue b/src/components/grid/DataGrid.vue index 0b30e096d5..a9237e351e 100644 --- a/src/components/grid/DataGrid.vue +++ b/src/components/grid/DataGrid.vue @@ -17,7 +17,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import type { QueryResult, ColumnInfo } from "@/types/database"; +import type { QueryResult, ColumnInfo, DatabaseType } from "@/types/database"; import { save as savePath } from "@tauri-apps/plugin-dialog"; import { writeTextFile } from "@tauri-apps/plugin-fs"; import * as api from "@/lib/tauri"; @@ -31,6 +31,7 @@ const props = defineProps<{ result: QueryResult; sql?: string; editable?: boolean; + databaseType?: DatabaseType; connectionId?: string; database?: string; tableMeta?: { @@ -361,10 +362,20 @@ function escapeVal(v: CellValue): string { return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`; } +function quoteIdent(name: string): string { + if (props.databaseType === "mysql") { + return `\`${name.replace(/`/g, "``")}\``; + } + return `"${name.replace(/"/g, '""')}"`; +} + function qualifiedTableName(): string { if (!props.tableMeta) return ""; const { schema, tableName } = props.tableMeta; - return schema ? `"${schema}"."${tableName}"` : `"${tableName}"`; + if ((props.databaseType === "postgres" || props.databaseType === "oracle") && schema) { + return `${quoteIdent(schema)}.${quoteIdent(tableName)}`; + } + return quoteIdent(tableName); } function generateSaveStatements(): string[] { @@ -378,10 +389,10 @@ function generateSaveStatements(): string[] { const row = sortedRows.value[rowIdx]; if (!row) continue; const sets = Array.from(changes.entries()) - .map(([colIdx, val]) => `"${cols[colIdx]}" = ${escapeVal(val)}`) + .map(([colIdx, val]) => `${quoteIdent(cols[colIdx])} = ${escapeVal(val)}`) .join(", "); const where = primaryKeys - .map((pk) => `"${pk}" = ${escapeVal(row[cols.indexOf(pk)])}`) + .map((pk) => `${quoteIdent(pk)} = ${escapeVal(row[cols.indexOf(pk)])}`) .join(" AND "); stmts.push(`UPDATE ${tbl} SET ${sets} WHERE ${where};`); } @@ -390,13 +401,13 @@ function generateSaveStatements(): string[] { const row = sortedRows.value[rowIdx]; if (!row) continue; const where = primaryKeys - .map((pk) => `"${pk}" = ${escapeVal(row[cols.indexOf(pk)])}`) + .map((pk) => `${quoteIdent(pk)} = ${escapeVal(row[cols.indexOf(pk)])}`) .join(" AND "); stmts.push(`DELETE FROM ${tbl} WHERE ${where};`); } for (const newRow of newRows.value) { - const colNames = cols.map((c) => `"${c}"`).join(", "); + const colNames = cols.map((c) => quoteIdent(c)).join(", "); const vals = newRow.map((v) => escapeVal(v)).join(", "); stmts.push(`INSERT INTO ${tbl} (${colNames}) VALUES (${vals});`); } @@ -737,7 +748,7 @@ function escapeAndHighlightKeywords(s: string): string {
{{ saveError }} - +
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 6e2fd72654..23bb9f2746 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -67,6 +67,7 @@ export default { rowsPerPage: "Rows per page", save: "Save", discard: "Discard", + dismiss: "Dismiss", addRow: "Add Row", deleteRow: "Delete Row", rowsPerPageShort: " rows", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index ce01abdd4b..98680b11f0 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -67,6 +67,7 @@ export default { rowsPerPage: "每页行数", save: "保存", discard: "放弃", + dismiss: "关闭", addRow: "新增行", deleteRow: "删除行", rowsPerPageShort: " 行/页",