docs/agents/database-security.md
This codebase uses Drizzle ORM with SQLite. All database queries must use parameterized SQL so user-controlled input never changes query structure.
sql tagged template literals for dynamic values.sql.join() for dynamic lists such as IN (...) clauses.SQL<unknown> fragments between functions, not strings.sql.raw() or string interpolation.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.sql Template StringsUse parameterized queries for single values:
import { sql } from 'drizzle-orm';
const query = sql`SELECT * FROM eval_results WHERE eval_id = ${evalId}`;
Use parameterized queries for multiple values:
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:
import { sql } from 'drizzle-orm';
const ids = ['id1', 'id2', 'id3'];
const query = sql`SELECT * FROM evals WHERE id IN (${sql.join(ids, sql`, `)})`;
Do not interpolate user-controlled values into raw SQL:
const query = sql.raw(`SELECT * FROM eval_results WHERE eval_id = '${evalId}'`);
Do not build SQL conditions with string concatenation:
import { sql } from 'drizzle-orm';
const whereClause = `eval_id = '${evalId}'`;
const query = sql.raw(`SELECT * FROM eval_results WHERE ${whereClause}`);
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:
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:
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.
When building complex queries, pass SQL<unknown> fragments instead of strings:
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);
src/models/eval.ts - Main eval queries and JSON-path helpersrc/util/calculateFilteredMetrics.ts - Metrics aggregation queriessrc/database/index.ts - Database connection