fix: resolve PostgreSQL/MySQL/SQL Server NUMERIC/DECIMAL display issues

- Add rust_decimal feature to sqlx and tiberius for proper Decimal type handling
- Fix try_get_unchecked<String> causing NULL or garbled output for numeric types
- Add numeric_precision and numeric_scale to ColumnInfo for all database drivers
- Update frontend to display precision (e.g., numeric(20,6)) in column headers
- Fix typeColorClass to handle type names with precision suffix

Root cause: PostgreSQL binary protocol sends NUMERIC as PgNumeric binary format,
not UTF-8 text. try_get_unchecked<String> attempted to decode binary bytes as UTF-8,
causing decode failures (NULL) or garbage characters (乱码).
This commit is contained in:
kingcanfish
2026-05-02 10:14:01 +08:00
parent 76dfe9935d
commit d1075c0042
13 changed files with 62 additions and 20 deletions
+2
View File
@@ -112,6 +112,8 @@ pub async fn get_columns(client: &ChClient, database: &str, table: &str) -> Resu
column_default,
is_primary_key: is_pk,
extra: None, comment: None,
numeric_precision: None,
numeric_scale: None,
}
}).collect())
}
+2
View File
@@ -31,6 +31,8 @@ pub struct ColumnInfo {
pub is_primary_key: bool,
pub extra: Option<String>,
pub comment: Option<String>,
pub numeric_precision: Option<i32>,
pub numeric_scale: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+7 -3
View File
@@ -1,4 +1,5 @@
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use rust_decimal::Decimal;
use sqlx::mysql::{MySqlPool, MySqlPoolOptions, MySqlRow};
use sqlx::{Column, Executor, Row, TypeInfo, ValueRef};
use std::time::{Duration, Instant};
@@ -72,8 +73,8 @@ fn mysql_value_to_json(row: &MySqlRow, idx: usize, type_name: &str) -> serde_jso
if upper_type == "DECIMAL" {
return row
.try_get_unchecked::<String, _>(idx)
.map(serde_json::Value::String)
.try_get::<Decimal, _>(idx)
.map(|v: Decimal| serde_json::Value::String(v.to_string()))
.unwrap_or(serde_json::Value::Null);
}
@@ -165,7 +166,8 @@ pub async fn get_columns(
) -> 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, c.COLUMN_COMMENT, \
CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK \
CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK, \
c.NUMERIC_PRECISION, c.NUMERIC_SCALE \
FROM information_schema.COLUMNS c \
LEFT JOIN information_schema.KEY_COLUMN_USAGE kcu \
ON c.TABLE_SCHEMA = kcu.TABLE_SCHEMA \
@@ -191,6 +193,8 @@ pub async fn get_columns(
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()),
numeric_precision: row.get::<Option<i32>, _>("NUMERIC_PRECISION"),
numeric_scale: row.get::<Option<i32>, _>("NUMERIC_SCALE"),
})
.collect())
}
+3 -1
View File
@@ -81,7 +81,7 @@ pub async fn get_columns(conn: &OracleClient, schema: &str, table: &str) -> Resu
let col_result = conn.query(
&format!(
"SELECT COLUMN_NAME, DATA_TYPE, NULLABLE \
"SELECT COLUMN_NAME, DATA_TYPE, NULLABLE, DATA_PRECISION, DATA_SCALE \
FROM ALL_TAB_COLUMNS \
WHERE OWNER = '{s}' AND TABLE_NAME = '{t}' \
ORDER BY COLUMN_ID"
@@ -98,6 +98,8 @@ pub async fn get_columns(conn: &OracleClient, schema: &str, table: &str) -> Resu
is_nullable: row.get_string(2).unwrap_or("N") == "Y",
column_default: None,
extra: None, comment: None,
numeric_precision: row.get_i64(3).map(|v| v as i32),
numeric_scale: row.get_i64(4).map(|v| v as i32),
}
}).collect())
}
+7 -3
View File
@@ -1,4 +1,5 @@
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use rust_decimal::Decimal;
use sqlx::postgres::{PgPool, PgPoolOptions, PgRow};
use sqlx::{Column, Executor, Row, TypeInfo, ValueRef};
use std::time::{Duration, Instant};
@@ -48,8 +49,8 @@ fn pg_value_to_json(row: &PgRow, idx: usize, type_name: &str) -> serde_json::Val
if upper == "NUMERIC" || upper == "DECIMAL" || upper == "MONEY" {
return row
.try_get_unchecked::<String, _>(idx)
.map(serde_json::Value::String)
.try_get::<Decimal, _>(idx)
.map(|v: Decimal| serde_json::Value::String(v.to_string()))
.unwrap_or(serde_json::Value::Null);
}
@@ -145,7 +146,8 @@ pub async fn get_columns(
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, \
col_description((c.table_schema || '.' || c.table_name)::regclass, c.ordinal_position) AS column_comment \
col_description((c.table_schema || '.' || c.table_name)::regclass, c.ordinal_position) AS column_comment, \
c.numeric_precision, c.numeric_scale \
FROM information_schema.columns c \
LEFT JOIN information_schema.key_column_usage kcu \
ON c.table_schema = kcu.table_schema \
@@ -174,6 +176,8 @@ pub async fn get_columns(
is_primary_key: row.get::<bool, _>("is_pk"),
extra: None,
comment: row.get::<Option<String>, _>("column_comment"),
numeric_precision: row.get::<Option<i32>, _>("numeric_precision"),
numeric_scale: row.get::<Option<i32>, _>("numeric_scale"),
})
.collect())
}
+2
View File
@@ -53,6 +53,8 @@ pub async fn get_columns(pool: &SqlitePool, _schema: &str, table: &str) -> Resul
column_default: row.get::<Option<String>, _>("dflt_value"),
is_primary_key: row.get::<i32, _>("pk") > 0,
extra: None, comment: None,
numeric_precision: None,
numeric_scale: None,
})
.collect())
}
+7 -1
View File
@@ -1,3 +1,4 @@
use rust_decimal::Decimal;
use tiberius::{AuthMethod, Client, Config};
use tokio::net::TcpStream;
use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};
@@ -39,6 +40,8 @@ fn row_to_json(row: &tiberius::Row) -> Vec<serde_json::Value> {
(0..row.len()).map(|i| {
if let Some(v) = row.try_get::<&str, _>(i).ok().flatten() {
serde_json::Value::String(v.to_string())
} else if let Some(v) = row.try_get::<Decimal, _>(i).ok().flatten() {
serde_json::Value::String(v.to_string())
} else if let Some(v) = row.try_get::<i32, _>(i).ok().flatten() {
serde_json::Value::Number(v.into())
} else if let Some(v) = row.try_get::<i64, _>(i).ok().flatten() {
@@ -93,7 +96,8 @@ pub async fn list_tables(client: &mut SqlServerClient, schema: &str) -> Result<V
pub async fn get_columns(client: &mut SqlServerClient, schema: &str, table: &str) -> Result<Vec<ColumnInfo>, String> {
let sql = format!(
"SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, \
CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK \
CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK, \
c.NUMERIC_PRECISION, c.NUMERIC_SCALE \
FROM INFORMATION_SCHEMA.COLUMNS c \
LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu \
ON c.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND c.TABLE_NAME = kcu.TABLE_NAME AND c.COLUMN_NAME = kcu.COLUMN_NAME \
@@ -112,6 +116,8 @@ pub async fn get_columns(client: &mut SqlServerClient, schema: &str, table: &str
column_default: row.get::<&str, _>(3).map(|s| s.to_string()),
is_primary_key: row.get::<i32, _>(4).unwrap_or(0) == 1,
extra: None, comment: None,
numeric_precision: row.get::<i32, _>(5),
numeric_scale: row.get::<i32, _>(6),
}
}).collect())
}