Before you start
Commit your work first. Then install both packages at exactly 1.0.0-rc.5. drizzle-kit rc5 expects drizzle-orm 1.0.0-rc.5 or later in your project.
npm i -E drizzle-orm@1.0.0-rc.5npm i -D -E drizzle-kit@1.0.0-rc.5
pnpm add -E drizzle-orm@1.0.0-rc.5pnpm add -D -E drizzle-kit@1.0.0-rc.5
yarn add -E drizzle-orm@1.0.0-rc.5yarn add -D -E drizzle-kit@1.0.0-rc.5
bun add -E drizzle-orm@1.0.0-rc.5bun add -d -E drizzle-kit@1.0.0-rc.5
rc5 also needs newer versions of some packages it works with. If you use one of them, upgrade it first. npm refuses to install rc5 next to an older one.
effectand the@effect/sql-*packages: 4.0.0 or later.@tursodatabase/database,database-common,database-wasmandsync: 0.7.1 or later.@tursodatabase/serverless: 1.4.0 or later.typebox: 1.2.0 or later.@op-engineering/op-sqlite: 17 or later.
The examples use a small blog with two tables, users and posts. Each post has an author, and relations tells db.query that a user has many posts. snakeCase.table stores isActive in a column named is_active.
import { defineRelations } from "drizzle-orm";import { boolean, integer, serial, snakeCase, text } from "drizzle-orm/pg-core";
export const users = snakeCase.table("users", { id: serial().primaryKey(), name: text().notNull(), isActive: boolean().notNull().default(true),});
export const posts = snakeCase.table("posts", { id: serial().primaryKey(), authorId: integer().notNull(), title: text().notNull(),});
export const relations = defineRelations({ users, posts }, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId }), },}));Every database
Reads go to the primary by default
- Add
$replicato each query that should read from a replica.
A read replica is a copy of your database that only serves reads. withReplicas() puts your primary database and its replicas behind one db. In rc4, reads such as select and db.query went to a replica. Writes went to the primary.
In rc5, every query goes to the primary, reads included. You won’t get an error. Your replicas just stop getting reads, and your primary takes all the traffic. Add $replica where you want a replica:
rc4
rc5
import { withReplicas } from "drizzle-orm/pg-core";
const primary = drizzle({ client: new PGlite() });const replica = drizzle({ client: new PGlite() });const db = withReplicas(primary, [replica]);
// Read from a replicaawait db.select().from(posts);import { withReplicas } from "drizzle-orm/pg-core";
const primary = drizzle({ client: new PGlite() });const replica = drizzle({ client: new PGlite() });const db = withReplicas(primary, [replica]);
// Read from a replicaawait db.$replica.select().from(posts);Each time you use db.$replica, it picks a random replica. To choose the replica yourself, pass a function as the third argument of withReplicas(), like in rc4. db.$primary still works, but it’s deprecated, because queries already go to the primary.
undefined in a filter now throws
- Replace
undefinedvalues indb.queryfilters withEmptyFilter. - Check
ORandANDarrays that can be empty.
In rc4, db.query ignored a filter whose value was undefined. So if you forgot to set a value, the query quietly returned every row. In rc5, the query throws:
Unexpected 'undefined' in filter value. Use 'EmptyFilter' if you want the filter field to be skipped.It also throws for filters inside with, for operators like { eq: undefined }, and for where: undefined. TypeScript accepts all of these, so you only find them when the query runs.
When a filter is optional, pass EmptyFilter from drizzle-orm instead of undefined:
import { EmptyFilter } from "drizzle-orm";
function listUsers(isActive?: boolean) { return db.query.users.findMany({ where: { isActive: isActive ?? EmptyFilter }, });}listUsers() returns every user, and listUsers(false) returns only inactive users. You can also leave isActive out of where when you don’t need it.
An empty OR or AND array throws too. In rc4, where: { OR: [] } was ignored and returned every row. If you build OR from a list that can be empty, leave OR out when the list is empty.
posts: {} no longer filters
- Replace empty relation filters such as
posts: {}withposts: true.
In rc4, where: { posts: {} } returned users with at least one post. In rc5, an empty filter is skipped, even on a relation. The same query now returns every user, with no error.
Use posts: true for users with at least one post, and posts: false for users with none:
// Users with at least one postconst authors = await db.query.users.findMany({ where: { posts: true } });
// Users without postsconst readers = await db.query.users.findMany({ where: { posts: false } });DrizzleConfig takes one type argument
- If you typed your config, change
DrizzleConfig<typeof schema, typeof relations>toDrizzleConfig<typeof relations>.
The first type argument was for schema, the option the old relational queries used before defineRelations(). rc5 removes it, so TypeScript flags the old form.
Params are out of query errors
- Set
paramsInErrors: trueif your logs need query params.
Params are the values you pass into a query. They can hold emails, tokens or passwords. When a query fails in rc5, DrizzleQueryError leaves them out of its message:
Failed query: select "id", "name", "is_active" from "users" where "users"."name" = $1Its params field is undefined too, and its type now says so. Code that reads error.params.length gets a type error.
To get them back, set paramsInErrors: true in drizzle():
const db = drizzle({ client, relations, paramsInErrors: true });The error message then ends with a params: line. This setting only changes DrizzleQueryError. A logger still logs params, and the database error in error.cause can still quote a value.
db.query writes faster SQL for some queries
- If a
db.querycall with rawsqlbreaks in rc5, addstrategy: "basic"to it.
On PostgreSQL, MySQL, CockroachDB and MSSQL, db.query now writes different SQL for some queries, so the database can run them faster. It happens when a query loads relations with with and also uses offset, limit with orderBy, or a filter on a relation. Drizzle calls this the optimized strategy, and it’s the new default.
Most apps don’t need to do anything. It matters when raw sql in where or orderBy reads the posts that Drizzle joins for with: { posts: true }. This query sorts users by their number of posts. Drizzle puts each user’s posts in a column it names "posts"."r":
rc4
rc5
import { sql } from "drizzle-orm";
// Users with the most posts firstconst topAuthors = await db.query.users.findMany({ with: { posts: true }, orderBy: () => sql`json_array_length("posts"."r") desc`, limit: 10,});import { sql } from "drizzle-orm";
// Users with the most posts firstconst topAuthors = await db.query.users.findMany({ with: { posts: true }, orderBy: () => sql`json_array_length("posts"."r") desc`, limit: 10, strategy: "basic",});With the optimized strategy, rc5 sorts and limits users before it joins their posts. "posts"."r" doesn’t exist yet at that point, so PostgreSQL fails:
missing FROM-clause entry for table "posts"strategy: "basic" turns the change off for that query only.
"posts"."r" is a name Drizzle uses internally, so it’s safer not to rely on it. A subquery on the posts table counts the same posts, and it works with both strategies:
import { sql } from "drizzle-orm";
const topAuthors = await db.query.users.findMany({ with: { posts: true }, orderBy: (users) => sql`(select count(*) from ${posts} where ${posts.authorId} = ${users.id}) desc`, limit: 10,});Your cache now stores db.query results
- If your cache uses the
allstrategy, addwithCache: falsetodb.querycalls that must always read fresh data.
A cache with the all strategy caches every query without you asking, for example upstashCache({ global: true }). In rc4, it only cached query builder calls like db.select(). In rc5, it caches db.query results too, so a db.query call can now return a cached result. This doesn’t apply to CockroachDB and MSSQL, which have no cache.
PostgreSQL
Run drizzle-kit up again
- Commit, then run
drizzle-kit upanddrizzle-kit generate.
drizzle-kit keeps a snapshot.json next to each migration, a record of your schema at that point. rc5 stores PostgreSQL snapshots in a new format, version 9. rc4 wrote version 8, the "version" field at the top of each file. Until you convert your migrations folder, drizzle-kit generate, check and migrate stop with this error:
drizzle/20261021200606_init/snapshot.json is not of the latest version, please run "drizzle-kit up"npx drizzle-kit upnpx drizzle-kit generate
pnpm drizzle-kit uppnpm drizzle-kit generate
yarn drizzle-kit upyarn drizzle-kit generate
bunx drizzle-kit upbunx drizzle-kit generate
up rewrites each snapshot.json and leaves every migration.sql as it is. Your database doesn’t change. migrate() in your app reads the same SQL files and the same migrations table, so it doesn’t run your old migrations again. After up, generate should say No schema changes, nothing to migrate.
MySQL, SQLite, SingleStore, MSSQL and CockroachDB don’t need this step. Their snapshot format didn’t change, so rc5 reads their rc4 folders as they are. Running up on a SQLite folder, for example, changes nothing. Its snapshots stay at version 7.
--ignore-conflicts is gone on PostgreSQL
- Remove
--ignore-conflictsfrom your PostgreSQL scripts and CI.
When two Git branches each add a migration, drizzle-kit checks that they don’t change the same table, column or other database object. Migrations in v1 explains how. rc4 let you skip this check with --ignore-conflicts. On PostgreSQL, rc5 rejects that flag in generate, check and migrate. Other dialects keep it.
When generate finds a conflict, it now lists the migration folders that --fix-conflicts would delete. This flag is new in rc5, and PostgreSQL only. If you run generate --fix-conflicts, it deletes the conflicting migrations from both branches, and every migration after them. Then it writes one new migration from your schema.
The deleted migrations can include custom migrations, the ones whose SQL you wrote by hand. drizzle-kit lists them too. Copy any SQL you still need before you run --fix-conflicts.
postgres-js: stop stringifying JSON
- Remove
JSON.stringify()around values you pass to rawsqlor to the postgres-js client.
In rc4, drizzle() changed how your postgres-js client turns JSON values into text. Passing a plain object in raw sql failed, so most people wrapped it in JSON.stringify(). In rc5, drizzle() leaves the client alone, and postgres-js stringifies the object for you.
If you keep JSON.stringify(), the value gets stringified twice. Your column stores a JSON string instead of an object, and nothing errors:
rc4
rc5
const settings = { theme: "dark" };
// Stores {"theme": "dark"} in rc4// Stores "{\"theme\":\"dark\"}" in rc5await db.execute( sql`insert into profiles (settings) values (${JSON.stringify(settings)})`,);const settings = { theme: "dark" };
await db.execute( sql`insert into profiles (settings) values (${settings})`,);Raw queries you send through the postgres-js client itself have the same problem. Inserts and updates through Drizzle’s query builder, like db.insert(), stored JSON correctly in both versions. You don’t need to change them.
If you also stringified arrays, there’s one catch. An array inside sql turns into one param per item, like ($1, $2). To pass a JSON array as one value, wrap it in sql.param():
const tags = ["drizzle", "postgres"];
await db.execute( sql`insert into profiles (tags) values (${sql.param(tags)})`,);If your app already ran on rc5 with JSON.stringify(), this query finds the jsonb rows saved as strings. Check them before you fix them: a column can also hold strings on purpose.
select id from profiles where jsonb_typeof(settings) = 'string';Raw reads change a little too. Some array columns, like numeric[], now come back from raw queries as arrays instead of text like "{1.5}". Reads through the query builder didn’t change.
MySQL and SingleStore
- Set
supportBigNumbers: trueyourself if raw queries read largebigintvalues.
In rc4, drizzle() changed a setting on some clients without telling you. It turned on supportBigNumbers for a mysql2 connection made with createConnection(). With SingleStore, it did the same for the pool it created from connection: "<url>".
rc5 leaves your client as you set it up. Drizzle’s own queries, like db.select(), still read these values correctly, because Drizzle converts them itself. Raw queries through db.execute() or db.$client now get the driver’s defaults. With mysql2, a bigint above Number.MAX_SAFE_INTEGER can come back as a rounded number.
const connection = await mysql.createConnection({ uri: process.env.DATABASE_URL, supportBigNumbers: true,});- With
drizzle-orm/singlestore-proxy, delete theschemaoption fromdrizzle().
It was only used by the old relational queries. TypeScript flags it in rc5.
SQLite
sqlite-proxy: one function per method
- Split your proxy callback into three functions:
run,allandget.
drizzle-orm/sqlite-proxy sends each query to code you write, often an HTTP call to the server that holds the database. In the examples, remote() stands for that call. rc4 sent every query to one callback. rc5 takes one function per method:
rc4
rc5
import { drizzle } from "drizzle-orm/sqlite-proxy";
const db = drizzle(async (sql, params, method) => { const rows = await remote({ sql, params, method }); return { rows };});const db = drizzle({ run: (sql, params) => remote({ sql, params, method: "run" }), all: (sql, params, rowMode) => remote({ sql, params, method: "all", rowMode }), get: (sql, params, rowMode) => remote({ sql, params, method: "get", rowMode }),});Your functions now return different things:
- Return the data itself, not
{ rows }. - Return rows in the shape
rowModeasks for."array"asks for each row as an array of values."object"asks for objects keyed by column name. getreturns one row, orundefinedwhen there is none.runreturns your driver’s result. Drizzle uses its type as the result type ofrunqueries.valuesis gone. Drizzle callsallwithrowMode: "array"instead.- Batches go to an optional
batchfunction. It gets every query with itsmethodandrowMode, and returns one result per query.
$defaultFn now wins over .default()
- Check columns that have both
.default()and.$defaultFn().
.default() is the column’s default value. .$defaultFn() is a function Drizzle calls when you insert without a value. On SQLite, rc4 ignored $defaultFn when a column had both, and inserted the .default() value. rc5 calls $defaultFn, like PostgreSQL and MySQL already did:
export const invites = sqliteTable("invites", { id: integer().primaryKey(), // rc4 inserts "pending", rc5 inserts a new UUID code: text().notNull().default("pending").$defaultFn(() => crypto.randomUUID()),});.get() can return undefined in more types
- Fix the type errors that
.get()now reports.
.get() returns the first row, or undefined when there is none. Some types were missing the undefined. rc5 adds it, for example to db.get(sql), and to .returning().get() after an update or after an insert that can skip the row, like one with .onConflictDoNothing().
Most drivers already returned undefined there in rc4, so the new errors point at code that could already crash. durable-sqlite is the exception. In rc4, its .get() threw when the query didn’t return exactly one row. In rc5, it returns the first row, or undefined.
React Native
- With op-sqlite, upgrade
@op-engineering/op-sqliteto version 17 or later. - With op-sqlite or expo-sqlite, read
blobcolumns asUint8Array.
rc5 needs op-sqlite 17, and npm refuses to install it next to an older one. With both drivers, blob columns come back as Uint8Array, and you can write blobs without Buffer. For async queries with expo-sqlite, import from the new drizzle-orm/expo-sqlite/async.
CockroachDB and MSSQL
- Move from
db._queryandschematodb.queryanddefineRelations().
In rc4, these two dialects only had the old relational queries, db._query, set up with the schema option. rc5 replaces them with db.query, the same API as on PostgreSQL, and removes schema. Relational Queries v2 shows how to move your relations.
- CockroachDB: declare arrays with
.array()for one dimension and.array("[][]")for two.
CockroachDB arrays now use the same syntax as PostgreSQL. In rc4, .array() took a number instead.
drizzle-kit
drizzle-kit also changes in smaller ways:
- When
pushdrops a column, it now asks you to confirm only if at least one row has a value in that column. In rc4, PostgreSQL, CockroachDB, MySQL, SingleStore and SQLite asked whenever the table had rows. drizzle-kit skillsnow installs one agent skill,drizzle, with instructions your AI coding agent reads. It also removes the seven olderdrizzle-*skills from your project without asking. If you edited them, commit your edits first.- If you run drizzle-kit from code,
generateMigrationandpushSchemafromdrizzle-kit/payload/*anddrizzle-kit/api-postgresnow accept{ hints }. Hints are your answers to drizzle-kit’s prompts, the same ones the CLI takes with--hints. When an answer is missing, they throw withcode: "missing_hints". - TypeScript now rejects a
defineConfig()that mixes fields from different dialects or drivers.
Check your work
- Run your typecheck. The
DrizzleConfig,error.params,.get(), sqlite-proxy and CockroachDB array changes show up here. - Search your code for what TypeScript doesn’t catch:
withReplicas,JSON.stringify, empty relation filters likeposts: {},ORandANDbuilt from lists, values that can beundefinedinwhere, and--ignore-conflicts. On SQLite, also look for columns with both.default()and.$defaultFn(). - On PostgreSQL, run
drizzle-kit up, thendrizzle-kit generate. Expect no schema changes. - Run your tests against a real database before you deploy.
How we checked
We ran each example against 1.0.0-rc.4 and 1.0.0-rc.5 on PGlite, an in-memory PostgreSQL. The postgres-js examples ran through PGlite’s socket server, and the sqlite-proxy examples used node:sqlite. We generated PostgreSQL, MySQL, SQLite, SingleStore, MSSQL and CockroachDB migrations with drizzle-kit rc4, then ran rc5’s generate, check, migrate and up on them. The MySQL and SingleStore client changes come from the rc4 to rc5 source diff (GitHub, opens in a new tab), since we had no MySQL server to run them on.
The full lists are in the drizzle-orm changelog (GitHub, opens in a new tab) and the drizzle-kit changelog (GitHub, opens in a new tab). Coming from Drizzle Legacy (0.x)? Start with the Upgrade to v1 guide (Drizzle docs, opens in a new tab).