fix(plugin-mongodb): write binary, nested and specially named fields from the grid as the values they are - #3159
Merged
Merged
Conversation
…from the grid as the values they are
…r a mixed field's type, and empty keys insert cannot write
… read wrappers by their whole shape, and refuse a pasted nested leaf its array does not hold
datlechin
force-pushed
the
fix/mongodb-grid-write-values
branch
from
September 26, 2026 19:22
b4c0a3a to
7657818
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #3149, the row-write channel (
generateRowWrites,PluginRowWrite,PluginRowWriteRefusal). This PR is its MongoDB adoption. Retarget tomainonce #3149 merges.Root cause
The grid's MongoDB writer spelled each value from its text. Four kinds of value were written as something else, and none of them said so.
Binary
PluginCellValue.byteshas no text, the same as.null. SogenerateUpdatesent an edited binary cell to$unsetand deleted the field.generateInsertleft binary out of duplicated and pasted rows.Empty rows
Field names
$setand$unsetkeys are paths, measured on 7.0.43:price.usdwalked into a sub-document.$pricewas refused with code 52.__proto__never reached the server, because the statement is a JavaScript object and that key sets its prototype.Nested values
Fix
The MongoDB driver now implements
generateRowWrites. Every statement names the rows it writes. A change the shell cannot carry as the value the grid shows throwsPluginRowWriteRefusalwith a reason, so it is never left out. PluginKit is unchanged.Binary
{"$binary": {"base64": …, "subType": …}}.MongoDBBinarySubtypeskeeps a SHA-256 digest for each field and value on every page read. It adds to the set rather than replacing it, up to 50,000 per collection.binData. A new row's change lists the cells the user filled in, so copied cells are told apart from typed ones._idfilters on$binarywith its own recorded subtype. It no longer borrows the subtype every sampled_idshares: the same bytes under another subtype are another_id, so a borrowed one could delete a different document.Empty rows
{}. A row whose only value was refused throws instead.Field names
$-prefixed, empty or__proto__name is sent as one pipelineupdateOneusing$setFieldand$unsetField. Every value is wrapped in$literal.$setoftags.1rewrites every element oftagsand still reports success. So a pipeline row writes every changed field whole and never names a path.$set/$unset.insertOneandinsertManyfail with[22] invalid document for insert: empty keyfor an empty name at the top level and inside a value. The shell inserts throughmongoc_collection_insert_manywith nil options, and libmongoc's default validation refuses the key. So an empty name at any depth refuses the row, before anything in the save is sent.insertOne({"__proto__": 5, "a": 1})succeeds and stores onlya. So__proto__refuses the row too.$setof{"": 6}and$setFieldof""or__proto__all succeed. The refusal says so: set the cell to NULL, save, then set it on the saved document. That route was checked live.Nested values
3.0.$numberLong,$oid,$numberDecimal,$binaryand$timestampkeep their wrappers.MongoDBFieldKinds). It adds to the record rather than replacing it, so a string on an earlier page is not forgotten when a later page holds only documents.$set/$unseton the paths that changed only when that row's value is known to be a document or array: the field held that shape and never a string, which could read the same. The column's majority kind no longer decides it. Otherwise the value is written whole, or refused when even its whole type is ambiguous:{"a":1}duplicated or restored used to be written as a document.9, 10, B, _, aa, b."4294967295"stays where it was written. The New Table field-name check uses the same rule.Timestamp(t, i),MinKeyorMaxKey. The shell's serializer turns every JavaScript number into a wrapper, and libbson reads those three only with bare numbers. A code$scopeis written as a document, so its numbers and key order hold too.Also
$oidalone,$binarywith$type,$codewith$scope,$regexwith$options, and so on. One that opens with$oidand goes on to other members is a document, so{"$oid": "x", "__proto__": 1}is refused like any document the shell would change, rather than written as a wrapper that drops__proto__.__DEFAULT__._idedit and a missing_idnow refuse the save instead of being skipped._idis matched in stored order.Verified
binData, where the same restore with the subtype recorded writes05.{"a":1}in a field of mostly documents, edited to{"a":2}, is refused and never becomes a path._idsubtype borrowed again, the nested empty-key check limited to the top level). Ten new cases failed, among themrestoreAfterARelaunchRefusesBytes,bytesOverNullWithoutASubtypeAreRefused,copiedBytesInADeclaredFieldAreRefused,binaryIdWithoutARecordedSubtypeIsRefused,jsonLookingStringInAMixedFieldIsRefusedandinsertRefusesANestedEmptyKey. The source was restored before the final run.stringOnlyFieldKeepsJSONShapedText,wrapperShapedDocumentIsCheckedAsADocumentandinsertRefusesPastedLeafThatDisagreesWithItsArrayfail against the round-2 sources and pass here.0eab09894, onmainatc21dc512e): the branch's own changes are unchanged, and only main's and fix(datagrid): refuse a save that would leave out a change its driver cannot write #3149's files differ from the round-2 head. The changed suites plus ElasticsearchDriverTests and StringCatalogIntegrityTests: 207 of 207.verify.sh build(TablePro and MongoDBDriver) andverify.sh plugins(all 40) pass.verify.sh docspasses, and every plugin string is in the catalog.sigasbinData, on a new driver that had read the schema and another row:{sig: BinData(5, "AAECAw==")}used to be restored as subtype00. The restore is now refused. The driver that read the row still restores it as05.00. They are now refused.{a: 1},{a: 1, b: 2},{c: 3}, the string'{"a":1}'and the string"{abc":{"a":2}used to send{"$set": {"f.a": 2}}, which failed with[28] Cannot create field 'a' in element. It is now refused before anything is sent."{abc"edited to"{abcd"used to be refused as not valid JSON. It is now stored as the string{abcd."{abc"edited to{"x":1}used to turn the string into a document. It is now refused.{"$set": {"f.a": 5}}.""cell saves through$setField. A duplicate is refused with the new reason, and with that cell set to NULL it saves. A duplicate of{m: {"": 5, k: 1}}is refused. Withmset to NULL it saves, and settingmon the saved copy stores{"": 5, k: 1}._id, the empty row, special names, nested edits and duplicates, two tabs, nested timestamps, the4294967295key and shortened values.probe_grid-serializer_*database was dropped.Deliberately not fixed here
emptyFieldNameInNewDocumentandemptyKeyInNewDocumentrefuse writes that would succeed, so both cases and their strings should go in the same change.__proto__stays refused until grid writes leave the JavaScript shell.generateIdentityPreservingInsertcan only return nil, so the reason (an unknown subtype, an empty key, a shortened value) is logged but not shown. Showing it needs a throwing restore on the row-write channel, which belongs to the channel PR rather than a competing API here.trueis still written as a document, a number or a boolean, the same as on main, because nothing says the user meant a string. The validator'sstringdeclaration keeps it text.nullwhere it should remove a field. So both go together in a follow-up PR._idon a page mixing_idtypes, such as the string"1001"next to the int1001. This needs the row locators from the document-editor work, which is not in this base.Timestamp(1700000000, 7),MinKey) in a VARCHAR column, and a duplicate writes that text as a string. This was measured on 7.0.43 and is the same as on main. Nested ones keep their type.__proto__refusals and the number wrapping.$type, such as{sig: {$type: "binData"}}, is refused by libbson as its legacy binary spelling. Fixed in fix(plugin-mongodb): send $type, $regex and $options objects as the operator documents they are written as #3156.1e20, as an invalid$numberLong. Fixed in fix(plugin-mongodb): store shell numbers past 2^53 as the doubles they are, define every constructor autocomplete offers, and write stored regexes and decimals back unchanged #3158.No UI test: CI has no MongoDB server, and every path here needs a connected collection. Unit tests cover the generator, the display and the per-field record, and the prelude round-trip tests cover the shell. The existing SaveCompletionTests and RowChangeStatementFactoryCoverageTests cover the channel's save behaviour.