Skip to content

feat(plugin-mongodb): edit MongoDB documents as Extended JSON, found by their stored _id and saved under an exact guard - #3152

Open
datlechin wants to merge 2 commits into
mainfrom
feat/mongodb-edit-document
Open

datlechin wants to merge 2 commits into
mainfrom
feat/mongodb-edit-document

Conversation

@datlechin

Copy link
Copy Markdown
Member

Closes the rest of #3132: you can now add, rename or remove a field in a MongoDB document. Follows #3140 (Insert Document).

Root cause

The grid kept only a row's display text, not its BSON identity:

  • an ObjectId became bare hex
  • a number lost its type
  • a document _id had its keys re-sorted

PluginQueryResult also had no per-row channel for anything else. So an edit had to guess the stored value from text, and the earlier attempt (448d9047e) reached the wrong document, or none:

  • a string with the same hex, once the ObjectId was gone
  • a numerically equal _id of another type
  • a case variant under the collection's collation

Its guard was a plain match, and a match is not equality: null matches a missing field, 5 matches [5, 6], and int32 5 matches int64 5. Its $set diff also could not keep the order the fields were written in, and it read a dotted name as a path.

Fix

Identity travels with the row. A find with no projection now tags each row with the compact canonical Extended JSON of its stored _id. The tag is read from the same libbson text the row comes from.

  • It travels as PluginQueryResult.rowLocators, an optional stored property set after construction (additive; the kit stays 33). From there it goes to QueryResult, QueryFetchResult and TableRows, keyed by RowID.
  • Only table tabs keep locators.
  • Edit Document is offered only on a row that has one. The row menu captures the locator when the menu is built.

Fetch (fetchDocument, a new requirement with a default):

  • The locator is read strictly: exactly one _id value, never an operator.
  • The version probe, the namespace check and the read run as one call on the connection's own queue. None of them blocks the caller's thread. The connection clears its cancellation latch once, when that call starts, and checks for a cancel before each step. Closing the sheet stops the read at the next step, and a sheet closed before the read starts sends nothing.
  • Only a plain collection opens. A view is refused, and so is a time-series collection, which refuses a replace and does not keep _id unique. So is any collection type the driver does not know, and any server older than 4.0.
  • The read goes to the primary under the collection's own collation, so a string _id uses its index. It carries maxTimeMS and runs under a cancellable lease.
  • The read has no limit. It skips documents whose _id only collates equal and stops at the second exact match. On a sharded collection each shard can return its own lenient match, and those can no longer use up a count.
  • Only a document whose _id bytes equal the locator counts.
  • Refused when the document opens:
    • a NaN, double or decimal, whatever its sign or payload
    • JavaScript code with a scope
    • anything libbson cannot write back exactly
    • a top-level $ field, or a repeated field
    • a top-level Timestamp(0, 0)
    • more than 28 levels of nesting
    • a guarded save over 16 MB

Save is a whole-document replaceOne under the simple collation.

  • _id goes first, then the fields in the order written. _id cannot change.
  • The filter is {_id, $expr: {$and: [$$ROOT == original, typeSignature == expected]}}. The signature holds one $map per nesting level, so its size grows with depth, not length.
  • The guard will not build over a NaN or over code with a scope. The server compares both by value: every NaN equals every other, and 1 equals NumberLong(1) inside a scope.
  • matchedCount 0 reports "changed on the server" and keeps the user's text.

Menus. Entry points are Edit > Edit Document… and the row's context menu. Edit Document and Insert Document dim in Agent mode through the same set that dims Add Row.

Guard check. scripts/check-mongodb-document-guard.sh builds filters with the guard's own sources and asks a live mongod about them. It also asserts that the guard refuses NaN and code with a scope.

