diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 4fad22c618900d..40515cff4997ce 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -2260,20 +2260,38 @@ bool StatementSync::BindValue(const Local& 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()->Value(); + const double val = value.As()->Value(); r = sqlite3_bind_double(statement_, index, val); } else if (value->IsString()) { - Utf8Value val(env()->isolate(), value.As()); - r = sqlite3_bind_text( - statement_, index, *val, val.length(), SQLITE_TRANSIENT); + Utf8Value val(isolate, value.As()); + 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(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(val.length()), + SQLITE_TRANSIENT, + SQLITE_UTF8); + } } else if (value->IsNull()) { r = sqlite3_bind_null(statement_, index); } else if (value->IsArrayBufferView()) { ArrayBufferViewContents buf(value); - r = sqlite3_bind_blob( - statement_, index, buf.data(), buf.length(), SQLITE_TRANSIENT); + r = sqlite3_bind_blob64(statement_, + index, + buf.data(), + static_cast(buf.length()), + SQLITE_TRANSIENT); } else if (value->IsBigInt()) { bool lossless; int64_t as_int = value.As()->Int64Value(&lossless); @@ -2284,13 +2302,13 @@ bool StatementSync::BindValue(const Local& 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; } diff --git a/test/parallel/test-sqlite-data-types.js b/test/parallel/test-sqlite-data-types.js index 590c6d5bdc1e6e..26af15a777d234 100644 --- a/test/parallel/test-sqlite-data-types.js +++ b/test/parallel/test-sqlite-data-types.js @@ -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(); });