From 4fcb1ede7db090837f71255c44ccb901d3bbb197 Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Mon, 23 Feb 2026 19:48:25 +0500 Subject: [PATCH 1/2] 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 fb229e19fd6aef..3e535022f20e39 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" @@ -55,6 +56,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); \ @@ -97,7 +111,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: { \ @@ -2147,6 +2162,7 @@ StatementSync::~StatementSync() { void StatementSync::Finalize() { sqlite3_finalize(statement_); statement_ = nullptr; + cached_column_names_.clear(); } inline bool StatementSync::IsFinalized() { @@ -2325,7 +2341,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, @@ -2347,7 +2396,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 {} @@ -3251,12 +3302,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 bd61fcd6ebcd4d..c8209bcdc148d4 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -12,6 +12,7 @@ #include #include #include +#include namespace node { namespace sqlite { @@ -228,6 +229,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(); @@ -243,6 +245,8 @@ class StatementSync : public BaseObject { bool allow_bare_named_params_; bool allow_unknown_named_params_; std::optional> bare_named_params_; + 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 fd4215dd7ba32828624191854bedac4370a29ed3 Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Tue, 24 Feb 2026 21:45:07 +0500 Subject: [PATCH 2/2] 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 3e535022f20e39..3ef4ec199d5f7b 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -57,16 +57,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) \ @@ -112,7 +113,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: { \ @@ -2346,6 +2349,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(); @@ -3302,6 +3308,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());