ReviewOS

also looking at this

stacks/bun-query-builder

fix(migrate): apply each migration atomically, and one run at a time

#1076
Merged glennmichael123 wants to merge fix/migrate-transaction-lock into main
3 files +364 -54
packages/bun-query-builder/src/client.tsmodified+51-8
Changes to packages/bun-query-builder/src/client.ts
@@ -2496,6 +2496,20 @@ finally { reserved.release() }
24962496 advisoryLock?: (key: number | string) => Promise<void>
24972497 /** Try to take an advisory lock and return false if unavailable (PostgreSQL only). */
24982498 tryAdvisoryLock?: (key: number | string) => Promise<boolean>
2499 /**
2500 * Release an advisory lock taken on THIS builder's connection.
2501 *
2502 * Both lock helpers existed without this one, which made them a trap rather
2503 * than a tool: a Postgres session lock is held until it is released or the
2504 * SESSION ends, and a pooled connection does not end when it is returned to
2505 * the pool. So `advisoryLock()` on a pooled builder leaked the lock onto
2506 * whichever connection happened to serve it, and the next caller waited
2507 * forever on a lock nobody was holding on purpose.
2508 *
2509 * Take the lock on a `reserve()`d connection and release it through the same
2510 * builder.
2511 */
2512 advisoryUnlock?: (key: number | string) => Promise<boolean>
24992513 /** Get all relationships defined for a table. */
25002514 getRelationships?: (table: string) => Record<string, any>
25012515 /** Check if a table has a specific relationship. */
@@ -2781,6 +2795,23 @@ export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Parti
27812795 const meta = state?.meta
27822796 const schema = state?.schema
27832797
2798 /**
2799 * The 32-bit key Postgres advisory locks take, derived from a string name.
2800 *
2801 * Extracted because lock, try-lock and unlock each carried their own copy:
2802 * three transcriptions of one hash, where any drift between them means
2803 * `advisoryUnlock` computes a different key than `advisoryLock` did and
2804 * silently releases nothing.
2805 */
2806 function advisoryLockKey(key: number | string): number {
2807 if (typeof key === 'number')
2808 return key
2809 const s = String(key)
2810 let hash = 7
2811 for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) | 0
2812 return Math.abs(hash)
2813 }
2814
27842815 /**
27852816 * The hooks this builder should fire.
27862817 *
@@ -7025,10 +7056,7 @@ export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Parti
70257056 },
70267057 async advisoryLock(key: number | string): Promise<void> {
70277058 if (config.dialect === 'postgres') {
7028 const s = String(key)
7029 let hash = 7
7030 for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) | 0
7031 const k = typeof key === 'number' ? key : Math.abs(hash)
7059 const k = advisoryLockKey(key)
70327060 const q = _sql`SELECT pg_advisory_lock(${k})`
70337061 await runWithHooks<any[]>(q, 'raw')
70347062 return
@@ -7049,10 +7077,7 @@ export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Parti
70497077 },
70507078 async tryAdvisoryLock(key: number | string): Promise<boolean> {
70517079 if (config.dialect === 'postgres') {
7052 const s = String(key)
7053 let hash = 7
7054 for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) | 0
7055 const k = typeof key === 'number' ? key : Math.abs(hash)
7080 const k = advisoryLockKey(key)
70567081 const q = _sql`SELECT pg_try_advisory_lock(${k}) as ok`
70577082 const rows = await runWithHooks<any[]>(q, 'raw')
70587083 return Boolean(rows?.[0]?.ok)
@@ -7071,6 +7096,24 @@ export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Parti
70717096 // when locks aren't held.
70727097 return false
70737098 },
7099 async advisoryUnlock(key: number | string): Promise<boolean> {
7100 // Must run on the same connection that took the lock — hence `_sql`,
7101 // and hence the requirement that callers hold a reserved builder.
7102 if (config.dialect === 'postgres') {
7103 const k = advisoryLockKey(key)
7104 const q = _sql`SELECT pg_advisory_unlock(${k}) as ok`
7105 const rows = await runWithHooks<any[]>(q, 'raw')
7106 return Boolean(rows?.[0]?.ok)
7107 }
7108 if (isMysqlLike(config.dialect)) {
7109 const lockName = `bqb:${String(key)}`
7110 const q = _sql`SELECT RELEASE_LOCK(${lockName}) AS ok`
7111 const rows = await runWithHooks<any[]>(q, 'raw')
7112 return Number(rows?.[0]?.ok) === 1
7113 }
7114 // SQLite never took one; mirror tryAdvisoryLock's graceful shape.
7115 return false
7116 },
70747117 unsafe(query: string, params?: any[]) {
70757118 // Use this builder's connection. Transaction callbacks receive a
70767119 // builder whose `_sql` is the reserved transaction connection; routing