postgres.js (3810B)
1 // src/lib/db/postgres.js - PostgreSQL driver built on pg 2 // 3 // pg is an optional dependency: it is only required when database.url uses a 4 // postgres scheme. 5 // 6 // Two adaptations are made so that portable queries keep working: 7 // - {name} markers compile to $n, see query.js 8 // - int8 is parsed to Number, since epoch millisecond timestamps are well 9 // inside the safe integer range and pg returns int8 as a string otherwise 10 // 11 // lastInsertId is always null here. The schema uses application generated 12 // string identifiers throughout, so nothing depends on it; retrieving one 13 // would need a RETURNING clause the other dialects do not accept. 14 15 import { compileCached, bindParams } from './query.js'; 16 17 async function loadDriver() { 18 try { 19 return (await import('pg')).default; 20 } catch (e) { 21 throw new Error( 22 'database.url uses a postgres scheme but the pg package is not installed. ' + 23 'Run `npm install pg`, or unset database.url to use sqlite.', 24 { cause: e } 25 ); 26 } 27 } 28 29 function fail(e, sql) { 30 const err = new Error(`postgres: ${e.message}\n sql: ${sql.trim().split('\n')[0]}`); 31 err.cause = e; 32 err.code = e.code; 33 throw err; 34 } 35 36 // Wraps either the pool or a single transaction client. 37 function wrap(runner) { 38 function compile(sql, params) { 39 const compiled = compileCached(sql, 'postgres'); 40 return { text: compiled.sql, values: bindParams(compiled.keys, params, sql) }; 41 } 42 43 return { 44 dialect: 'postgres', 45 46 async all(sql, params = {}) { 47 const { text, values } = compile(sql, params); 48 try { 49 const res = await runner.query(text, values); 50 return res.rows; 51 } catch (e) { 52 fail(e, sql); 53 } 54 }, 55 56 async get(sql, params = {}) { 57 const { text, values } = compile(sql, params); 58 try { 59 const res = await runner.query(text, values); 60 return res.rows[0]; 61 } catch (e) { 62 fail(e, sql); 63 } 64 }, 65 66 async run(sql, params = {}) { 67 const { text, values } = compile(sql, params); 68 try { 69 const res = await runner.query(text, values); 70 return { changes: res.rowCount ?? 0, lastInsertId: null }; 71 } catch (e) { 72 fail(e, sql); 73 } 74 }, 75 76 // Raw DDL, no bind markers are interpreted. 77 async exec(sql) { 78 try { 79 await runner.query(sql); 80 } catch (e) { 81 fail(e, sql); 82 } 83 }, 84 }; 85 } 86 87 export async function openPostgres(cfg) { 88 const pg = await loadDriver(); 89 90 // int8 (oid 20) would otherwise arrive as a string. 91 pg.types.setTypeParser(20, (v) => (v === null ? null : Number(v))); 92 93 const pool = new pg.Pool({ 94 connectionString: cfg.database.url, 95 max: cfg.database.connection_limit, 96 }); 97 98 const api = wrap(pool); 99 100 api.transaction = async function transaction(fn) { 101 const client = await pool.connect(); 102 const tx = wrap(client); 103 let depth = 0; 104 105 tx.transaction = async (inner) => { 106 const name = `sp_${depth}`; 107 depth += 1; 108 await client.query(`SAVEPOINT ${name}`); 109 try { 110 const r = await inner(tx); 111 await client.query(`RELEASE SAVEPOINT ${name}`); 112 depth -= 1; 113 return r; 114 } catch (e) { 115 try { 116 await client.query(`ROLLBACK TO SAVEPOINT ${name}`); 117 } catch { 118 // Preserve the original failure. 119 } 120 depth -= 1; 121 throw e; 122 } 123 }; 124 125 try { 126 await client.query('BEGIN'); 127 const result = await fn(tx); 128 await client.query('COMMIT'); 129 return result; 130 } catch (e) { 131 try { 132 await client.query('ROLLBACK'); 133 } catch { 134 // Preserve the original failure. 135 } 136 throw e; 137 } finally { 138 client.release(); 139 } 140 }; 141 142 api.close = async () => { 143 await pool.end(); 144 }; 145 146 return api; 147 }