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

From PrismaSupported

Prisma's schema lives in its own DSL and its client is generated. zmdb's schema _is_ a TypeScript type and nothing is generated into your repo, so this migration replaces the build step as well as the query API.

schema.prisma → a TypeScript module#

model User {
  id     Int     @id @default(autoincrement())
  email  String  @unique
  posts  Post[]
}
import { schemaOf } from '@zmdb/schema';
import type { OneToMany, PrimaryKey, Serial, Sql, Table, Unique } from '@zmdb/core/tags';

export interface User extends Table<'users'> {
  id: number & Sql<'integer'> & Serial & PrimaryKey;
  email: string & Sql<'text'> & Unique;
  posts?: Post[] & OneToMany<'posts', 'authorId'>;
}

export const userSchema = schemaOf<User>();

The mapping is close to line-for-line: @id is PrimaryKey, @default(autoincrement()) is Serial, @unique is Unique, Post[] is Post[] & OneToMany<'posts', 'authorId'>. What Prisma spells with attributes, zmdb spells with intersections.

The consequences of leaving the DSL:

AOT Setup.

📝 Note

The relation _tag_ documents the relationship and reaches the derived documents, but the repository's populate still needs the runtime relations map beside the schema. The two are not yet one source. See Relations.

Client → repository#

Prismazmdb
prisma.user.findUnique({ where: { id } })repo.findById(id)
prisma.user.findFirst({ where })repo.findOne(where)
prisma.user.findMany({ where })repo.find(where)
prisma.user.findMany({ include: { posts: true } })repo.findAll({ populate: ['posts'] })
prisma.user.findMany({ select: { id: true } })repo.list({ select: ['id'] })
prisma.user.create({ data })repo.create(data)
prisma.user.update({ where, data })repo.update(id, data)
prisma.user.delete({ where })repo.delete(id)
prisma.user.upsert(...)— see Upsert
prisma.user.aggregate(...)repo.aggregate(spec)
prisma.$transaction([...])batch([...]) — see Batch API
prisma.$transaction(async (tx) => ...)createTransactionalDb(conn).transaction(...)
prisma.$queryRawdriver.execute(compiled) — see Raw SQL

select: { id: true } becoming select: ['id'] also changes the return type the same way Prisma's does — the row type narrows to the selected keys. See Projections.

Filters#

Prisma's nested filter objects are close to zmdb's WhereDTO:

// { where: { age: { gte: 18 }, email: { contains: '@x' } } }  ->
{ age: { gte: 18 }, email: { like: '%@x%' } }

contains / startsWith / endsWith become like with the wildcards written out. mode: 'insensitive' becomes ilike (Postgres). See Filters & Operators.

Migrations#

prisma migrate dev becomes a generate script over a committed snapshot. Both approaches produce SQL files you review and commit; zmdb's diff runs against the snapshot rather than a shadow database, so it needs no second connection and works offline.

prisma db pull becomes zmdb pull. It reads through the configured driver, writes generated declarations under .zmdb/introspected, refuses to overwrite a file whose generated header was removed, and offers --dry-run and --check; see pull.

Where the models went#

Prisma returns plain objects too, so this is the one migration where the _shape_ of a result does not change. What changes is that relations are not sometimes-present: if you did not populate, the key is absent from the type rather than typed as optional. That turns a class of runtime undefined into compile errors.

Things Prisma has that zmdb does not#

Things zmdb has that Prisma does not#

---

See also: Why zmdb · Repository · Migrations