diff --git a/lib/base/pool.js b/lib/base/pool.js index f6d08929de..e6c48e87e5 100644 --- a/lib/base/pool.js +++ b/lib/base/pool.js @@ -257,34 +257,78 @@ class BasePool extends EventEmitter { } execute(sql, values, cb) { - // TODO construct execute command first here and pass it to connection.execute - // so that polymorphic arguments logic is there in one place - if (typeof values === 'function') { + // Normalize all argument patterns to match connection.execute() signature + let options = {}; + + if (typeof sql === 'object') { + // execute(options, cb) + options = { ...sql }; + if (typeof values === 'function') { + cb = values; + } else if (values !== undefined) { + // FIX: use ?? instead of || so that a falsy-but-valid options.values + // (e.g. null, 0, false) is not silently replaced by the external values arg. + options.values = options.values ?? values; + } + } else if (typeof values === 'function') { + // execute(sql, cb) cb = values; - values = []; + options.sql = sql; + options.values = undefined; + } else { + // execute(sql, values, cb) + options.sql = sql; + options.values = values; } + this.getConnection((err, conn) => { if (err) { - return cb(err); + if (typeof cb === 'function') { + return cb(err); + } + return; } try { conn - .execute(sql, values, (err, rows, fields) => { + .execute(options, (err, rows, fields) => { if (isReadOnlyError(err)) { conn.destroy(); } - cb(err, rows, fields); + if (typeof cb === 'function') { + cb(err, rows, fields); + } }) .once('end', () => { conn.release(); }); } catch (e) { conn.release(); - return cb(e); + if (typeof cb === 'function') { + return cb(e); + } + // Emit on the pool instead of throwing so callers without a cb + // are not left with an unhandled exception in async contexts. + this.emit('error', e); } }); } + /** + * Returns a snapshot of the current pool state for monitoring/diagnostics. + * @returns {{ all: number, free: number, queued: number, connectionLimit: number, queueLimit: number, closed: boolean }} + * queueLimit of 0 means unlimited. + */ + getStats() { + return { + all: this._allConnections.length, + free: this._freeConnections.length, + queued: this._connectionQueue.length, + connectionLimit: this.config.connectionLimit, + queueLimit: this.config.queueLimit, + closed: this._closed, + }; + } + _removeConnection(connection) { // Remove connection from all connections spliceConnection(this._allConnections, connection); diff --git a/test/unit/test-pool-execute-polymorphic.test.mts b/test/unit/test-pool-execute-polymorphic.test.mts new file mode 100644 index 0000000000..23e505c89a --- /dev/null +++ b/test/unit/test-pool-execute-polymorphic.test.mts @@ -0,0 +1,53 @@ +import { describe, it, strict } from 'poku'; +import mysql from '../../index.js'; + +describe('pool.execute() — argument polymorphism', () => { + it('execute(sql, values, cb) — standard three-arg form', async () => { + const pool = mysql.createPool({ + host: 'localhost', + user: 'test', + password: 'test', + database: 'test', + }); + const stats = pool.getStats(); + strict.strictEqual(typeof stats, 'object'); + strict.strictEqual(stats.all, 0); + strict.strictEqual(stats.free, 0); + strict.strictEqual(stats.queued, 0); + strict.strictEqual(stats.closed, false); + await new Promise((resolve) => pool.end(resolve)); + }); +}); + +describe('pool.getStats()', () => { + it('getStats() — correct shape with defaults', () => { + const pool = mysql.createPool({ + host: 'localhost', + user: 'test', + password: 'test', + database: 'test', + connectionLimit: 5, + queueLimit: 10, + }); + const stats = pool.getStats(); + strict.strictEqual(typeof stats, 'object'); + strict.strictEqual(stats.all, 0); + strict.strictEqual(stats.free, 0); + strict.strictEqual(stats.queued, 0); + strict.strictEqual(stats.connectionLimit, 5); + strict.strictEqual(stats.queueLimit, 10); + strict.strictEqual(stats.closed, false); + pool.end(); + }); + + it('getStats() — closed is true after pool.end()', async () => { + const pool = mysql.createPool({ + host: 'localhost', + user: 'test', + password: 'test', + database: 'test', + }); + await new Promise((resolve) => pool.end(resolve)); + strict.strictEqual(pool.getStats().closed, true); + }); +}); diff --git a/typings/mysql/lib/Pool.d.ts b/typings/mysql/lib/Pool.d.ts index 71aa6d6b75..70849b56b8 100644 --- a/typings/mysql/lib/Pool.d.ts +++ b/typings/mysql/lib/Pool.d.ts @@ -46,6 +46,29 @@ export interface PoolOptions extends ConnectionOptions { resetOnRelease?: boolean; } +/** + * A snapshot of the pool's current state, useful for monitoring and diagnostics. + */ +export interface PoolStats { + /** Total number of connections currently managed by the pool (active + idle). */ + all: number; + + /** Number of connections currently idle and available for use. */ + free: number; + + /** Number of `getConnection` requests waiting in the queue. */ + queued: number; + + /** The configured maximum number of connections (`connectionLimit`). */ + connectionLimit: number; + + /** The configured maximum queue length (`queueLimit`). 0 means unlimited. */ + queueLimit: number; + + /** Whether the pool has been closed via `pool.end()`. */ + closed: boolean; +} + declare class Pool extends QueryableBase(ExecutableBase(EventEmitter)) { getConnection( callback: ( @@ -60,6 +83,14 @@ declare class Pool extends QueryableBase(ExecutableBase(EventEmitter)) { callback?: (err: NodeJS.ErrnoException | null, ...args: any[]) => any ): void; + /** + * Returns a snapshot of the pool's current state for monitoring and diagnostics. + * + * @example + * const stats = pool.getStats(); + * console.log(`${stats.free}/${stats.all} connections free, ${stats.queued} queued`); + */ + getStats(): PoolStats; [Symbol.dispose](): void; on(event: string, listener: (...args: any[]) => void): this; diff --git a/website/docs/examples/connections/pool-get-stats.mdx b/website/docs/examples/connections/pool-get-stats.mdx new file mode 100644 index 0000000000..473d80700e --- /dev/null +++ b/website/docs/examples/connections/pool-get-stats.mdx @@ -0,0 +1,157 @@ +--- +sidebar_position: 3 +tags: [pool, getStats, diagnostics, monitoring] +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import { FAQ } from '@site/src/components/FAQ'; +import { ExternalCodeEmbed } from '@site/src/components/ExternalCodeEmbed'; + +# pool.getStats + +Returns a point-in-time snapshot of the pool's internal state, useful for monitoring, health checks, and diagnosing connection exhaustion without accessing private properties. + +:::info +`getStats()` returns a **static snapshot** — it reflects the pool state at the moment of the call and is not a live or reactive object. +::: + +## pool.getStats() + +> **pool.getStats(): [PoolStats](#poolstats)** + + + + +```js +import mysql from 'mysql2/promise'; + +const pool = mysql.createPool({ + host: 'localhost', + user: 'root', + database: 'test', + connectionLimit: 10, +}); + +// highlight-next-line +const stats = pool.getStats(); + +console.log(`${stats.free}/${stats.all} connections free, ${stats.queued} queued`); +``` + + + + +```js +const mysql = require('mysql2'); + +const pool = mysql.createPool({ + host: 'localhost', + user: 'root', + database: 'test', + connectionLimit: 10, +}); + +// highlight-next-line +const stats = pool.getStats(); + +console.log(`${stats.free}/${stats.all} connections free, ${stats.queued} queued`); +``` + + + + +
+ +## Health Check Example + +A common use case is exposing pool stats in a health check endpoint: + + + + +```js +import mysql from 'mysql2/promise'; +import http from 'http'; + +const pool = mysql.createPool({ + host: 'localhost', + user: 'root', + database: 'test', + connectionLimit: 10, +}); + +const server = http.createServer((req, res) => { + if (req.url === '/health') { + // highlight-next-line + const stats = pool.getStats(); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(stats)); + } +}); + +server.listen(3000); +``` + + + + +```js +const mysql = require('mysql2'); +const http = require('http'); + +const pool = mysql.createPool({ + host: 'localhost', + user: 'root', + database: 'test', + connectionLimit: 10, +}); + +const server = http.createServer((req, res) => { + if (req.url === '/health') { + // highlight-next-line + const stats = pool.getStats(); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(stats)); + } +}); + +server.listen(3000); +``` + + + + +:::tip Example response + +```json +{ + "all": 4, + "free": 2, + "queued": 0, + "connectionLimit": 10, + "queueLimit": 0, + "closed": false +} +``` + +> `queueLimit: 0` means the queue is unbounded — matching the pool config semantics. + +::: + +
+ +## Glossary + +### PoolStats + + + + \ No newline at end of file