zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Databases

MongoDBTODO

🚧 TODO — not yet implemented. This capability is on the roadmap and is not an anti-pattern for zmdb; it simply isn't built yet. refused: a Serial key has no MongoDB equivalent, aggregate hands SQL to application code, and savepoint has none Track / contribute via the issue tracker.

ToDo / not planned. No MongoDB target ships. The feasibility study refused it because the current declaration and repository contracts cannot be implemented faithfully: a Serial key has no MongoDB equivalent, aggregate hands SQL to application code, and the public transaction surface requires savepoints.

Repository method matrix#

This is a feasibility record, not a support table. None of these methods has a MongoDB implementation. “Expressible” means the accepted target study found a faithful MongoDB operation for that method; it does not mean zmdb executes it.

Repository methodMongoDB feasibility
findByIdexpressible as findOne({ _id }), but the current Serial key cannot round-trip
findOneexpressible
findexpressible; the filter differences below still apply
findAllexpressible
listneeds translation: keyset branches require explicit $and/$or nesting
findByFullTextrefused: MongoDB searches one text index per collection, so the column argument cannot be honoured
findJoinedtranslatable for the current inner/left forms with $lookup and $unwind
aggregaterefused: the callback form gives application code a SQL aggregate builder
findAllWithManyexpressible as a second find({ fk: { $in: [...] } }); this is not $lookup
createexpressible as insertOne, but the current Serial key cannot round-trip
upsertneeds translation to a filter-matched upsert and a unique index on the conflict target
updateexpressible as findOneAndUpdate with the updated document returned
deleteexpressible as deleteOne, with deletedCount supplying the boolean result
withTransactionrefused: MongoDB has no savepoints, while zmdb's public TransactionContext requires savepoint(...)

Eight methods are expressible as written, three need a target-specific translation, and three are refused. That partial surface is deliberately not reported as support.

Why the target was refused#

The compiler output shape was not the blocker. The DTO fold is already target-neutral: compileWhere, applyOrderBy, applyKeysetFilter and applyPagination drive structural WhereTarget and OrderTarget interfaces that mention neither SQL nor CompiledQuery. A future target could provide its own builders, compiled command shape and driver without widening the SQL path.

The target still fails the criterion of covering the full read/write surface, relations and transactions:

key truthfully.

translate.

All six current Dialect values are SQL dialects. MongoDB is not one: it changes the command representation, schema model and transaction capabilities rather than only how SQL is written. That requires a separate structural target, not another branch in SQL quoting or placeholder code.

Filters are close, not identical#

Most WhereDTO operators have a direct command-document spelling:

FieldOpsMongoDB assessment
eq, ne, lt, ltedirect $eq, $ne, $lt, $lte
gt, gte, in, nindirect $gt, $gte, $in, $nin
like, ilikemust translate SQL wildcards into an escaped, anchored regular expression
isNull$eq: null also matches an absent field, unlike SQL IS NULL
notNullmaps to $ne: null, with MongoDB's field-existence semantics
l2, cosine, iprefused: these are PostgreSQL vector extension operators, not universal DTO operations
exists, notExists, groupingrequire a target-native nested predicate tree rather than SQL's implicit precedence

A LIKE pattern cannot be passed to $regex verbatim. SQL uses % and _ as wildcards, while regular expressions give characters such as ., *, +, ?, (, [ and \ special meaning. A faithful translation first escapes regular-expression metacharacters, then maps % to .* and _ to ., and anchors the result. An unanchored regular expression can also turn an indexed lookup into a collection scan.

What works today: schema validation#

toJsonSchema is useful input to a MongoDB collection validator, but its output is not accepted unchanged. MongoDB's JSON Schema subset does not support format, while zmdb emits format: 'date-time' and format: 'int64'. MongoDB also omits the standard integer type in favour of BSON numeric types.

import { toJsonSchema } from '@zmdb/schema/openapi';

// Application code: zmdb does not ship this adapter.
const mongoSchema = adaptJsonSchemaForMongo(toJsonSchema(users, 'entity'));

await db.createCollection('users', {
  validator: { $jsonSchema: mongoSchema },
});

The adapter must at least remove unsupported format keywords and translate type: 'integer' to the BSON numeric types the application actually stores. That keeps the useful part of the recipe — one declaration feeding validation — without claiming that OpenAPI JSON Schema and MongoDB's validator dialect are identical.

Using zmdb with MongoDB today#

The layers that do not execute database queries remain usable:

import { assert } from '@zmdb/validator';
import { type CreateDTO, type Entity } from '@zmdb/schema';

@Controller('/users')
export class UsersController {
  @Post('/')
  async create(ctx: Ctx<Record<never, string>, unknown>) {
    const dto = assert<CreateDTO<User>>(ctx.body);
    const { insertedId } = await col.insertOne(dto);
    return { ...dto, id: insertedId.toString() };
  }

  @Get('/:id')
  async get(ctx: Ctx<{ id: string }>) {
    const doc = await col.findOne({ _id: new ObjectId(ctx.params.id) });
    return doc === null ? undefined : assert<Entity<User>>(normalise(doc));
  }
}

This keeps AOT validation, the web layer, OpenAPI generation and a schema declaration as input to an application-owned MongoDB validator adapter. It does not provide the query builder, repository, migrations, relations or transactions. The normalise step is load-bearing: an ObjectId does not satisfy a declared numeric key.

What would reopen the decision#

All three blockers need an answer:

  1. the schema vocabulary can declare an externally generated or opaque primary key without changing the declaration per target;
  2. aggregate has a complete declarative form instead of requiring the SQL-builder callback; and
  3. the transaction contract either stops requiring savepoints or explicitly permits a target to refuse that named capability.

Any future implementation would use the existing structural target seam. It would not widen CompiledQuery, add a Target<Q> parameter to every SQL consumer, or map populate to $lookup. No such work is planned.

---

See also: Query Compiler · toJsonSchema · Gel