From 883c1be012bb2a205979e10117e6b08c0072fa8c Mon Sep 17 00:00:00 2001 From: Matt Skelley Date: Thu, 22 Jan 2026 18:34:51 +0800 Subject: [PATCH] sqlite: add support for reading NULL as undefined Add a statement-level flag to control NULL conversion. Expose setReadNullAsUndefined() on StatementSync. Apply to row-reading paths. Fixes: https://github.com/nodejs/node/issues/59457 --- src/node_sqlite.cc | 102 +++++++++++++++++++++++++++++++++++++++------ src/node_sqlite.h | 24 +++++++++-- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 6d35236dce0f82..01ed53f786a2cf 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -121,6 +121,56 @@ using v8::Value; } \ } while (0) + #define SQLITE_VALUE_TO_JS_READ(from, isolate, use_big_int_args, \ + read_null_as_undef, result, ...) \ + do { \ + switch (sqlite3_##from##_type(__VA_ARGS__)) { \ + case SQLITE_INTEGER: { \ + sqlite3_int64 val = sqlite3_##from##_int64(__VA_ARGS__); \ + if ((use_big_int_args)) { \ + (result) = BigInt::New((isolate), val); \ + } else if (std::abs(val) <= kMaxSafeJsInteger) { \ + (result) = Number::New((isolate), val); \ + } else { \ + THROW_ERR_OUT_OF_RANGE((isolate), \ + "Value is too large to be represented as a " \ + "JavaScript number: %" PRId64, \ + val); \ + } \ + break; \ + } \ + case SQLITE_FLOAT: { \ + (result) = \ + Number::New((isolate), sqlite3_##from##_double(__VA_ARGS__)); \ + break; \ + } \ + case SQLITE_TEXT: { \ + const char* v = \ + reinterpret_cast(sqlite3_##from##_text(__VA_ARGS__)); \ + (result) = String::NewFromUtf8((isolate), v).As(); \ + break; \ + } \ + case SQLITE_NULL: { \ + (result) = (read_null_as_undef) ? Undefined((isolate)) : Null((isolate)); \ + break; \ + } \ + case SQLITE_BLOB: { \ + size_t size = \ + static_cast(sqlite3_##from##_bytes(__VA_ARGS__)); \ + auto data = reinterpret_cast( \ + sqlite3_##from##_blob(__VA_ARGS__)); \ + auto store = ArrayBuffer::NewBackingStore( \ + (isolate), size, BackingStoreInitializationMode::kUninitialized); \ + memcpy(store->Data(), data, size); \ + auto ab = ArrayBuffer::New((isolate), std::move(store)); \ + (result) = Uint8Array::New(ab, 0, size); \ + break; \ + } \ + default: \ + UNREACHABLE("Bad SQLite value"); \ + } \ + } while (0) + namespace { Local getLazyIterTemplate(Environment* env) { auto iter_template = env->iter_template(); @@ -2196,7 +2246,7 @@ bool StatementSync::BindValue(const Local& value, const int index) { MaybeLocal StatementSync::ColumnToValue(const int column) { return StatementExecutionHelper::ColumnToValue( - env(), statement_, column, use_big_ints_); + env(), statement_, column, use_big_ints_, read_null_as_undefined_); } MaybeLocal StatementSync::ColumnNameToName(const int column) { @@ -2212,10 +2262,12 @@ MaybeLocal StatementSync::ColumnNameToName(const int column) { MaybeLocal StatementExecutionHelper::ColumnToValue(Environment* env, sqlite3_stmt* stmt, const int column, - bool use_big_ints) { + bool use_big_ints, + bool read_null_as_undefined) { Isolate* isolate = env->isolate(); MaybeLocal js_val = MaybeLocal(); - SQLITE_VALUE_TO_JS(column, isolate, use_big_ints, js_val, stmt, column); + SQLITE_VALUE_TO_JS_READ( + column, isolate, use_big_ints, read_null_as_undefined, js_val, stmt, column); return js_val; } @@ -2237,12 +2289,13 @@ Maybe ExtractRowValues(Environment* env, sqlite3_stmt* stmt, int num_cols, bool use_big_ints, + bool read_null_as_undefined, LocalVector* row_values) { row_values->clear(); row_values->reserve(num_cols); for (int i = 0; i < num_cols; ++i) { Local val; - if (!StatementExecutionHelper::ColumnToValue(env, stmt, i, use_big_ints) + if (!StatementExecutionHelper::ColumnToValue(env, stmt, i, use_big_ints, read_null_as_undefined) .ToLocal(&val)) { return Nothing(); } @@ -2255,7 +2308,8 @@ MaybeLocal StatementExecutionHelper::All(Environment* env, DatabaseSync* db, sqlite3_stmt* stmt, bool return_arrays, - bool use_big_ints) { + bool use_big_ints, + bool read_null_as_undefined) { Isolate* isolate = env->isolate(); EscapableHandleScope scope(isolate); int r; @@ -2265,7 +2319,7 @@ MaybeLocal StatementExecutionHelper::All(Environment* env, LocalVector row_keys(isolate); while ((r = sqlite3_step(stmt)) == SQLITE_ROW) { - if (ExtractRowValues(env, stmt, num_cols, use_big_ints, &row_values) + if (ExtractRowValues(env, stmt, num_cols, use_big_ints, read_null_as_undefined, &row_values) .IsNothing()) { return MaybeLocal(); } @@ -2370,7 +2424,8 @@ MaybeLocal StatementExecutionHelper::Get(Environment* env, DatabaseSync* db, sqlite3_stmt* stmt, bool return_arrays, - bool use_big_ints) { + bool use_big_ints, + bool read_null_as_undefined) { Isolate* isolate = env->isolate(); EscapableHandleScope scope(isolate); auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt); }); @@ -2388,7 +2443,7 @@ MaybeLocal StatementExecutionHelper::Get(Environment* env, } LocalVector row_values(isolate); - if (ExtractRowValues(env, stmt, num_cols, use_big_ints, &row_values) + if (ExtractRowValues(env, stmt, num_cols, use_big_ints, read_null_as_undefined, &row_values) .IsNothing()) { return MaybeLocal(); } @@ -2434,7 +2489,8 @@ void StatementSync::All(const FunctionCallbackInfo& args) { stmt->db_.get(), stmt->statement_, stmt->return_arrays_, - stmt->use_big_ints_) + stmt->use_big_ints_, + stmt->read_null_as_undefined_) .ToLocal(&result)) { args.GetReturnValue().Set(result); } @@ -2481,7 +2537,8 @@ void StatementSync::Get(const FunctionCallbackInfo& args) { stmt->db_.get(), stmt->statement_, stmt->return_arrays_, - stmt->use_big_ints_) + stmt->use_big_ints_, + stmt->read_null_as_undefined_) .ToLocal(&result)) { args.GetReturnValue().Set(result); } @@ -2638,6 +2695,22 @@ void StatementSync::SetReadBigInts(const FunctionCallbackInfo& args) { stmt->use_big_ints_ = args[0]->IsTrue(); } +void StatementSync::SetReadNullAsUndefined(const FunctionCallbackInfo& args) { + StatementSync* stmt; + ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); + Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsFinalized(), "statement has been finalized"); + + if (!args[0]->IsBoolean()) { + THROW_ERR_INVALID_ARG_TYPE( + env->isolate(), "The \"readNullAsUndefined\" argument must be a boolean."); + return; + } + + stmt->read_null_as_undefined_ = args[0]->IsTrue(); +} + void StatementSync::SetReturnArrays(const FunctionCallbackInfo& args) { StatementSync* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); @@ -2840,7 +2913,8 @@ void SQLTagStore::Get(const FunctionCallbackInfo& args) { stmt->db_.get(), stmt->statement_, stmt->return_arrays_, - stmt->use_big_ints_) + stmt->use_big_ints_, + stmt->read_null_as_undefined_) .ToLocal(&result)) { args.GetReturnValue().Set(result); } @@ -2880,7 +2954,8 @@ void SQLTagStore::All(const FunctionCallbackInfo& args) { stmt->db_.get(), stmt->statement_, stmt->return_arrays_, - stmt->use_big_ints_) + stmt->use_big_ints_, + stmt->read_null_as_undefined_) .ToLocal(&result)) { args.GetReturnValue().Set(result); } @@ -3014,6 +3089,8 @@ Local StatementSync::GetConstructorTemplate( isolate, tmpl, "setReadBigInts", StatementSync::SetReadBigInts); SetProtoMethod( isolate, tmpl, "setReturnArrays", StatementSync::SetReturnArrays); + SetProtoMethod( + isolate, tmpl, "setReadNullAsUndefined", StatementSync::SetReadNullAsUndefined); env->set_sqlite_statement_sync_constructor_template(tmpl); } return tmpl; @@ -3119,6 +3196,7 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { iter->stmt_->statement_, num_cols, iter->stmt_->use_big_ints_, + iter->stmt_->read_null_as_undefined_, &row_values) .IsNothing()) { return; diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 2641c9d4f1e8c5..8b70dfd95b8473 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -65,6 +65,14 @@ class DatabaseOpenConfiguration { return allow_unknown_named_params_; } + inline void set_read_null_as_undefined(bool flag) { + read_null_as_undefined_ = flag; + } + + inline bool get_read_null_as_undefined() const { + return read_null_as_undefined_; + } + inline void set_enable_defensive(bool flag) { defensive_ = flag; } inline bool get_enable_defensive() const { return defensive_; } @@ -79,6 +87,7 @@ class DatabaseOpenConfiguration { bool return_arrays_ = false; bool allow_bare_named_params_ = true; bool allow_unknown_named_params_ = false; + bool read_null_as_undefined_ = false; bool defensive_ = false; }; @@ -93,7 +102,8 @@ class StatementExecutionHelper { DatabaseSync* db, sqlite3_stmt* stmt, bool return_arrays, - bool use_big_ints); + bool use_big_ints, + bool read_null_as_undefined); static v8::MaybeLocal Run(Environment* env, DatabaseSync* db, sqlite3_stmt* stmt, @@ -103,7 +113,8 @@ class StatementExecutionHelper { static v8::MaybeLocal ColumnToValue(Environment* env, sqlite3_stmt* stmt, const int column, - bool use_big_ints); + bool use_big_ints, + bool read_null_as_undefined); static v8::MaybeLocal ColumnNameToName(Environment* env, sqlite3_stmt* stmt, const int column); @@ -111,7 +122,8 @@ class StatementExecutionHelper { DatabaseSync* db, sqlite3_stmt* stmt, bool return_arrays, - bool use_big_ints); + bool use_big_ints, + bool read_null_as_undefined); }; class DatabaseSync : public BaseObject { @@ -168,6 +180,9 @@ class DatabaseSync : public BaseObject { bool allow_unknown_named_params() const { return open_config_.get_allow_unknown_named_params(); } + bool read_null_as_undefined() const { + return open_config_.get_read_null_as_undefined(); + } sqlite3* Connection(); // In some situations, such as when using custom functions, it is possible @@ -226,6 +241,8 @@ class StatementSync : public BaseObject { const v8::FunctionCallbackInfo& args); static void SetReadBigInts(const v8::FunctionCallbackInfo& args); static void SetReturnArrays(const v8::FunctionCallbackInfo& args); + static void SetReadNullAsUndefined( + const v8::FunctionCallbackInfo& args); v8::MaybeLocal ColumnToValue(const int column); v8::MaybeLocal ColumnNameToName(const int column); void Finalize(); @@ -242,6 +259,7 @@ class StatementSync : public BaseObject { bool use_big_ints_; bool allow_bare_named_params_; bool allow_unknown_named_params_; + bool read_null_as_undefined_; std::optional> bare_named_params_; bool BindParams(const v8::FunctionCallbackInfo& args); bool BindValue(const v8::Local& value, const int index);