Skip to content

feat(plugin-mongodb): decode legacy binary UUIDs with a per-connection byte order - #2089

Merged
datlechin merged 1 commit into
mainfrom
fix/2086-mongodb-legacy-uuid
Aug 11, 2026
Merged

feat(plugin-mongodb): decode legacy binary UUIDs with a per-connection byte order#2089
datlechin merged 1 commit into
mainfrom
fix/2086-mongodb-legacy-uuid

Conversation

@datlechin

Copy link
Copy Markdown
Member

Closes #2086.

MongoDB stores UUIDs as BSON binary. TablePro showed them as hex.

Root cause

MongoDBConnection.unwrapExtendedJson read $binary.base64 and never $binary.subType, so the byte that says which byte order applies was thrown away before any consumer could see it. Two things followed from that:

  • a top-level binary field rendered as 0x2443254aeb03d08c...
  • a binary nested inside a document already rendered as UUID("...") for any 16-byte value, regardless of subtype, so a legacy UUID was shown with the wrong byte order and an MD5 hash was shown as a UUID

The fix carries the subtype through the pipeline as MongoDBBinaryValue and decides rendering from it.

Behaviour

Subtype 4 is unambiguous and always decodes to UUID("...").

Subtype 3 is the older format that the Java, C# and Python drivers each wrote with a different byte order, and nothing in the stored bytes says which one produced them. It stays BinData(3, "...") until you pick a byte order in Legacy UUID Encoding on the connection, then reads as LegacyJavaUUID("..."), LegacyCSharpUUID("...") or LegacyPythonUUID("...").

Refusing to guess is what the BSON Binary UUID spec requires of drivers under unspecified, and it is what Compass 1.49 and DataGrip both default to. The names match mongosh 2.6 and Compass 1.49, so a copied value pastes into either. The Studio 3T spellings (JUUID, CSUUID, NUUID, PYUUID, LUUID) are accepted as input.

The setting is per connection, not global, because byte order is a property of the application that wrote the collection. It is also a real connection-string option, so pasting a URL with uuidRepresentation=javaLegacy sets it and Copy URL emits it. It is never forwarded to libmongoc, which does not support the option and would only log a warning.

Design

Decoding is decided once per column, never per value. A decoded cell is .text and an undecoded one is .bytes, and CellDisplayFormatter runs blob formatting over a .text cell whenever its column type is BLOB, so a single UUID decoded inside a column the app still types BLOB would render as 0x4c65676163.... BsonDocumentFlattener.columnKinds decides for the whole column, and typeName returns exactly "BLOB" for undecoded binary because ColumnTypeClassifier keys on that name.

Once a column decodes, both edit guards (isBlobType and asBytes != nil) fall together, so every write path parses the wrapper back to $binary: the statement generator's jsonValue and idValueJson, the query builder's jsonValue and its =, != and IN arms, and MQLExportHelpers.mqlJsonValue. An _id filter left as wrapper text would match zero documents while the UI reported success.

Two duplicated code paths that would have made exports disagree with the grid are now one: streamCellValue and bsonTypeToStreamString in the streaming path were copies of the flattener and bsonTypeToString.

The Int32 BSON type codes are replaced by a BsonValueKind enum. Sentinel integers for the UUID cases would have needed a warning about Swift binding a bare identifier in a case pattern instead of comparing against it.

Also fixed

  • A delete is now anchored on _id or it does not run. generateDelete used to fall back to a filter built from the other columns, which dropped every value it could not stringify (all binary) and then deleteOned the first partial match, so a document with a binary _id could delete a different document. generateUpdate already skipped with a warning; both do now.

Tests

209 tests in 17 suites pass. Byte orders are asserted against the BSON Binary UUID spec's own test-plan vectors, mongosh's e2e vectors, and the UUID from the issue. Coverage includes the 16-byte guard, non-UUID subtypes, round-tripping every representation through its own wrapper, the column-gating rule in both directions, and the four write paths.

TableProTests did not actually cover any of this before. BsonDocumentFlattenerTests tested a hand-copied duplicate of the flattener whose flatten already returned a different type from production, and MongoDBExtendedJsonTests is wrapped in #if canImport(CLibMongoc), which is never true in the test target. The duplicate is deleted and the real file is compiled into the test target; the byte-order logic lives in TableProPluginKit so it is testable with no gate.

ABI and release

The PluginKit diff is purely additive, so no currentPluginKitVersion bump and no bulk re-release. MongoDB is registry-only, so the driver half reaches users through a plugin-mongodb-v1.0.38 tag; the URL round-trip and the connection-form mirror ship with the app.

Deliberately not in this PR

  • MQL export still hardcodes "subType": "00" for genuinely non-UUID binary. Carrying a subtype for arbitrary blobs needs a case on PluginCellValue, which is @frozen, so it is an ABI break and its own change.
  • The query editor rejects all shell helper syntax, ObjectId(...) and ISODate(...) included, so it is not specific to UUIDs. Teaching it only UUID(...) would be a special case; general support is a separate feature.
  • The statement preview shows exact Extended JSON rather than sugaring $binary. The legacy tag is per-connection state the sheet does not have, and a guessed tag would contradict the grid.
  • The four new localized keys are not in the catalog yet. They fall back to their English text.

https://claude.ai/code/session_01628orR3jwc3ATfcFCLVUMB

@mintlify

mintlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Aug 11, 2026, 1:05 PM

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

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin datlechin added the abi-additive PluginKit ABI diff reviewed as additive; no version bump needed label Aug 11, 2026
@datlechin
datlechin merged commit c9e13bd into main Aug 11, 2026
4 checks passed
@datlechin
datlechin deleted the fix/2086-mongodb-legacy-uuid branch August 11, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

abi-additive PluginKit ABI diff reviewed as additive; no version bump needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UUID render as JUUID

1 participant