From b6274858fecc262d21d8edccc41ac48359d20c74 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 3 Jul 2026 11:41:21 +0800 Subject: [PATCH] fix(agent): preserve JDBC decimal precision --- .../java/com/dbx/agent/JsonRpcServer.java | 17 +++++++- .../agent/CommonJavaCompatibilityTest.java | 40 +++++++++++++++++++ crates/dbx-core/src/data_grid_sql.rs | 23 +++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java index 243863f35..d7c348a21 100644 --- a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java +++ b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java @@ -1,15 +1,20 @@ package com.dbx.agent; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializer; import com.google.gson.reflect.TypeToken; import java.io.BufferedReader; import java.io.InputStreamReader; import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; import java.sql.Connection; import java.util.Collections; import java.util.List; @@ -18,7 +23,17 @@ public final class JsonRpcServer { private static final long CONNECTION_VALIDATION_INTERVAL_MILLIS = 5_000L; private final DatabaseAgent agent; - private final Gson gson = new Gson(); + private final Gson gson = new GsonBuilder() + // JDBC DECIMAL/NUMERIC values can exceed JavaScript Number precision after JSON-RPC parsing. + .registerTypeAdapter( + BigDecimal.class, + (JsonSerializer) (value, type, context) -> new JsonPrimitive(value.toPlainString()) + ) + .registerTypeAdapter( + BigInteger.class, + (JsonSerializer) (value, type, context) -> new JsonPrimitive(value.toString()) + ) + .create(); private ConnectParams lastConnectParams; private long lastConnectionValidationTimeMillis; diff --git a/agents/common/src/test/java/com/dbx/agent/CommonJavaCompatibilityTest.java b/agents/common/src/test/java/com/dbx/agent/CommonJavaCompatibilityTest.java index 304fc9ec0..048d1c7b7 100644 --- a/agents/common/src/test/java/com/dbx/agent/CommonJavaCompatibilityTest.java +++ b/agents/common/src/test/java/com/dbx/agent/CommonJavaCompatibilityTest.java @@ -11,6 +11,8 @@ import java.io.InputStreamReader; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.math.BigDecimal; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.ArrayList; @@ -70,6 +72,28 @@ class CommonJavaCompatibilityTest { assertTrue(containsCapability(result.getAsJsonArray("capabilities"), "metadata")); } + @Test + void jsonRpcServerSerializesArbitraryPrecisionNumbersAsStrings() { + JsonRpcServer server = new JsonRpcServer(new PreciseNumberAgent()); + + String response = server.handleRequest( + "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"" + AgentProtocol.METHOD_EXECUTE_QUERY + "\",\"params\":{\"sql\":\"select n from t\"}}" + ); + + JsonArray row = JsonParser.parseString(response) + .getAsJsonObject() + .getAsJsonObject("result") + .getAsJsonArray("rows") + .get(0) + .getAsJsonArray(); + assertEquals("12345678901234567890.1234", row.get(0).getAsString()); + assertTrue(row.get(0).getAsJsonPrimitive().isString()); + assertEquals("12345678901234567890", row.get(1).getAsString()); + assertTrue(row.get(1).getAsJsonPrimitive().isString()); + assertEquals(42, row.get(2).getAsInt()); + assertTrue(row.get(2).getAsJsonPrimitive().isNumber()); + } + @Test void jsonRpcServerReconnectsWhenStoredJdbcConnectionIsStale() { ReconnectingAgent agent = new ReconnectingAgent(); @@ -370,6 +394,22 @@ class CommonJavaCompatibilityTest { } } + private static final class PreciseNumberAgent extends MinimalAgent { + @Override + public QueryResult executeQuery(String sql, String schema, ExecuteQueryOptions options) { + return new QueryResult( + Arrays.asList("decimal_value", "integer_value", "safe_int"), + Collections.singletonList(Arrays.asList( + new BigDecimal("12345678901234567890.1234"), + new BigInteger("12345678901234567890"), + 42 + )), + 0L, + 0L + ); + } + } + private static final class ReconnectingAgent extends MinimalAgent { private int connectCount; private int disconnectCount; diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index 645a7bb2d..33cd1098b 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -1029,6 +1029,10 @@ pub fn format_grid_sql_literal( return format_pg_array_sql_literal(arr); } let text = value.as_str().map_or_else(|| value.to_string(), ToString::to_string); + if column_info.map(|column| is_numeric_type(&column.data_type)).unwrap_or(false) && is_numeric_literal(&text) { + // BigDecimal/BigInteger cells cross JSON-RPC as strings so browsers cannot round them. + return text; + } if database_type == Some(DatabaseType::ManticoreSearch) { if let Some(typed_value) = manticore_typed_attribute_value(&text, column_info) { return format_grid_sql_literal(&typed_value, database_type, column_info); @@ -2312,6 +2316,25 @@ mod tests { ); } + #[test] + fn formats_numeric_string_literals_for_numeric_columns_without_quotes() { + let number = column("amount", "NUMBER(20,0)", true, None); + let text = column("code", "VARCHAR2(32)", true, None); + + assert_eq!( + format_grid_sql_literal(&json!("12345678901234567890"), Some(DatabaseType::Oracle), Some(&number)), + "12345678901234567890" + ); + assert_eq!( + format_grid_sql_literal(&json!("12345678901234567890"), Some(DatabaseType::Oracle), Some(&text)), + "'12345678901234567890'" + ); + assert_eq!( + format_grid_sql_literal(&json!("123-not-a-number"), Some(DatabaseType::Oracle), Some(&number)), + "'123-not-a-number'" + ); + } + #[test] fn prepares_oracle_timestamp_insert_from_iso_grid_value() { let result = prepare_data_grid_save(DataGridSaveStatementOptions {