diff --git a/src/sqlite-api.js b/src/sqlite-api.js index befcca21..c6bf619c 100644 --- a/src/sqlite-api.js +++ b/src/sqlite-api.js @@ -493,10 +493,29 @@ export function Factory(Module) { const rc = await retry(() => f(zFilename, tmpPtr[0], flags, zVfs)); const db = Module.getValue(tmpPtr[0], '*'); - databases.add(db); + if (rc !== SQLite.SQLITE_OK) { + // sqlite3_open_v2 usually returns a database handle even when opening fails. + // Capture its error before closing because close may replace the message. + const message = db ? + Module.ccall('sqlite3_errmsg', 'string', ['number'], [db]) : + fname; + const error = new SQLiteError(message, rc); + + if (db) { + databases.add(db); + try { + await sqlite3.close(db); + } catch { + // Closing an errored handle is best effort; preserve the open error. + } finally { + databases.delete(db); + } + } + throw error; + } + databases.add(db); Module.ccall('RegisterExtensionFunctions', 'void', ['number'], [db]); - check(fname, rc); return db; } finally { Module._sqlite3_free(zVfs); diff --git a/test/sqlite-api-open.test.js b/test/sqlite-api-open.test.js new file mode 100644 index 00000000..5527a2df --- /dev/null +++ b/test/sqlite-api-open.test.js @@ -0,0 +1,43 @@ +import * as SQLite from '../src/sqlite-api.js'; + +describe('SQLite API open cleanup', function() { + it('closes the database handle returned by a failed open', async function() { + const failedHandle = 42; + const close = jasmine.createSpy('sqlite3_close').and.resolveTo(SQLite.SQLITE_OK); + const registerExtensionFunctions = jasmine.createSpy('RegisterExtensionFunctions'); + const module = { + HEAPU8: new Uint8Array(64), + _getSqliteFree: () => 0, + _malloc: () => 8, + _sqlite3_free: () => {}, + _sqlite3_malloc: () => 32, + ccall: name => { + if (name === 'sqlite3_errmsg') return 'unable to open database file'; + if (name === 'RegisterExtensionFunctions') registerExtensionFunctions(); + }, + cwrap: name => { + if (name === 'sqlite3_open_v2') { + return async () => SQLite.SQLITE_CANTOPEN; + } + if (name === 'sqlite3_close') return close; + return () => SQLite.SQLITE_OK; + }, + getTempRet0: () => 0, + getValue: () => failedHandle, + vfs_register: () => SQLite.SQLITE_OK, + }; + const sqlite3 = SQLite.Factory(module); + + await expectAsync(sqlite3.open_v2('/cannot-open.db')).toBeRejectedWithError( + SQLite.SQLiteError, + 'unable to open database file', + ); + + expect(close).toHaveBeenCalledOnceWith(failedHandle); + expect(registerExtensionFunctions).not.toHaveBeenCalled(); + await expectAsync(sqlite3.close(failedHandle)).toBeRejectedWithError( + SQLite.SQLiteError, + 'not a database', + ); + }); +});