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

From Drizzle ORMSupported

Drizzle is the closest neighbour: both compile to SQL, both derive types from a schema object, neither tracks entities. The move is mostly mechanical.

Schema#

// Drizzle
import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: text('email').notNull().unique(),
  active: boolean('active').default(true).notNull(),
});
// zmdb
import type { HasDefault, 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;
  active: boolean & HasDefault;
}

Differences that matter:

Rowstore because distribution/storage cannot be inferred safely.

build-time strategy; the explicit-name tag remains tracked on Naming Strategy.

Drizzle expresses that a declaration cannot.

Types#

Drizzlezmdb
typeof users.$inferSelectEntity<User>
typeof users.$inferInsertCreateDTO<User>
UpdateDTO<User>
WhereDTO<User>

The zmdb column takes the declared interface, not typeof a value — the declaration is already the type, so there is nothing to read it back out of.

Queries#

Drizzle's db.select().from(users).where(eq(users.email, x)) becomes either a repository call or a compiler call:

import { trustedTable } from '@zmdb/sql';

import { postgres } from '@zmdb/postgres';

// repository — typed against the schema
await repo.findOne({ email: { eq: 'a@b.c' } });

// compiler — SQL text, no connection
createQueryCompiler(postgres).selectFrom(trustedTable('users')).where('email', '=', 'a@b.c').compile();

Note the two operator vocabularies: the DTO uses eq / gte / in, the builder uses '=' / '>=' / 'in'. The DTO one is typed per column; the builder one is closer to the SQL.

Relational queries#

db.query.users.findMany({ with: { posts: true } }) becomes:

await repo.findAll({ populate: ['posts'] });

Same shape of result, same one-query-per-relation strategy. See Loading Strategies.

Migrations#

drizzle-kit generate becomes a script calling snapshot() + diff() + emitUp(). The snapshot file plays the same role as Drizzle's meta/_journal.json + snapshot pair. See generate for the script and CLI Overview for what is missing.

For an existing Drizzle-managed database, start with schema-first adoption instead of translating the schema object blind: introspect into a staging directory, review the generated tags and warnings, commit a baseline snapshot, and run detectDrift() against a restored database in CI. The pull command packages that library workflow rather than defining a second one. It writes protected staging declarations under .zmdb/introspected, with --dry-run for review and --check for CI.

Validation#

Drop drizzle-zod. assert<CreateDTO<User>>(body) is generated from the same declaration by the transformer, so there is no second schema to keep in sync. See assert().

What you lose#

What you gain#

---

See also: Why zmdb · Schema Declaration · Tag Reference · Filters & Operators