Loading...
Loading...
Direct MySQL database access with mysql2 driver -- connection pools, prepared statements, transactions, streaming, typed queries, error handling
npx skill4agent add agents-inc/skills api-database-mysqlQuick Guide: Use mysql2/promise for all new code -- it provides async/await support over the mysql2 callback API. Always use(nevercreatePool()in production) withcreateConnection()for parameterized queries (prepared statements, LRU-cached). Type query results withexecute()generics for SELECTs andRowDataPacketfor INSERT/UPDATE/DELETE. For transactions, acquire a dedicated connection withResultSetHeader, wrap in try/finally to guaranteepool.getConnection(). Never interpolate user input into SQL strings -- always useconnection.release()placeholders. Handle?andER_DUP_ENTRYexplicitly in catch blocks.ER_LOCK_DEADLOCK
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,, named constants)import type
execute()?pool.getConnection()finallypool.execute()mysql2/promisemysql2awaiterrormysql2/promiseexecute()?RowDataPacketResultSetHeadergetConnectionbeginTransactioncommitrollbackreleaseconnection.query().stream()ER_DUP_ENTRYER_LOCK_DEADLOCKacquirereleaseenqueuecreatePool()createConnection()execute()query()RowDataPacketResultSetHeaderanyRowDataPacketpool.execute()pool.query()pool.getConnection()connection.release()finallycodeER_DUP_ENTRYER_LOCK_DEADLOCKerror.code// Good Example - Production pool setup
import mysql from "mysql2/promise";
import type { Pool } from "mysql2/promise";
const DEFAULT_CONNECTION_LIMIT = 10;
const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
function createDatabasePool(): Pool {
const url = process.env.DATABASE_URL;
if (!url) {
throw new Error("DATABASE_URL environment variable is required");
}
return mysql.createPool({
uri: url,
waitForConnections: true,
connectionLimit: DEFAULT_CONNECTION_LIMIT,
maxIdle: DEFAULT_CONNECTION_LIMIT,
idleTimeout: DEFAULT_IDLE_TIMEOUT_MS,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
});
}
export { createDatabasePool };waitForConnections: trueenableKeepAlive// Bad Example - Hardcoded single connection
import mysql from "mysql2/promise";
const connection = await mysql.createConnection({
host: "localhost",
user: "root",
password: "password123",
database: "mydb",
});
// Hardcoded credentials, single connection exhausts under load, no poolRowDataPacketResultSetHeader// Good Example - Typed SELECT and INSERT
import type { Pool, RowDataPacket, ResultSetHeader } from "mysql2/promise";
interface UserRow extends RowDataPacket {
id: number;
email: string;
name: string;
created_at: Date;
}
async function getUserById(
pool: Pool,
userId: number,
): Promise<UserRow | null> {
const [rows] = await pool.execute<UserRow[]>(
"SELECT id, email, name, created_at FROM users WHERE id = ?",
[userId],
);
return rows[0] ?? null;
}
async function createUser(
pool: Pool,
email: string,
name: string,
): Promise<number> {
const [result] = await pool.execute<ResultSetHeader>(
"INSERT INTO users (email, name) VALUES (?, ?)",
[email, name],
);
return result.insertId;
}RowDataPacketexecute()[rows]// Bad Example - Untyped query with interpolation
const [rows] = await pool.query(`SELECT * FROM users WHERE id = ${userId}`);
// SQL injection vulnerability, untyped results, query() skips prepared statementsanyquery()// Good Example - Transfer with transaction
async function transferFunds(
pool: Pool,
fromId: number,
toId: number,
amount: number,
): Promise<void> {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.execute(
"UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
[amount, fromId, amount],
);
await connection.execute(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
[amount, toId],
);
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}getConnection()finallyrollback().stream()// Good Example - Stream rows without loading all into memory
import mysql from "mysql2";
function streamUsers(
pool: ReturnType<typeof mysql.createPool>,
): NodeJS.ReadableStream {
return pool
.query("SELECT * FROM users WHERE active = 1")
.stream({ highWaterMark: 100 });
}highWaterMarkcodeerrno// Good Example - Structured error handling
import type { Pool, ResultSetHeader } from "mysql2/promise";
const MYSQL_ER_DUP_ENTRY = "ER_DUP_ENTRY";
interface MysqlError extends Error {
code: string;
errno: number;
sqlState: string;
sqlMessage: string;
}
function isMysqlError(error: unknown): error is MysqlError {
return error instanceof Error && "code" in error && "errno" in error;
}
async function createUserSafe(
pool: Pool,
email: string,
name: string,
): Promise<{ insertId: number } | { duplicate: true }> {
try {
const [result] = await pool.execute<ResultSetHeader>(
"INSERT INTO users (email, name) VALUES (?, ?)",
[email, name],
);
return { insertId: result.insertId };
} catch (error) {
if (isMysqlError(error) && error.code === MYSQL_ER_DUP_ENTRY) {
return { duplicate: true };
}
throw error;
}
}execute()execute()query()query().stream()connectionLimit = (number of CPU cores * 2) + number of disk spindlesconnectionLimit: 10max_connectionsenableKeepAlive: trueECONNRESETINSERT ... VALUES (...), (...), (...)What am I building?
-- Production server handling concurrent requests? -> createPool()
-- One-off CLI script or migration? -> createConnection() is acceptable
-- Serverless function (Lambda, Vercel)? -> createPool() with connectionLimit: 1Does the SQL have user-provided parameters?
-- YES -> execute() with ? placeholders (ALWAYS)
-- NO, but same SQL runs repeatedly? -> execute() (benefits from LRU cache)
-- NO, SQL text itself is dynamic? -> query() (cannot prepare dynamic SQL)
-- Need to stream results? -> query().stream() on the callback APIIs this a single query?
-- YES -> pool.execute() or pool.query() (auto-acquires and releases)
-- NO, multiple queries needing same connection? -> pool.getConnection()
-- Transaction? -> pool.getConnection() (REQUIRED)What MySQL error did I get?
-- ER_DUP_ENTRY (1062) -> Handle as business logic (return conflict, not throw)
-- ER_LOCK_DEADLOCK (1213) -> Retry the entire transaction (MySQL rolled it back)
-- ER_LOCK_WAIT_TIMEOUT (1205) -> Retry or fail with timeout message
-- ECONNREFUSED / PROTOCOL_CONNECTION_LOST -> Connection issue, pool will reconnect
-- ER_ACCESS_DENIED_ERROR (1045) -> Configuration error, fail fast\) -- SQL injection vulnerability, always use placeholders withpool.execute()pool.query()pool.getConnection()mysql2mysql2/promiseawaitpool.getConnection()finallyquery()execute()errorconnectionLimitenableKeepAlive: trueECONNRESETpool.end()rows.lengthresult.affectedRowsResultSetHeaderinsertIdINSERT ... ON DUPLICATE KEY UPDATEinsertId0connection.release()connection.destroy()nullundefinednullNULLundefinedDECIMALBIGINTDATEDateDATETIMEDateexecute()namedPlaceholders: true?multipleStatements: true;query()waitForConnections: falsetrueResultSetHeader.warningStatusOkPacketwarningCountResultSetHeaderwarningStatusAll code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,, named constants)import type
execute()?pool.getConnection()finallypool.execute()mysql2/promisemysql2awaiterror