mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-01 16:28:46 +00:00
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:
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user