fix(plugin-mongodb): store Set NULL as null, tell a missing field from null, and add Remove Field - #3160
Merged
Conversation
datlechin
added this pull request to stack #3162
September 26, 2026 19:22
datlechin
force-pushed
the
fix/mongodb-null-vs-missing-field
branch
from
September 26, 2026 19:22
bf122bf to
72319cf
Compare
…m null, and add Remove Field
datlechin
force-pushed
the
fix/mongodb-null-vs-missing-field
branch
from
September 26, 2026 19:23
72319cf to
95a18f2
Compare
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
This branch was successfully deployed
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 #3159 (the grid write serializer), which is stacked on #3149 (the row-write channel). Retarget each to
mainas the one below it merges. Part of #3132.Root cause
The grid had one value for a MongoDB field that is null and for one that is missing:
.null.BsonDocumentFlattener.flattenreturns.nullfor a key the document lacks and for a key holding null, and nothing else said which it was.MongoDBStatementGeneratorread.nullas "remove":$unset.So:
required: ["deletedAt"]andbsonType: ["date", "null"]the save failed withDocument failed validation. Without a validator,{deletedAt: {$exists: true}}stopped matching.{deletedAt: null}dropped the field.PluginCellValueis@frozen, so a third state could not be added to the value itself. Presence therefore travels beside the value, and every path that copies, compares, undoes, redoes, rewinds or pastes a row value has to carry it.Data Rewind had a second, released problem on MongoDB. It compared the rows it read back by position against the recorded columns, but a MongoDB read returns only the fields its documents have. Measured on MongoDB 7.0.43: after
$unsetofnick, reading that document back returned the columns[_id, name]against a record of[_id, name, nick]. A field added since returned[_id, name, zeta], so the comparison read the wrong field.Two gates decided what the grid and the inspector offer, and each read less than the server does:
bsonTypereturned early, sobsonType: ["string", "null"]withenum: ["draft"]read as nullable while the server refuses{status: null}with 121. Rules over the whole document were not read at all._idis immutable too, so the inspector took typing, Set NULL and Remove Field on it, and staging then refused the edit and left it pending in the field. The typing and Set NULL part shipped in v0.75.0.Fix
Absence travels beside the value, never as a sentinel. An engine that cannot tell the two apart never sets it, so its comparisons, statements and menus stay as they were.
PluginKit (additive, kit 33 reused)
PluginQueryResult.absentCells: row index to the columns that row lacks. An optional stored var, decoded withdecodeIfPresent.PluginRowChange.absentColumns: the fields an update removes, or a new row leaves out.DriverPlugin.supportsFieldRemoval: defaults to false and is carried through the metadata snapshot. A plugin built before it existed offers none of the new UI.generateIdentityPreservingInsert(…, absentCells:): its default forwards to the old requirement, so DynamoDB and Spanner are unchanged.MongoDB driver
$set: {field: null}. Only a field the change removes gets$unset($unsetFieldinside the pipeline route).__proto__refusals used to say "Set it to NULL". NULL is now stored, and measured through the shell on 7.0.43,insertOne({"": null, "a": 1})still fails with[22] invalid document for insert: empty keyand{"__proto__": null, "b": 2}stores onlyb. The refusals and the docs now say to remove the field, which leaves it out, and that insert succeeds.Which fields take NULL
nullor is not declared, and anyenumlistsnulltoo. A combinator on the field counts as refusing it.minLength,patternand the other type-specific keywords never see null, measured on 7.0.43.patternProperties,dependencies, a documentenum, or a query operator beside$jsonSchema.additionalPropertiesschema decides NULL for the fieldspropertiesdoes not name.validationAction: "warn"andvalidationLevel: "off"refuse nothing, so every field takes NULL there.requiredno longer decides nullability at all.Grid and inspector
GridRowsClipboardPayload(columns:copying:projection:), so Copy with Headers from the row menu carries missing fields as well. The unusedTableViewCoordinator.copyRows(at:)is gone.FieldPickerSentinel.choose. Choosing the No Field or Multiple values row writes nothing.$set: {field: null}for a removal.DataChangeManager.unwritableColumns(among:), which isisColumnWritableover every column: generated columns plus the driver's immutable ones. A MongoDB_idis read-only in the inspector and offers no typing, Set NULL or Remove Field. Weaviate'suuidandvectorget the same.Undo, redo and discard
Data Rewind
Verified
Everything here ran on this pass, on the rebased and amended commit.
Restacked onto fix(plugin-mongodb): write binary, nested and specially named fields from the grid as the values they are #3159's head (
b4c0a3a46), which is the serializer rebuilt on fix(datagrid): refuse a save that would leave out a change its driver cannot write #3149 plus its round-3 fixes. The commit applied with one string catalog merge. The changed suites plus MongoDBNestedValueWriteTests, MongoDBStatementGeneratorTests and StringCatalogIntegrityTests: 304 of 304. The app,MongoDBDriverandAllPluginsbuild, and every plugin string is in the catalog.Rebase: the two conflicts were resolved keeping the serializer's provenance, field kinds and refusals and this branch's presence rules. Before continuing, 36 suites (this branch's, the MongoDB write suites, row-write coverage, save completion) ran 641 of 641 passed.
Build:
verify.sh build(TablePro and MongoDBDriver) andverify.sh plugins(all 40) pass.ABI: against the serializer head it was built on, additions only: 8 lines added, none removed. The kit stays 33, since v0.75.0 ships 32.
Tests:
Lint, docs and strings: 0 violations on the 76 Swift files the branch changes.
verify.sh docspasses. Both string catalogs verify, and every plugin string is in the catalog.Live on MongoDB 7.0.43, with a harness running this branch's driver:
Offered NULL against the server's own answer to
$set: {s: null}, one collection per validator:s: ["string","null"]+enum: ["draft"]s: ["string","null"]+enum: ["draft", null]s: ["string","null"]+minLength: 3,patternanyOfconstrainings{$type: "string"}onsbeside$jsonSchemapatternProperties: {"^s": string}additionalProperties: string, undeclareduadditionalProperties: string, declaredt: ["string","null"]s: stringwithvalidationAction: "warn"orvalidationLevel: "off"An empty field name holding null fails the insert with
[22], and__proto__: nullis dropped. The same row with that field removed inserts{"name": "Grace"}.Set NULL sent
{"$set": {"deletedAt": null}}, and$exists: truewith$type: "null"matches.Remove Field sent
{"$unset": {"nick": ""}}, and the field is gone. On therequiredfield the server refuses with 121.Duplicating
{deletedAt: null}inserted{"deletedAt": null, "name": "b"}. The old statement,insertOne({"name": "b"}), is refused with 121.Rewinding a delete inserted
{"_id": 2, "deletedAt": null, "name": "b"}: null kept,nickstill missing.The rewind read-back after a
$unsetreturned[_id, name], and after a new field[_id, name, zeta], against a record of[_id, name, nick]. Reading both reported the missing cells{0: [3], 1: [2]}.Every
probe_null-vs-absent_*database was dropped.Codex reviews:
$set null. The pickers sent the No Field tag as a value. Copy with Headers dropped missing fields. All five were fixed in the previous pass.enumrefuses it. Remove Field was offered on_idin the inspector. Both are fixed here, together with the rest of each class.Deliberately not fixed here
{field: null}does in mongosh. The docs say so and give the$exists: falsequery. A separate operator would touch every engine's filter bar.anyOfor a query operator beside$jsonSchema, measured live, fields those rules never name lose Set Value > NULL too, although the server stores null in them. Modeling combinators and match expressions field by field is a larger change.validationLevel: "moderate"is treated as enforced. The docs say both.requiredfield is offered, and the server refuses it at save._idkind is last-writer-wins across every result on the session. A query whose projection turns_idinto a string retypes the next_idfilter until the collection is browsed again.No UI test: CI has no MongoDB server, and every path here needs a connected collection with sparse documents. The host side runs through the real MongoDB generator in unit tests.