fix(agent): preserve JDBC decimal precision

This commit is contained in:
t8y2 2026-07-03 11:41:21 +08:00
parent 710eb3d008
commit b6274858fe
3 changed files with 79 additions and 1 deletions

View File

@ -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<BigDecimal>) (value, type, context) -> new JsonPrimitive(value.toPlainString())
)
.registerTypeAdapter(
BigInteger.class,
(JsonSerializer<BigInteger>) (value, type, context) -> new JsonPrimitive(value.toString())
)
.create();
private ConnectParams lastConnectParams;
private long lastConnectionValidationTimeMillis;

View File

@ -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;

View File

@ -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 {