Skip to main content
doc0blade47/drizzle-ormdocumentation home
Sign in

Sessions and Transactions

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

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 ClassDriver Client TypeExecution ModeSupported Query Operations
LibSQLSession@libsql/client ClientAsyncprepareQuery, batch, migrate, transaction
SQLiteRemoteSessionRemoteCallbackAsyncprepareQuery, batch, transaction
BunSQLSessionBun SQLAsyncprepareQuery, query, transaction, unsafe
NeonSessionNeon Pool / ClientAsyncprepareQuery, query, queryObjects, count, transaction
PgliteSessionPgliteClientAsyncprepareQuery, transaction, count
GelDbSessionGel Client / TransactionAsyncprepareQuery, transaction, count
SQLJsSessionsql.js DatabaseSyncprepareQuery, transaction
PrismaSQLiteSessionPrisma Client extensionAsyncprepareQuery, 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 ClassNested Implementation MechanismSavepoint Naming PatternRollback on Error
BunSQLTransactionclient.savepoint()Managed by Bun SQLAutomatic via driver block
NeonTransactionsavepoint / rollback to savepointsp${this.nestedIndex + 1}Explicit SQL command
TiDBServerlessTransactionsavepoint / rollback to savepointsp${this.nestedIndex + 1}Explicit SQL command
PgliteTransactionsavepoint / rollback to savepointsp${this.nestedIndex + 1}Explicit SQL command
AwsDataApiTransactionsavepoint / rollback to savepointsp${this.nestedIndex + 1}Explicit SQL command
SQLJsTransactionsavepoint / rollback to savepointsp${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 MethodOP-SQLite Driver CallReturn Type
runclient.executeAsync(query, params)QueryResult
allclient.execute(query, params).rows?._arrayT['all']
valuesclient.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 ClassTransaction Support StrategyBehavior on transaction()
NeonHttpSessionNone (Stateless HTTP)Throws 'No transactions support in neon-http driver'
NetlifyDbSessionWebSocket Pool ClientAcquires PoolClient, runs begin/commit/rollback, releases client
AwsDataApiSessionAPI Transaction IDSends BeginTransactionCommand, tracks transactionId, sends CommitTransactionCommand or RollbackTransactionCommand
PgRemoteSessionRemote Proxy CallbackThrows '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

Was this page helpful?