Skip to content

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

Merged
datlechin merged 6 commits into
mainfrom
fix/3131-mongodb-create-collection
Sep 26, 2026
Merged

datlechin merged 6 commits into
mainfrom
fix/3131-mongodb-create-collection

Conversation

@datlechin

@datlechin datlechin commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Fixes #3131

Root cause

  • No create hooks in the MongoDB driver. It never implemented generateCreateTableSQL or generateAddIndexSQL, so the Create Table composer read nil as "This database cannot create a table from the visual editor." and dimmed Create Table.
  • The app never asked. New Table… was offered on every engine without asking the driver. The same dead end hit 13 engines: MongoDB, Redis, Cassandra, Elasticsearch, BigQuery, Spanner, Etcd, Kafka, Typesense, Weaviate, SurrealDB, Beancount and R2SQL. Each refused only after the grid was filled in.
  • A create hook alone would not have been enough. A MongoDB collection's fields exist on the server only as a $jsonSchema validator.
    • The plugin never read a validator back as columns, so a new collection showed _id and nothing else: the reporter's step 3.
    • The grid writer typed values by guesswork. A date went out as a string and a whole number as int32, so the validator would have rejected TablePro's own inserts.

What changed

MongoDB plugin (registry-only)

  • Create. MongoDBCollectionDDL turns the draft into db.createCollection("articles", {"validator": {"$jsonSchema": …}}).
    • Fields keep their row order. NOT NULL fields go in required; nullable fields also accept null.
    • The server's own strict / error defaults stay in force.
    • A collection with no fields beyond _id is created without a validator.
  • Indexes become createIndex with the keys in order. BTREE→1, HASH→hashed, FULLTEXT→text, SPATIAL→2dsphere.
  • Refusals, each naming the field. Anything a validator would reject or a JavaScript object would mangle is refused before anything runs:
    • a type that is not a BSON type (matched case-insensitively, so ObjectId works)
    • a primary key on anything but _id (the reporter's id row)
    • _id typed other than objectId
    • $, dotted, integer-like and __proto__ field names
    • unique hashed or text indexes, and a plain index on _id alone
  • Reading fields back. MongoDBCollectionSchema parses the validator from listCollections, keeping declared order. Two readers use it:
    • fetchColumns merges the declared fields in, reusing the call it already made for enums.
    • The session driver reads the schema on a first-page find. An empty collection's grid then shows its declared fields, and the writer and filters know their types. A role without listCollections falls back to sampling; a Stop propagates.
  • Writing by type. Each value is written in its field's declared type first, then its sampled type:
    • dates as $date, including the picker's local-time text
    • ObjectIds as $oid
    • declared string fields always as strings
    • int in canonical decimal, because a bare 010 is octal 8 in JavaScriptCore
    • integers past 2^53 as $numberLong
    • _id by a kind only when every sampled _id shares it
  • Security. The statement is JavaScript the shell evaluates, so three injection routes are closed:
    • {…}/[…] text is pasted in only when it is strict JSON. Before, a stored [db.getCollection("audit").drop()] ran when its row was duplicated or its delete undone.
    • The three MongoDB JSON escapers and MongoScriptJson's scanner now work scalar by scalar. A Unicode Prepend character (U+0600) fused with the " after it into one Character, which went out unescaped and closed the string literal.
    • MongoCollectionAccessor writes db.<name> only for ASCII identifiers. A U+0D4E letter joined to ( passed isLetter.

App

  • New Table… gate. The item is dimmed in the menu bar and absent from the sidebar menu, per the HIG, when the driver can't create a table. CreateTableEligibility asks the live driver for its form spec or a one-column generateCreateTableSQL probe, the same shape as DatabaseObjectToolEligibility. No PluginKit change.
  • Name-only collections. A name alone plans on an engine that supplies its own key (defaultPrimaryKeyColumn).
  • Preview highlighting. The Create Table preview is highlighted in the engine's language, so JavaScript for MongoDB.
  • Row import. A row import refuses before creating a new table it cannot fill. Row import into MongoDB has no SQL dialect, and would otherwise have left an empty collection.

Before / After

Before, from the report. Every row gets the same dead-end message, and Create Table is dimmed:

Create Table on MongoDB refusing with "This database cannot create a table from the visual editor"

After, with the reporter's first row as typed: the refusal names the field and says what to do:

Create Table refusing the id row because only _id can be the primary key

After, with _id and four fields. SQL Preview shows the collection and its validator:

SQL Preview showing db.createCollection with a $jsonSchema validator for articles

After Create Table, the new, empty collection shows its declared fields:

Empty articles grid with _id, title, tags, date and schemaVersion columns

Structure shows the declared types, with title NOT NULL:

Structure tab listing the five declared fields with their types and nullability

Verification

  • Unit tests: verify.sh test on the rebased branch, 28 suites. New suites:
    • MongoDBCollectionDDLTests: statement text, refusals, index mapping and key order
    • MongoDBCollectionSchemaTests: parse order, escaped and Prepend keys, enums
    • CreateTableEligibilityTests: stub drivers
    • additions to MongoDBWriteBackTypeTests, MongoScriptPreludeTests (the generated statements run through the real prelude in JavaScriptCore), MongoDBQueryBuilderTests, BsonDocumentFlattenerTests, CreateTableDraftBuilderTests, MainMenuValidationTests and DatabaseTreeMenuSpecTests
  • Builds: verify.sh build for the app and MongoDBDriver both pass on the rebased branch.
  • Lint: swiftlint lint --strict over every changed file shows no new findings.
  • Docs: check-writing-style.sh and check-docs-against-source.py both pass.
  • Live check: a swiftc harness over the real MongoDBPluginDriver, against mongo:7.0:
    • it creates the reporter's collection and two indexes
    • fetchColumns and the empty browse return the declared fields
    • a grid insert stores date, double, int, array, string and long, and 9007199254740993 is not rounded
    • a missing required field and a wrong type both fail with 121 Document failed validation, which relies on fix(plugin-mongodb): report shell errors and rejected writes instead of an empty success #3139
    • a stored hostile string stays a string, and the victim collection survives
  • Screenshots come from a sandboxed Debug build.
  • One existing test changed: insertQuotesInt64Overflow expected 9223372036854775807 bare. JavaScriptCore reads that literal as 9223372036854776000 (measured with jsc), so it now crosses as $numberLong.
  • No UI automation: creating a collection needs a live MongoDB server, which CI doesn't run.

Reviewed by Codex. Every finding from both passes was fixed or is noted here, along with a separate security review. One finding is left for its own PR: PluginKit's default escapedParameterValue has the same per-Character escaping bug for SQL literals.

Notes

Adversarial review

The Codex adversarial pass returned four findings.

  • JSON prefix followed by code (critical): refuted by measurement. On macOS 27, JSONSerialization refuses {}, injected: db.dropDatabase(), tail: {} and each variant tried, so none reaches the statement as source. jsonPrefixWithTrailingCodeIsAString pins that for update and restore.
  • Stop during the schema read (medium): fixed in a6ec907.
    • listCollections now carries maxTimeMS: 5000.
    • It runs only for finds that return whole documents.
    • A Stop propagates instead of being swallowed.
  • Same-spelled _id of different BSON types on one page (high): already true before this change, not fixed here. A page holding both "1001" and 1001 still falls back to reading the text. The fix is carrying each row's canonical _id through PluginRowChange, and the Can't create,edit or remove properties from MongoDB database. Why?????? #3132 work is taking it on. A page whose _ids share one kind now uses that kind, and date and bool _id filters are typed.
  • Editing a binary cell emits $unset (high): already true before this change, not fixed here. It lives in generateUpdate/generateInsert, which the Can't create,edit or remove properties from MongoDB database. Why?????? #3132 grid-serializer PR rewrites.

…lds as a validator, read the fields back, and write values in their declared types
… cancellation intact on the new write and schema paths
@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, 6:18 AM

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

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
…te-collection

# Conflicts:
#	TablePro/Resources/Localizable.xcstrings
#	TableProTests/Core/Menu/MainMenuBuilderTests.swift
#	docs/databases/mongodb.mdx

This branch was successfully deployed

1 active deployment
staging - docs — 3da4cfd3 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.

Can't create a database collection from the visual editor

1 participant