From d205f47a8f9707cae4c39e81d6f8301b406e3249 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 3 Jul 2026 16:08:20 +0800 Subject: [PATCH] fix(grid): preserve high-precision numeric edits --- apps/desktop/src/lib/dataGridCellCoercion.ts | 73 +++++++++++++++++-- crates/dbx-core/src/data_grid_sql.rs | 22 ++++++ .../app-tests/dataGridCellCoercion.test.ts | 56 ++++++++++++++ 3 files changed, 146 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/lib/dataGridCellCoercion.ts b/apps/desktop/src/lib/dataGridCellCoercion.ts index 34165c237..c0712dd11 100644 --- a/apps/desktop/src/lib/dataGridCellCoercion.ts +++ b/apps/desktop/src/lib/dataGridCellCoercion.ts @@ -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 | 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; } diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index 89f731836..f01b53435 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -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 { diff --git a/packages/app-tests/dataGridCellCoercion.test.ts b/packages/app-tests/dataGridCellCoercion.test.ts index 26a78cddf..fb158e333 100644 --- a/packages/app-tests/dataGridCellCoercion.test.ts +++ b/packages/app-tests/dataGridCellCoercion.test.ts @@ -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({