Verified

  • Codex reviewed this branch twice.
    • Round 1 found five P2s: a scope's contents missing from the guard, NaN payloads the guard could not tell apart, limit: 2 applied before exact _id matching, time-series collections not refused, and Edit Document still enabled in Agent mode. All five were fixed before round 2.
    • Round 2 found one P1: the version probe blocked the main actor. It also found one P2: a cancel was lost between the fetch's steps. Both are fixed in this final pass. The P1 was narrower than described, as measured below.
  • Live against MongoDB 7.0.43, with the real plugin sources linked against libmongoc, comparing the previous commit with this one:
    • Called through any PluginDatabaseDriver, which is how PluginDriverAdapter calls it, the fetch already ran off the main thread, so the app did not freeze. What the probe did was block a cooperative-pool thread in queue.sync. A direct call from the main actor ran buildInfo on the main thread. After the fix it runs on the connection's queue.
    • Task cancelled before the call: the old code ran every step and returned the document. Now it throws CancellationError and sends nothing.
    • Task cancel plus the driver cancel during the namespace step (delay injected in a probe copy): the old code still ran the find and returned the document. Now it stops before the find.
    • Re-run on the new sources, these still hold:
      • Plain, Go-payload and decimal NaN are refused as NaN.
      • Code with a scope, a view and a time-series collection are each refused.
      • Under strength 2, locator abc with ABC stored reads as gone, and ABC opens.
      • A decimal Infinity document opens, saves an edited field, and reads back with the change.
  • Tests: 288/288 in 19 suites (the MongoDB document suites, the row-locator suites, MainMenuValidationTests and StringCatalogIntegrityTests). With the between-step cancel checks removed, cancelStopsTheNextStep fails (18 run, 1 failed).
  • Builds: MongoDBDriver, the app, and AllPlugins (40).
  • Lint: 0 violations on the Swift files changed in the final pass.
  • ABI against the merge base cec9b57c5: 4 additions, none removed. The kit stays 33 (v0.75.0 ships 32).
  • Docs check, localization check, shellcheck on the guard script, and the test-suite attribute check: clean.
  • Rebased onto main at c21dc512e with no conflicts, then rerun: MongoDBDriver and the app build, 106/106 in the 13 suites this branch changes, 0 lint violations, plugin localization check clean.

Deliberately not fixed here

  • A cancel that lands during buildInfo or listCollections stops the read after that step, not inside it. Neither command runs in a session killSessions can reach, and buildInfo carries no maxTimeMS. The find, the only step that can scan, runs in a session that can be killed.
  • A decimal written with non-canonical bytes (an Infinity with trailing bits, or a coefficient past 10^34, which reads as zero) compares and prints as its canonical value (measured). So the guard cannot see one replace a canonical value. IEEE 754 defines those bytes as that value and no driver writes them, so a save over one loses no value, unlike a NaN payload.
  • Sharded behaviour was not measured, because no cluster was available. It rests on MongoDB's documented per-shard _id uniqueness.
  • In a collection whose default collation is not simple, saving a document with a string _id scans the collection. The query timeout cannot bound that scan: libmongoc 1.28.1 puts maxTimeMS inside updates.0, where the server refuses it (40415). This is documented under Limitations.
  • A write-concern error cannot be produced on a standalone server.
  • Insert Document still refuses a field named "", because insert_one runs libmongoc's default validation.
  • MongoScriptFindShape.returnsWholeDocuments (fix(plugin-mongodb): create collections from New Table with their fields as a validator, read the fields back, and write values in their declared types #3145) and MongoScriptCursor.returnsWholeDocuments here spell the same predicate twice. Make one read the other when this rebases.
  • Lead, from reading the code only: Duplicate Row, Paste, Delete, Truncate Table and Copy have no Agent-mode condition in the menu validator and are not in the browse-only set.

No UI test: CI has no MongoDB server, so the flow cannot run deterministically. The Agent-mode rule is covered by a unit test on the menu validator, and the cancellation order by unit tests on the read's steps.

…by their stored _id and saved under an exact guard
@mintlify

mintlify Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 26, 2026, 5:03 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

This branch was successfully deployed

1 active (outdated) deployment
staging - docs — 346884cf Deployed Sep 26, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant