From 82de30fee032ff617b43a08ad9ee7f278b4cf22b Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Mon, 23 Feb 2026 19:48:25 +0500 Subject: [PATCH 1/3] sqlite: use OneByte for ASCII text and internalize col names Use simdutf to detect ASCII text values and create them via NewFromOneByte for compact one-byte representation. Internalize column name strings with kInternalized so V8 shares hidden classes across row objects. Cache column names on StatementSync for iterate(), invalidated via SQLITE_STMTSTATUS_REPREPARE on schema changes. Refs: https://github.com/nodejs/performance/issues/181 --- src/node_sqlite.cc | 64 +++++++++++++++++++++++++++++++++++++++------- src/node_sqlite.h | 4 +++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 91b80b4fb44c26..57ebdf0119c33a 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -8,6 +8,7 @@ #include "node_errors.h" #include "node_mem-inl.h" #include "node_url.h" +#include "simdutf.h" #include "sqlite3.h" #include "threadpoolwork-inl.h" #include "util-inl.h" @@ -63,6 +64,19 @@ using v8::TryCatch; using v8::Uint8Array; using v8::Value; +inline MaybeLocal Utf8StringMaybeOneByte(Isolate* isolate, + const char* data, + size_t length) { + int len = static_cast(length); + if (simdutf::validate_ascii(data, length)) { + return String::NewFromOneByte(isolate, + reinterpret_cast(data), + NewStringType::kNormal, + len); + } + return String::NewFromUtf8(isolate, data, NewStringType::kNormal, len); +} + #define CHECK_ERROR_OR_THROW(isolate, db, expr, expected, ret) \ do { \ int r_ = (expr); \ @@ -105,7 +119,8 @@ using v8::Value; case SQLITE_TEXT: { \ const char* v = \ reinterpret_cast(sqlite3_##from##_text(__VA_ARGS__)); \ - (result) = String::NewFromUtf8((isolate), v).As(); \ + int v_len = sqlite3_##from##_bytes(__VA_ARGS__); \ + (result) = Utf8StringMaybeOneByte((isolate), v, v_len).As(); \ break; \ } \ case SQLITE_NULL: { \ @@ -2416,6 +2431,7 @@ StatementSync::~StatementSync() { void StatementSync::Finalize() { sqlite3_finalize(statement_); statement_ = nullptr; + cached_column_names_.clear(); } inline bool StatementSync::IsFinalized() { @@ -2599,7 +2615,40 @@ MaybeLocal StatementSync::ColumnNameToName(const int column) { return MaybeLocal(); } - return String::NewFromUtf8(env()->isolate(), col_name).As(); + return String::NewFromUtf8( + env()->isolate(), col_name, NewStringType::kInternalized) + .As(); +} + +bool StatementSync::GetCachedColumnNames(LocalVector* keys) { + Isolate* isolate = env()->isolate(); + + int reprepare_count = + sqlite3_stmt_status(statement_, SQLITE_STMTSTATUS_REPREPARE, 0); + if (reprepare_count != cached_column_names_reprepare_count_) { + cached_column_names_.clear(); + int num_cols = sqlite3_column_count(statement_); + if (num_cols == 0) { + cached_column_names_reprepare_count_ = reprepare_count; + return true; + } + cached_column_names_.reserve(num_cols); + for (int i = 0; i < num_cols; ++i) { + Local key; + if (!ColumnNameToName(i).ToLocal(&key)) { + cached_column_names_.clear(); + return false; + } + cached_column_names_.emplace_back(Global(isolate, key)); + } + cached_column_names_reprepare_count_ = reprepare_count; + } + + keys->reserve(cached_column_names_.size()); + for (const auto& name : cached_column_names_) { + keys->emplace_back(name.Get(isolate)); + } + return true; } MaybeLocal StatementExecutionHelper::ColumnToValue(Environment* env, @@ -2621,7 +2670,9 @@ MaybeLocal StatementExecutionHelper::ColumnNameToName(Environment* env, return MaybeLocal(); } - return String::NewFromUtf8(env->isolate(), col_name).As(); + return String::NewFromUtf8( + env->isolate(), col_name, NewStringType::kInternalized) + .As(); } void StatementSync::MemoryInfo(MemoryTracker* tracker) const {} @@ -3531,12 +3582,7 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { if (iter->stmt_->return_arrays_) { row_value = Array::New(isolate, row_values.data(), row_values.size()); } else { - row_keys.reserve(num_cols); - for (int i = 0; i < num_cols; ++i) { - Local key; - if (!iter->stmt_->ColumnNameToName(i).ToLocal(&key)) return; - row_keys.emplace_back(key); - } + if (!iter->stmt_->GetCachedColumnNames(&row_keys)) return; DCHECK_EQ(row_keys.size(), row_values.size()); row_value = Object::New( diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 3ee79cc10ec562..2d73c2ca3ad9bd 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace node { namespace sqlite { @@ -277,6 +278,7 @@ class StatementSync : public BaseObject { static void SetReturnArrays(const v8::FunctionCallbackInfo& args); v8::MaybeLocal ColumnToValue(const int column); v8::MaybeLocal ColumnNameToName(const int column); + bool GetCachedColumnNames(v8::LocalVector* keys); void Finalize(); bool IsFinalized(); @@ -294,6 +296,8 @@ class StatementSync : public BaseObject { uint64_t reset_generation_ = 0; std::optional> bare_named_params_; inline int ResetStatement(); + std::vector> cached_column_names_; + int cached_column_names_reprepare_count_ = -1; bool BindParams(const v8::FunctionCallbackInfo& args); bool BindValue(const v8::Local& value, const int index); From cbcd03f94fb14e895b5e94f004b63a3d8e964f4b Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Tue, 24 Feb 2026 21:45:07 +0500 Subject: [PATCH 2/3] resolve feedback --- src/node_sqlite.cc | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 57ebdf0119c33a..788e29d26dd17b 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -65,16 +65,17 @@ using v8::Uint8Array; using v8::Value; inline MaybeLocal Utf8StringMaybeOneByte(Isolate* isolate, - const char* data, - size_t length) { - int len = static_cast(length); - if (simdutf::validate_ascii(data, length)) { - return String::NewFromOneByte(isolate, - reinterpret_cast(data), - NewStringType::kNormal, - len); + std::string_view input) { + int len = static_cast(input.size()); + if (simdutf::validate_ascii(input.data(), input.size())) { + return String::NewFromOneByte( + isolate, + reinterpret_cast(input.data()), + NewStringType::kNormal, + len); } - return String::NewFromUtf8(isolate, data, NewStringType::kNormal, len); + return String::NewFromUtf8( + isolate, input.data(), NewStringType::kNormal, len); } #define CHECK_ERROR_OR_THROW(isolate, db, expr, expected, ret) \ @@ -120,7 +121,9 @@ inline MaybeLocal Utf8StringMaybeOneByte(Isolate* isolate, const char* v = \ reinterpret_cast(sqlite3_##from##_text(__VA_ARGS__)); \ int v_len = sqlite3_##from##_bytes(__VA_ARGS__); \ - (result) = Utf8StringMaybeOneByte((isolate), v, v_len).As(); \ + (result) = \ + Utf8StringMaybeOneByte((isolate), std::string_view(v, v_len)) \ + .As(); \ break; \ } \ case SQLITE_NULL: { \ @@ -2620,6 +2623,9 @@ MaybeLocal StatementSync::ColumnNameToName(const int column) { .As(); } +// Returns cached internalized column name strings for this statement, +// invalidating the cache when SQLite re-prepares the statement (e.g. after +// schema changes like ALTER TABLE) detected via SQLITE_STMTSTATUS_REPREPARE. bool StatementSync::GetCachedColumnNames(LocalVector* keys) { Isolate* isolate = env()->isolate(); @@ -3582,6 +3588,8 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { if (iter->stmt_->return_arrays_) { row_value = Array::New(isolate, row_values.data(), row_values.size()); } else { + // Use cached internalized column names to avoid repeated V8 string + // creation and enable hidden class sharing across row objects. if (!iter->stmt_->GetCachedColumnNames(&row_keys)) return; DCHECK_EQ(row_keys.size(), row_values.size()); From 3f8cce791226466baaa0f4bbab39546097a7d742 Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Tue, 3 Mar 2026 19:05:48 +0500 Subject: [PATCH 3/3] resolve feedback --- src/node_sqlite.cc | 21 ++++++++++++--------- src/node_sqlite.h | 1 + 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 788e29d26dd17b..b91241ba201182 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -66,7 +66,7 @@ using v8::Value; inline MaybeLocal Utf8StringMaybeOneByte(Isolate* isolate, std::string_view input) { - int len = static_cast(input.size()); + const int len = static_cast(input.size()); if (simdutf::validate_ascii(input.data(), input.size())) { return String::NewFromOneByte( isolate, @@ -120,7 +120,7 @@ inline MaybeLocal Utf8StringMaybeOneByte(Isolate* isolate, case SQLITE_TEXT: { \ const char* v = \ reinterpret_cast(sqlite3_##from##_text(__VA_ARGS__)); \ - int v_len = sqlite3_##from##_bytes(__VA_ARGS__); \ + const int v_len = sqlite3_##from##_bytes(__VA_ARGS__); \ (result) = \ Utf8StringMaybeOneByte((isolate), std::string_view(v, v_len)) \ .As(); \ @@ -2434,6 +2434,10 @@ StatementSync::~StatementSync() { void StatementSync::Finalize() { sqlite3_finalize(statement_); statement_ = nullptr; + InvalidateColumnNameCache(); +} + +void StatementSync::InvalidateColumnNameCache() { cached_column_names_.clear(); } @@ -2623,17 +2627,16 @@ MaybeLocal StatementSync::ColumnNameToName(const int column) { .As(); } -// Returns cached internalized column name strings for this statement, -// invalidating the cache when SQLite re-prepares the statement (e.g. after -// schema changes like ALTER TABLE) detected via SQLITE_STMTSTATUS_REPREPARE. +// Populates `keys` with cached column names, rebuilding the cache if the +// statement was re-prepared. bool StatementSync::GetCachedColumnNames(LocalVector* keys) { Isolate* isolate = env()->isolate(); - int reprepare_count = - sqlite3_stmt_status(statement_, SQLITE_STMTSTATUS_REPREPARE, 0); + const int reprepare_count = + sqlite3_stmt_status(statement_, SQLITE_STMTSTATUS_REPREPARE, false); if (reprepare_count != cached_column_names_reprepare_count_) { cached_column_names_.clear(); - int num_cols = sqlite3_column_count(statement_); + const int num_cols = sqlite3_column_count(statement_); if (num_cols == 0) { cached_column_names_reprepare_count_ = reprepare_count; return true; @@ -2642,7 +2645,7 @@ bool StatementSync::GetCachedColumnNames(LocalVector* keys) { for (int i = 0; i < num_cols; ++i) { Local key; if (!ColumnNameToName(i).ToLocal(&key)) { - cached_column_names_.clear(); + InvalidateColumnNameCache(); return false; } cached_column_names_.emplace_back(Global(isolate, key)); diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 2d73c2ca3ad9bd..1ff5804e704231 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -298,6 +298,7 @@ class StatementSync : public BaseObject { inline int ResetStatement(); std::vector> cached_column_names_; int cached_column_names_reprepare_count_ = -1; + void InvalidateColumnNameCache(); bool BindParams(const v8::FunctionCallbackInfo& args); bool BindValue(const v8::Local& value, const int index);