# Database Security (SQL Injection Prevention)

This codebase uses Drizzle ORM with SQLite. All database queries must use parameterized SQL so user-controlled input never changes query structure.

## Quick Rules

- Use Drizzle's `sql` tagged template literals for dynamic values.
- Use `sql.join()` for dynamic lists such as `IN (...)` clauses.
- Pass `SQL<unknown>` fragments between functions, not strings.
- Do not build queries with `sql.raw()` or string interpolation.
- Prefer `json_each()` for user-selected JSON keys. When `json_extract()` is appropriate, bind a path built with the vetted `buildSafeJsonPath` helper in `src/models/eval.ts`.

## Required Pattern: Use `sql` Template Strings

Use parameterized queries for single values:

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

const query = sql`SELECT * FROM eval_results WHERE eval_id = ${evalId}`;
```

Use parameterized queries for multiple values:

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

const query = sql`
  SELECT * FROM eval_results
  WHERE eval_id = ${evalId} AND success = ${1}
`;
```

Use `sql.join()` for dynamic `IN (...)` lists:

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

const ids = ['id1', 'id2', 'id3'];
const query = sql`SELECT * FROM evals WHERE id IN (${sql.join(ids, sql`, `)})`;
```

## Forbidden Pattern: Raw SQL with Dynamic Content

Do not interpolate user-controlled values into raw SQL:

```typescript
const query = sql.raw(`SELECT * FROM eval_results WHERE eval_id = '${evalId}'`);
```

Do not build SQL conditions with string concatenation:

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

const whereClause = `eval_id = '${evalId}'`;
const query = sql.raw(`SELECT * FROM eval_results WHERE ${whereClause}`);
```

## JSON Paths in SQLite

SQLite's `json_extract()` accepts its JSON path as a bound value. Do not use `sql.raw()` to splice user-controlled JSON paths into SQL. Prefer `json_each()` when filtering by dynamic keys, because the key can be compared as a normal parameter:

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

const query = sql`
  SELECT *
  FROM eval_results
  WHERE EXISTS (
    SELECT 1
    FROM json_each(metadata)
    WHERE json_each.key = ${field} AND json_each.value = ${value}
  )
`;
```

If you construct a JSON path for `json_extract()`, use `buildSafeJsonPath` from `src/models/eval.ts`, which escapes backslashes and double quotes for JSON path syntax and returns a value to bind:

```typescript
import { sql } from 'drizzle-orm';
import { buildSafeJsonPath } from '../../src/models/eval';

const jsonPath = buildSafeJsonPath(userField);
const query = sql`
  SELECT * FROM eval_results
  WHERE json_extract(metadata, ${jsonPath}) = ${value}
`;
```

If you need a new JSON-path helper, match the guarantees in `buildSafeJsonPath` and keep the implementation audited in one shared utility.

## Passing SQL Fragments Between Functions

When building complex queries, pass `SQL<unknown>` fragments instead of strings:

```typescript
import { type SQL, sql } from 'drizzle-orm';

function queryWithFilter(whereSql: SQL<unknown>): Promise<Result[]> {
  const query = sql`SELECT * FROM eval_results WHERE ${whereSql}`;
  return db.all(query);
}

const filter = sql`eval_id = ${evalId} AND success = ${1}`;
const results = await queryWithFilter(filter);
```

## Key Files with Database Queries

- `src/models/eval.ts` - Main eval queries and JSON-path helper
- `src/util/calculateFilteredMetrics.ts` - Metrics aggregation queries
- `src/database/index.ts` - Database connection

## Transaction Handles

Inside `db.transaction(async (tx) => ...)`, use `tx` for every query and pass it
into helpers that need database access. Root `db.run`, `db.all`, query builders,
and client methods reject inside the callback. Catching that error leaves the
transaction usable; letting it escape rolls the transaction back. Nested root
`db.transaction` callbacks reuse the active transaction and do not commit it
independently. The context expires when the callback settles, so asynchronous
work that runs later queues as a new top-level operation.

Promptfoo serializes top-level operations and configures libSQL with one pooled
connection so foreign-key, busy-timeout, and WAL settings survive transaction
reuse. Reconnecting during a transaction would close its connection and must not
be used to recover a root call.

For direct libSQL clients in fixtures, hold `await client.transaction('write')`
(or `'read'`) and call `commit()`, `rollback()`, or `close()` on that handle.
libSQL 0.18 rolls back unfinished transactions when a client operation returns
its connection to the pool, so separate `client.execute('BEGIN')` and
`client.execute('ROLLBACK')` calls cannot hold a lock across operations.
