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 }