From 688f4bcb58157715be4cfa1f2ad4d950acd5307c Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Fri, 16 Oct 2026 06:18:59 +0100 Subject: [PATCH] Fix silent exit when a query runs on a connection left idle by a prompt An idle Bun.SQL connection does not keep the event loop alive. After an inquirer prompt released stdin, the next query let Bun exit with code 0 mid-query: `schema` never applied confirmed destructive changes, and `admin` table views exited instead of showing results. Hold the loop open for the duration of every query and transaction. Co-Authored-By: Claude Opus 5.5 --- dist/lib/db-handler.d.ts | 7 +++++++ dist/lib/db-handler.js | 17 ++++++++++++++++- dist/lib/mariadb/db-update.js | 6 +++--- package.json | 2 +- src/lib/db-handler.ts | 17 ++++++++++++++++- src/lib/mariadb/db-update.ts | 6 +++--- 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/dist/lib/db-handler.d.ts b/dist/lib/db-handler.d.ts index aea8e7c..83b5a0b 100644 --- a/dist/lib/db-handler.d.ts +++ b/dist/lib/db-handler.d.ts @@ -15,6 +15,13 @@ type Param = { * and CRUD do not open a new connection per query. */ export declare function resolveClient(config?: BunMariaDBConfig): Bun.SQL | undefined; +/** + * Keep the event loop alive until `fn` settles. + * An idle Bun.SQL connection does not hold the process open, so a query sent + * on it after something else released the loop (e.g. an inquirer prompt + * releasing stdin) lets Bun exit silently mid-query. + */ +export declare function holdEventLoop(fn: () => Promise): Promise; /** * # Main DB Handler Function */ diff --git a/dist/lib/db-handler.js b/dist/lib/db-handler.js index 1b2a065..06cc618 100644 --- a/dist/lib/db-handler.js +++ b/dist/lib/db-handler.js @@ -16,6 +16,21 @@ export function resolveClient(config) { } return grabMariaDBClient({ config }); } +/** + * Keep the event loop alive until `fn` settles. + * An idle Bun.SQL connection does not hold the process open, so a query sent + * on it after something else released the loop (e.g. an inquirer prompt + * releasing stdin) lets Bun exit silently mid-query. + */ +export async function holdEventLoop(fn) { + const timer = setInterval(() => { }, 1 << 30); + try { + return await fn(); + } + finally { + clearInterval(timer); + } +} /** * Convert driver rows to plain objects. * - BigInt -> number (or string when outside the safe integer range) @@ -54,7 +69,7 @@ export default async function dbHandler({ query, values, config, client }) { if (!CLIENT) { throw new Error(`Couldn't grab MariaDB Client.`); } - const res = await CLIENT.unsafe(query, values); + const res = await holdEventLoop(() => CLIENT.unsafe(query, values)); const count = res.count; const res_array = normalizeRows(res); const last_insert_id = res_array?.[0] ? res_array[0]?.id : undefined; diff --git a/dist/lib/mariadb/db-update.js b/dist/lib/mariadb/db-update.js index b7ed54d..9a64229 100644 --- a/dist/lib/mariadb/db-update.js +++ b/dist/lib/mariadb/db-update.js @@ -1,4 +1,4 @@ -import dbHandler, { resolveClient } from "../db-handler"; +import dbHandler, { holdEventLoop, resolveClient } from "../db-handler"; import sqlGenerator from "../../utils/sql-generator"; import sanitizeHtmlFields from "../../utils/sanitize-html-fields"; import { quoteIdentifier } from "../../utils/sql-identifier"; @@ -62,7 +62,7 @@ export default async function DbUpdate({ table, data, query, targetId, config, } * all in one transaction. Re-fetching by id still works when the update * changes a column used in the WHERE clause. */ - return await CLIENT.begin(async (tx) => { + return await holdEventLoop(() => CLIENT.begin(async (tx) => { const matched_res = await dbHandler({ query: `SELECT id FROM ${quoteIdentifier(table)} ${whereClause} FOR UPDATE`, values: sqlQueryObj.whereValues, @@ -111,7 +111,7 @@ export default async function DbUpdate({ table, data, query, targetId, config, } }, db_res: res.db_res, }; - }); + })); } catch (error) { if (error instanceof UpdateAbort) { diff --git a/package.json b/package.json index 8ae2eca..65c5fc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moduletrace/bun-mariadb", - "version": "1.0.23", + "version": "1.0.24", "description": "Schema-driven MariaDB manager for Bun", "author": "Benjamin Toby", "license": "MIT", diff --git a/src/lib/db-handler.ts b/src/lib/db-handler.ts index ce1adf3..721bef4 100644 --- a/src/lib/db-handler.ts +++ b/src/lib/db-handler.ts @@ -42,6 +42,21 @@ export function resolveClient(config?: BunMariaDBConfig): Bun.SQL | undefined { return grabMariaDBClient({ config }); } +/** + * Keep the event loop alive until `fn` settles. + * An idle Bun.SQL connection does not hold the process open, so a query sent + * on it after something else released the loop (e.g. an inquirer prompt + * releasing stdin) lets Bun exit silently mid-query. + */ +export async function holdEventLoop(fn: () => Promise): Promise { + const timer = setInterval(() => {}, 1 << 30); + try { + return await fn(); + } finally { + clearInterval(timer); + } +} + /** * Convert driver rows to plain objects. * - BigInt -> number (or string when outside the safe integer range) @@ -88,7 +103,7 @@ export default async function dbHandler< throw new Error(`Couldn't grab MariaDB Client.`); } - const res = await CLIENT.unsafe(query, values); + const res = await holdEventLoop(() => CLIENT.unsafe(query, values)); const count = res.count; diff --git a/src/lib/mariadb/db-update.ts b/src/lib/mariadb/db-update.ts index 22c7f7e..bcebed2 100644 --- a/src/lib/mariadb/db-update.ts +++ b/src/lib/mariadb/db-update.ts @@ -1,4 +1,4 @@ -import dbHandler, { resolveClient } from "../db-handler"; +import dbHandler, { holdEventLoop, resolveClient } from "../db-handler"; import type { BunMariaDBConfig, DBResponseObject, @@ -103,7 +103,7 @@ export default async function DbUpdate< * all in one transaction. Re-fetching by id still works when the update * changes a column used in the WHERE clause. */ - return await CLIENT.begin(async (tx) => { + return await holdEventLoop(() => CLIENT.begin(async (tx) => { const matched_res = await dbHandler<{ id: number }>({ query: `SELECT id FROM ${quoteIdentifier(table)} ${whereClause} FOR UPDATE`, values: sqlQueryObj.whereValues as any, @@ -162,7 +162,7 @@ export default async function DbUpdate< }, db_res: res.db_res, } as DBResponseObject; - }); + })); } catch (error: any) { if (error instanceof UpdateAbort) { return error.response;