fix(grid): preserve high-precision numeric edits
This commit is contained in:
parent
e7ec2ecaf2
commit
d205f47a8f
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in New Issue