# `src/server/db`

Helpers for the raw-SQL paths in `src/server/services/*`. The bulk of those
services hand-build dynamic `INSERT` / `UPDATE` statements with drizzle's
`sql` template tag and `sql.join(sets, sql\`, \`)`.

## The drizzle "fan-out" gotcha

When you write:

```ts
sql`UPDATE t SET col = ${value}`
```

drizzle inspects `value`. If it's a JS array (`['a','b']`) it spreads each
element into its own placeholder, so the compiled SQL ends up as
`SET col = ($1, $2)` — a record/tuple, not an array. Postgres then rejects
that against any array-typed column with:

```
42804: column "col" is of type text[] but expression is of type record
```

Today only `ai_agents.channels` is `text[]`, but the moment anyone adds
another array column (`tags`, `allowed_origins`, `role_scopes`,
`cuisine_tags`, …) without thinking about this, the same outage recurs
silently.

## The fix: always bind via `bindValue`

`./bind.ts` exports `bindValue(v)` which normalises any dynamic value into
exactly one bind parameter:

- JS arrays → a single `'{...}'::text[]` literal
- Plain objects → `JSON.stringify(v)::jsonb`
- Everything else → a normal scalar bind

Every dynamic builder (anything using `sql.join(sets, sql\`, \`)` for SET
clauses, or fanning `vals.map(...)` into a VALUES list) MUST route each
dynamic value through `bindValue`:

```ts
import { bindValue } from '@server/db/bind';

sets.push(sql`${sql.raw(col)} = ${bindValue(val)}`);
```

If you're hand-rolling a new dynamic builder, do the same. If you're adding
a new array column, no extra work is needed — `bindValue` already handles
it correctly. See `scripts/test-bind.ts` for the round-trip escaping cases
the helper guarantees.
