fix(mongodb): keep Int32 and Double types when editing numbers

This commit is contained in:
Loc Nguyen
2026-10-13 10:31:36 +08:00
committed by GitHub
parent 29ed4c47af
commit 31a0a7f893
3 changed files with 84 additions and 2 deletions
@@ -1577,7 +1577,7 @@ async function gridSave(changes: DocumentGridChanges) {
continue;
}
const updateDoc = buildMongoUpdateDocument(dirtyCols, cols, documents.value[rowIdx]);
const updateDoc = buildMongoUpdateDocument(dirtyCols, cols, mongoTypedSourceDocument(rowIdx));
if (Object.keys(updateDoc).length === 0) continue;
const documentId = documents.value[rowIdx]?._id ?? id;
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, serializeMongoDocumentId(documentId), JSON.stringify(updateDoc));
@@ -1648,6 +1648,11 @@ function buildPathIdentityUpdatedDocument(document: JsonRecord, changes: Map<num
return updated;
}
/** The loaded document with its BSON number types intact (canonical extended JSON), so edits keep Int32/Double fields typed. */
function mongoTypedSourceDocument(rowIdx: number): JsonRecord | undefined {
return (mongoCopyDocumentsAvailable.value ? copyDocuments.value[rowIdx] : undefined) ?? documents.value[rowIdx];
}
function buildMongoGridInsertDocument(row: MongoInputValue[], columns: string[], meta?: GridNewRowMeta): Record<string, unknown> {
const sourceIndex = meta?.sourceIndex;
const sourceDocument = sourceIndex === undefined ? undefined : copyDocuments.value[sourceIndex];
@@ -1725,7 +1730,7 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
stmts.push(kind === "solr" ? formatSolrDocumentOperationPreview({ action: "update", core: coll, id: documentId, document: writeDocument }) : formatMeilisearchDocumentOperationPreview({ action: "update", index: coll, id: documentId, document: writeDocument }));
}
} else {
const updateDoc = buildMongoUpdateDocument(dirtyCols, columns, documents.value[rowIdx]);
const updateDoc = buildMongoUpdateDocument(dirtyCols, columns, mongoTypedSourceDocument(rowIdx));
stmts.push(`db.${coll}.updateOne({_id: ${formatMongoShellLiteral(documents.value[rowIdx]?._id ?? id)}}, ${formatMongoShellLiteral(updateDoc)})`);
}
}
@@ -404,9 +404,44 @@ function parseMongoExistingFieldInputValue(raw: Exclude<MongoInputValue, null>,
if (typeof originalValue === "string") {
return typeof raw === "string" ? raw : String(raw);
}
const typedNumber = mongoTypedNumberInputValue(raw, mongoDocumentNumericValueType(originalValue));
if (typedNumber !== undefined) return typedNumber;
return parseMongoDocumentInputValue(raw);
}
const MONGO_DECIMAL_INPUT_PATTERN = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
const MIN_BSON_INT32 = -2147483648n;
const MAX_BSON_INT32 = 2147483647n;
/**
* Re-wraps a number typed over an existing typed BSON number in the original
* wrapper. A bare JSON number is always saved as Int64, so without this an
* edited Int32 or Double silently changes type. Input the original type cannot
* hold (a fraction for Int32, an Int32 overflow) falls back to normal parsing.
*/
function mongoTypedNumberInputValue(raw: Exclude<MongoInputValue, null>, numericType: string | undefined): unknown {
if (!numericType || numericType === "number" || typeof raw === "boolean") return undefined;
const text = String(raw).trim();
if (!MONGO_DECIMAL_INPUT_PATTERN.test(text)) return undefined;
switch (numericType) {
case "int32":
case "int64": {
if (!MONGO_INTEGER_PATTERN.test(text)) return undefined;
const integer = BigInt(text);
if (numericType === "int32") return integer >= MIN_BSON_INT32 && integer <= MAX_BSON_INT32 ? { $numberInt: integer.toString() } : undefined;
return integer >= MIN_BSON_INT64 && integer <= MAX_BSON_INT64 ? { $numberLong: integer.toString() } : undefined;
}
case "double": {
const double = Number(text);
return Number.isFinite(double) ? { $numberDouble: String(double) } : undefined;
}
case "decimal128":
return { $numberDecimal: text };
default:
return undefined;
}
}
function mongoDocumentFieldValue(document: unknown, field: string): unknown {
if (!document || typeof document !== "object" || Array.isArray(document)) return undefined;
return (document as Record<string, unknown>)[field];
+42
View File
@@ -169,6 +169,48 @@ test("preserves existing Mongo strings that resemble typed literals", () => {
});
});
test("keeps the BSON number type of edited Mongo fields", () => {
const original = {
_id: { $oid: "65f000000000000000000001" },
count: { $numberInt: "1" },
total: { $numberLong: "2" },
ratio: { $numberDouble: "1.5" },
price: { $numberDecimal: "9.99" },
};
const changes = new Map<number, string | number | boolean | null>([
[1, "200"],
[2, 7],
[3, "3"],
[4, "10.50"],
]);
assert.deepEqual(buildMongoUpdateDocument(changes, ["_id", "count", "total", "ratio", "price"], original), {
$set: {
count: { $numberInt: "200" },
total: { $numberLong: "7" },
ratio: { $numberDouble: "3" },
price: { $numberDecimal: "10.50" },
},
});
});
test("falls back to inferred types when an edit no longer fits the Mongo number type", () => {
const original = { _id: "1", count: { $numberInt: "1" }, other: { $numberInt: "1" }, label: { $numberInt: "1" } };
const changes = new Map<number, string | number | boolean | null>([
[1, "2147483648"],
[2, "1.5"],
[3, "abc"],
]);
assert.deepEqual(buildMongoUpdateDocument(changes, ["_id", "count", "other", "label"], original), {
$set: {
count: 2147483648,
other: 1.5,
label: "abc",
},
});
});
test("keeps JSON inference for fields without an existing Mongo type", () => {
const changes = new Map<number, string | number | boolean | null>([
[1, '{"enabled":true}'],