From f61def85ecb545083acd07b494258f922efcadae Mon Sep 17 00:00:00 2001 From: cjihrig Date: Sun, 26 Jan 2025 14:36:57 -0500 Subject: [PATCH] sqlite: handle conflicting SQLite and JS errors This commit adds support for the situation where SQLite is trying to report an error while JavaScript already has an exception pending. Fixes: https://github.com/nodejs/node/issues/56772 --- src/node_sqlite.cc | 293 +++++++++--------- src/node_sqlite.h | 25 ++ test/parallel/test-sqlite-custom-functions.js | 34 ++ 3 files changed, 214 insertions(+), 138 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 2c830961e72817..8c999082188f27 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -101,9 +101,16 @@ inline MaybeLocal CreateSQLiteError(Isolate* isolate, sqlite3* db) { return e; } -inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, sqlite3* db) { +class DatabaseSync; + +inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, DatabaseSync* db) { + if (db->ShouldIgnoreSQLiteError()) { + db->SetIgnoreNextSQLiteError(false); + return; + } + Local e; - if (CreateSQLiteError(isolate, db).ToLocal(&e)) { + if (CreateSQLiteError(isolate, db->Connection()).ToLocal(&e)) { isolate->ThrowException(e); } } @@ -128,122 +135,130 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, int errcode) { isolate->ThrowException(error); } -class UserDefinedFunction { - public: - explicit UserDefinedFunction(Environment* env, - Local fn, - bool use_bigint_args) - : env_(env), fn_(env->isolate(), fn), use_bigint_args_(use_bigint_args) {} - virtual ~UserDefinedFunction() {} - - static void xFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) { - UserDefinedFunction* self = - static_cast(sqlite3_user_data(ctx)); - Environment* env = self->env_; - Isolate* isolate = env->isolate(); - auto recv = Undefined(isolate); - auto fn = self->fn_.Get(isolate); - LocalVector js_argv(isolate); - - for (int i = 0; i < argc; ++i) { - sqlite3_value* value = argv[i]; - MaybeLocal js_val; - - switch (sqlite3_value_type(value)) { - case SQLITE_INTEGER: { - sqlite3_int64 val = sqlite3_value_int64(value); - if (self->use_bigint_args_) { - js_val = BigInt::New(isolate, val); - } else if (std::abs(val) <= kMaxSafeJsInteger) { - js_val = Number::New(isolate, val); - } else { - THROW_ERR_OUT_OF_RANGE(isolate, - "Value is too large to be represented as a " - "JavaScript number: %" PRId64, - val); - return; - } - break; - } - case SQLITE_FLOAT: - js_val = Number::New(isolate, sqlite3_value_double(value)); - break; - case SQLITE_TEXT: { - const char* v = - reinterpret_cast(sqlite3_value_text(value)); - js_val = String::NewFromUtf8(isolate, v).As(); - break; - } - case SQLITE_NULL: - js_val = Null(isolate); - break; - case SQLITE_BLOB: { - size_t size = static_cast(sqlite3_value_bytes(value)); - auto data = - reinterpret_cast(sqlite3_value_blob(value)); - auto store = ArrayBuffer::NewBackingStore(isolate, size); - memcpy(store->Data(), data, size); - auto ab = ArrayBuffer::New(isolate, std::move(store)); - js_val = Uint8Array::New(ab, 0, size); - break; +UserDefinedFunction::UserDefinedFunction(Environment* env, + Local fn, + DatabaseSync* db, + bool use_bigint_args) + : env_(env), + fn_(env->isolate(), fn), + db_(db), + use_bigint_args_(use_bigint_args) {} + +UserDefinedFunction::~UserDefinedFunction() {} + +void UserDefinedFunction::xFunc(sqlite3_context* ctx, + int argc, + sqlite3_value** argv) { + UserDefinedFunction* self = + static_cast(sqlite3_user_data(ctx)); + Environment* env = self->env_; + Isolate* isolate = env->isolate(); + auto recv = Undefined(isolate); + auto fn = self->fn_.Get(isolate); + LocalVector js_argv(isolate); + + for (int i = 0; i < argc; ++i) { + sqlite3_value* value = argv[i]; + MaybeLocal js_val; + + switch (sqlite3_value_type(value)) { + case SQLITE_INTEGER: { + sqlite3_int64 val = sqlite3_value_int64(value); + if (self->use_bigint_args_) { + js_val = BigInt::New(isolate, val); + } else if (std::abs(val) <= kMaxSafeJsInteger) { + js_val = Number::New(isolate, val); + } else { + // Ignore the SQLite error because a JavaScript exception is being + // thrown. + self->db_->SetIgnoreNextSQLiteError(true); + sqlite3_result_error(ctx, "", 0); + THROW_ERR_OUT_OF_RANGE(isolate, + "Value is too large to be represented as a " + "JavaScript number: %" PRId64, + val); + return; } - default: - UNREACHABLE("Bad SQLite value"); + break; } - - Local local; - if (!js_val.ToLocal(&local)) { - return; + case SQLITE_FLOAT: + js_val = Number::New(isolate, sqlite3_value_double(value)); + break; + case SQLITE_TEXT: { + const char* v = + reinterpret_cast(sqlite3_value_text(value)); + js_val = String::NewFromUtf8(isolate, v).As(); + break; } - - js_argv.emplace_back(local); + case SQLITE_NULL: + js_val = Null(isolate); + break; + case SQLITE_BLOB: { + size_t size = static_cast(sqlite3_value_bytes(value)); + auto data = reinterpret_cast(sqlite3_value_blob(value)); + auto store = ArrayBuffer::NewBackingStore(isolate, size); + memcpy(store->Data(), data, size); + auto ab = ArrayBuffer::New(isolate, std::move(store)); + js_val = Uint8Array::New(ab, 0, size); + break; + } + default: + UNREACHABLE("Bad SQLite value"); } - MaybeLocal retval = - fn->Call(env->context(), recv, argc, js_argv.data()); - Local result; - if (!retval.ToLocal(&result)) { + Local local; + if (!js_val.ToLocal(&local)) { + // Ignore the SQLite error because a JavaScript exception is pending. + self->db_->SetIgnoreNextSQLiteError(true); + sqlite3_result_error(ctx, "", 0); return; } - if (result->IsUndefined() || result->IsNull()) { - sqlite3_result_null(ctx); - } else if (result->IsNumber()) { - sqlite3_result_double(ctx, result.As()->Value()); - } else if (result->IsString()) { - Utf8Value val(isolate, result.As()); - sqlite3_result_text(ctx, *val, val.length(), SQLITE_TRANSIENT); - } else if (result->IsArrayBufferView()) { - ArrayBufferViewContents buf(result); - sqlite3_result_blob(ctx, buf.data(), buf.length(), SQLITE_TRANSIENT); - } else if (result->IsBigInt()) { - bool lossless; - int64_t as_int = result.As()->Int64Value(&lossless); - if (!lossless) { - sqlite3_result_error(ctx, "BigInt value is too large for SQLite", -1); - return; - } - sqlite3_result_int64(ctx, as_int); - } else if (result->IsPromise()) { - sqlite3_result_error( - ctx, "Asynchronous user-defined functions are not supported", -1); - } else { - sqlite3_result_error( - ctx, - "Returned JavaScript value cannot be converted to a SQLite value", - -1); - } + js_argv.emplace_back(local); } - static void xDestroy(void* self) { - delete static_cast(self); + MaybeLocal retval = + fn->Call(env->context(), recv, argc, js_argv.data()); + Local result; + if (!retval.ToLocal(&result)) { + // Ignore the SQLite error because a JavaScript exception is pending. + self->db_->SetIgnoreNextSQLiteError(true); + sqlite3_result_error(ctx, "", 0); + return; } - private: - Environment* env_; - Global fn_; - bool use_bigint_args_; -}; + if (result->IsUndefined() || result->IsNull()) { + sqlite3_result_null(ctx); + } else if (result->IsNumber()) { + sqlite3_result_double(ctx, result.As()->Value()); + } else if (result->IsString()) { + Utf8Value val(isolate, result.As()); + sqlite3_result_text(ctx, *val, val.length(), SQLITE_TRANSIENT); + } else if (result->IsArrayBufferView()) { + ArrayBufferViewContents buf(result); + sqlite3_result_blob(ctx, buf.data(), buf.length(), SQLITE_TRANSIENT); + } else if (result->IsBigInt()) { + bool lossless; + int64_t as_int = result.As()->Int64Value(&lossless); + if (!lossless) { + sqlite3_result_error(ctx, "BigInt value is too large for SQLite", -1); + return; + } + sqlite3_result_int64(ctx, as_int); + } else if (result->IsPromise()) { + sqlite3_result_error( + ctx, "Asynchronous user-defined functions are not supported", -1); + } else { + sqlite3_result_error( + ctx, + "Returned JavaScript value cannot be converted to a SQLite value", + -1); + } +} + +void UserDefinedFunction::xDestroy(void* self) { + delete static_cast(self); +} DatabaseSync::DatabaseSync(Environment* env, Local object, @@ -255,6 +270,7 @@ DatabaseSync::DatabaseSync(Environment* env, connection_ = nullptr; allow_load_extension_ = allow_load_extension; enable_load_extension_ = allow_load_extension; + ignore_next_sqlite_error_ = false; if (open) { Open(); @@ -297,18 +313,18 @@ bool DatabaseSync::Open() { : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; int r = sqlite3_open_v2( open_config_.location().c_str(), &connection_, flags, nullptr); - CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false); + CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false); r = sqlite3_db_config(connection_, SQLITE_DBCONFIG_DQS_DML, static_cast(open_config_.get_enable_dqs()), nullptr); - CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false); + CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false); r = sqlite3_db_config(connection_, SQLITE_DBCONFIG_DQS_DDL, static_cast(open_config_.get_enable_dqs()), nullptr); - CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false); + CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false); int foreign_keys_enabled; r = sqlite3_db_config( @@ -316,7 +332,7 @@ bool DatabaseSync::Open() { SQLITE_DBCONFIG_ENABLE_FKEY, static_cast(open_config_.get_enable_foreign_keys()), &foreign_keys_enabled); - CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false); + CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false); CHECK_EQ(foreign_keys_enabled, open_config_.get_enable_foreign_keys()); if (allow_load_extension_) { @@ -329,7 +345,7 @@ bool DatabaseSync::Open() { const int load_extension_ret = sqlite3_db_config( connection_, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, nullptr); CHECK_ERROR_OR_THROW( - env()->isolate(), connection_, load_extension_ret, SQLITE_OK, false); + env()->isolate(), this, load_extension_ret, SQLITE_OK, false); } return true; @@ -358,6 +374,14 @@ inline sqlite3* DatabaseSync::Connection() { return connection_; } +void DatabaseSync::SetIgnoreNextSQLiteError(bool ignore) { + ignore_next_sqlite_error_ = ignore; +} + +bool DatabaseSync::ShouldIgnoreSQLiteError() { + return ignore_next_sqlite_error_; +} + void DatabaseSync::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -491,7 +515,7 @@ void DatabaseSync::Close(const FunctionCallbackInfo& args) { db->FinalizeStatements(); db->DeleteSessions(); int r = sqlite3_close_v2(db->connection_); - CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); db->connection_ = nullptr; } @@ -510,7 +534,7 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { Utf8Value sql(env->isolate(), args[0].As()); sqlite3_stmt* s = nullptr; int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, 0); - CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); BaseObjectPtr stmt = StatementSync::Create(env, db, s); db->statements_.insert(stmt.get()); args.GetReturnValue().Set(stmt->object()); @@ -530,7 +554,7 @@ void DatabaseSync::Exec(const FunctionCallbackInfo& args) { Utf8Value sql(env->isolate(), args[0].As()); int r = sqlite3_exec(db->connection_, *sql, nullptr, nullptr, nullptr); - CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); } void DatabaseSync::CustomFunction(const FunctionCallbackInfo& args) { @@ -655,7 +679,7 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo& args) { } UserDefinedFunction* user_data = - new UserDefinedFunction(env, fn, use_bigint_args); + new UserDefinedFunction(env, fn, db, use_bigint_args); int text_rep = SQLITE_UTF8; if (deterministic) { @@ -675,7 +699,7 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo& args) { nullptr, nullptr, UserDefinedFunction::xDestroy); - CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); } void DatabaseSync::CreateSession(const FunctionCallbackInfo& args) { @@ -732,11 +756,11 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo& args) { sqlite3_session* pSession; int r = sqlite3session_create(db->connection_, db_name.c_str(), &pSession); - CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); db->sessions_.insert(pSession); r = sqlite3session_attach(pSession, table == "" ? nullptr : table.c_str()); - CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); BaseObjectPtr session = Session::Create(env, BaseObjectWeakPtr(db), pSession); @@ -881,8 +905,7 @@ void DatabaseSync::EnableLoadExtension( db->enable_load_extension_ = enable; const int load_extension_ret = sqlite3_db_config( db->connection_, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, enable, nullptr); - CHECK_ERROR_OR_THROW( - isolate, db->connection_, load_extension_ret, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(isolate, db, load_extension_ret, SQLITE_OK, void()); } void DatabaseSync::LoadExtension(const FunctionCallbackInfo& args) { @@ -954,8 +977,7 @@ inline bool StatementSync::IsFinalized() { bool StatementSync::BindParams(const FunctionCallbackInfo& args) { int r = sqlite3_clear_bindings(statement_); - CHECK_ERROR_OR_THROW( - env()->isolate(), db_->Connection(), r, SQLITE_OK, false); + CHECK_ERROR_OR_THROW(env()->isolate(), db_, r, SQLITE_OK, false); int anon_idx = 1; int anon_start = 0; @@ -1085,8 +1107,7 @@ bool StatementSync::BindValue(const Local& value, const int index) { return false; } - CHECK_ERROR_OR_THROW( - env()->isolate(), db_->Connection(), r, SQLITE_OK, false); + CHECK_ERROR_OR_THROW(env()->isolate(), db_, r, SQLITE_OK, false); return true; } @@ -1152,7 +1173,7 @@ void StatementSync::All(const FunctionCallbackInfo& args) { env, stmt->IsFinalized(), "statement has been finalized"); Isolate* isolate = env->isolate(); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW(isolate, stmt->db_->Connection(), r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(isolate, stmt->db_, r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; @@ -1181,8 +1202,7 @@ void StatementSync::All(const FunctionCallbackInfo& args) { rows.emplace_back(row); } - CHECK_ERROR_OR_THROW( - isolate, stmt->db_->Connection(), r, SQLITE_DONE, void()); + CHECK_ERROR_OR_THROW(isolate, stmt->db_, r, SQLITE_DONE, void()); args.GetReturnValue().Set(Array::New(isolate, rows.data(), rows.size())); } @@ -1250,8 +1270,7 @@ void StatementSync::IterateNextCallback( int r = sqlite3_step(stmt->statement_); if (r != SQLITE_ROW) { - CHECK_ERROR_OR_THROW( - env->isolate(), stmt->db_->Connection(), r, SQLITE_DONE, void()); + CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_DONE, void()); // cleanup when no more rows to fetch sqlite3_reset(stmt->statement_); @@ -1303,8 +1322,7 @@ void StatementSync::Iterate(const FunctionCallbackInfo& args) { auto isolate = env->isolate(); auto context = env->context(); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW( - env->isolate(), stmt->db_->Connection(), r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; @@ -1368,7 +1386,7 @@ void StatementSync::Get(const FunctionCallbackInfo& args) { env, stmt->IsFinalized(), "statement has been finalized"); Isolate* isolate = env->isolate(); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW(isolate, stmt->db_->Connection(), r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(isolate, stmt->db_, r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; @@ -1378,7 +1396,7 @@ void StatementSync::Get(const FunctionCallbackInfo& args) { r = sqlite3_step(stmt->statement_); if (r == SQLITE_DONE) return; if (r != SQLITE_ROW) { - THROW_ERR_SQLITE_ERROR(isolate, stmt->db_->Connection()); + THROW_ERR_SQLITE_ERROR(isolate, stmt->db_); return; } @@ -1414,8 +1432,7 @@ void StatementSync::Run(const FunctionCallbackInfo& args) { THROW_AND_RETURN_ON_BAD_STATE( env, stmt->IsFinalized(), "statement has been finalized"); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW( - env->isolate(), stmt->db_->Connection(), r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; @@ -1424,7 +1441,7 @@ void StatementSync::Run(const FunctionCallbackInfo& args) { auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); r = sqlite3_step(stmt->statement_); if (r != SQLITE_ROW && r != SQLITE_DONE) { - THROW_ERR_SQLITE_ERROR(env->isolate(), stmt->db_->Connection()); + THROW_ERR_SQLITE_ERROR(env->isolate(), stmt->db_); return; } @@ -1649,7 +1666,6 @@ void Session::Changeset(const FunctionCallbackInfo& args) { Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); Environment* env = Environment::GetCurrent(args); - sqlite3* db = session->database_ ? session->database_->connection_ : nullptr; THROW_AND_RETURN_ON_BAD_STATE( env, !session->database_->IsOpen(), "database is not open"); THROW_AND_RETURN_ON_BAD_STATE( @@ -1658,7 +1674,8 @@ void Session::Changeset(const FunctionCallbackInfo& args) { int nChangeset; void* pChangeset; int r = sqliteChangesetFunc(session->session_, &nChangeset, &pChangeset); - CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW( + env->isolate(), session->database_, r, SQLITE_OK, void()); auto freeChangeset = OnScopeLeave([&] { sqlite3_free(pChangeset); }); diff --git a/src/node_sqlite.h b/src/node_sqlite.h index e78aa39abb3ba5..35ad4378eccd62 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -68,6 +68,13 @@ class DatabaseSync : public BaseObject { bool IsOpen(); sqlite3* Connection(); + // In some situations, such as when using custom functions, it is possible + // that SQLite reports an error while JavaScript already has a pending + // exception. In this case, the SQLite error should be ignored. These methods + // enable that use case. + void SetIgnoreNextSQLiteError(bool ignore); + bool ShouldIgnoreSQLiteError(); + SET_MEMORY_INFO_NAME(DatabaseSync) SET_SELF_SIZE(DatabaseSync) @@ -80,6 +87,7 @@ class DatabaseSync : public BaseObject { bool allow_load_extension_; bool enable_load_extension_; sqlite3* connection_; + bool ignore_next_sqlite_error_; std::set sessions_; std::unordered_set statements_; @@ -161,6 +169,23 @@ class Session : public BaseObject { BaseObjectWeakPtr database_; // The Parent Database }; +class UserDefinedFunction { + public: + UserDefinedFunction(Environment* env, + v8::Local fn, + DatabaseSync* db, + bool use_bigint_args); + ~UserDefinedFunction(); + static void xFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv); + static void xDestroy(void* self); + + private: + Environment* env_; + v8::Global fn_; + DatabaseSync* db_; + bool use_bigint_args_; +}; + } // namespace sqlite } // namespace node diff --git a/test/parallel/test-sqlite-custom-functions.js b/test/parallel/test-sqlite-custom-functions.js index 2c854d47102f9b..b509ebb3d4c76c 100644 --- a/test/parallel/test-sqlite-custom-functions.js +++ b/test/parallel/test-sqlite-custom-functions.js @@ -339,6 +339,40 @@ suite('DatabaseSync.prototype.function()', () => { }); }); + suite('handles conflicting errors from SQLite and JavaScript', () => { + test('throws if value cannot fit in a number', () => { + const db = new DatabaseSync(':memory:'); + const expected = { __proto__: null, id: 5, data: 'foo' }; + db.function('custom', (arg) => {}); + db.exec('CREATE TABLE test (id NUMBER NOT NULL PRIMARY KEY, data TEXT)'); + db.prepare('INSERT INTO test (id, data) VALUES (?, ?)').run(5, 'foo'); + assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected); + assert.throws(() => { + db.exec(`UPDATE test SET data = CUSTOM(${Number.MAX_SAFE_INTEGER + 1})`); + }, { + code: 'ERR_OUT_OF_RANGE', + message: /Value is too large to be represented as a JavaScript number: 9007199254740992/, + }); + assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected); + }); + + test('propagates JavaScript errors', () => { + const db = new DatabaseSync(':memory:'); + const expected = { __proto__: null, id: 5, data: 'foo' }; + const err = new Error('boom'); + db.function('throws', () => { + throw err; + }); + db.exec('CREATE TABLE test (id NUMBER NOT NULL PRIMARY KEY, data TEXT)'); + db.prepare('INSERT INTO test (id, data) VALUES (?, ?)').run(5, 'foo'); + assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected); + assert.throws(() => { + db.exec('UPDATE test SET data = THROWS()'); + }, err); + assert.deepStrictEqual(db.prepare('SELECT * FROM test').get(), expected); + }); + }); + test('supported argument types', () => { const db = new DatabaseSync(':memory:'); db.function('arguments', (i, f, s, n, b) => {