NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #4298 most downloaded on npm
Drizzle ORM package for SQL databases
Last release 7 days ago
21 Sep 2026
Ships fairly regularly
a new release about every 2 weeks
Nearly every release is documented
notes for 60 of the last 60 stable releases
291 versions withdrawn
withdrawn after publishing
5 years old
564 releases · first in 2021
One column per quarter.
Fix AWS Data API type hints bugs in RQB
AnySQLiteUpdate - thanks @veloiiFix typings for indexes and allow creating indexes on 3+ columns mixing columns and expressions - thanks @lbguilherme!
Source: As mentioned, the serial type in Postgres is outdated and should be deprecated. Ideally, you should not use it. Identity columns are the recom…
drizzle-orm@0.32.0 and drizzle-kit@0.23.0It's not mandatory to upgrade both packages, but if you want to use the new features in both queries and migrations, you will need to upgrade both packages
$returningId() functionMySQL itself doesn't have native support for RETURNING after using INSERT. There is only one way to do it for primary keys with autoincrement (or serial) types, where you can access insertId and affectedRows fields. We've prepared an automatic way for you to handle such cases with Drizzle and automatically receive all inserted IDs as separate objects
import { boolean, int, text, mysqlTable } from 'drizzle-orm/mysql-core';
const usersTable = mysqlTable('users', {
id: int('id').primaryKey(),
name: text('name').notNull(),
verified: boolean('verified').notNull().default(false),
});
const result = await db.insert(usersTable).values([{ name: 'John' }, { name: 'John1' }]).$returningId();
// ^? { id: number }[]
Also with Drizzle, you can specify a primary key with $default function that will generate custom primary keys at runtime. We will also return those generated keys for you in the $returningId() call
import { varchar, text, mysqlTable } from 'drizzle-orm/mysql-core';
import { createId } from '@paralleldrive/cuid2';
const usersTableDefFn = mysqlTable('users_default_fn', {
customId: varchar('id', { length: 256 }).primaryKey().$defaultFn(createId),
name: text('name').notNull(),
});
const result = await db.insert(usersTableDefFn).values([{ name: 'John' }, { name: 'John1' }]).$returningId();
// ^? { customId: string }[]
If there is no primary keys -> type will be
{}[]for such queries
You can now specify sequences in Postgres within any schema you need and define all the available properties
import { pgSchema, pgSequence } from "drizzle-orm/pg-core";
// No params specified
export const customSequence = pgSequence("name");
// Sequence with params
export const customSequence = pgSequence("name", {
startWith: 100,
maxValue: 10000,
minValue: 100,
cycle: true,
cache: 10,
increment: 2
});
// Sequence in custom schema
export const customSchema = pgSchema('custom_schema');
export const customSequence = customSchema.sequence("name");
Source: As mentioned, the serial type in Postgres is outdated and should be deprecated. Ideally, you should not use it. Identity columns are the recommended way to specify sequences in your schema, which is why we are introducing the identity columns feature
import { pgTable, integer, text } from 'drizzle-orm/pg-core'
export const ingredients = pgTable("ingredients", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity({ startWith: 1000 }),
name: text("name").notNull(),
description: text("description"),
});
You can specify all properties available for sequences in the .generatedAlwaysAsIdentity() function. Additionally, you can specify custom names for these sequences
PostgreSQL docs reference.
You can now specify generated columns on any column supported by PostgreSQL to use with generated columns
tsvectorNote: we will add
tsVectorcolumn type before latest release
import { SQL, sql } from "drizzle-orm";
import { customType, index, integer, pgTable, text } from "drizzle-orm/pg-core";
const tsVector = customType<{ data: string }>({
dataType() {
return "tsvector";
},
});
export const test = pgTable(
"test",
{
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
content: text("content"),
contentSearch: tsVector("content_search", {
dimensions: 3,
}).generatedAlwaysAs(
(): SQL => sql`to_tsvector('english', ${test.content})`
),
},
(t) => ({
idx: index("idx_content_search").using("gin", t.contentSearch),
})
);
In case you don't need to reference any columns from your table, you can use just sql template or a string
export const users = pgTable("users", {
id: integer("id"),
name: text("name"),
generatedName: text("gen_name").generatedAlwaysAs(sql`hello world!`),
generatedName1: text("gen_name1").generatedAlwaysAs("hello world!"),
}),
You can now specify generated columns on any column supported by MySQL to use with generated columns
You can specify both stored and virtual options, for more info you can check MySQL docs
Also MySQL has a few limitation for such columns usage, which is described here
Drizzle Kit will also have limitations for push command:
You can't change the generated constraint expression and type using push. Drizzle-kit will ignore this change. To make it work, you would need to drop the column, push, and then add a column with a new expression. This was done due to the complex mapping from the database side, where the schema expression will be modified on the database side and, on introspection, we will get a different string. We can't be sure if you changed this expression or if it was changed and formatted by the database. As long as these are generated columns and push is mostly used for prototyping on a local database, it should be fast to drop and create generated columns. Since these columns are generated, all the data will be restored
generate should have no limitations
export const users = mysqlTable("users", {
id: int("id"),
id2: int("id2"),
name: text("name"),
generatedName: text("gen_name").generatedAlwaysAs(
(): SQL => sql`${schema2.users.name} || 'hello'`,
{ mode: "stored" }
),
generatedName1: text("gen_name1").generatedAlwaysAs(
(): SQL => sql`${schema2.users.name} || 'hello'`,
{ mode: "virtual" }
),
}),
In case you don't need to reference any columns from your table, you can use just sql template or a string in .generatedAlwaysAs()
You can now specify generated columns on any column supported by SQLite to use with generated columns
You can specify both stored and virtual options, for more info you can check SQLite docs
Also SQLite has a few limitation for such columns usage, which is described here
Drizzle Kit will also have limitations for push and generate command:
You can't change the generated constraint expression with the stored type in an existing table. You would need to delete this table and create it again. This is due to SQLite limitations for such actions. We will handle this case in future releases (it will involve the creation of a new table with data migration).
You can't add a stored generated expression to an existing column for the same reason as above. However, you can add a virtual expression to an existing column.
You can't change a stored generated expression in an existing column for the same reason as above. However, you can change a virtual expression.
You can't change the generated constraint type from virtual to stored for the same reason as above. However, you can change from stored to virtual.
PostgreSQL sequences, identity columns and generated columns for all dialects
--force for drizzle-kit pushYou can auto-accept all data-loss statements using the push command. It's only available in CLI parameters. Make sure you always use it if you are fine with running data-loss statements on your database
migrations flag prefixYou can now customize migration file prefixes to make the format suitable for your migration tools:
index is the default type and will result in 0001_name.sql file names;supabase and timestamp are equal and will result in 20240627123900_name.sql file names;unix will result in unix seconds prefixes 1719481298_name.sql file names;none will omit the prefix completely;import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
migrations: {
prefix: 'supabase'
}
});
Nothing published for this version
Mark prisma clients package as optional - thanks @Cherry
🛠️ Fixed RQB behavior for tables with same names in different schemas
import { PrismaClient } from '@prisma/client';
import { drizzle } from 'drizzle-orm/prisma/pg';
import { User } from './drizzle';
const prisma = new PrismaClient().$extends(drizzle());
const users = await prisma.$drizzle.select().from(User);
For more info, check docs: https://orm.drizzle.team/docs/prisma
🎉 Added support for TiDB Cloud Serverless driver:
🎉 Added support for TiDB Cloud Serverless driver:
import { connect } from '@tidbcloud/serverless';
import { drizzle } from 'drizzle-orm/tidb-serverless';
const client = connect({ url: '...' });
const db = drizzle(client);
await db.select().from(...);
> ### For a full explanation about Drizzle + Expo welcome to discussions
For a full explanation about Drizzle + Expo welcome to discussions
As of v0.31.1 Drizzle ORM now has native support for Expo SQLite Live Queries!
We've implemented a native useLiveQuery React Hook which observes necessary database changes and automatically re-runs database queries. It works with both SQL-like and Drizzle Queries:
import { useLiveQuery, drizzle } from 'drizzle-orm/expo-sqlite';
import { openDatabaseSync } from 'expo-sqlite/next';
import { users } from './schema';
import { Text } from 'react-native';
const expo = openDatabaseSync('db.db', { enableChangeListener: true }); // <-- enable change listeners
const db = drizzle(expo);
const App = () => {
// Re-renders automatically when data changes
const { data } = useLiveQuery(db.select().from(users));
// const { data, error, updatedAt } = useLiveQuery(db.query.users.findFirst());
// const { data, error, updatedAt } = useLiveQuery(db.query.users.findMany());
return <Text>{JSON.stringify(data)}</Text>;
};
export default App;
We've intentionally not changed the API of ORM itself to stay with conventional React Hook API, so we have useLiveQuery(databaseQuery) as opposed to db.select().from(users).useLive() or db.query.users.useFindMany()
We've also decided to provide data, error and updatedAt fields as a result of hook for concise explicit error handling following practices of React Query and Electric SQL
> Note: drizzle-orm@0.31.0 can be used with drizzle-kit@0.22.0 or higher. The same applies to Drizzle Kit. If you run a Drizzle Kit command, it will c
Note:
drizzle-orm@0.31.0can be used withdrizzle-kit@0.22.0or higher. The same applies to Drizzle Kit. If you run a Drizzle Kit command, it will check and prompt you for an upgrade (if needed). You can check for Drizzle Kit updates. below
The previous Drizzle+PostgreSQL indexes API was incorrect and was not aligned with the PostgreSQL documentation. The good thing is that it was not used in queries, and drizzle-kit didn't support all properties for indexes. This means we can now change the API to the correct one and provide full support for it in drizzle-kit
Previous API
.on..using and .on in our case are the same thing, so the API is incorrect here..asc(), .desc(), .nullsFirst(), and .nullsLast() should be specified for each column or expression on indexes, but not on an index itself.// Index declaration reference
index('name')
.on(table.column1, table.column2, ...) or .onOnly(table.column1, table.column2, ...)
.concurrently()
.using(sql``) // sql expression
.asc() or .desc()
.nullsFirst() or .nullsLast()
.where(sql``) // sql expression
Current API
// First example, with `.on()`
index('name')
.on(table.column1.asc(), table.column2.nullsFirst(), ...) or .onOnly(table.column1.desc().nullsLast(), table.column2, ...)
.concurrently()
.where(sql``)
.with({ fillfactor: '70' })
// Second Example, with `.using()`
index('name')
.using('btree', table.column1.asc(), sql`lower(${table.column2})`, table.column1.op('text_ops'))
.where(sql``) // sql expression
.with({ fillfactor: '70' })
There is no specific code to create an extension inside the Drizzle schema. We assume that if you are using vector types, indexes, and queries, you have a PostgreSQL database with the
pg_vectorextension installed.
You can now specify indexes for pg_vector and utilize pg_vector functions for querying, ordering, etc.
Let's take a few examples of pg_vector indexes from the pg_vector docs and translate them to Drizzle
// CREATE INDEX ON items USING hnsw (embedding vector_l2_ops);
// CREATE INDEX ON items USING hnsw (embedding vector_ip_ops);
// CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
const table = pgTable('items', {
embedding: vector('embedding', { dimensions: 3 })
}, (table) => ({
l2: index('l2_index').using('hnsw', table.embedding.op('vector_l2_ops'))
ip: index('ip_index').using('hnsw', table.embedding.op('vector_ip_ops'))
cosine: index('cosine_index').using('hnsw', table.embedding.op('vector_cosine_ops'))
}))
// CREATE INDEX ON items USING hnsw (embedding vector_l1_ops);
// CREATE INDEX ON items USING hnsw (embedding bit_hamming_ops);
// CREATE INDEX ON items USING hnsw (embedding bit_jaccard_ops);
const table = pgTable('table', {
embedding: vector('embedding', { dimensions: 3 })
}, (table) => ({
l1: index('l1_index').using('hnsw', table.embedding.op('vector_l1_ops'))
hamming: index('hamming_index').using('hnsw', table.embedding.op('bit_hamming_ops'))
bit: index('bit_jaccard_index').using('hnsw', table.embedding.op('bit_jaccard_ops'))
}))
For queries, you can use predefined functions for vectors or create custom ones using the SQL template operator.
You can also use the following helpers:
import { l2Distance, l1Distance, innerProduct,
cosineDistance, hammingDistance, jaccardDistance } from 'drizzle-orm'
l2Distance(table.column, [3, 1, 2]) // table.column <-> '[3, 1, 2]'
l1Distance(table.column, [3, 1, 2]) // table.column <+> '[3, 1, 2]'
innerProduct(table.column, [3, 1, 2]) // table.column <#> '[3, 1, 2]'
cosineDistance(table.column, [3, 1, 2]) // table.column <=> '[3, 1, 2]'
hammingDistance(table.column, '101') // table.column <~> '101'
jaccardDistance(table.column, '101') // table.column <%> '101'
If pg_vector has some other functions to use, you can replicate implimentation from existing one we have. Here is how it can be done
export function l2Distance(
column: SQLWrapper | AnyColumn,
value: number[] | string[] | TypedQueryBuilder<any> | string,
): SQL {
if (is(value, TypedQueryBuilder<any>) || typeof value === 'string') {
return sql`${column} <-> ${value}`;
}
return sql`${column} <-> ${JSON.stringify(value)}`;
}
Name it as you wish and change the operator. This example allows for a numbers array, strings array, string, or even a select query. Feel free to create any other type you want or even contribute and submit a PR
Let's take a few examples of pg_vector queries from the pg_vector docs and translate them to Drizzle
import { l2Distance } from 'drizzle-orm';
// SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;
db.select().from(items).orderBy(l2Distance(items.embedding, [3,1,2]))
// SELECT embedding <-> '[3,1,2]' AS distance FROM items;
db.select({ distance: l2Distance(items.embedding, [3,1,2]) })
// SELECT * FROM items ORDER BY embedding <-> (SELECT embedding FROM items WHERE id = 1) LIMIT 5;
const subquery = db.select({ embedding: items.embedding }).from(items).where(eq(items.id, 1));
db.select().from(items).orderBy(l2Distance(items.embedding, subquery)).limit(5)
// SELECT (embedding <#> '[3,1,2]') * -1 AS inner_product FROM items;
db.select({ innerProduct: sql`(${maxInnerProduct(items.embedding, [3,1,2])}) * -1` }).from(items)
// and more!
point, lineYou can now use point and line from PostgreSQL Geometric Types
Type point has 2 modes for mappings from the database: tuple and xy.
tuple will be accepted for insert and mapped on select to a tuple. So, the database Point(1,2) will be typed as [1,2] with drizzle.
xy will be accepted for insert and mapped on select to an object with x, y coordinates. So, the database Point(1,2) will be typed as { x: 1, y: 2 } with drizzle
const items = pgTable('items', {
point: point('point'),
pointObj: point('point_xy', { mode: 'xy' }),
});
Type line has 2 modes for mappings from the database: tuple and abc.
tuple will be accepted for insert and mapped on select to a tuple. So, the database Line{1,2,3} will be typed as [1,2,3] with drizzle.
abc will be accepted for insert and mapped on select to an object with a, b, and c constants from the equation Ax + By + C = 0. So, the database Line{1,2,3} will be typed as { a: 1, b: 2, c: 3 } with drizzle.
const items = pgTable('items', {
line: line('line'),
lineObj: point('line_abc', { mode: 'abc' }),
});
There is no specific code to create an extension inside the Drizzle schema. We assume that if you are using postgis types, indexes, and queries, you have a PostgreSQL database with the
postgisextension installed.
geometry type from postgis extension:
const items = pgTable('items', {
geo: geometry('geo', { type: 'point' }),
geoObj: geometry('geo_obj', { type: 'point', mode: 'xy' }),
geoSrid: geometry('geo_options', { type: 'point', mode: 'xy', srid: 4000 }),
});
mode
Type geometry has 2 modes for mappings from the database: tuple and xy.
tuple will be accepted for insert and mapped on select to a tuple. So, the database geometry will be typed as [1,2] with drizzle.xy will be accepted for insert and mapped on select to an object with x, y coordinates. So, the database geometry will be typed as { x: 1, y: 2 } with drizzletype
The current release has a predefined type: point, which is the geometry(Point) type in the PostgreSQL PostGIS extension. You can specify any string there if you want to use some other type
drizzle-kit@0.22.0Release notes here are partially duplicated from drizzle-kit@0.22.0
Drizzle Kit can now handle:
point and line from PostgreSQLvector from the PostgreSQL pg_vector extensiongeometry from the PostgreSQL PostGIS extensionextensionsFiltersThe PostGIS extension creates a few internal tables in the public schema. This means that if you have a database with the PostGIS extension and use push or introspect, all those tables will be included in diff operations. In this case, you would need to specify tablesFilter, find all tables created by the extension, and list them in this parameter.
We have addressed this issue so that you won't need to take all these steps. Simply specify extensionsFilters with the name of the extension used, and Drizzle will skip all the necessary tables.
Currently, we only support the postgis option, but we plan to add more extensions if they create tables in the public schema.
The postgis option will skip the geography_columns, geometry_columns, and spatial_ref_sys tables
import { defineConfig } from 'drizzle-kit'
export default defaultConfig({
dialect: "postgresql",
extensionsFilters: ["postgis"],
})
import { defineConfig } from 'drizzle-kit'
export default defaultConfig({
dialect: "postgresql",
dbCredentials: {
ssl: true, //"require" | "allow" | "prefer" | "verify-full" | options from node:tls
}
})
import { defineConfig } from 'drizzle-kit'
export default defaultConfig({
dialect: "mysql",
dbCredentials: {
ssl: "", // string | SslOptions (ssl options from mysql2 package)
}
})
libsql and better-sqlite3 driversThose drivers have different file path patterns, and Drizzle Kit will accept both and create a proper file path format for each
In this release MySQL and SQLite will properly map expressions into SQL query. Expressions won't be escaped in string but columns will be
export const users = sqliteTable(
'users',
{
id: integer('id').primaryKey(),
email: text('email').notNull(),
},
(table) => ({
emailUniqueIndex: uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),
}),
);
-- before
CREATE UNIQUE INDEX `emailUniqueIndex` ON `users` (`lower("users"."email")`);
-- now
CREATE UNIQUE INDEX `emailUniqueIndex` ON `users` (lower("email"));
push and generate works for indexesExample
index().on(table.id, table.email) // will work well and name will be autogeneretaed
index('my_name').on(table.id, table.email) // will work well
// but
index().on(sql`lower(${table.email})`) // error
index('my_name').on(sql`lower(${table.email})`) // will work well
.on() and .using().where() statements.op() on columnsIf you are using push workflows and want to change these fields in the index, you would need to:
For the generate command, drizzle-kit will be triggered by any changes in the index for any property in the new drizzle indexes API, so there are no limitations here.
`ts async function someFunction(categories: string[] = [], views = 0) { await db .select() .from(users) .where( and( gt(posts.views, views).if(views >
.if() function added to all WHERE expressionsasync function someFunction(categories: string[] = [], views = 0) {
await db
.select()
.from(users)
.where(
and(
gt(posts.views, views).if(views > 100),
inArray(posts.category, categories).if(categories.length > 0),
),
);
}
.all, .values, .execute functions in AWS DataAPI🐛 Fixed migrator in AWS Data API
setWhere and targetWhere fields to .onConflictDoUpdate() config in SQLite instead of single where fielddb._.fullSchema🎉 Added custom schema support to enums in Postgres (fixes #669 via #2048):
:warning: Only available in
drizzle-ormfor now,drizzle-kitsupport will arrive soon
import { pgSchema } from 'drizzle-orm/pg-core';
const mySchema = pgSchema('mySchema');
const colors = mySchema.enum('colors', ['red', 'green', 'blue']);
migrate() function to use batch API (#2137)where clause in Postgres .onConflictDoUpdate method into setWhere and targetWhere clauses, to support both where cases in on conflict ... clause (fixes #1628, #1302 via #2056)where clause in Postgres .onConflictDoNothing method, as it was placed in a wrong spot (fixes #1628 via #2056)Thanks @hugo082 and @livingforjesus!
Add mappings for @vercel/postgres package
@vercel/postgres packageneon drivers - #1542PGlite is a WASM Postgres build packaged into a TypeScript client library that enables you to run Postgres in the browser, Node.js and Bun, with no ne
PGlite is a WASM Postgres build packaged into a TypeScript client library that enables you to run Postgres in the browser, Node.js and Bun, with no need to install any other dependencies. It is only 2.6mb gzipped.
It can be used as an ephemeral in-memory database, or with persistence either to the file system (Node/Bun) or indexedDB (Browser).
Unlike previous "Postgres in the browser" projects, PGlite does not use a Linux virtual machine - it is simply Postgres in WASM.
Usage Example
import { PGlite } from '@electric-sql/pglite';
import { drizzle } from 'drizzle-orm/pglite';
// In-memory Postgres
const client = new PGlite();
const db = drizzle(client);
await db.select().from(users);
There are currently 2 limitations, that should be fixed on Pglite side:
Adds a dynamic update value to the column. The function will be called when the row is updated, and the returned value will be used as the column valu
$onUpdate functionality for PostgreSQL, MySQL and SQLiteAdds a dynamic update value to the column.
The function will be called when the row is updated, and the returned value will be used as the column value if none is provided.
If no default (or $defaultFn) value is provided, the function will be called when the row is inserted as well, and the returned value will be used as the column value.
Note: This value does not affect the
drizzle-kitbehavior, it is only used at runtime indrizzle-orm.
const usersOnUpdate = pgTable('users_on_update', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
updateCounter: integer('update_counter').default(sql`1`).$onUpdateFn(() => sql`update_counter + 1`),
updatedAt: timestamp('updated_at', { mode: 'date', precision: 3 }).$onUpdate(() => new Date()),
alwaysNull: text('always_null').$type<string | null>().$onUpdate(() => null),
});
Thanks @Angelelz and @gabrielDonnantuoni!
According their [official website](https://xata.io), Xata is a Postgres data platform with a focus on reliability, scalability, and developer experien
According their official website, Xata is a Postgres data platform with a focus on reliability, scalability, and developer experience. The Xata Postgres service is currently in beta, please see the Xata docs on how to enable it in your account.
Drizzle ORM natively supports both the xata driver with drizzle-orm/xata package and the postgres or pg drivers for accessing a Xata Postgres database.
The following example use the Xata generated client, which you obtain by running the xata init CLI command.
pnpm add drizzle-orm @xata.io/client
import { drizzle } from 'drizzle-orm/xata-http';
import { getXataClient } from './xata'; // Generated client
const xata = getXataClient();
const db = drizzle(xata);
const result = await db.select().from(...);
You can also connect to Xata using pg or postgres.js drivers
🎉 Added raw query support (db.execute(...)) to batch API in Neon HTTP driver
db.execute(...)) to batch API in Neon HTTP driver@neondatabase/serverless HTTP driver types issue (#1945, neondatabase/serverless#66).run() result (https://github.com/drizzle-team/drizzle-orm/pull/2038)LibSQL migrations have been updated to utilize batch execution instead of transactions. As stated in the documentation, LibSQL now supports batch oper
LibSQL migrations have been updated to utilize batch execution instead of transactions. As stated in the documentation, LibSQL now supports batch operations
A batch consists of multiple SQL statements executed sequentially within an implicit transaction. The backend handles the transaction: success commits all changes, while any failure results in a full rollback with no modifications.
```ts import { open } from '@op-engineering/op-sqlite'; import { drizzle } from 'drizzle-orm/op-sqlite';
Usage Example
import { open } from '@op-engineering/op-sqlite';
import { drizzle } from 'drizzle-orm/op-sqlite';
const opsqlite = open({
name: 'myDB',
});
const db = drizzle(opsqlite);
await db.select().from(users);
For more usage and setup details, please check our op-sqlite docs
The Postgres timestamp mapping has been changed to align all drivers with the same behavior.
The Postgres timestamp mapping has been changed to align all drivers with the same behavior.
❗ We've modified the postgres.js driver instance to always return strings for dates, and then Drizzle will provide you with either strings of mapped dates, depending on the selected mode. The only issue you may encounter is that once you provide the `postgres.js`` driver instance inside Drizzle, the behavior of this object will change for dates, which will always be strings.
We've made this change as a minor release, just as a warning, that:
If you were using timestamps and were waiting for a specific response, the behavior will now be changed.
When mapping to the driver, we will always use .toISOString for both timestamps with timezone and without timezone.
If you were using the postgres.js driver outside of Drizzle, all postgres.js clients passed to Drizzle will have mutated behavior for dates. All dates will be strings in the response.
Parsers that were changed for postgres.js.
const transparentParser = (val: any) => val;
// Override postgres.js default date parsers: https://github.com/porsager/postgres/discussions/761
for (const type of ['1184', '1082', '1083', '1114']) {
client.options.parsers[type as any] = transparentParser;
client.options.serializers[type as any] = transparentParser;
}
Ideally, as is the case with almost all other drivers, we should have the possibility to mutate mappings on a per-query basis, which means that the driver client won't be mutated. We will be reaching out to the creator of the postgres.js library to inquire about the possibility of specifying per-query mapping interceptors and making this flow even better for all users.
If we've overlooked this capability and it is already available with `postgres.js``, please ping us in our Discord!
A few more references for timestamps without and with timezones can be found in our docs
Big thanks to @Angelelz!
You can now use WITH statements with INSERT, UPDATE and DELETE statements
You can now use WITH statements with INSERT, UPDATE and DELETE statements
Usage examples
const averageAmount = db.$with('average_amount').as(
db.select({ value: sql`avg(${orders.amount})`.as('value') }).from(orders),
);
const result = await db
.with(averageAmount)
.delete(orders)
.where(gt(orders.amount, sql`(select * from ${averageAmount})`))
.returning({
id: orders.id,
});
Generated SQL:
with "average_amount" as (select avg("amount") as "value" from "orders")
delete from "orders"
where "orders"."amount" > (select * from "average_amount")
returning "id"
For more examples for all statements, check docs:
By default, all information about executed migrations will be stored in the database inside the __drizzle_migrations table,
and for PostgreSQL, inside the drizzle schema. However, you can configure where to store those records.
To add a custom table name for migrations stored inside your database, you should use the migrationsTable option
Usage example
await migrate(db, {
migrationsFolder: './drizzle',
migrationsTable: 'my_migrations',
});
Works only with PostgreSQL databases
To add a custom schema name for migrations stored inside your database, you should use the migrationsSchema option
Usage example
await migrate(db, {
migrationsFolder: './drizzle',
migrationsSchema: 'custom',
});
You can now use .query.findFirst and .query.findMany syntax with sqlite proxy driver
SQLite Proxy supports batch requests, the same as it's done for all other drivers. Check full docs
You will need to specify a specific callback for batch queries and handle requests to proxy server:
import { drizzle } from 'drizzle-orm/sqlite-proxy';
type ResponseType = { rows: any[][] | any[] }[];
const db = drizzle(
async (sql, params, method) => {
// single query logic
},
// new batch callback
async (
queries: {
sql: string;
params: any[];
method: 'all' | 'run' | 'get' | 'values';
}[],
) => {
try {
const result: ResponseType = await axios.post(
'http://localhost:3000/batch',
{ queries },
);
return result;
} catch (e: any) {
console.error('Error from sqlite proxy server:', e);
throw e;
}
},
);
And then you can use db.batch([]) method, that will proxy all queries
Response from the batch should be an array of raw values (an array within an array), in the same order as they were sent to the proxy server
> Warning: In this version, there are no breaking changes, but starting from version 0.30.0, you will encounter an error if you attempt to use anythin…
For more info you can check Neon docs
Example
const batchResponse: BatchType = await db.batch([
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({
id: usersTable.id,
}),
db.insert(usersTable).values({ id: 2, name: 'Dan' }),
db.query.usersTable.findMany({}),
db.query.usersTable.findFirst({}),
]);
type BatchType = [
{
id: number;
}[],
NeonHttpQueryResult<never>,
{
id: number;
name: string;
verified: number;
invitedBy: number | null;
}[],
{
id: number;
name: string;
verified: number;
invitedBy: number | null;
} | undefined,
];
Thanks to the database-js and PlanetScale teams, we have updated the default behavior and instances of database-js.
As suggested by the database-js core team, you should use the Client instance instead of connect():
import { Client } from '@planetscale/database';
import { drizzle } from 'drizzle-orm/planetscale-serverless';
// create the connection
const client = new Client({
host: process.env['DATABASE_HOST'],
username: process.env['DATABASE_USERNAME'],
password: process.env['DATABASE_PASSWORD'],
});
const db = drizzle(client);
Warning: In this version, there are no breaking changes, but starting from version
0.30.0, you will encounter an error if you attempt to use anything other than aClientinstance.We suggest starting to change connections to PlanetScale now to prevent any runtime errors in the future.
Previously our docs stated to use connect() and only this function was can be passed to drizzle. In this realase we are adding support for new Client() and deprecating connect(), by suggesting from database-js team. In this release you will see a warning when trying to pass connect() function result:
Warning text
Warning: You need to pass an instance of Client:
import { Client } from "@planetscale/database";
const client = new Client({
host: process.env["DATABASE_HOST"],
username: process.env["DATABASE_USERNAME"],
password: process.env["DATABASE_PASSWORD"],
});
const db = drizzle(client);
Starting from version 0.30.0, you will encounter an error if you attempt to use anything other than a Client instance.
Please make the necessary changes now to prevent any runtime errors in the future
fix: make expo peer dependencies optional #1714
Added improvements to the planescale relational tests #1579 - thanks @Angelelz
npm i eslint-plugin-drizzle@0.2.3
🎉 [ESLint] Add support for functions and improve error messages #1586 - thanks @ngregrichardson
For starting with Expo SQLite Driver, you need to install expo-sqlite and drizzle-orm packages.
npm install drizzle-orm expo-sqlite@next
Then, you can use it like this:
import { drizzle } from "drizzle-orm/expo-sqlite";
import { openDatabaseSync } from "expo-sqlite/next";
const expoDb = openDatabaseSync("db.db");
const db = drizzle(expoDb);
await db.select().from(...)...
// or
db.select().from(...).then(...);
// or
db.select().from(...).all();
If you want to use Drizzle Migrations, you need to update babel and metro configuration files.
babel-plugin-inline-import package.npm install babel-plugin-inline-import
babel.config.js and metro.config.js files.babel.config.js
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
+ plugins: [["inline-import", { "extensions": [".sql"] }]]
};
};
metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
+config.resolver.sourceExts.push('sql');
module.exports = config;
drizzle.config.ts file in your project root folder.import type { Config } from 'drizzle-kit';
export default {
schema: './db/schema.ts',
out: './drizzle',
driver: 'expo',
} satisfies Config;
After creating schema file and drizzle.config.ts file, you can generate migrations like this:
npx drizzle-kit generate:sqlite
Then you need to import migrations.js file in your App.tsx file from ./drizzle folder and use hook useMigrations or migrate function.
import { drizzle } from "drizzle-orm/expo-sqlite";
import { openDatabaseSync } from "expo-sqlite/next";
import { useMigrations } from 'drizzle-orm/expo-sqlite/migrator';
import migrations from './drizzle/migrations';
const expoDb = openDatabaseSync("db.db");
const db = drizzle(expoDb);
export default function App() {
const { success, error } = useMigrations(db, migrations);
if (error) {
return (
<View>
<Text>Migration error: {error.message}</Text>
</View>
);
}
if (!success) {
return (
<View>
<Text>Migration is in progress...</Text>
</View>
);
}
return ...your application component;
}
This plugin exports an `all` config that makes use of all rules (except for deprecated ones).
You can now access more information, hints, documentation links, etc. while developing and using JSDoc right in your IDE. Previously, we had them only for filter expressions, but now you can see them for all parts of the Drizzle query builder
Remember, aggregation functions are often used with the GROUP BY clause of the SELECT statement. So if you are selecting using aggregating functions and other columns in one query, be sure to use the
.groupByclause
Here is a list of functions and equivalent using sql template
count
await db.select({ value: count() }).from(users);
await db.select({ value: count(users.id) }).from(users);
// It's equivalent to writing
await db.select({
value: sql`count('*'))`.mapWith(Number)
}).from(users);
await db.select({
value: sql`count(${users.id})`.mapWith(Number)
}).from(users);
countDistinct
await db.select({ value: countDistinct(users.id) }).from(users);
// It's equivalent to writing
await db.select({
value: sql`count(${users.id})`.mapWith(Number)
}).from(users);
avg
await db.select({ value: avg(users.id) }).from(users);
// It's equivalent to writing
await db.select({
value: sql`avg(${users.id})`.mapWith(String)
}).from(users);
avgDistinct
await db.select({ value: avgDistinct(users.id) }).from(users);
// It's equivalent to writing
await db.select({
value: sql`avg(distinct ${users.id})`.mapWith(String)
}).from(users);
sum
await db.select({ value: sum(users.id) }).from(users);
// It's equivalent to writing
await db.select({
value: sql`sum(${users.id})`.mapWith(String)
}).from(users);
sumDistinct
await db.select({ value: sumDistinct(users.id) }).from(users);
// It's equivalent to writing
await db.select({
value: sql`sum(distinct ${users.id})`.mapWith(String)
}).from(users);
max
await db.select({ value: max(users.id) }).from(users);
// It's equivalent to writing
await db.select({
value: sql`max(${expression})`.mapWith(users.id)
}).from(users);
min
await db.select({ value: min(users.id) }).from(users);
// It's equivalent to writing
await db.select({
value: sql`min(${users.id})`.mapWith(users.id)
}).from(users);
For cases where it's impossible to perform type checks for specific scenarios, or where it's possible but error messages would be challenging to understand, we've decided to create an ESLint package with recommended rules. This package aims to assist developers in handling crucial scenarios during development
Big thanks to @Angelelz for initiating the development of this package and transferring it to the Drizzle Team's npm
[ npm | yarn | pnpm | bun ] install eslint eslint-plugin-drizzle
You can install those packages for typescript support in your IDE
[ npm | yarn | pnpm | bun ] install @typescript-eslint/eslint-plugin @typescript-eslint/parser
Create a .eslintrc.yml file, add drizzle to the plugins, and specify the rules you want to use. You can find a list of all existing rules below
root: true
parser: '@typescript-eslint/parser'
parserOptions:
project: './tsconfig.json'
plugins:
- drizzle
rules:
'drizzle/enforce-delete-with-where': "error"
'drizzle/enforce-update-with-where': "error"
This plugin exports an all config that makes use of all rules (except for deprecated ones).
root: true
extends:
- "plugin:drizzle/all"
parser: '@typescript-eslint/parser'
parserOptions:
project: './tsconfig.json'
plugins:
- drizzle
At the moment, all is equivalent to recommended
root: true
extends:
- "plugin:drizzle/recommended"
parser: '@typescript-eslint/parser'
parserOptions:
project: './tsconfig.json'
plugins:
- drizzle
enforce-delete-with-where: Enforce using delete with the.where() clause in the .delete() statement. Most of the time, you don't need to delete all rows in the table and require some kind of WHERE statements.
Error Message:
Without `.where(...)` you will delete all the rows in a table. If you didn't want to do it, please use `db.delete(...).where(...)` instead. Otherwise you can ignore this rule here
Optionally, you can define a drizzleObjectName in the plugin options that accept a string or string[]. This is useful when you have objects or classes with a delete method that's not from Drizzle. Such a delete method will trigger the ESLint rule. To avoid that, you can define the name of the Drizzle object that you use in your codebase (like db) so that the rule would only trigger if the delete method comes from this object:
Example, config 1:
"rules": {
"drizzle/enforce-delete-with-where": ["error"]
}
class MyClass {
public delete() {
return {}
}
}
const myClassObj = new MyClass();
// ---> Will be triggered by ESLint Rule
myClassObj.delete()
const db = drizzle(...)
// ---> Will be triggered by ESLint Rule
db.delete()
Example, config 2:
"rules": {
"drizzle/enforce-delete-with-where": ["error", { "drizzleObjectName": ["db"] }],
}
class MyClass {
public delete() {
return {}
}
}
const myClassObj = new MyClass();
// ---> Will NOT be triggered by ESLint Rule
myClassObj.delete()
const db = drizzle(...)
// ---> Will be triggered by ESLint Rule
db.delete()
enforce-update-with-where: Enforce using update with the.where() clause in the .update() statement. Most of the time, you don't need to update all rows in the table and require some kind of WHERE statements.
Error Message:
Without `.where(...)` you will update all the rows in a table. If you didn't want to do it, please use `db.update(...).set(...).where(...)` instead. Otherwise you can ignore this rule here
Optionally, you can define a drizzleObjectName in the plugin options that accept a string or string[]. This is useful when you have objects or classes with a delete method that's not from Drizzle. Such as update method will trigger the ESLint rule. To avoid that, you can define the name of the Drizzle object that you use in your codebase (like db) so that the rule would only trigger if the delete method comes from this object:
Example, config 1:
"rules": {
"drizzle/enforce-update-with-where": ["error"]
}
class MyClass {
public update() {
return {}
}
}
const myClassObj = new MyClass();
// ---> Will be triggered by ESLint Rule
myClassObj.update()
const db = drizzle(...)
// ---> Will be triggered by ESLint Rule
db.update()
Example, config 2:
"rules": {
"drizzle/enforce-update-with-where": ["error", { "drizzleObjectName": ["db"] }],
}
class MyClass {
public update() {
return {}
}
}
const myClassObj = new MyClass();
// ---> Will NOT be triggered by ESLint Rule
myClassObj.update()
const db = drizzle(...)
// ---> Will be triggered by ESLint Rule
db.update()
…to upgrade Drizzle Kit. This may result in some breaking changes throughout the versions, especially if you need to upgrade Drizzle Kit and your Drizz…
Drizzle ORM version
0.29.0will require a minimum Drizzle Kit version of0.20.0, and vice versa. Therefore, when upgrading to a newer version of Drizzle ORM, you will also need to upgrade Drizzle Kit. This may result in some breaking changes throughout the versions, especially if you need to upgrade Drizzle Kit and your Drizzle ORM version is older than<0.28.0
unsigned option for bigintYou can now specify bigint unsigned type
const table = mysqlTable('table', {
id: bigint('id', { mode: 'number', unsigned: true }),
});
Read more in docs
Starting from 0.29.0 by default, as all the query builders in Drizzle try to conform to SQL as much as possible, you can only invoke most of the methods once. For example, in a SELECT statement there might only be one WHERE clause, so you can only invoke .where() once:
const query = db
.select()
.from(users)
.where(eq(users.id, 1))
.where(eq(users.name, 'John')); // ❌ Type error - where() can only be invoked once
This behavior is useful for conventional query building, i.e. when you create the whole query at once. However, it becomes a problem when you want to build a query dynamically, i.e. if you have a shared function that takes a query builder and enhances it. To solve this problem, Drizzle provides a special 'dynamic' mode for query builders, which removes the restriction of invoking methods only once. To enable it, you need to call .$dynamic() on a query builder.
Let's see how it works by implementing a simple withPagination function that adds LIMIT and OFFSET clauses to a query based on the provided page number and an optional page size:
function withPagination<T extends PgSelect>(
qb: T,
page: number,
pageSize: number = 10,
) {
return qb.limit(pageSize).offset(page * pageSize);
}
const query = db.select().from(users).where(eq(users.id, 1));
withPagination(query, 1); // ❌ Type error - the query builder is not in dynamic mode
const dynamicQuery = query.$dynamic();
withPagination(dynamicQuery, 1); // ✅ OK
Note that the withPagination function is generic, which allows you to modify the result type of the query builder inside it, for example by adding a join:
function withFriends<T extends PgSelect>(qb: T) {
return qb.leftJoin(friends, eq(friends.userId, users.id));
}
let query = db.select().from(users).where(eq(users.id, 1)).$dynamic();
query = withFriends(query);
Read more in docs
There is an issue when constraint names exceed the 64-character limit of the database. This causes the database engine to truncate the name, potentially leading to issues. Starting from 0.29.0, you have the option to specify custom names for both primaryKey() and foreignKey(). We have also deprecated the old primaryKey() syntax, which can still be used but will be removed in future releases
const table = pgTable('table', {
id: integer('id'),
name: text('name'),
}, (table) => ({
cpk: primaryKey({ name: 'composite_key', columns: [table.id, table.name] }),
cfk: foreignKey({
name: 'fkName',
columns: [table.id],
foreignColumns: [table.name],
}),
}));
Read more in docs
You can now use the Drizzle withReplica function to specify different database connections for read replicas and the main instance for write operations. By default, withReplicas will use a random read replica for read operations and the main instance for all other data modification operations. You can also specify custom logic for choosing which read replica connection to use. You have the freedom to make any weighted, custom decision for that. Here are some usage examples:
const primaryDb = drizzle(client);
const read1 = drizzle(client);
const read2 = drizzle(client);
const db = withReplicas(primaryDb, [read1, read2]);
// read from primary
db.$primary.select().from(usersTable);
// read from either read1 connection or read2 connection
db.select().from(usersTable)
// use primary database for delete operation
db.delete(usersTable).where(eq(usersTable.id, 1))
Implementation example of custom logic for selecting read replicas, where the first replica has a 70% chance of being chosen, and the second replica has a 30% chance of being chosen. Note that you can implement any type of random selection for read replicas
const db = withReplicas(primaryDb, [read1, read2], (replicas) => {
const weight = [0.7, 0.3];
let cumulativeProbability = 0;
const rand = Math.random();
for (const [i, replica] of replicas.entries()) {
cumulativeProbability += weight[i]!;
if (rand < cumulativeProbability) return replica;
}
return replicas[0]!
});
withReplicas function is available for all dialects in Drizzle ORM
Read more in docs
Huge thanks to @Angelelz for the significant contribution he made, from API discussions to proper type checks and runtime logic, along with an extensive set of tests. This greatly assisted us in delivering this feature in this release
Usage examples:
All set operators can be used in a two ways: import approach or builder approach
import { union } from 'drizzle-orm/pg-core'
const allUsersQuery = db.select().from(users);
const allCustomersQuery = db.select().from(customers);
const result = await union(allUsersQuery, allCustomersQuery)
const result = await db.select().from(users).union(db.select().from(customers));
Read more in docs
A new driver has been released, allowing you to create your own implementation for an HTTP driver using a MySQL database. You can find usage examples in the ./examples/mysql-proxy folder
You need to implement two endpoints on your server that will be used for queries and migrations(Migrate endpoint is optional and only if you want to use drizzle migrations). Both the server and driver implementation are up to you, so you are not restricted in any way. You can add custom mappings, logging, and much more
You can find both server and driver implementation examples in the ./examples/mysql-proxy folder
// Driver
import axios from 'axios';
import { eq } from 'drizzle-orm/expressions';
import { drizzle } from 'drizzle-orm/mysql-proxy';
import { migrate } from 'drizzle-orm/mysql-proxy/migrator';
import { cities, users } from './schema';
async function main() {
const db = drizzle(async (sql, params, method) => {
try {
const rows = await axios.post(`${process.env.REMOTE_DRIVER}/query`, {
sql,
params,
method,
});
return { rows: rows.data };
} catch (e: any) {
console.error('Error from pg proxy server:', e.response.data);
return { rows: [] };
}
});
await migrate(db, async (queries) => {
try {
await axios.post(`${process.env.REMOTE_DRIVER}/migrate`, { queries });
} catch (e) {
console.log(e);
throw new Error('Proxy server cannot run migrations');
}
}, { migrationsFolder: 'drizzle' });
await db.insert(cities).values({ id: 1, name: 'name' });
await db.insert(users).values({
id: 1,
name: 'name',
email: 'email',
cityId: 1,
});
const usersToCityResponse = await db.select().from(users).leftJoin(
cities,
eq(users.cityId, cities.id),
);
}
Same as MySQL you can now implement your own http driver for PostgreSQL database. You can find usage examples in the ./examples/pg-proxy folder
You need to implement two endpoints on your server that will be used for queries and migrations (Migrate endpoint is optional and only if you want to use drizzle migrations). Both the server and driver implementation are up to you, so you are not restricted in any way. You can add custom mappings, logging, and much more
You can find both server and driver implementation examples in the ./examples/pg-proxy folder
import axios from 'axios';
import { eq } from 'drizzle-orm/expressions';
import { drizzle } from 'drizzle-orm/pg-proxy';
import { migrate } from 'drizzle-orm/pg-proxy/migrator';
import { cities, users } from './schema';
async function main() {
const db = drizzle(async (sql, params, method) => {
try {
const rows = await axios.post(`${process.env.REMOTE_DRIVER}/query`, { sql, params, method });
return { rows: rows.data };
} catch (e: any) {
console.error('Error from pg proxy server:', e.response.data);
return { rows: [] };
}
});
await migrate(db, async (queries) => {
try {
await axios.post(`${process.env.REMOTE_DRIVER}/query`, { queries });
} catch (e) {
console.log(e);
throw new Error('Proxy server cannot run migrations');
}
}, { migrationsFolder: 'drizzle' });
const insertedCity = await db.insert(cities).values({ id: 1, name: 'name' }).returning();
const insertedUser = await db.insert(users).values({ id: 1, name: 'name', email: 'email', cityId: 1 });
const usersToCityResponse = await db.select().from(users).leftJoin(cities, eq(users.cityId, cities.id));
}
D1 Batch API supportReference: https://developers.cloudflare.com/d1/platform/client-api/#dbbatch
Batch API usage example:
const batchResponse = await db.batch([
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({
id: usersTable.id,
}),
db.update(usersTable).set({ name: 'Dan' }).where(eq(usersTable.id, 1)),
db.query.usersTable.findMany({}),
db.select().from(usersTable).where(eq(usersTable.id, 1)),
db.select({ id: usersTable.id, invitedBy: usersTable.invitedBy }).from(
usersTable,
),
]);
Type for batchResponse in this example would be:
type BatchResponse = [
{
id: number;
}[],
D1Result,
{
id: number;
name: string;
verified: number;
invitedBy: number | null;
}[],
{
id: number;
name: string;
verified: number;
invitedBy: number | null;
}[],
{
id: number;
invitedBy: number | null;
}[],
];
All possible builders that can be used inside db.batch:
`db.all()`,
`db.get()`,
`db.values()`,
`db.run()`,
`db.query.<table>.findMany()`,
`db.query.<table>.findFirst()`,
`db.select()...`,
`db.update()...`,
`db.delete()...`,
`db.insert()...`,
More usage examples here: integration-tests/tests/d1-batch.test.ts and in docs
defineConfig functionbigint unsigned supportprimaryKeys and foreignKeys now can have custom namesYou can read more about drizzle-kit updates here
> MySQL datetime with mode: 'date' will now store dates in UTC strings and retrieve data in UTC as well to align with MySQL behavior for datetime. If
Note: MySQL
datetimewithmode: 'date'will now store dates in UTC strings and retrieve data in UTC as well to align with MySQL behavior fordatetime. If you need a different behavior and want to handledatetimemapping in a different way, please usemode: 'string'or Custom Types implementation
Check Fix Datetime mapping for MySQL for implementation details
LibSQL batch api supportReference: https://docs.turso.tech/reference/client-access/javascript-typescript-sdk#execute-a-batch-of-statements
Batch API usage example:
const batchResponse = await db.batch([
db.insert(usersTable).values({ id: 1, name: 'John' }).returning({
id: usersTable.id,
}),
db.update(usersTable).set({ name: 'Dan' }).where(eq(usersTable.id, 1)),
db.query.usersTable.findMany({}),
db.select().from(usersTable).where(eq(usersTable.id, 1)),
db.select({ id: usersTable.id, invitedBy: usersTable.invitedBy }).from(
usersTable,
),
]);
Type for batchResponse in this example would be:
type BatchResponse = [
{
id: number;
}[],
ResultSet,
{
id: number;
name: string;
verified: number;
invitedBy: number | null;
}[],
{
id: number;
name: string;
verified: number;
invitedBy: number | null;
}[],
{
id: number;
invitedBy: number | null;
}[],
];
All possible builders that can be used inside db.batch:
`db.all()`,
`db.get()`,
`db.values()`,
`db.run()`,
`db.query.<table>.findMany()`,
`db.query.<table>.findFirst()`,
`db.select()...`,
`db.update()...`,
`db.delete()...`,
`db.insert()...`,
More usage examples here: integration-tests/tests/libsql-batch.test.ts and in docs
Example
const test = sqliteTable('test', {
dataTyped: text('data_typed', { mode: 'json' }).$type<{ a: 1 }>().notNull(),
});
.toSQL() to Relational Query API callsExample
const query = db.query.usersTable.findFirst().toSQL();
List of operators and usage examples
arrayContains, arrayContained, arrayOverlaps
const contains = await db.select({ id: posts.id }).from(posts)
.where(arrayContains(posts.tags, ['Typescript', 'ORM']));
const contained = await db.select({ id: posts.id }).from(posts)
.where(arrayContained(posts.tags, ['Typescript', 'ORM']));
const overlaps = await db.select({ id: posts.id }).from(posts)
.where(arrayOverlaps(posts.tags, ['Typescript', 'ORM']));
const withSubQuery = await db.select({ id: posts.id }).from(posts)
.where(arrayContains(
posts.tags,
db.select({ tags: posts.tags }).from(posts).where(eq(posts.id, 1)),
));
Before
import { inArray } from "drizzle-orm/pg-core";
await db.users.findFirst({
where: (table, _) => inArray(table.id, [ ... ])
})
After
await db.users.findFirst({
where: (table, { inArray }) => inArray(table.id, [ ... ])
})
🐛 Fixed incorrect OpenTelemetry type import that caused a runtime error
The OpenTelemetry logic currently present in the ORM isn't meant to be used by Drizzle and no stats have ever been collected by Drizzle using drizzle-orm. OpenTelemetry is simply a protocol. If you take a look at the actual code that utilizes it in drizzle-orm, it simply uses the tracer to collect the query stats and doesn't send it anywhere. It was designed for the ORM users to be able to send those stats to their own telemetry consumers.
The important thing is - the OpenTelemetry logic is disabled on the current version. It literally does nothing. We experimented with it at some point in the past, but disabled it before the release.
As to the reason of the issue in the last release: it happened because of an incorrect type import on this line - https://github.com/drizzle-team/drizzle-orm/blob/594e96538e588fee5748e372884dbaf32c331524/drizzle-orm/src/tracing.ts#L1. We've used import { type ... } syntax instead of import type { ... }, which resulted in the import '@opentelemetry/api' line leaking to the runtime.
🐛 Fixed imports in ESM-based projects
⚠ If you are facing a Cannot find package '@opentelemetry/api' error, please update to 0.28.5, it's fixed there.
🛠 Deprecated InferModel type in favor of more explicit InferSelectModel and InferInsertModel
0.28.3 release introduced a bug with types specifically for esm projects. Everything works as expected with cjs. If you're experiencing any type-related bugs as described in https://github.com/drizzle-team/drizzle-orm/issues/1090, please downgrade to 0.28.2. We are working on releasing 0.28.4 with all the fixes asap🎉 Added SQLite simplified query API
🎉 Added .$defaultFn() / .$default() methods to column builders
You can specify any logic and any implementation for a function like cuid() for runtime defaults. Drizzle won't limit you in the number of implementations you can add.
Note: This value does not affect the
drizzle-kitbehavior, it is only used at runtime indrizzle-orm
import { varchar, mysqlTable } from "drizzle-orm/mysql-core";
import { createId } from '@paralleldrive/cuid2';
const table = mysqlTable('table', {
id: varchar('id', { length: 128 }).$defaultFn(() => createId()),
});
🎉 Added table.$inferSelect / table._.inferSelect and table.$inferInsert / table._.inferInsert for more convenient table model type inference
🛠 Deprecated InferModel type in favor of more explicit InferSelectModel and InferInsertModel
import { InferSelectModel, InferInsertModel } from 'drizzle-orm'
const usersTable = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
verified: boolean('verified').notNull().default(false),
jsonb: jsonb('jsonb').$type<string[]>(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});
type SelectUser = typeof usersTable.$inferSelect;
type InsertUser = typeof usersTable.$inferInsert;
type SelectUser2 = InferSelectModel<typeof usersTable>;
type InsertUser2 = InferInsertModel<typeof usersTable>;
.d.ts files bundling.get() when the result is emptyAdded a set of tests for d1. Thanks to @AdiRishi!
Added a support for Typebox in drizzle-typebox package. Thanks to @Bulbang!
Please check documentation page for more usage examples: https://orm.drizzle.team/docs/typebox
🐛 Fixed Postgres array-related issues introduced by 0.28.0 (#983, #992)
Current example won't work in 0.28.0:
Current example won't work in 0.28.0:
const usersWithPosts = await db.query.users.findMany({
where: (table, { sql }) => (sql`json_array_length(${table.posts}) > 0`),
with: {
posts: true,
},
});
The table object in the where callback won't have fields from with and extras. We removed them to be able to build more efficient relational queries, which improved row reads and performance.
If you have used those fields in the where callback before, there are several workarounds:
mode config for mysql2 driverDrizzle relational queries always generate exactly one SQL statement to run on the database and it has certain caveats. To have best in class support for every database out there we've introduced modes.
Drizzle relational queries use lateral joins of subqueries under the hood and for now PlanetScale does not support them.
When using mysql2 driver with regular MySQL database - you should specify mode: "default".
When using mysql2 driver with PlanetScale - you need to specify mode: "planetscale".
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import * as schema from './schema';
const connection = await mysql.createConnection({
uri: process.env.PLANETSCALE_DATABASE_URL,
});
const db = drizzle(connection, { schema, mode: 'planetscale' });
We've run the diagnostics on a database schema with 85 tables, 666 columns, 26 enums, 172 indexes and 133 foreign keys. We've optimized internal types which resulted in 430% speed up in IntelliSense.
In this release we've fully changed a way query is generated for Relational Queri API.
As a summary we've made current set of changes in query generation startegy:
Lateral Joins: In the new version we're utilizing lateral joins, denoted by the "LEFT JOIN LATERAL" clauses, to retrieve specific data from related tables efficiently For MySQL in PlanetScale and SQLite, we've used simple subquery selects, which improved a query plan and overall performance
Selective Data Retrieval: In the new version we're retrieving only the necessary data from tables. This targeted data retrieval reduces the amount of unnecessary information fetched, resulting in a smaller dataset to process and faster execution.
Reduced Aggregations: In the new version we've reduced the number of aggregation functions (e.g., COUNT, json_agg). By using json_build_array directly within the lateral joins, drizzle is aggregating the data in a more streamlined manner, leading to improved query performance.
Simplified Grouping: In the new version the GROUP BY clause is removed, as the lateral joins and subqueries already handle data aggregation more efficiently.
For this drizzle query
const items = await db.query.comments.findMany({
limit,
orderBy: comments.id,
with: {
user: {
columns: { name: true },
},
post: {
columns: { title: true },
with: {
user: {
columns: { name: true },
},
},
},
},
});
Query that Drizzle generates now
select "comments"."id",
"comments"."user_id",
"comments"."post_id",
"comments"."content",
"comments_user"."data" as "user",
"comments_post"."data" as "post"
from "comments"
left join lateral (select json_build_array("comments_user"."name") as "data"
from (select *
from "users" "comments_user"
where "comments_user"."id" = "comments"."user_id"
limit 1) "comments_user") "comments_user" on true
left join lateral (select json_build_array("comments_post"."title", "comments_post_user"."data") as "data"
from (select *
from "posts" "comments_post"
where "comments_post"."id" = "comments"."post_id"
limit 1) "comments_post"
left join lateral (select json_build_array("comments_post_user"."name") as "data"
from (select *
from "users" "comments_post_user"
where "comments_post_user"."id" = "comments_post"."user_id"
limit 1) "comments_post_user") "comments_post_user"
on true) "comments_post" on true
order by "comments"."id"
limit 1
Query generated before:
SELECT "id",
"user_id",
"post_id",
"content",
"user"::JSON,
"post"::JSON
FROM
(SELECT "comments".*,
CASE
WHEN count("comments_post"."id") = 0 THEN '[]'
ELSE json_agg(json_build_array("comments_post"."title", "comments_post"."user"::JSON))::text
END AS "post"
FROM
(SELECT "comments".*,
CASE
WHEN count("comments_user"."id") = 0 THEN '[]'
ELSE json_agg(json_build_array("comments_user"."name"))::text
END AS "user"
FROM "comments"
LEFT JOIN
(SELECT "comments_user".*
FROM "users" "comments_user") "comments_user" ON "comments"."user_id" = "comments_user"."id"
GROUP BY "comments"."id",
"comments"."user_id",
"comments"."post_id",
"comments"."content") "comments"
LEFT JOIN
(SELECT "comments_post".*
FROM
(SELECT "comments_post".*,
CASE
WHEN count("comments_post_user"."id") = 0 THEN '[]'
ELSE json_agg(json_build_array("comments_post_user"."name"))
END AS "user"
FROM "posts" "comments_post"
LEFT JOIN
(SELECT "comments_post_user".*
FROM "users" "comments_post_user") "comments_post_user" ON "comments_post"."user_id" = "comments_post_user"."id"
GROUP BY "comments_post"."id") "comments_post") "comments_post" ON "comments"."post_id" = "comments_post"."id"
GROUP BY "comments"."id",
"comments"."user_id",
"comments"."post_id",
"comments"."content",
"comments"."user") "comments"
LIMIT 1
You can now provide an empty object or an array of empty objects, and Drizzle will insert all defaults into the database.
// Insert 1 row with all defaults
await db.insert(usersTable).values({});
// Insert 2 rows with all defaults
await db.insert(usersTable).values([{}, {}]);
For PostgreSQL, unique constraints can be defined at the column level for single-column constraints, and in the third parameter for multi-column const
UNIQUE constraints in PostgreSQL, MySQL, SQLiteFor PostgreSQL, unique constraints can be defined at the column level for single-column constraints, and in the third parameter for multi-column constraints. In both cases, it will be possible to define a custom name for the constraint. Additionally, PostgreSQL will receive the NULLS NOT DISTINCT option to restrict having more than one NULL value in a table. Reference
Examples that just shows a different unique usage. Please don't search a real usage for those tables
// single column
const table = pgTable('table', {
id: serial('id').primaryKey(),
name: text('name').notNull().unique(),
state: char('state', { length: 2 }).unique('custom'),
field: char('field', { length: 2 }).unique('custom_field', { nulls: 'not distinct' }),
});
// multiple columns
const table = pgTable('table', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
state: char('state', { length: 2 }),
}, (t) => ({
first: unique('custom_name').on(t.name, t.state).nullsNotDistinct(),
second: unique('custom_name1').on(t.name, t.state),
}));
For MySQL, everything will be the same except for the NULLS NOT DISTINCT option. It appears that MySQL does not support it
Examples that just shows a different unique usage. Please don't search a real usage for those tables
// single column
const table = mysqlTable('table', {
id: serial('id').primaryKey(),
name: text('name').notNull().unique(),
state: text('state').unique('custom'),
field: text('field').unique('custom_field'),
});
// multiple columns
const table = mysqlTable('cities1', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
state: text('state'),
}, (t) => ({
first: unique().on(t.name, t.state),
second: unique('custom_name1').on(t.name, t.state),
}));
In SQLite unique constraints are the same as unique indexes. As long as you can specify a name for the unique index in SQLite - we will treat all unique constraints as unique indexes in internal implementation
// single column
const table = sqliteTable('table', {
id: int('id').primaryKey(),
name: text('name').notNull().unique(),
state: text('state').unique('custom'),
field: text('field').unique(),
});
// multiple columns
const table = sqliteTable('table', {
id: int('id').primaryKey(),
name: text('name').notNull(),
state: text('state'),
}, (t) => ({
first: unique().on(t.name, t.state),
second: unique('custom').on(t.name, t.state),
}));
🎉 Added support for Neon HTTP driver
import { neon, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
neonConfig.fetchConnectionCache = true;
const sql = neon(process.env.DRIZZLE_DATABASE_URL!);
const db = drizzle(sql);
db.select(...)
Replacing all instanceof statements with a custom is() function allowed us to handle multiple Drizzle packages interacting properly.
Replacing all instanceof statements with a custom is() function allowed us to handle multiple Drizzle packages interacting properly.
It also fixes one of our biggest Discord tickets: maximum call stack exceeded 🎉
You should now use is() instead of instanceof to check if specific objects are instances of specific Drizzle types. It might be useful if you are building something on top of the Drizzle API.
import { is, Column } from 'drizzle-orm'
if (is(value, Column)) {
// value's type is narrowed to Column
}
distinct clause supportawait db.selectDistinct().from(usersDistinctTable).orderBy(
usersDistinctTable.id,
usersDistinctTable.name,
);
Also, distinct on clause is available for PostgreSQL:
await db.selectDistinctOn([usersDistinctTable.id]).from(usersDistinctTable).orderBy(
usersDistinctTable.id,
);
await db.selectDistinctOn([usersDistinctTable.name], { name: usersDistinctTable.name }).from(
usersDistinctTable,
).orderBy(usersDistinctTable.name);
bigint and boolean support for SQLiteContributed by @MrRahulRamkumar (#558), @raducristianpopa (#411) and @meech-ward (#725)
const users = sqliteTable('users', {
bigintCol: blob('bigint', { mode: 'bigint' }).notNull(),
boolCol: integer('bool', { mode: 'boolean' }).notNull(),
});
where callback in RQB for tables without relations- 🎉 Added bigint mode to SQLite
🐛 Fixed AWS Data API mapping in relational queries (#677, #681)
Disabled OTEL integration due to the top-level await issues
🐛 Fixed upsert targeting composite keys for SQLite
🐛 Fixed including multiple relations on the same level in RQB
The documentation has been completely reworked and updated with additional examples and explanations. You can find it here: https://orm.drizzle.team.
The documentation has been completely reworked and updated with additional examples and explanations. You can find it here: https://orm.drizzle.team.
Furthermore, the entire documentation has been made open source, allowing you to edit and add any information you deem important for the community.
Visit https://github.com/drizzle-team/drizzle-orm-docs to access the open-sourced documentation.
Additionally, you can create specific documentation issues in this repository
Introducing our first helper built on top of Drizzle Core API syntax: the Relational Queries! 🎉
With Drizzle RQ you can do:
where statements, allowing you to define custom conditions beyond the predefined ones available in the Drizzle Core API.Most importantly, regardless of the size of your query, Drizzle will always generate a SINGLE optimized query.
This efficiency extends to the usage of Prepared Statements, which are fully supported within the Relational Query Builder.
For more info: Prepared Statements in Relational Query Builder
Example of setting one-to-many relations
As you can observe,
relationsare a distinct concept that coexists alongside the main Drizzle schema. You have the flexibility to opt-in or opt-out of them at any time without affecting thedrizzle-kitmigrations or the logic for Core API's types and runtime.
import { integer, serial, text, pgTable } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
export const usersConfig = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
content: text('content').notNull(),
authorId: integer('author_id').notNull(),
});
export const postsConfig = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
Example of querying you database
Step 1: Provide all tables and relations to drizzle function
drizzleimport depends on the database driver you're using
import * as schema from './schema';
import { drizzle } from 'drizzle-orm/...';
const db = drizzle(client, { schema });
await db.query.users.findMany(...);
If you have schema in multiple files
import * as schema1 from './schema1';
import * as schema2 from './schema2';
import { drizzle } from 'drizzle-orm/...';
const db = drizzle(client, { schema: { ...schema1, ...schema2 } });
await db.query.users.findMany(...);
Step 2: Query your database with Relational Query Builder
Select all users
const users = await db.query.users.findMany();
Select first users
.findFirst()will add limit 1 to the query
const user = await db.query.users.findFirst();
Select all users
Get all posts with just id, content and include comments
const posts = await db.query.posts.findMany({
columns: {
id: true,
content: true,
},
with: {
comments: true,
}
});
Select all posts excluding content column
const posts = await db.query.posts.findMany({
columns: {
content: false,
},
});
For more examples you can check full docs for Relational Queries
You can now specify the configuration not only in the .json format but also in .ts and .js formats.
</br>
TypeScript example
import { Config } from "drizzle-kit";
export default {
schema: "",
connectionString: process.env.DB_URL,
out: "",
breakpoints: true
} satisfies Config;
JavaScript example
/** @type { import("drizzle-kit").Config } */
export default {
schema: "",
connectionString: "",
out: "",
breakpoints: true
};
drizzle-kit push:mysqlYou can now push your MySQL schema directly to the database without the need to create and manage migration files. This feature proves to be particularly useful for rapid local development and when working with PlanetScale databases.
By pushing the MySQL schema directly to the database, you can streamline the development process and avoid the overhead of managing migration files. This allows for more efficient iteration and quick deployment of schema changes during local development.
For this feature, you need to create a drizzle.config.[ts|js|json] file. We recommend using .ts or .js files as they allow you to easily provide the database connection information as secret variables
You'll need to specify schema and connectionString(or db, port, host, password, etc.) to make drizzle-kit push:mysql work
drizzle.config.ts example
import { Config } from "src";
export default {
schema: "./schema.ts",
connectionString: process.env.DB_URL,
} satisfies Config;
Run drizzle-kit push:mysql
If Drizzle detects any potential data-loss issues during a migration, it will prompt you to approve whether the data should be truncated or not in order to ensure a successful migration
Approve or reject the action that Drizzle needs to perform in order to push your schema changes to the database.
Done ✅
🎉 Added support for Vercel Postgres
import { drizzle } from 'drizzle-orm/vercel-postgres';
import { sql } from "@vercel/postgres";
const db = drizzle(sql);
db.select(...)
🐛 Add "types" and "default" fields to "exports" entries in package.json
pg imports in ESM mode (#505)🎉 Documentation improvements (#495, #507)
"sideEffects": false to package.json (#515)🐛 Fix package.json exports field
exports field🎉 Added ESM support! You can now use drizzle-orm in both ESM and CJS environments.
drizzle-orm in both ESM and CJS environments.drizzle-orm/sql and drizzle-orm/expressions should now be imported from drizzle-orm instead.Add possibility to have placeholders in .limit() and .offset()
.limit() and .offset()const stmt = db
.select({
id: usersTable.id,
name: usersTable.name,
})
.from(usersTable)
.limit(placeholder('limit'))
.offset(placeholder('offset'))
.prepare('stmt');
const result = await stmt.execute({ limit: 1, offset: 1 });
🐛 Added verbose error when .values() is called without values
🐛 Fixed query generation when selecting from alias
🐛 Pool connections opened for transactions are now closed after the transaction is committed or rolled back
🐛 Fix onConflict targets in #475 - thanks @wkunert ❤️
🎉 Added iterator support to mysql2 (sponsored by @rizen ❤). Read more in the docs
mysql2 (sponsored by @rizen ❤). Read more in the docs.prepare() in MySQL no longer requires a name argument🎉 All enum and text enum columns now have a properly typed enumValues property
enumValues property🐛 Fixed multi-level join results (e.g. joining a subquery with a nested join)
🐛 Fix migrator function for PostgreSQL
Would suggest to upgrade to this version anyone who is using postgres dialect.
0.23.9and0.23.10are broken for postgresql migrations
🐛 Added missing config argument to transactions API
You can now use transactions with all the supported databases and drivers.
You can now use transactions with all the supported databases and drivers.
node-postgres example:
await db.transaction(async (tx) => {
await tx.insert(users).values(newUser);
await tx.update(users).set({ name: 'Mr. Dan' }).where(eq(users.name, 'Dan'));
await tx.delete(users).where(eq(users.name, 'Dan'));
});
For more information, see transactions docs:
🐛 Fixed dates timezone differences for timestamps in Postgres and MySQL (contributed by @AppelBoomHD via #288)
🎉 Added INSERT IGNORE support for MySQL (contributed by @FugiTech via #305)
INSERT IGNORE support for MySQL (contributed by @FugiTech via #305)Your coding agent can read these notes before it upgrades. Set up the MCP server →