Repository: nodejs/node
Pull request: sqlite: use DictionaryTemplate for run() result

Reduce allocation overhead in run() result object creation by caching DictionaryTemplate.
Improves INSERT performance by ~17–24%, SELECT performance remains unchanged.

results: 

```
➜  node git:(mert/sqlite-optimize-run-result) ✗ node-benchmark-compare ./result.csv
                                                                                                                                                              confidence improvement accuracy (*)    (**)   (***)
sqlite/sqlite-is-transaction.js transaction='false' n=10000000                                                                                                               -5.98 %       ±7.16%  ±9.88% ±13.61%
sqlite/sqlite-is-transaction.js transaction='true' n=10000000                                                                                                                 0.13 %       ±1.57%  ±2.15%  ±2.93%
sqlite/sqlite-prepare-insert.js statement='INSERT INTO all_column_types (text_column, integer_column, real_column, blob_column) VALUES (?, ?, ?, ?)' n=100000        ***     17.24 %       ±1.15%  ±1.59%  ±2.22%
sqlite/sqlite-prepare-insert.js statement='INSERT INTO blob_column_type (blob_column) VALUES (?)' n=100000                                                           ***     22.91 %       ±1.25%  ±1.71%  ±2.33%
sqlite/sqlite-prepare-insert.js statement='INSERT INTO integer_column_type (integer_column) VALUES (?)' n=100000                                                     ***     23.71 %       ±1.36%  ±1.87%  ±2.57%
sqlite/sqlite-prepare-insert.js statement='INSERT INTO large_text (text_8kb_column) VALUES (?)' n=100000                                                                     -1.50 %       ±6.67%  ±9.29% ±12.99%
sqlite/sqlite-prepare-insert.js statement='INSERT INTO missing_required_value (any_value, required_value) VALUES (?, ?)' n=100000                                            -1.17 %       ±1.97%  ±2.72%  ±3.75%
sqlite/sqlite-prepare-insert.js statement='INSERT INTO real_column_type (real_column) VALUES (?)' n=100000                                                           ***     21.04 %       ±1.43%  ±2.01%  ±2.84%
sqlite/sqlite-prepare-insert.js statement='INSERT INTO text_column_type (text_column) VALUES (?)' n=100000                                                           ***     19.58 %       ±1.41%  ±1.95%  ±2.68%
sqlite/sqlite-prepare-select-all.js statement='SELECT * FROM foo LIMIT 1' tableSeedSize=100000 n=100000                                                                       3.78 %       ±9.61% ±13.77% ±20.18%
sqlite/sqlite-prepare-select-all.js statement='SELECT * FROM foo LIMIT 100' tableSeedSize=100000 n=100000                                                                    -0.40 %       ±1.60%  ±2.20%  ±3.00%
sqlite/sqlite-prepare-select-all.js statement='SELECT 1' tableSeedSize=100000 n=100000                                                                                       -1.97 %       ±4.19%  ±5.83%  ±8.14%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_8kb_column FROM foo_large LIMIT 1' tableSeedSize=100000 n=100000                                                   4.02 %       ±8.16% ±11.59% ±16.73%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_8kb_column FROM foo_large LIMIT 100' tableSeedSize=100000 n=100000                                                 1.32 %       ±2.78%  ±3.88%  ±5.44%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_column FROM foo LIMIT 1' tableSeedSize=100000 n=100000                                                     **     -2.76 %       ±1.66%  ±2.29%  ±3.16%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_column FROM foo LIMIT 100' tableSeedSize=100000 n=100000                                                           0.59 %       ±0.96%  ±1.32%  ±1.80%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_column, integer_column FROM foo LIMIT 1' tableSeedSize=100000 n=100000                                      *     -2.37 %       ±2.28%  ±3.15%  ±4.36%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_column, integer_column FROM foo LIMIT 100' tableSeedSize=100000 n=100000                                           0.71 %       ±2.21%  ±3.05%  ±4.22%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_column, integer_column, real_column FROM foo LIMIT 1' tableSeedSize=100000 n=100000                               -1.15 %       ±2.02%  ±2.77%  ±3.79%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_column, integer_column, real_column FROM foo LIMIT 100' tableSeedSize=100000 n=100000                              0.53 %       ±1.93%  ±2.72%  ±3.87%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_column, integer_column, real_column, blob_column FROM foo LIMIT 1' tableSeedSize=100000 n=100000           **      1.87 %       ±1.31%  ±1.80%  ±2.45%
sqlite/sqlite-prepare-select-all.js statement='SELECT text_column, integer_column, real_column, blob_column FROM foo LIMIT 100' tableSeedSize=100000 n=100000                 1.26 %       ±2.84%  ±3.94%  ±5.49%
sqlite/sqlite-prepare-select-get.js statement='SELECT * FROM foo LIMIT 1' tableSeedSize=100000 n=100000                                                                       0.83 %       ±1.37%  ±1.88%  ±2.56%
sqlite/sqlite-prepare-select-get.js statement='SELECT 1' tableSeedSize=100000 n=100000                                                                                       -0.22 %       ±1.32%  ±1.83%  ±2.54%
sqlite/sqlite-prepare-select-get.js statement='SELECT text_8kb_column FROM foo_large LIMIT 1' tableSeedSize=100000 n=100000                                            *      1.40 %       ±1.04%  ±1.42%  ±1.93%
sqlite/sqlite-prepare-select-get.js statement='SELECT text_column FROM foo LIMIT 1' tableSeedSize=100000 n=100000                                                             1.17 %       ±1.71%  ±2.36%  ±3.25%
sqlite/sqlite-prepare-select-get.js statement='SELECT text_column, integer_column FROM foo LIMIT 1' tableSeedSize=100000 n=100000                                            -5.25 %      ±10.77% ±15.42% ±22.57%
sqlite/sqlite-prepare-select-get.js statement='SELECT text_column, integer_column, real_column FROM foo LIMIT 1' tableSeedSize=100000 n=100000                                0.18 %       ±1.46%  ±2.01%  ±2.74%
sqlite/sqlite-prepare-select-get.js statement='SELECT text_column, integer_column, real_column, blob_column FROM foo LIMIT 1' tableSeedSize=100000 n=100000                   0.49 %       ±1.81%  ±2.51%  ±3.48%

Be aware that when doing many comparisons the risk of a false-positive result increases.
In this case, there are 29 comparisons, you can thus expect the following amount of false-positive results:
  1.45 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.29 false positives, when considering a   1% risk acceptance (**, ***),
  0.03 false positives, when considering a 0.1% risk acceptance (***)
➜  node git:(mert/sqlite-optimize-run-result) ✗ 
```

