@@ -15,14 +15,32 @@ export type DbTransaction = Parameters<Parameters<typeof db.transaction>[0]>[0]
1515const READ_STATEMENT_TIMEOUT_MS = 15_000
1616const READ_LOCK_TIMEOUT_MS = 3_000
1717
18- async function setReadTimeouts ( trx : DbTransaction ) : Promise < void > {
19- await trx . execute ( sql . raw ( `SET LOCAL statement_timeout = '${ READ_STATEMENT_TIMEOUT_MS } ms'` ) )
20- await trx . execute ( sql . raw ( `SET LOCAL lock_timeout = '${ READ_LOCK_TIMEOUT_MS } ms'` ) )
18+ /**
19+ * Applies every guard in ONE round-trip. Each `trx.execute` is its own serial round-trip (the
20+ * driver runs `prepare: false`), and every user-table read opens a transaction, so issuing these
21+ * separately cost 2–3 round-trips on every page, count, and drain batch.
22+ *
23+ * `set_config(name, value, is_local => true)` is exactly `SET LOCAL` — transaction-scoped, dying
24+ * with the commit — but it is a function call, so several fit in a single `SELECT`. Semicolon-
25+ * joining `SET LOCAL` statements would not work here: the driver sends this over the extended
26+ * protocol, which rejects multiple commands in one message.
27+ */
28+ async function setReadGuards ( trx : DbTransaction , seqscanOff : boolean ) : Promise < void > {
29+ /**
30+ * Only ever set to `off`, never explicitly to `on` — the unflagged path must leave whatever
31+ * the server default is, exactly as the separate `SET LOCAL enable_seqscan = off` did.
32+ */
33+ const seqscan = seqscanOff ? sql `, set_config('enable_seqscan', 'off', true)` : sql ``
34+ await trx . execute ( sql `
35+ select
36+ set_config('statement_timeout', ${ `${ READ_STATEMENT_TIMEOUT_MS } ms` } , true),
37+ set_config('lock_timeout', ${ `${ READ_LOCK_TIMEOUT_MS } ms` } , true)${ seqscan }
38+ ` )
2139}
2240
2341/**
2442 * Runs a user-table read inside a transaction that always caps `statement_timeout`
25- * / `lock_timeout` (see {@link setReadTimeouts }). Pass `seqscanOff` for queries
43+ * / `lock_timeout` (see {@link setReadGuards }). Pass `seqscanOff` for queries
2644 * with no tenant-bounded index plan — custom column sorts and filtered counts —
2745 * where the planner otherwise seq-scans the whole shared `user_table_rows`
2846 * relation (every tenant's rows); see {@link withSeqscanOff} for the measured
@@ -34,8 +52,7 @@ export async function withReadGuards<T>(
3452 opts ?: { seqscanOff ?: boolean }
3553) : Promise < T > {
3654 return db . transaction ( async ( trx ) => {
37- await setReadTimeouts ( trx )
38- if ( opts ?. seqscanOff ) await trx . execute ( sql `SET LOCAL enable_seqscan = off` )
55+ await setReadGuards ( trx , opts ?. seqscanOff ?? false )
3956 return fn ( trx )
4057 } )
4158}
0 commit comments