also looking at this
fix(migrate): apply each migration atomically, and one run at a time
#1076
3 files
+364
-54
| @@ -2496,6 +2496,20 @@ finally { reserved.release() } | ||
| 2496 | 2496 | advisoryLock?: (key: number | string) => Promise<void> |
| 2497 | 2497 | /** Try to take an advisory lock and return false if unavailable (PostgreSQL only). */ |
| 2498 | 2498 | 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> | |
| 2499 | 2513 | /** Get all relationships defined for a table. */ |
| 2500 | 2514 | getRelationships?: (table: string) => Record<string, any> |
| 2501 | 2515 | /** Check if a table has a specific relationship. */ |
| @@ -2781,6 +2795,23 @@ export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Parti | ||
| 2781 | 2795 | const meta = state?.meta |
| 2782 | 2796 | const schema = state?.schema |
| 2783 | 2797 | |
| 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 | ||
| 2784 | 2815 | /** |
| 2785 | 2816 | * The hooks this builder should fire. |
| 2786 | 2817 | * |
| @@ -7025,10 +7056,7 @@ export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Parti | ||
| 7025 | 7056 | }, |
| 7026 | 7057 | async advisoryLock(key: number | string): Promise<void> { |
| 7027 | 7058 | 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) | |
| 7032 | 7060 | const q = _sql`SELECT pg_advisory_lock(${k})` |
| 7033 | 7061 | await runWithHooks<any[]>(q, 'raw') |
| 7034 | 7062 | return |
| @@ -7049,10 +7077,7 @@ export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Parti | ||
| 7049 | 7077 | }, |
| 7050 | 7078 | async tryAdvisoryLock(key: number | string): Promise<boolean> { |
| 7051 | 7079 | 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) | |
| 7056 | 7081 | const q = _sql`SELECT pg_try_advisory_lock(${k}) as ok` |
| 7057 | 7082 | const rows = await runWithHooks<any[]>(q, 'raw') |
| 7058 | 7083 | return Boolean(rows?.[0]?.ok) |
| @@ -7071,6 +7096,24 @@ export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Parti | ||
| 7071 | 7096 | // when locks aren't held. |
| 7072 | 7097 | return false |
| 7073 | 7098 | }, |
| 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 | }, | |
| 7074 | 7117 | unsafe(query: string, params?: any[]) { |
| 7075 | 7118 | // Use this builder's connection. Transaction callbacks receive a |
| 7076 | 7119 | // builder whose `_sql` is the reserved transaction connection; routing |