conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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 }