Core concepts

Raw SQL

Escape hatches for raw SQL — $raw for reads, $executeRaw for writes, and $rawUnsafe when you opt in.

The delegate API covers routine work; raw SQL covers the rest. better-drizzle keeps the raw surface deliberately explicit and safe by default.

MethodForSafety
$rawreads that return rowsparameterized
$executeRawwrite statementsparameterized
$rawUnsaferaw string SQLoff by default

$raw

As a tagged template — interpolated values are bound as parameters, not concatenated:

const rows = await client.$raw<{ id: number; email: string }>`
	select id, email
	from users
	where active = ${true}
	order by id asc
`;

Or with a Drizzle sql object:

import { sql } from 'drizzle-orm';

const rows = await client.$raw<{ id: number; email: string }>(
	sql`select id, email from users where email like ${'%@example.com'}`,
);

$executeRaw

For statements that change data. Returns a normalized result:

const result = await client.$executeRaw`
	update users set active = ${false} where id = ${1}
`;

result.rowsAffected;

Row mapping and options

The sql-object form of $raw accepts options — including a map function applied to each row, plus tracing metadata and a timeout:

import { sql } from 'drizzle-orm';

const rows = await client.$raw(
	sql`select id, email from users order by id asc`,
	{
		map: (row: { id: number; email: string }) => ({
			id: row.id,
			email: row.email.toLowerCase(),
		}),
		name: 'users.list.raw',
		comment: 'raw.users.list',
		timeoutMs: 5000,
	},
);

Supported per-call options on raw methods:

OptionPurpose
namehuman-readable label for logs/tracing
commentSQL comment metadata (PostgreSQL-oriented)
timeoutMsabort after a duration
signalcancel via AbortSignal
maptransform each returned row

$rawUnsafe

$rawUnsafe runs a raw SQL string (with optional bindings). Because it does not enforce parameterization for you, it is blocked unless you explicitly enable it:

const client = better(db, {
	schema,
	raw: { allowUnsafe: true },
});

const rows = await client.$rawUnsafe<{ id: number }>(
	'select id from users where email = ?',
	['alice@example.com'],
);

Only when you must

Prefer $raw / $executeRaw. Reach for $rawUnsafe only for SQL you cannot express as a tagged template or sql object, and never interpolate untrusted input into the string.

Requiring comments

For teams that want every raw call tagged for observability, require a comment and set defaults globally:

const client = better(db, {
	schema,
	raw: {
		requireComment: true,
		timeoutMs: 3000,
		unsupportedOptions: 'warn',
	},
});

See the client reference for every raw option.

Raw hooks

Raw methods intentionally bypass model transforms and CRUD hooks, but they still participate in dedicated raw lifecycle hooks:

  • beforeRaw
  • afterRaw
  • onRawError

That makes raw SQL usable in systems that still need observability, tracing, metrics, or policy checks around escape-hatch queries.

When raw is the right call

  • database-specific SQL functions, window functions, CTEs
  • reporting queries that read better as SQL
  • one-off statements the delegate API should not try to model

When to stay on the delegate API

  • routine CRUD and relation loading
  • typed filters and pagination
  • anything that would otherwise duplicate projection and payload shaping

On this page