# Raw SQL

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

URL: https://better-drizzle.com/docs/advanced/raw-sql

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

| Method        | For                    | Safety             |
| ------------- | ---------------------- | ------------------ |
| `$raw`        | reads that return rows | parameterized      |
| `$executeRaw` | write statements       | parameterized      |
| `$rawUnsafe`  | raw string SQL         | **off by default** |

## `$raw`

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



**better-drizzle**

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

**Drizzle**

```ts  
import { sql } from 'drizzle-orm';

// node-postgres driver
const { rows } = await db.execute<{ id: number; email: string }>(sql`
	select id, email
	from users
	where active = ${true}
	order by id asc
`);
```

Or with a Drizzle `sql` object:



**better-drizzle**

```ts  
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'}`,
);
```

**Drizzle**

```ts  
import { sql } from 'drizzle-orm';

// node-postgres driver
const { rows } = await db.execute<{ 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:



**better-drizzle**

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

result.rowsAffected;
```

**Drizzle**

```ts  
import { sql } from 'drizzle-orm';

// node-postgres driver
const result = await db.execute(sql`
	update users set active = ${false} where id = ${1}
`);

result.rowCount;
```

## 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:



**better-drizzle**

```ts  
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,
	},
);
```

**Drizzle**

```ts  
import { sql } from 'drizzle-orm';

// node-postgres driver; no name, comment, or timeout options
const result = await db.execute<{ id: number; email: string }>(
	sql`select id, email from users order by id asc`,
);

const rows = result.rows.map((row) => ({
	id: row.id,
	email: row.email.toLowerCase(),
}));
```

Supported per-call options on raw methods:

| Option      | Purpose                                    |
| ----------- | ------------------------------------------ |
| `name`      | human-readable label for logs/tracing      |
| `comment`   | SQL comment metadata (PostgreSQL-oriented) |
| `timeoutMs` | abort after a duration                     |
| `signal`    | cancel via `AbortSignal`                   |
| `map`       | transform 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:

```ts
const client = better(db, {
	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:

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

See the [client reference](/docs/reference/client) 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
