diff --git a/src/js/internal/sql/sqlite.ts b/src/js/internal/sql/sqlite.ts index 6ab1c8fa156d..80e8318e0517 100644 --- a/src/js/internal/sql/sqlite.ts +++ b/src/js/internal/sql/sqlite.ts @@ -298,6 +298,8 @@ class SQLiteAdapter implements DatabaseAdapter> = new Set(); + /// One entry per begin() whose callback is still running; close() rejects them. + private closeHandlers: Set<(err: Error) => void> = new Set(); constructor(connectionInfo: Bun.SQL.__internal.DefinedSQLiteOptions) { this.connectionInfo = connectionInfo; @@ -474,6 +476,15 @@ class SQLiteAdapter implements DatabaseAdapter 0) { + this.closeHandlers = new Set(); + const err = this.connectionClosedError(); + for (const handler of closeHandlers) { + handler(err); + } + } } flush() { @@ -497,6 +508,15 @@ class SQLiteAdapter implements DatabaseAdapter void): void { + this.closeHandlers.add(handler); + } + + detachConnectionCloseHandler(_connection: BunSQLiteModule.Database, handler: (err: Error) => void): void { + this.closeHandlers.delete(handler); + } + array(_values: any[], _typeNameOrID?: number | ArrayType): SQLArrayParameter { throw new Error("SQLite doesn't support arrays"); } diff --git a/test/js/sql/sqlite-sql.test.ts b/test/js/sql/sqlite-sql.test.ts index ed0567c186fb..fd90f0cddd0c 100644 --- a/test/js/sql/sqlite-sql.test.ts +++ b/test/js/sql/sqlite-sql.test.ts @@ -2080,6 +2080,50 @@ describe("Connection management", () => { } }); + test("close() rejects a begin() whose callback is still running", async () => { + const sql = new SQL("sqlite://:memory:"); + const started = Promise.withResolvers(); + const begin = sql.begin(async tx => { + await tx`SELECT 1`; + started.resolve(); + await new Promise(() => {}); + }); + const rejection = begin.catch(err => err); + + await started.promise; + await sql.close(); + + // Like postgres and mysql after a forced close: begin() is settled by the time close() resolves. + expect(Bun.peek.status(begin)).toBe("rejected"); + expect(await rejection).toMatchObject({ + name: "SQLiteError", + code: "ERR_SQLITE_CONNECTION_CLOSED", + message: "Connection closed", + }); + }); + + test("a begin() callback that outlives close() gets ERR_SQLITE_CONNECTION_CLOSED from its queries", async () => { + const sql = new SQL("sqlite://:memory:"); + const started = Promise.withResolvers(); + const closed = Promise.withResolvers(); + const queryAfterClose = Promise.withResolvers(); + const begin = sql.begin(async tx => { + await tx`SELECT 1`; + started.resolve(); + await closed.promise; + queryAfterClose.resolve(tx`SELECT 2`.catch(err => err)); + }); + const rejection = begin.catch(err => err); + + await started.promise; + await sql.close(); + closed.resolve(); + + const connectionClosed = { name: "SQLiteError", code: "ERR_SQLITE_CONNECTION_CLOSED" }; + expect(await queryAfterClose.promise).toMatchObject(connectionClosed); + expect(await rejection).toMatchObject(connectionClosed); + }); + test("reserve throws for SQLite", async () => { const sql = new SQL("sqlite://:memory:");