Changed files:
filename: src/env_properties.h
patch: @@ -434,6 +434,7 @@
   V(socketaddress_constructor_template, v8::FunctionTemplate)                  \
   V(space_stats_template, v8::DictionaryTemplate)                              \
   V(sqlite_column_template, v8::DictionaryTemplate)                            \
+  V(sqlite_run_result_template, v8::DictionaryTemplate)                        \
   V(sqlite_statement_sync_constructor_template, v8::FunctionTemplate)          \
   V(sqlite_statement_sync_iterator_constructor_template, v8::FunctionTemplate) \
   V(sqlite_session_constructor_template, v8::FunctionTemplate)                 \

filename: src/node_sqlite.cc
patch: @@ -2406,7 +2406,7 @@ MaybeLocal<Object> StatementExecutionHelper::Run(Environment* env,
   sqlite3_step(stmt);
   int r = sqlite3_reset(stmt);
   CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, MaybeLocal<Object>());
-  Local<Object> result = Object::New(isolate);
+
   sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(db->Connection());
   sqlite3_int64 changes = sqlite3_changes64(db->Connection());
   Local<Value> last_insert_rowid_val;
@@ -2420,13 +2420,18 @@ MaybeLocal<Object> StatementExecutionHelper::Run(Environment* env,
     changes_val = Number::New(isolate, changes);
   }
 
-  if (result
-          ->Set(env->context(),
-                env->last_insert_rowid_string(),
-                last_insert_rowid_val)
-          .IsNothing() ||
-      result->Set(env->context(), env->changes_string(), changes_val)
-          .IsNothing()) {
+  auto run_result_template = env->sqlite_run_result_template();
+  if (run_result_template.IsEmpty()) {
+    static constexpr std::string_view run_result_keys[] = {"changes",
+                                                           "lastInsertRowid"};
+    run_result_template = DictionaryTemplate::New(isolate, run_result_keys);
+    env->set_sqlite_run_result_template(run_result_template);
+  }
+
+  MaybeLocal<Value> values[] = {changes_val, last_insert_rowid_val};
+  Local<Object> result;
+  if (!NewDictionaryInstance(env->context(), run_result_template, values)
+           .ToLocal(&result)) {
     return MaybeLocal<Object>();
   }