From e1fbfcbd0d467cb1e17144639a674003395f50f9 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 98612f39695897..daa52e70c6e8ab 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: { \ @@ -2415,6 +2430,7 @@ StatementSync::~StatementSync() { void StatementSync::Finalize() { sqlite3_finalize(statement_); statement_ = nullptr; + cached_column_names_.clear(); } inline bool StatementSync::IsFinalized() { @@ -2598,7 +2614,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, @@ -2620,7 +2669,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 {} @@ -3530,12 +3581,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 85837477ce7ddab3c11e999887fe480cb1828768 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 daa52e70c6e8ab..5b7b98300f2b2e 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: { \ @@ -2619,6 +2622,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(); @@ -3581,6 +3587,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 3d6d7ffdd5d8f3ed002cacac7f005c4c5af96bf7 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 5b7b98300f2b2e..f4d88f039c3490 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(); \ @@ -2433,6 +2433,10 @@ StatementSync::~StatementSync() { void StatementSync::Finalize() { sqlite3_finalize(statement_); statement_ = nullptr; + InvalidateColumnNameCache(); +} + +void StatementSync::InvalidateColumnNameCache() { cached_column_names_.clear(); } @@ -2622,17 +2626,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; @@ -2641,7 +2644,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);