Relevant source files
The following files were used as context for generating this wiki page:
- drizzle-orm/src/libsql/session.ts
- drizzle-orm/src/sqlite-proxy/session.ts
- drizzle-orm/src/singlestore/session.ts
- drizzle-orm/src/bun-sql/session.ts
- drizzle-orm/src/netlify-db/session.ts
- drizzle-orm/src/planetscale-serverless/session.ts
- drizzle-orm/src/neon-serverless/session.ts
- drizzle-orm/src/tidb-serverless/session.ts
- drizzle-orm/src/mysql2/session.ts
- drizzle-orm/src/pglite/session.ts
- drizzle-orm/src/postgres-js/session.ts
- drizzle-orm/src/node-postgres/session.ts
- drizzle-orm/src/singlestore-core/session.ts
- drizzle-orm/src/aws-data-api/pg/session.ts
- drizzle-orm/src/gel/session.ts
- drizzle-orm/src/mysql-core/session.ts
- drizzle-orm/src/op-sqlite/session.ts
- drizzle-orm/src/vercel-postgres/session.ts
- drizzle-orm/src/pg-proxy/session.ts
- drizzle-orm/src/d1/session.ts
- drizzle-orm/src/neon-http/session.ts
- drizzle-orm/src/pg-core/session.ts
- drizzle-orm/src/bun-sqlite/session.ts
- drizzle-orm/src/pg-core/db.ts
- drizzle-orm/src/sqlite-core/session.ts
- drizzle-orm/src/better-sqlite3/session.ts
- drizzle-orm/src/sql-js/session.ts
- drizzle-orm/src/sqlite-core/db.ts
- drizzle-orm/src/prisma/sqlite/session.ts
- drizzle-orm/src/gel-core/session.ts
Overview
Sessions and transactions form the execution layer that bridges high-level query builders and low-level database drivers across diverse relational dialects and runtimes. By defining unified session abstractions like PostgreSQL Core, MySQL Core, and SQLite Core, the system standardizes statement preparation, execution pipelines, and result mapping regardless of whether queries run synchronously in Node.js or asynchronously across edge and serverless environments.
Sources: drizzle-orm/src/pg-core/session.ts:168-234, drizzle-orm/src/mysql-core/session.ts:170-238, drizzle-orm/src/sqlite-core/session.ts:212-323
Session Architecture and Prepared Queries
Statement Execution Methods and Runtimes
The execution interface centers around dialect-specific session abstractions—such as LibSQLSession, SQLiteRemoteSession, BunSQLSession, NeonSession, PgliteSession, GelDbSession, SQLJsSession, and PrismaSQLiteSession—which inherit from shared base classes like SQLiteSession and PgSession. These sessions encapsulate database clients, configuration options, and telemetry loggers (NoopLogger or custom implementations), providing uniform methods for query compilation, statement preparation, and execution dispatch across diverse underlying runtimes.
Prepared statements are managed by specialized subclasses including LibSQLPreparedQuery, RemotePreparedQuery, BunSQLPreparedQuery, NeonPreparedQuery, GelDbPreparedQuery, PreparedQuery (for sql.js), and PrismaSQLitePreparedQuery. These query objects accept query fragments and parameters, resolve placeholder values via fillPlaceholders, record execution metrics through logging and tracing hooks, and map raw driver outputs into structured domain entities.
Sources: drizzle-orm/src/libsql/session.ts:29-75, drizzle-orm/src/sqlite-proxy/session.ts:29-74, drizzle-orm/src/bun-sql/session.ts:19-103, drizzle-orm/src/neon-serverless/session.ts:188-233, drizzle-orm/src/pglite/session.ts:165-195, drizzle-orm/src/gel/session.ts:17-102, drizzle-orm/src/sql-js/session.ts:24-66, drizzle-orm/src/prisma/sqlite/session.ts:62-83
Prepared query contracts support a standard suite of execution methods—run, all, get, values, and execute—whose availability and return formats vary by driver implementation. For instance, synchronous sql.js prepared queries handle direct statement preparation, step iteration, result object extraction, and explicit resource freeing via stmt.free(), whereas asynchronous Bun SQL and Neon queries wrap execution within telemetry spans and cache layers.
| Session Class | Driver Client Type | Execution Mode | Supported Query Operations |
|---|---|---|---|
LibSQLSession | @libsql/client Client | Async | prepareQuery, batch, migrate, transaction |
SQLiteRemoteSession | RemoteCallback | Async | prepareQuery, batch, transaction |
BunSQLSession | Bun SQL | Async | prepareQuery, query, transaction, unsafe |
NeonSession | Neon Pool / Client | Async | prepareQuery, query, queryObjects, count, transaction |
PgliteSession | PgliteClient | Async | prepareQuery, transaction, count |
GelDbSession | Gel Client / Transaction | Async | prepareQuery, transaction, count |
SQLJsSession | sql.js Database | Sync | prepareQuery, transaction |
PrismaSQLiteSession | Prisma Client extension | Async | prepareQuery, transaction (unimplemented) |
Sources: drizzle-orm/src/libsql/session.ts:29-129, drizzle-orm/src/sqlite-proxy/session.ts:29-105, drizzle-orm/src/bun-sql/session.ts:110-188, drizzle-orm/src/neon-serverless/session.ts:188-281, drizzle-orm/src/pglite/session.ts:165-195, drizzle-orm/src/gel/session.ts:109-168, drizzle-orm/src/sql-js/session.ts:24-66, drizzle-orm/src/prisma/sqlite/session.ts:62-89
Note
In sql.js sessions, binary field values (Uint8Array) are automatically normalized via normalizeFieldValue into Node Buffer instances when available, or decoded using TextDecoder if Buffer is absent.
Sources: drizzle-orm/src/sql-js/session.ts:197-210
Transaction Lifecycles and Rollback Semantics
Demarcation and Scopes
Transaction boundaries are managed via driver-specific demarcation methods wrapped in session abstractions. In BunSQLSession, transactions use the native client.begin() method, supplying a transaction-scoped client to instantiate BunSQLTransaction. Similarly, PgliteSession delegates directly to the underlying client.transaction() API, while AwsDataApiSession explicitly dispatches BeginTransactionCommand, CommitTransactionCommand, and RollbackTransactionCommand via the AWS Data API client.
override async transaction<T>(
transaction: (tx: AwsDataApiTransaction<TFullSchema, TSchema>) => Promise<T>,
config?: PgTransactionConfig | undefined,
): Promise<T> {
const { transactionId } = await this.client.send(new BeginTransactionCommand(this.rawQuery));
const session = new AwsDataApiSession(this.client, this.dialect, this.schema, this.options, transactionId);
const tx = new AwsDataApiTransaction<TFullSchema, TSchema>(this.dialect, session, this.schema);
if (config) {
await tx.setTransaction(config);
}
try {
const result = await transaction(tx);
await this.client.send(new CommitTransactionCommand({ ...this.rawQuery, transactionId }));
return result;
} catch (e) {
await this.client.send(new RollbackTransactionCommand({ ...this.rawQuery, transactionId }));
throw e;
}
}Sources: drizzle-orm/src/bun-sql/session.ts:170-187, drizzle-orm/src/pglite/session.ts:170-187, drizzle-orm/src/aws-data-api/pg/session.ts:238-256
Neon and SQL.js sessions handle transaction boundaries by executing explicit raw SQL statements (begin, commit, and rollback) wrapped in try...catch blocks. In NeonSession, pool connections check whether the client instance is a Pool to release the client back to the pool in a finally block.
override async transaction<T>(
transaction: (tx: NeonTransaction<TFullSchema, TSchema>) => Promise<T>,
config: PgTransactionConfig = {},
): Promise<T> {
const session = this.client instanceof Pool // eslint-disable-line no-instanceof/no-instanceof
? new NeonSession(await this.client.connect(), this.dialect, this.schema, this.options)
: this;
const tx = new NeonTransaction<TFullSchema, TSchema>(this.dialect, session, this.schema);
await tx.execute(sql`begin ${tx.getTransactionConfigSQL(config)}`);
try {
const result = await transaction(tx);
await tx.execute(sql`commit`);
return result;
} catch (error) {
await tx.execute(sql`rollback`);
throw error;
} finally {
if (this.client instanceof Pool) { // eslint-disable-line no-instanceof/no-instanceof
(session.client as PoolClient).release();
}
}
}Sources: drizzle-orm/src/neon-serverless/session.ts:260-281, drizzle-orm/src/sql-js/session.ts:51-65
Note
In SQLJsSession, transaction configuration behaviors (such as deferred, immediate, or exclusive) are appended dynamically to the begin statement using config.behavior.
Sources: drizzle-orm/src/sql-js/session.ts:51-56
Nested Scopes and Savepoints
Nested transaction blocks within driver implementations rely on database savepoints rather than multi-tier native transactions. Each nested scope increments nestedIndex to generate unique savepoint identifiers matching the pattern sp1, sp2, and so forth.
| Transaction Class | Nested Implementation Mechanism | Savepoint Naming Pattern | Rollback on Error |
|---|---|---|---|
BunSQLTransaction | client.savepoint() | Managed by Bun SQL | Automatic via driver block |
NeonTransaction | savepoint / rollback to savepoint | sp${this.nestedIndex + 1} | Explicit SQL command |
TiDBServerlessTransaction | savepoint / rollback to savepoint | sp${this.nestedIndex + 1} | Explicit SQL command |
PgliteTransaction | savepoint / rollback to savepoint | sp${this.nestedIndex + 1} | Explicit SQL command |
AwsDataApiTransaction | savepoint / rollback to savepoint | sp${this.nestedIndex + 1} | Explicit SQL command |
SQLJsTransaction | savepoint / rollback to savepoint | sp${this.nestedIndex + 1} | Explicit SQL command |
Sources: drizzle-orm/src/bun-sql/session.ts:206-219, drizzle-orm/src/neon-serverless/session.ts:290-302, drizzle-orm/src/tidb-serverless/session.ts:209-228, drizzle-orm/src/pglite/session.ts:203-220, drizzle-orm/src/aws-data-api/pg/session.ts:265-284, drizzle-orm/src/sql-js/session.ts:74-86
Caution
When an error occurs inside a nested transaction block utilizing savepoints, the execution path triggers rollback to savepoint spN and releases or throws the error, preserving the outer transaction state.
Sources: drizzle-orm/src/neon-serverless/session.ts:298-301, drizzle-orm/src/sql-js/session.ts:82-85
Driver Connection Wrappers and Execution
Connection Bridging and Client Execution
Drizzle abstracts disparate underlying driver connection interfaces into uniform session and prepared query instances across runtimes. In environments powered by Bun SQL (BunSQLSession) and Pglite (PgliteSession), queries are dispatched using driver-native .unsafe() and .transaction() wrappers while capturing telemetry through OpenTelemetry spans.
async execute(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['execute']> {
return tracer.startActiveSpan('drizzle.execute', async (span) => {
const params = fillPlaceholders(this.params, placeholderValues);
span?.setAttributes({
'drizzle.query.text': this.queryString,
'drizzle.query.params': JSON.stringify(params),
});
this.logger.logQuery(this.queryString, params);
const { fields, queryString: query, client, joinsNotNullableMap, customResultMapper } = this;
if (!fields && !customResultMapper) {
return tracer.startActiveSpan('drizzle.driver.execute', async () => {
return await this.queryWithCache(query, params, async () => {
return await client.unsafe(query, params as any[]);
});
});
}Sources: drizzle-orm/src/bun-sql/session.ts:40-58, drizzle-orm/src/pglite/session.ts:165-187
Note
Client adapters like PrismaSQLitePreparedQuery bridge SQLite execution by mapping calls directly to Prisma's $queryRawUnsafe API, while throwing unsupported errors for raw value arrays and transaction blocks.
Sources: drizzle-orm/src/prisma/sqlite/session.ts:33-56, drizzle-orm/src/prisma/sqlite/session.ts:84-90
Synchronous and Asynchronous SQLite Execution Pipelines
SQLite runtimes split execution semantics between synchronous in-memory drivers like sql.js and asynchronous mobile/edge drivers like OP-SQLite. SQLJsSession utilizes explicit statement preparation, parameter binding via BindParams, stepping via stmt.step(), and manual resource freeing via stmt.free().
run(placeholderValues?: Record<string, unknown>): void {
const stmt = this.client.prepare(this.query.sql);
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
this.logger.logQuery(this.query.sql, params);
const result = stmt.run(params as BindParams);
stmt.free();
return result;
}Sources: drizzle-orm/src/sql-js/session.ts:106-116, drizzle-orm/src/sql-js/session.ts:118-143
In contrast, OPSQLiteSession routes execution methods through asynchronous connection bindings:
| Execution Method | OP-SQLite Driver Call | Return Type |
|---|---|---|
run | client.executeAsync(query, params) | QueryResult |
all | client.execute(query, params).rows?._array | T['all'] |
values | client.executeRawAsync(query, params) | T['values'] |
Sources: drizzle-orm/src/op-sqlite/session.ts:135-142, drizzle-orm/src/op-sqlite/session.ts:144-160, drizzle-orm/src/op-sqlite/session.ts:187-193
Warning
When decoding binary results in SQLJsSession, normalizeFieldValue checks Uint8Array instances and converts them using Buffer.from() if available, or TextDecoder otherwise, throwing an error if neither polyfill exists.
Sources: drizzle-orm/src/sql-js/session.ts:197-210
HTTP and Serverless Transaction Emulation
Stateless HTTP Gateways and WebSocket Upgrades
Stateless HTTP gateways and serverless environments present distinct coordination challenges for transaction semantics because HTTP protocols do not inherently maintain persistent connection state between requests. Drivers such as Neon HTTP (NeonHttpSession) execute queries over stateless HTTPS connections using @neondatabase/serverless, which inherently prevents multi-statement transaction execution blocks since each query can hit a different serverless container or worker instance. Consequently, calling transaction() on NeonHttpSession throws an error explicitly stating that transactions are unsupported in the neon-http driver.
override async transaction<T>(
_transaction: (tx: NeonTransaction<TFullSchema, TSchema>) => Promise<T>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_config: PgTransactionConfig = {},
): Promise<T> {
throw new Error('No transactions support in neon-http driver');
}Sources: drizzle-orm/src/neon-http/session.ts:248-254
In contrast, hybrid adapters like Netlify DB (NetlifyDbSession) manage this limitation by bridging HTTP and WebSocket protocols. When a transaction is initiated, NetlifyDbSession acquires a dedicated PoolClient instance from an underlying connection pool, establishes a persistent WebSocket connection, executes begin alongside transaction configuration parameters, and executes the transactional callback inside NetlifyDbTransaction.
await tx.execute(sql`begin ${tx.getTransactionConfigSQL(config)}`);
try {
const result = await transaction(tx);
await tx.execute(sql`commit`);
return result;
} catch (error) {
await tx.execute(sql`rollback`);
throw error;
} finally {
poolClient.release();
}
}Sources: drizzle-orm/src/netlify-db/session.ts:168-179
Warning
Netlify DB WebSocket sessions (NetlifyDbWsSession) explicitly throw an error if nested transaction() calls are invoked directly on the session instance, enforcing that inner scopes route through NetlifyDbTransaction savepoint handling instead.
Sources: drizzle-orm/src/netlify-db/session.ts:259-265
Client-Side Transaction Coordination
AWS Data API and remote proxy runtimes coordinate transactions by passing explicit transaction identifiers across stateless API requests. AwsDataApiSession manages transaction blocks by sending a BeginTransactionCommand to the AWS Data API client, receiving a unique transactionId string, and injecting that identifier into every subsequent prepared query statement executed within that scope.
override async transaction<T>(
transaction: (tx: AwsDataApiTransaction<TFullSchema, TSchema>) => Promise<T>,
config?: PgTransactionConfig | undefined,
): Promise<T> {
const { transactionId } = await this.client.send(new BeginTransactionCommand(this.rawQuery));
const session = new AwsDataApiSession(this.client, this.dialect, this.schema, this.options, transactionId);
const tx = new AwsDataApiTransaction<TFullSchema, TSchema>(this.dialect, session, this.schema);
if (config) {
await tx.setTransaction(config);
}
try {
const result = await transaction(tx);
await this.client.send(new CommitTransactionCommand({ ...this.rawQuery, transactionId }));
return result;
} catch (e) {
await this.client.send(new RollbackTransactionCommand({ ...this.rawQuery, transactionId }));
throw e;
}
}Sources: drizzle-orm/src/aws-data-api/pg/session.ts:238-256
Remote proxy drivers (PgRemoteSession) bypass transaction handling entirely because the underlying remote callback transport lacks stateful transaction coordination, throwing an error on invocation.
| Driver Session Class | Transaction Support Strategy | Behavior on transaction() |
|---|---|---|
NeonHttpSession | None (Stateless HTTP) | Throws 'No transactions support in neon-http driver' |
NetlifyDbSession | WebSocket Pool Client | Acquires PoolClient, runs begin/commit/rollback, releases client |
AwsDataApiSession | API Transaction ID | Sends BeginTransactionCommand, tracks transactionId, sends CommitTransactionCommand or RollbackTransactionCommand |
PgRemoteSession | Remote Proxy Callback | Throws 'Transactions are not supported by the Postgres Proxy driver' |
Sources: drizzle-orm/src/neon-http/session.ts:248-254, drizzle-orm/src/netlify-db/session.ts:168-179, drizzle-orm/src/aws-data-api/pg/session.ts:238-256, drizzle-orm/src/pg-proxy/session.ts:70-75
Note
Netlify DB explicitly verifies that a WebSocket constructor is available globally before initializing serverless query execution, assigning WebSocket to neonConfig.webSocketConstructor if unassigned.
Sources: drizzle-orm/src/netlify-db/session.ts:39-45
Batch Pipelines and Proxy Sessions
Batch Item Aggregation
Batch execution models in Drizzle aggregate arrays of query items into single pipeline requests dispatched to remote drivers or database clients. In LibSQLSession, the batch() method iterates over incoming queries, invokes _prepare() on each item to generate a prepared query, extracts the underlying SQL string and parameter arguments via getQuery(), and pushes them into an InStatement array.
async batch<T extends BatchItem<'sqlite'>[] | readonly BatchItem<'sqlite'>[]>(queries: T) {
const preparedQueries: PreparedQuery[] = [];
const builtQueries: InStatement[] = [];
for (const query of queries) {
const preparedQuery = query._prepare();
const builtQuery = preparedQuery.getQuery();
preparedQueries.push(preparedQuery);
builtQueries.push({ sql: builtQuery.sql, args: builtQuery.params as InArgs });
}Sources: drizzle-orm/src/libsql/session.ts:77-86
Once the built statements are assembled, LibSQLSession invokes this.client.batch(builtQueries) and maps the resulting array of driver outputs back through each individual prepared query's mapResult() handler with array mode enabled.
const batchResults = await this.client.batch(builtQueries);
return batchResults.map((result, i) => preparedQueries[i]!.mapResult(result, true));
}Sources: drizzle-orm/src/libsql/session.ts:88-90
Similarly, LibSQLSession provides a migrate() method that mirrors the exact structure of batch() but delegates to this.client.migrate(builtQueries) for migration-specific pipeline execution.
async migrate<T extends BatchItem<'sqlite'>[] | readonly BatchItem<'sqlite'>[]>(queries: T) {
const preparedQueries: PreparedQuery[] = [];
const builtQueries: InStatement[] = [];
for (const query of queries) {
const preparedQuery = query._prepare();
const builtQuery = preparedQuery.getQuery();
preparedQueries.push(preparedQuery);
builtQueries.push({ sql: builtQuery.sql, args: builtQuery.params as InArgs });
}
const batchResults = await this.client.migrate(builtQueries);
return batchResults.map((result, i) => preparedQueries[i]!.mapResult(result, true));
}Sources: drizzle-orm/src/libsql/session.ts:92-105
Remote Proxy Transports
Proxy and HTTP-backed drivers implement batch pipelines by marshalling query descriptors across remote transport boundaries. In NeonHttpSession, the batch() method processes incoming queries by creating NeonQueryPromise instances with fullResults: true and array mode determined by each prepared query's isResponseInArrayMode() configuration.
async batch<U extends BatchItem<'pg'>, T extends Readonly<[U, ...U[]]>>(
queries: T,
) {
const preparedQueries: PreparedQuery[] = [];
const builtQueries: NeonQueryPromise<any, true>[] = [];
for (const query of queries) {
const preparedQuery = query._prepare();
const builtQuery = preparedQuery.getQuery();
preparedQueries.push(preparedQuery);
builtQueries.push(
this.clientQuery(builtQuery.sql, builtQuery.params, {
fullResults: true,
arrayMode: preparedQuery.isResponseInArrayMode(),
}),
);
}Sources: drizzle-orm/src/neon-http/session.ts:199-214
The collected promises are then dispatched to Neon's HTTP client transaction endpoint using this.client.transaction(builtQueries, queryConfig) before mapping the batch results back to their respective prepared queries.
const batchResults = await this.client.transaction(builtQueries, queryConfig);
return batchResults.map((result, i) => preparedQueries[i]!.mapResult(result, true)) as any;Sources: drizzle-orm/src/neon-http/session.ts:216-218
For SQLite proxy sessions (SQLiteRemoteSession), batch aggregation maps query parameters alongside explicit execution methods (run, all, values, or get) into remote transport payloads before invoking this.batchCLient.
async batch<T extends BatchItem<'sqlite'>[] | readonly BatchItem<'sqlite'>[]>(queries: T) {
const preparedQueries: PreparedQuery[] = [];
const builtQueries: { sql: string; params: any[]; method: 'run' | 'all' | 'values' | 'get' }[] = [];
for (const query of queries) {
const preparedQuery = query._prepare();
const builtQuery = (preparedQuery as RemotePreparedQuery).getQuery();
preparedQueries.push(preparedQuery);
builtQueries.push({ sql: builtQuery.sql, params: builtQuery.params, method: builtQuery.method });
}
const batchResults = await (this.batchCLient as AsyncBatchRemoteCallback)(builtQueries);
return batchResults.map((result, i) => preparedQueries[i]!.mapResult(result, true));
}Sources: drizzle-orm/src/sqlite-proxy/session.ts:76-89
Note
SQLiteRemoteSession extracts raw batch result rows using session-specific helpers like extractRawAllValueFromBatchResult, extractRawGetValueFromBatchResult, and extractRawValuesValueFromBatchResult tailored to SqliteRemoteResult structures.
Sources: drizzle-orm/src/sqlite-proxy/session.ts:107-117
Related
On This Page
OverviewSession Architecture and Prepared QueriesStatement Execution Methods and RuntimesTransaction Lifecycles and Rollback SemanticsDemarcation and ScopesNested Scopes and SavepointsDriver Connection Wrappers and ExecutionConnection Bridging and Client ExecutionSynchronous and Asynchronous SQLite Execution PipelinesHTTP and Serverless Transaction EmulationStateless HTTP Gateways and WebSocket UpgradesClient-Side Transaction CoordinationBatch Pipelines and Proxy SessionsBatch Item AggregationRemote Proxy TransportsRelated