Upgrade to 0.3 (Drizzle ORM 1.x)
Move from better-drizzle 0.2 on Drizzle 0.x to 0.3 on Drizzle ORM 1.x with defineRelations.
Follow this guide when you move a project from better-drizzle@0.2.x on drizzle-orm 0.x to 0.3.x on Drizzle ORM 1.x. Runtime behavior changes and new features are listed in 0.3 changes.
better-drizzle 0.3.0 supports only Drizzle ORM 1.x: the drizzle-orm
peer range is >=1.0.0-rc.4 <1.0.0-rc.5, and the release is tested against
1.0.0-rc.4. Drizzle 1.x replaces the relations(...) helpers with
defineRelations(...) (relational queries v2). better-drizzle now reads
tables and relations from the Drizzle instance, so better() no longer takes
a schema option.
Staying on drizzle-orm 0.x
The 0.2.x line is the last one that works with drizzle-orm 0.x (peer
^0.30.0). It keeps the better(db, { schema }) API. Pin
better-drizzle@~0.2.0 until you upgrade Drizzle itself. There is no
release that supports both major versions of Drizzle.
At a glance
0.2.x (Drizzle 0.x) | 0.3.x (Drizzle 1.x) | |
|---|---|---|
drizzle-orm peer | ^0.30.0 | >=1.0.0-rc.4 <1.0.0-rc.5 |
| Relations | relations(table, ...) per table | one defineRelations(schema, ...) call |
| Drizzle instance | drizzle(client, { schema }) | drizzle({ client, relations }) |
| Client | better(db, { schema, ...options }) | better(db, options?) |
| Where tables come from | the schema option | db._.relations |
| Type parameter | typeof schema | typeof relations |
| Many-to-many | inferred junctions + relations.manyToMany | explicit .through() relations |
Plugin / hook ctx.schema | the schema module | the relations config |
| Driver errors | thrown as-is | wrapped in DrizzleQueryError, driver error on cause |
The delegate API keeps its methods, arguments, and result shapes, apart from
the type changes and
behavior changes listed below. That covers findMany, cursor, upsertMany,
include._count, connect / set, locks, $withContext, raw SQL,
transactions, and every plugin option. The migration touches the lines that
build the client and the type annotations that name the schema.
1. Upgrade drizzle-orm
npm install drizzle-orm@1.0.0-rc.4 better-drizzle@^0.3.0Follow Drizzle's own 1.x migration notes for driver and schema changes. The steps below cover what better-drizzle needs.
2. Move relations to defineRelations
Keep tables in schema.ts and declare every relation in one
defineRelations call:
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));import { defineRelations } from 'drizzle-orm';
import * as schema from './schema';
export const relations = defineRelations(schema, (r) => ({
users: { posts: r.many.posts() },
posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id }) },
}));Use defineRelations(schema) without a callback when the schema has no
relations. When two relations connect the same pair of tables, give both sides
the same alias. The old relationName option did this job.
3. Pass relations to drizzle() and drop schema from better()
import { schema } from './schema';
const db = drizzle(pool, { schema });
export const client = better(db, { schema, plugins });
import { relations } from './relations';
const db = drizzle({ client: pool, relations });
export const client = better(db, { plugins });better() reads tables and relations from db. Every other option -
plugins, hooks, raw, locks, transaction - is unchanged, and the
options argument is now optional: better(db). A Drizzle instance created
without relations has no tables to read, so better() throws
No tables found on the Drizzle instance. Pass your relations to drizzle(), e.g. drizzle({ client, relations: defineRelations(schema) }).
Only tables that appear in the relations config get a delegate. Pass the whole
schema module to defineRelations(schema, ...), not only the tables that have
relations.
4. Switch type generics to typeof relations
The client's type parameter is now the relations config:
type Db = BetterDrizzleClient<typeof schema>;
type Key = BetterTableKey<typeof schema>;
type UserWhere = WhereArg<typeof schema, 'users'>;
type Db = BetterDrizzleClient<typeof relations>;
type Key = BetterTableKey<typeof relations>;
type UserWhere = WhereArg<typeof relations, 'users'>;The same applies to explicit better<...>() calls and to every exported
helper type. The schema field of plugin setup and hook contexts is the
relations config as well. It is keyed by table key, and each entry is
{ table, name, relations }, so plugin code that iterated the schema module
and checked isTable(...) should read ctx.schema[key].table instead.
5. Replace many-to-many config with .through()
The relations: { inferManyToMany, manyToMany } option is removed, and
junction tables are no longer inferred. Declare many-to-many relations with
.through():
const client = better(db, {
schema,
relations: {
manyToMany: [
{ through: 'userGroups', left: { relation: 'user' }, right: { relation: 'group' } },
],
},
});
users: {
groups: r.many.groups({
from: r.users.id.through(r.userGroups.userId),
to: r.groups.id.through(r.userGroups.groupId),
}),
},Relation filters, include / select, _count, and connect /
disconnect / set work through .through() relations. A junction you
relied on through inference now needs an explicit .through() relation.
Without one, the relation does not exist and using it is a type error. See
relations.
6. Check error handling and plugin column requirements
- Drizzle 1.x wraps driver errors in
DrizzleQueryError(Failed query: ...) with the driver error ascause.BetterDrizzleError.from(...),getDatabaseErrorInfo, andisUniqueViolation/isForeignKeyViolation/isNotNullViolation/isCheckViolationread the wrapped driver error, so codes and constraint names still resolve. Code that inspects raw driver errors directly, such aserror.code === '23505'in acatcharound raw Drizzle calls, should readerror.cause. - Column
dataTypeis now"<type> <constraint>"(for examplenumber int32,object date,string uuid). A plugin'sconfig.requires.columns[].typematches the column'scolumnType(PgTimestamp), its fulldataType, or one part of it ('date','string'). Your own plugin code that comparescolumn.dataType === 'string'must compare the first part instead:column.dataType.startsWith('string'). - PostgreSQL array columns are now the element column with
dimensions > 0; Drizzle 1.x has noPgArraycolumn class orbaseColumn. Plugin code that detected arrays withinstanceof PgArrayshould checkcolumn.dimensions. - JSON path filters require
jsonb. At the type level Drizzle 1.x no longer tellsjsonfromjsonb, so a path filter on ajsoncolumn type-checks but throws at runtime:JSON path filters require a jsonb column; json columns only support whole-document filters. - On Bun SQLite, Drizzle 1.x requires raw
db.transactioncallbacks to be synchronous. better-drizzle'sclient.transaction(async (tx) => ...)still accepts async callbacks.
7. Typecheck
npx tsc --noEmitLeftover schema options, typeof schema generics, and manyToMany config
fail the typecheck.
The delegate argument types are also stricter in 0.3.0. Unknown keys are
rejected at the top level and inside where, data, create, update,
select, include, and orderBy, and so is select combined with
include at the same level. Previously TypeScript let them through. A typo such as { where: { emial: '...' } } is now a compile error.
When a check fails, remove the key or fix its name.
Other type changes the typecheck surfaces:
OrderTypewas removed frombetter-drizzleandbetter-drizzle/zod. Write'asc'/'desc'instead ofOrderType.Asc/OrderType.Desc.cursor()after/beforeaccept only a cursor object ornull. String cursors are a type error; the runtime already rejected them.nextCursorandpreviousCursorare now typed as that cursor object (ornull), so you can pass them straight back without a cast. See cursor pagination.create({ skipDuplicates })is typed nullable. WhenskipDuplicatesis set (and notfalse), the result type includesnull, matching the runtime result for a skipped insert. Handle thenullcase.update()anddelete()returnThrowingWriteResult. The type no longer exposes.explain(), which writes never supported at runtime..throw()is unchanged.
Breaking plugin API changes
better-drizzle/rules:mergeRuleswas removed. Usemerge({ extends, rules }).extendstakes one config or an array applied in order, andrulesholds overrides applied last. Falsy entries inextendsare skipped. See rules.
rules(mergeRules(safe(), { noRawMutation: 'warn' }));
rules(merge({ extends: [safe()], rules: { noRawMutation: 'warn' } }));Not supported with Drizzle 1.x
better-drizzle cannot load or filter by relations with a relation-level
where, or by one relations declared through a junction. Loading one
throws Relation "x" on "t" cannot be loaded: ..., and filtering through one
throws ... cannot be filtered: .... They still work
with Drizzle's own db.query API on the same db. See
limitations.
Plugin authors
ctx.schemainsetup, hooks, transforms, and extensions is the relations config. See step 4.- Declare typed per-table model extensions with an interface extending
ModelExtensionTypeResolver. Generic function resolvers still work, but with Drizzle 1.x types they can hitType instantiation is excessively deep(TS2589). See writing plugins. config.requires.columnsnow enforcesoptionalandtype. Anoptionalcolumn may be missing (it used to fail bootstrap anyway), and atypethat does not match the column fails bootstrap withPLUGIN_REQUIRED_COLUMN_TYPE;typeused to be informational. See setup and fail-fast requirements.- Plugins can add typed raw SQL options by augmenting the
RawOptionsExtensionsinterface ofbetter-drizzle. Raw hooks receive unknown options unchanged inrawOptions; the cache plugin'scache: { invalidate }raw option uses this. better-drizzle/zodneeds a Zod version with Zod 4-compatible schema types. The peer range is^3.25.0 || ^4.0.0.