@@ -15,14 +15,36 @@ 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 awaited `trx.execute` is its own round-trip, and
20+ * every user-table read opens a transaction, so issuing these separately cost 2–3 round-trips on
21+ * 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, and reverting the same way on a savepoint rollback — but it is a function call,
25+ * so several fit in one `SELECT`. It also takes the values as bound parameters, which `SET LOCAL`
26+ * cannot. That is the reason they must be one statement rather than semicolon-joined: a bound
27+ * parameter forces the extended protocol, which rejects multiple commands per message.
28+ *
29+ * The guards are the first statement in the transaction, so an invalid value aborts it before
30+ * `fn(trx)` can run — there is no path where a read proceeds unguarded.
31+ */
32+ async function setReadGuards ( trx : DbTransaction , seqscanOff : boolean ) : Promise < void > {
33+ /**
34+ * Only ever set to `off`, never explicitly to `on` — the unflagged path must leave whatever
35+ * the server default is, exactly as the separate `SET LOCAL enable_seqscan = off` did.
36+ */
37+ const seqscan = seqscanOff ? sql `, set_config('enable_seqscan', 'off', true)` : sql ``
38+ await trx . execute ( sql `
39+ select
40+ set_config('statement_timeout', ${ `${ READ_STATEMENT_TIMEOUT_MS } ms` } , true),
41+ set_config('lock_timeout', ${ `${ READ_LOCK_TIMEOUT_MS } ms` } , true)${ seqscan }
42+ ` )
2143}
2244
2345/**
2446 * Runs a user-table read inside a transaction that always caps `statement_timeout`
25- * / `lock_timeout` (see {@link setReadTimeouts }). Pass `seqscanOff` for queries
47+ * / `lock_timeout` (see {@link setReadGuards }). Pass `seqscanOff` for queries
2648 * with no tenant-bounded index plan — custom column sorts and filtered counts —
2749 * where the planner otherwise seq-scans the whole shared `user_table_rows`
2850 * relation (every tenant's rows); see {@link withSeqscanOff} for the measured
@@ -34,8 +56,7 @@ export async function withReadGuards<T>(
3456 opts ?: { seqscanOff ?: boolean }
3557) : Promise < T > {
3658 return db . transaction ( async ( trx ) => {
37- await setReadTimeouts ( trx )
38- if ( opts ?. seqscanOff ) await trx . execute ( sql `SET LOCAL enable_seqscan = off` )
59+ await setReadGuards ( trx , opts ?. seqscanOff ?? false )
3960 return fn ( trx )
4061 } )
4162}
0 commit comments