Repository: nodejs/node
Pull request: sqlite: avoid extra copy for large text binds

## Summary
- When binding UTF-8 strings to SQLite statements, transfer ownership of the malloc-backed `Utf8Value` buffer to SQLite to avoid an extra copy for larger strings.
- Use `sqlite3_bind_blob64()` for BLOB parameters to avoid truncation for larger payloads.

Changed files:
filename: src/node_sqlite.cc
patch: @@ -2261,20 +2261,38 @@ bool StatementSync::BindValue(const Local<Value>& value, const int index) {
   // Dates could be supported by converting them to numbers. However, there
   // would not be a good way to read the values back from SQLite with the
   // original type.
+  Isolate* isolate = env()->isolate();
   int r;
   if (value->IsNumber()) {
-    double val = value.As<Number>()->Value();
+    const double val = value.As<Number>()->Value();
     r = sqlite3_bind_double(statement_, index, val);
   } else if (value->IsString()) {
-    Utf8Value val(env()->isolate(), value.As<String>());
-    r = sqlite3_bind_text(
-        statement_, index, *val, val.length(), SQLITE_TRANSIENT);
+    Utf8Value val(isolate, value.As<String>());
+    if (val.IsAllocated()) {
+      // Avoid an extra SQLite copy for large strings by transferring ownership
+      // of the malloc()'d buffer to SQLite.
+      char* data = *val;
+      const sqlite3_uint64 length = static_cast<sqlite3_uint64>(val.length());
+      val.Release();
+      r = sqlite3_bind_text64(
+          statement_, index, data, length, std::free, SQLITE_UTF8);
+    } else {
+      r = sqlite3_bind_text64(statement_,
+                              index,
+                              *val,
+                              static_cast<sqlite3_uint64>(val.length()),
+                              SQLITE_TRANSIENT,
+                              SQLITE_UTF8);
+    }
   } else if (value->IsNull()) {
     r = sqlite3_bind_null(statement_, index);
   } else if (value->IsArrayBufferView()) {
     ArrayBufferViewContents<uint8_t> buf(value);
-    r = sqlite3_bind_blob(
-        statement_, index, buf.data(), buf.length(), SQLITE_TRANSIENT);
+    r = sqlite3_bind_blob64(statement_,
+                            index,
+                            buf.data(),
+                            static_cast<sqlite3_uint64>(buf.length()),
+                            SQLITE_TRANSIENT);
   } else if (value->IsBigInt()) {
     bool lossless;
     int64_t as_int = value.As<BigInt>()->Int64Value(&lossless);
@@ -2285,13 +2303,13 @@ bool StatementSync::BindValue(const Local<Value>& value, const int index) {
     r = sqlite3_bind_int64(statement_, index, as_int);
   } else {
     THROW_ERR_INVALID_ARG_TYPE(
-        env()->isolate(),
+        isolate,
         "Provided value cannot be bound to SQLite parameter %d.",
         index);
     return false;
   }
 
-  CHECK_ERROR_OR_THROW(env()->isolate(), db_.get(), r, SQLITE_OK, false);
+  CHECK_ERROR_OR_THROW(isolate, db_.get(), r, SQLITE_OK, false);
   return true;
 }
 

filename: test/parallel/test-sqlite-data-types.js
patch: @@ -82,6 +82,41 @@ suite('data binding and mapping', () => {
     });
   });
 
+  test('large strings are bound correctly', (t) => {
+    const db = new DatabaseSync(nextDb());
+    t.after(() => { db.close(); });
+    const setup = db.exec(
+      'CREATE TABLE data(key INTEGER PRIMARY KEY, text TEXT) STRICT;'
+    );
+    t.assert.strictEqual(setup, undefined);
+
+    t.assert.deepStrictEqual(
+      db.prepare('INSERT INTO data (key, text) VALUES (?, ?)').run(1, ''),
+      { changes: 1, lastInsertRowid: 1 },
+    );
+
+    const update = db.prepare('UPDATE data SET text = ? WHERE key = 1');
+
+    // > 1024 bytes so `Utf8Value` uses heap storage internally.
+    const largeAscii = 'a'.repeat(8 * 1024);
+    // Force a non-one-byte string path through UTF-8 conversion.
+    const largeUnicode = '\u2603'.repeat(2048);
+
+    const res = update.run(largeAscii);
+    t.assert.strictEqual(res.changes, 1);
+
+    t.assert.strictEqual(
+      db.prepare('SELECT text FROM data WHERE key = 1').get().text,
+      largeAscii,
+    );
+
+    t.assert.strictEqual(update.run(largeUnicode).changes, 1);
+    t.assert.strictEqual(
+      db.prepare('SELECT text FROM data WHERE key = 1').get().text,
+      largeUnicode,
+    );
+  });
+
   test('unsupported data types', (t) => {
     const db = new DatabaseSync(nextDb());
     t.after(() => { db.close(); });