fix(grid): preserve high-precision numeric edits

This commit is contained in:
t8y2 2026-07-03 16:08:20 +08:00
parent e7ec2ecaf2
commit d205f47a8f
3 changed files with 146 additions and 5 deletions

View File

@ -16,7 +16,15 @@ export function coerceDataGridCellValue(options: CoerceDataGridCellValueOptions)
if (postgresArrayValue !== undefined) return postgresArrayValue;
if (typeof oldValue === "number") {
const num = Number(value);
if (!Number.isNaN(num)) return num;
if (!Number.isNaN(num)) {
if (shouldPreserveNumericText(options, num)) {
// Keep precision-sensitive numeric edits as text; JS Number rounds 64-bit integers.
const text = value.trim();
if (text === String(oldValue)) return oldValue;
return text;
}
return num;
}
}
if (typeof oldValue === "boolean") {
return value === "true" || value === "1";
@ -56,7 +64,9 @@ function coercePostgresArrayValue(options: CoerceDataGridCellValueOptions): unkn
if (trimmed.startsWith("{")) {
try {
const parsed = parsePostgresArrayText(trimmed);
const parsed = parsePostgresArrayText(trimmed, {
numericDataType: postgresArrayElementDataType(options.columnInfo?.data_type),
});
if (Array.isArray(options.oldValue) && deepEqual(parsed, options.oldValue)) {
return options.oldValue;
}
@ -79,6 +89,57 @@ function isPostgresArrayColumn(columnInfo: Pick<ColumnInfo, "data_type"> | undef
return dataType === "array" || dataType.endsWith("[]") || dataType.startsWith("_");
}
function shouldPreserveNumericText(options: CoerceDataGridCellValueOptions, parsedNumber: number): boolean {
const text = options.value.trim();
if (!isNumericLiteralText(text)) return false;
return shouldPreserveNumericTextForType(options.columnInfo?.data_type, text, parsedNumber);
}
function postgresArrayElementDataType(dataType: string | undefined): string {
const normalized = normalizeDataType(dataType);
if (normalized.startsWith("_")) return normalized.slice(1);
if (normalized.endsWith("[]")) return normalized.slice(0, -2).trim();
return normalized;
}
function shouldPreserveNumericTextForType(dataType: string | undefined, text: string, parsedNumber: number): boolean {
const normalized = normalizeDataType(dataType);
if (isExactDecimalDataType(normalized)) return true;
if (isLargeIntegerDataType(normalized)) return !Number.isSafeInteger(parsedNumber);
return numericTextWouldLosePrecision(text, parsedNumber);
}
function normalizeDataType(dataType: string | undefined): string {
return (dataType ?? "").trim().toLowerCase();
}
function isExactDecimalDataType(dataType: string): boolean {
return /\b(?:decimal|numeric|number|dec|money|smallmoney|bigdecimal|bignumeric|big_numeric|fixed)\b/.test(dataType);
}
function isLargeIntegerDataType(dataType: string): boolean {
return /\b(?:bigint|int8|int64|uint64|u64|bigserial|serial8|int128|uint128|int256|uint256)\b/.test(dataType);
}
function numericTextWouldLosePrecision(text: string, parsedNumber: number): boolean {
if (isIntegerLiteralText(text)) return !Number.isSafeInteger(parsedNumber);
return significantDigitCount(text) > 15;
}
function isNumericLiteralText(text: string): boolean {
return /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(text);
}
function isIntegerLiteralText(text: string): boolean {
return /^[+-]?\d+$/.test(text);
}
function significantDigitCount(text: string): number {
const mantissa = text.replace(/^[+-]/, "").split(/[eE]/)[0].replace(".", "");
const withoutLeadingZeros = mantissa.replace(/^0+/, "");
return withoutLeadingZeros.length;
}
function normalizeSmartQuotedJsonInput(value: string): string {
// Check for smart double quotes that input methods might insert.
// U+201C, U+201D, U+201E, U+201F, U+FF02
@ -148,7 +209,7 @@ function needsQuotedPostgresArrayElement(value: string): boolean {
return value === "" || /[\s,"{}\\]/.test(value) || value.toUpperCase() === "NULL";
}
function parsePostgresArrayText(value: string): unknown[] {
function parsePostgresArrayText(value: string, options: { numericDataType?: string } = {}): unknown[] {
const trimmed = value.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
throw new Error("Invalid PG array literal");
@ -194,7 +255,7 @@ function parsePostgresArrayText(value: string): unknown[] {
}
i++;
}
element = parsePostgresArrayText(inner.slice(start, i));
element = parsePostgresArrayText(inner.slice(start, i), options);
} else {
let start = i;
while (i < inner.length && inner[i] !== "," && inner[i] !== "}") i++;
@ -204,7 +265,9 @@ function parsePostgresArrayText(value: string): unknown[] {
} else if (/^(true|false)$/i.test(token)) {
element = token.toLowerCase() === "true";
} else if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(token)) {
element = Number(token);
const num = Number(token);
// JS numbers cannot carry 64-bit integer or high-precision decimal array elements exactly.
element = shouldPreserveNumericTextForType(options.numericDataType, token, num) ? token : num;
} else {
element = token;
}

View File

@ -2426,6 +2426,28 @@ mod tests {
);
}
#[test]
fn prepares_sqlserver_bigint_update_from_numeric_string() {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::SqlServer),
table_meta: DataGridTableMeta {
schema: Some("dbo".to_string()),
table_name: "users".to_string(),
primary_keys: vec!["Id".to_string()],
columns: Some(vec![column("Id", "int", false, None), column("UserId", "bigint", true, None)]),
},
columns: vec!["Id".to_string(), "UserId".to_string()],
source_columns: None,
rows: vec![vec![json!(1), json!(142189065666650_i64)]],
dirty_rows: vec![(0, vec![(1, json!("144847503924137986"))])],
deleted_rows: vec![],
new_rows: vec![],
});
assert_eq!(result.validation_error, None);
assert_eq!(result.statements, vec!["UPDATE [dbo].[users] SET [UserId] = 144847503924137986 WHERE [Id] = 1;"]);
}
#[test]
fn prepares_oracle_timestamp_insert_from_iso_grid_value() {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {

View File

@ -47,6 +47,50 @@ test("coerces PG brace-style input for Postgres array columns", () => {
);
});
test("preserves high precision numeric edits as text", () => {
assert.equal(
coerceDataGridCellValue({
value: "144847503924137986",
oldValue: 142189065666650,
databaseType: "sqlserver",
columnInfo: { data_type: "bigint" },
}),
"144847503924137986",
);
assert.equal(
coerceDataGridCellValue({
value: "12345678901234567890123456789012345678",
oldValue: 1,
databaseType: "postgres",
columnInfo: { data_type: "numeric(38,0)" },
}),
"12345678901234567890123456789012345678",
);
});
test("keeps safe numeric edits on the existing number path", () => {
assert.equal(
coerceDataGridCellValue({
value: "42",
oldValue: 1,
databaseType: "sqlserver",
columnInfo: { data_type: "int" },
}),
42,
);
assert.strictEqual(
coerceDataGridCellValue({
value: "42",
oldValue: 42,
databaseType: "sqlserver",
columnInfo: { data_type: "bigint" },
}),
42,
);
});
test("coerces PG brace-style string array", () => {
assert.deepEqual(
coerceDataGridCellValue({
@ -59,6 +103,18 @@ test("coerces PG brace-style string array", () => {
);
});
test("preserves high precision numeric tokens in Postgres arrays", () => {
assert.deepEqual(
coerceDataGridCellValue({
value: "{144847503924137986,2}",
oldValue: [],
databaseType: "postgres",
columnInfo: { data_type: "_int8" },
}),
["144847503924137986", 2],
);
});
test("coerces PG brace-style array with NULL", () => {
assert.deepEqual(
coerceDataGridCellValue({