Drizzle ORM: how to review AI-generated database code when a TypeScript-first query builder infers schema types and generates migrations
Drizzle ORM is a TypeScript-first database toolkit that lets you define your schema in TypeScript, infer query result types directly from those definitions, and generate SQL migrations through the drizzle-kit CLI. Unlike traditional ORMs that require separate model classes and type annotations, Drizzle derives types from the schema itself: define a table with pgTable or sqliteTable, and the TypeScript type of every query against that table is automatically inferred. Schema changes are tracked by drizzle-kit generate, which produces SQL migration files, and applied by drizzle-kit migrate or drizzle-kit push.
Because Drizzle’s schema DSL is pure TypeScript with no decorators or code generation step, AI coding tools produce it readily. The assistant writes table definitions, relations, and queries that compile without errors, pass TypeScript type checks, and run against the database. The code looks complete and correct at every static analysis level. Three specific review gaps appear consistently in AI-generated Drizzle ORM code — gaps that TypeScript validation cannot catch because they involve runtime database state, enforcement mechanics, and the boundary between declared schema expectations and actual data in the database.
The three Drizzle ORM code review traps
1. Migration statements run without transaction isolation
Drizzle’s migration tooling applies schema changes through SQL DDL statements — CREATE TABLE, ALTER TABLE, ADD COLUMN, and similar operations. AI-generated deployment scripts commonly invoke drizzle-kit push to apply the current schema state directly, or drizzle-kit migrate to run the generated migration files. The migration output is a sequence of SQL statements. In most Drizzle configurations, each statement runs independently: no transaction wraps the full migration, and no rollback is triggered automatically if a statement fails midway through a multi-statement migration file.
The review gap is that a partial migration leaves the database schema in a state that is neither the previous version nor the intended new version. A migration file that adds two tables and a foreign key constraint between them can succeed in creating the first table and fail on the second, leaving the first table present and unreferenced in a schema that has no corresponding TypeScript code yet merged to production. Subsequent deployments will attempt to create the second table again and may fail with a “table already exists” error that is difficult to diagnose because the migration system believes the first deployment was incomplete while the database believes part of it succeeded.
AI-generated migration code compounds this by not checking whether the database driver and version support transactional DDL. PostgreSQL supports transactional DDL — CREATE TABLE, ALTER TABLE, and DROP COLUMN can all be wrapped in a transaction and rolled back. MySQL and SQLite have more limited transactional DDL semantics: some DDL statements in MySQL cause an implicit commit, so a transaction boundary around a migration provides weaker guarantees than in PostgreSQL. AI-generated Drizzle migrations do not include this check, and the same migration strategy is applied regardless of the target database engine.
The review check: for every AI-generated Drizzle migration that contains more than one DDL statement, confirm whether the migration is wrapped in a transaction and whether the database engine supports transactional DDL for the specific statement types used. For PostgreSQL, verify that the migration client wraps the statements in BEGIN/COMMIT blocks or uses Drizzle’s db.transaction() helper. For MySQL deployments, identify any multi-statement migrations that include ALTER TABLE operations and confirm there is a manual rollback plan that does not depend on the database restoring the previous state automatically. Do not treat a migration that “applies successfully in staging” as evidence that partial failure on a different database state or engine version will be handled correctly.
2. TypeScript relations treated as database-enforced foreign key constraints
Drizzle provides a relations() function for defining relationships between tables. AI-generated code uses it extensively: one-to-one, one-to-many, and many-to-many relations are defined in the schema file alongside the table definitions, and queries use the with: option to perform relational joins. The TypeScript types for these joins are fully inferred: querying a users table with: { posts: true } returns a type that includes an array of Post objects nested under each user. The schema compiles; the query runs; the result is typed. Reviewers who see Drizzle relations() definitions alongside the table definitions treat them as evidence that referential integrity is enforced: the database knows about these relationships and will reject data that violates them.
The review gap is that Drizzle relations() definitions are TypeScript-only helpers. They exist to enable Drizzle’s query API to perform joins and infer result types. They do not create foreign key constraints in the database. A one() or many() relation declaration in Drizzle’s relations() function generates no FOREIGN KEY constraint in the migration output unless the actual foreign key reference is also declared with .references() on the column definition. AI-generated Drizzle schemas frequently include relations() definitions without the corresponding .references() on the column, producing schemas where the TypeScript API enforces relationship types at query time but the database imposes no constraint at insert or update time.
The practical consequence is that orphaned records accumulate silently. Deleting a user whose posts have no CASCADE constraint will succeed — the database will not reject it, the posts will remain with a userId pointing to a non-existent user, and Drizzle’s relational query for those posts will return them with a typed user field that is null even though the TypeScript type says it should be a User object. The data is inconsistent; the TypeScript types say it cannot be; the mismatch is invisible until a query downstream tries to access a property on the null user and throws at runtime.
The review check: for every relations() definition in AI-generated Drizzle schema code, verify whether a corresponding .references() declaration exists on the foreign key column. Open the migration SQL output and search for FOREIGN KEY or REFERENCES — if the migration file does not contain these keywords for a relationship that the TypeScript relations() definition describes, the database will not enforce it. If the application requires referential integrity (which it almost always does for owned data), add the .references() call to the column definition and confirm the migration output changes accordingly. Do not treat the presence of Drizzle relations() as confirmation that the database enforces those relationships.
3. Schema type inference trusted as runtime validation
Drizzle’s core design value is that TypeScript types are inferred from the schema definition rather than declared separately. Define a users table with a name: text(‘name’).notNull() column, and every Drizzle query against that table returns results typed as { name: string; ... } — not string | null, because the schema says notNull(). AI-generated application code takes full advantage of this: it accesses user.name without null checks, it passes Drizzle query results directly to functions that expect typed inputs, and it uses z.infer<typeof insertUserSchema> from Drizzle’s Zod integration as both database input validation and application-layer business logic validation. The code compiles; the types are consistent; reviewers who read through the TypeScript treat the type system as evidence that the application will not encounter unexpected null values or type mismatches at runtime.
The review gap is that Drizzle’s inferred types reflect what the schema declares, not what the database actually contains. The database can hold rows that violate TypeScript assumptions through three common paths. First, direct SQL inserts executed outside Drizzle — raw migrations, database management tools, scripts from before Drizzle was adopted, or other services that write to the same database without using Drizzle — can insert rows that satisfy the database’s own constraints but not Drizzle’s TypeScript types. A column marked notNull() in Drizzle might have been added with a DEFAULT NULL to avoid locking a large table during migration, meaning existing rows have null values that Drizzle’s TypeScript says are impossible. Second, schema changes that weaken constraints in the database without updating the Drizzle schema leave rows that contradict the TypeScript types. Third, Drizzle’s notNull() constraint on a column generates a NOT NULL SQL constraint, but database-level defaults and check constraints can still be bypassed by some database-specific operations.
When Drizzle reads a row that has null in a column declared notNull() in the schema, it returns the null value. TypeScript does not flag this at runtime — TypeScript types are a compile-time construct. The application code that received a { name: string } type now holds a { name: null } value and will throw a runtime error at the first property access or function call that assumes the value is a string. The error will appear to be an application bug in code that accesses the field, not a data quality bug in the rows returned by the query, making it harder to trace back to the mismatched schema assumption.
The review check: for AI-generated Drizzle code that accesses query results without null guards on notNull() columns, identify whether the application has any data ingestion path that bypasses Drizzle — raw SQL, another service, a data import script, or a migration that adds a column with a nullable default. If such paths exist, the TypeScript types do not guarantee the values Drizzle returns at runtime. Add runtime validation at the boundary where data enters the application from the database: a Zod schema parsed against the query result will surface type mismatches at query time rather than at a downstream property access. Do not treat Drizzle’s notNull() annotation in the schema definition as a runtime guarantee for rows that existed before the constraint was added or that were inserted outside Drizzle.
Reviewing Drizzle ORM code without treating TypeScript correctness as database correctness
Drizzle’s TypeScript-first design is a genuine engineering contribution: inferred types from schema definitions eliminate the sync drift between ORM models and type annotations that plagues heavier frameworks. The review problem is that TypeScript correctness is not database correctness. A schema that compiles cleanly can still apply migrations partially without rollback on failure, declare relationships that the database never enforces, and infer types for data that the database can return in states the TypeScript says are impossible.
A practical review approach for AI-generated Drizzle code: when you see a migration with multiple DDL statements, ask whether it is transactional and what happens if the third statement fails after the first two have committed. When you see relations() definitions, open the migration SQL and confirm FOREIGN KEY constraints are present — the TypeScript API and the database constraint are two separate things that must both exist. When you see query results used without null guards, ask whether any data path exists that bypasses Drizzle and could produce values that contradict the inferred types. TypeScript validates the schema as declared; these three questions cover what it does not.
Related reading: Supabase on reviewing AI-generated backend code where row-level security policies and database function boundaries create authorization gaps that compile correctly but enforce incorrectly at runtime. Prisma on reviewing AI-generated ORM code where schema migrations and type inference create similar database-versus-TypeScript trust boundaries. How to review AI-generated code for the general checklist that applies when AI generates database schema and query code for any TypeScript ORM.
The schema compiles. ZenCode checks whether it’s correct.
ZenCode surfaces one concrete review question before you commit — including when AI-generated Drizzle ORM code passes all TypeScript checks but carries silent migration gaps, unenforced relation constraints, or type inference that diverges from actual database state.
Try ZenCode free