Files
bun.sh/test/regression/issue/26030.test.ts
robobun a9b5f5cbd1 fix(sql): prevent hang in sequential MySQL transactions with returned array queries (#26048)
## Summary

- Fix a hang in sequential MySQL transactions where an INSERT is awaited
followed by a SELECT returned in an array
- The issue occurred because `handleResultSetOK`'s defer block only
called `queue.advance()` without flushing, causing queries added during
the JS callback to not be properly sent
- Changed to call `flushQueue()` instead of just `advance()` to ensure
data is actually sent to the server

Fixes #26030

## Test plan

- Added regression test `test/regression/issue/26030.test.ts` with three
test cases:
- `Sequential transactions with INSERT and returned SELECT should not
hang` - reproduces the exact pattern from the bug report
- `Sequential transactions with returned array of multiple queries` -
tests returning multiple queries in array
- `Many sequential transactions with awaited INSERT and returned SELECT`
- stress tests with 5 sequential transactions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 12:53:04 -08:00

134 lines
4.6 KiB
TypeScript

import { SQL, randomUUIDv7 } from "bun";
import { beforeEach, expect, test } from "bun:test";
import { describeWithContainer } from "harness";
describeWithContainer(
"mysql",
{
image: "mysql_plain",
env: {},
args: [],
},
container => {
const getOptions = () => ({
url: `mysql://root@${container.host}:${container.port}/bun_sql_test`,
max: 1,
bigint: true,
});
beforeEach(async () => {
await container.ready;
});
// Regression test for https://github.com/oven-sh/bun/issues/26030
// Bun hangs when executing multiple sequential MySQL transactions in a loop where:
// 1. An INSERT is awaited inside the transaction callback
// 2. A SELECT query (e.g., SELECT LAST_INSERT_ID()) is returned as an array without being awaited
test("Sequential transactions with INSERT and returned SELECT should not hang", async () => {
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
// Create a table similar to the reproduction case
await sql`CREATE TABLE IF NOT EXISTS ${sql(random_name)} (
id INT AUTO_INCREMENT PRIMARY KEY,
contract_name VARCHAR(255),
amount INT
)`;
try {
const rows = [
{ contract_name: "Contract A", amount: 100000 },
{ contract_name: "Contract B", amount: 200000 },
{ contract_name: "Contract C", amount: 300000 },
];
const contractIds: number[] = [];
for (const row of rows) {
// This is the pattern from the bug report:
// - INSERT is awaited
// - SELECT LAST_INSERT_ID() is returned as array (not awaited individually)
const [[result]] = await sql.begin(async tx => {
await tx`
INSERT INTO ${sql(random_name)} (contract_name, amount)
VALUES (${row.contract_name}, ${row.amount})
`;
// Return array with non-awaited query - this triggers the hang
return [tx`SELECT LAST_INSERT_ID() as id`];
});
contractIds.push(Number(result.id));
}
// Verify all transactions completed
expect(contractIds.length).toBe(3);
expect(contractIds[0]).toBe(1);
expect(contractIds[1]).toBe(2);
expect(contractIds[2]).toBe(3);
// Verify data in database
const count = await sql`SELECT COUNT(*) as count FROM ${sql(random_name)}`;
expect(Number(count[0].count)).toBe(3);
} finally {
await sql`DROP TABLE IF EXISTS ${sql(random_name)}`;
}
});
test("Sequential transactions with returned array of multiple queries", async () => {
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TABLE IF NOT EXISTS ${sql(random_name)} (
id INT AUTO_INCREMENT PRIMARY KEY,
value INT
)`;
try {
for (let i = 0; i < 3; i++) {
const results = await sql.begin(async tx => {
await tx`INSERT INTO ${sql(random_name)} (value) VALUES (${i * 10})`;
// Return multiple queries as array
return [tx`SELECT LAST_INSERT_ID() as id`, tx`SELECT COUNT(*) as count FROM ${sql(random_name)}`];
});
expect(results.length).toBe(2);
}
const count = await sql`SELECT COUNT(*) as count FROM ${sql(random_name)}`;
expect(Number(count[0].count)).toBe(3);
} finally {
await sql`DROP TABLE IF EXISTS ${sql(random_name)}`;
}
});
test("Many sequential transactions with awaited INSERT and returned SELECT", async () => {
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TABLE IF NOT EXISTS ${sql(random_name)} (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255)
)`;
try {
// Multiple sequential transactions with awaited INSERT and returned SELECT
for (let i = 0; i < 5; i++) {
const [[result]] = await sql.begin(async tx => {
// First insert
await tx`INSERT INTO ${sql(random_name)} (name) VALUES (${"item_" + i})`;
// Return array with SELECT
return [tx`SELECT LAST_INSERT_ID() as id`];
});
expect(Number(result.id)).toBe(i + 1);
}
const count = await sql`SELECT COUNT(*) as count FROM ${sql(random_name)}`;
expect(Number(count[0].count)).toBe(5);
} finally {
await sql`DROP TABLE IF EXISTS ${sql(random_name)}`;
}
});
},
);