conductor

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

mysql.js (3694B)


      1 // src/lib/db/mysql.js - MySQL and TiDB driver built on mysql2
      2 //
      3 // mysql2 is an optional dependency: it is only required when database.url
      4 // uses a mysql scheme, so a default sqlite install does not need it.
      5 
      6 import { compileCached, bindParams } from './query.js';
      7 
      8 async function loadDriver() {
      9   try {
     10     return (await import('mysql2/promise')).default;
     11   } catch (e) {
     12     throw new Error(
     13       'database.url uses a mysql scheme but the mysql2 package is not installed. ' +
     14       'Run `npm install mysql2`, or unset database.url to use sqlite.',
     15       { cause: e }
     16     );
     17   }
     18 }
     19 
     20 function fail(e, sql) {
     21   const err = new Error(`mysql: ${e.message}\n  sql: ${sql.trim().split('\n')[0]}`);
     22   err.cause = e;
     23   err.code = e.code;
     24   throw err;
     25 }
     26 
     27 // Wraps either the pool or a single transaction connection.
     28 function wrap(runner) {
     29   function compile(sql, params) {
     30     const compiled = compileCached(sql, 'mysql');
     31     return { text: compiled.sql, values: bindParams(compiled.keys, params, sql) };
     32   }
     33 
     34   return {
     35     dialect: 'mysql',
     36 
     37     async all(sql, params = {}) {
     38       const { text, values } = compile(sql, params);
     39       try {
     40         const [rows] = await runner.query(text, values);
     41         return rows;
     42       } catch (e) {
     43         fail(e, sql);
     44       }
     45     },
     46 
     47     async get(sql, params = {}) {
     48       const { text, values } = compile(sql, params);
     49       try {
     50         const [rows] = await runner.query(text, values);
     51         return rows[0];
     52       } catch (e) {
     53         fail(e, sql);
     54       }
     55     },
     56 
     57     async run(sql, params = {}) {
     58       const { text, values } = compile(sql, params);
     59       try {
     60         const [res] = await runner.query(text, values);
     61         return {
     62           changes: res.affectedRows ?? 0,
     63           lastInsertId: res.insertId === undefined || res.insertId === 0 ? null : Number(res.insertId),
     64         };
     65       } catch (e) {
     66         fail(e, sql);
     67       }
     68     },
     69 
     70     // Raw DDL, no bind markers are interpreted.
     71     async exec(sql) {
     72       try {
     73         await runner.query(sql);
     74       } catch (e) {
     75         fail(e, sql);
     76       }
     77     },
     78   };
     79 }
     80 
     81 export async function openMysql(cfg) {
     82   const mysql = await loadDriver();
     83   const pool = mysql.createPool({
     84     uri: cfg.database.url,
     85     connectionLimit: cfg.database.connection_limit,
     86     waitForConnections: true,
     87     // Epoch millisecond timestamps sit well inside the safe integer range,
     88     // so BIGINT should come back as a number rather than a string.
     89     supportBigNumbers: true,
     90     bigNumberStrings: false,
     91     dateStrings: true,
     92     multipleStatements: false,
     93   });
     94 
     95   const api = wrap(pool);
     96 
     97   api.transaction = async function transaction(fn) {
     98     const conn = await pool.getConnection();
     99     const tx = wrap(conn);
    100     let depth = 0;
    101 
    102     // Savepoints give the same composability as the other drivers.
    103     tx.transaction = async (inner) => {
    104       const name = `sp_${depth}`;
    105       depth += 1;
    106       await conn.query(`SAVEPOINT ${name}`);
    107       try {
    108         const r = await inner(tx);
    109         await conn.query(`RELEASE SAVEPOINT ${name}`);
    110         depth -= 1;
    111         return r;
    112       } catch (e) {
    113         try {
    114           await conn.query(`ROLLBACK TO SAVEPOINT ${name}`);
    115         } catch {
    116           // Preserve the original failure.
    117         }
    118         depth -= 1;
    119         throw e;
    120       }
    121     };
    122 
    123     try {
    124       await conn.beginTransaction();
    125       const result = await fn(tx);
    126       await conn.commit();
    127       return result;
    128     } catch (e) {
    129       try {
    130         await conn.rollback();
    131       } catch {
    132         // Preserve the original failure.
    133       }
    134       throw e;
    135     } finally {
    136       conn.release();
    137     }
    138   };
    139 
    140   api.close = async () => {
    141     await pool.end();
    142   };
    143 
    144   return api;
    145 }