Skip to content
60 changes: 52 additions & 8 deletions lib/base/pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
53 changes: 53 additions & 0 deletions test/unit/test-pool-execute-polymorphic.test.mts
Original file line number Diff line number Diff line change
@@ -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<void>((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<void>((resolve) => pool.end(resolve));
strict.strictEqual(pool.getStats().closed, true);
});
});
31 changes: 31 additions & 0 deletions typings/mysql/lib/Pool.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand All @@ -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;
Expand Down
157 changes: 157 additions & 0 deletions website/docs/examples/connections/pool-get-stats.mdx
Original file line number Diff line number Diff line change
@@ -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)**

<Tabs>
<TabItem value='promise.js' default>

```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`);
```

</TabItem>
<TabItem value='callback.js'>

```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`);
```

</TabItem>
</Tabs>

<hr />

## Health Check Example

A common use case is exposing pool stats in a health check endpoint:

<Tabs>
<TabItem value='promise.js' default>

```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);
```

</TabItem>
<TabItem value='callback.js'>

```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);
```

</TabItem>
</Tabs>

:::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.

:::

<hr />

## Glossary

### PoolStats

<FAQ title='PoolStats Specification'>
<ExternalCodeEmbed
language='ts'
url='https://raw.githubusercontent.com/sidorares/node-mysql2/master/typings/mysql/lib/Pool.d.ts'
extractMethod='PoolStats'
methodType='interface'
/>
</FAQ>
Loading