Skip to content

Commit 92bb01c

Browse files
committed
sqlite: avoid extra copy for large text binds
When binding UTF-8 strings to prepared statements, transfer ownership of malloc-backed Utf8Value buffers to SQLite to avoid an extra copy for large strings. Use sqlite3_bind_blob64() when binding BLOB parameters.
1 parent 37e4004 commit 92bb01c

File tree

2 files changed

+63
-8
lines changed

2 files changed

+63
-8
lines changed

src/node_sqlite.cc

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2260,20 +2260,38 @@ bool StatementSync::BindValue(const Local<Value>& value, const int index) {
22602260
// Dates could be supported by converting them to numbers. However, there
22612261
// would not be a good way to read the values back from SQLite with the
22622262
// original type.
2263+
Isolate* isolate = env()->isolate();
22632264
int r;
22642265
if (value->IsNumber()) {
2265-
double val = value.As<Number>()->Value();
2266+
const double val = value.As<Number>()->Value();
22662267
r = sqlite3_bind_double(statement_, index, val);
22672268
} else if (value->IsString()) {
2268-
Utf8Value val(env()->isolate(), value.As<String>());
2269-
r = sqlite3_bind_text(
2270-
statement_, index, *val, val.length(), SQLITE_TRANSIENT);
2269+
Utf8Value val(isolate, value.As<String>());
2270+
if (val.IsAllocated()) {
2271+
// Avoid an extra SQLite copy for large strings by transferring ownership
2272+
// of the malloc()'d buffer to SQLite.
2273+
char* data = *val;
2274+
const sqlite3_uint64 length = static_cast<sqlite3_uint64>(val.length());
2275+
val.Release();
2276+
r = sqlite3_bind_text64(
2277+
statement_, index, data, length, std::free, SQLITE_UTF8);
2278+
} else {
2279+
r = sqlite3_bind_text64(statement_,
2280+
index,
2281+
*val,
2282+
static_cast<sqlite3_uint64>(val.length()),
2283+
SQLITE_TRANSIENT,
2284+
SQLITE_UTF8);
2285+
}
22712286
} else if (value->IsNull()) {
22722287
r = sqlite3_bind_null(statement_, index);
22732288
} else if (value->IsArrayBufferView()) {
22742289
ArrayBufferViewContents<uint8_t> buf(value);
2275-
r = sqlite3_bind_blob(
2276-
statement_, index, buf.data(), buf.length(), SQLITE_TRANSIENT);
2290+
r = sqlite3_bind_blob64(statement_,
2291+
index,
2292+
buf.data(),
2293+
static_cast<sqlite3_uint64>(buf.length()),
2294+
SQLITE_TRANSIENT);
22772295
} else if (value->IsBigInt()) {
22782296
bool lossless;
22792297
int64_t as_int = value.As<BigInt>()->Int64Value(&lossless);
@@ -2284,13 +2302,13 @@ bool StatementSync::BindValue(const Local<Value>& value, const int index) {
22842302
r = sqlite3_bind_int64(statement_, index, as_int);
22852303
} else {
22862304
THROW_ERR_INVALID_ARG_TYPE(
2287-
env()->isolate(),
2305+
isolate,
22882306
"Provided value cannot be bound to SQLite parameter %d.",
22892307
index);
22902308
return false;
22912309
}
22922310

2293-
CHECK_ERROR_OR_THROW(env()->isolate(), db_.get(), r, SQLITE_OK, false);
2311+
CHECK_ERROR_OR_THROW(isolate, db_.get(), r, SQLITE_OK, false);
22942312
return true;
22952313
}
22962314

test/parallel/test-sqlite-data-types.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,43 @@ suite('data binding and mapping', () => {
8282
});
8383
});
8484

85+
test('large strings are bound correctly', (t) => {
86+
const db = new DatabaseSync(nextDb());
87+
t.after(() => { db.close(); });
88+
const setup = db.exec(
89+
'CREATE TABLE data(key INTEGER PRIMARY KEY, text TEXT) STRICT;'
90+
);
91+
t.assert.strictEqual(setup, undefined);
92+
93+
t.assert.deepStrictEqual(
94+
db.prepare('INSERT INTO data (key, text) VALUES (?, ?)').run(1, ''),
95+
{ changes: 1, lastInsertRowid: 1 },
96+
);
97+
98+
const update = db.prepare('UPDATE data SET text = ? WHERE key = 1');
99+
100+
// > 1024 bytes so `Utf8Value` uses heap storage internally.
101+
const largeAscii = 'a'.repeat(8 * 1024);
102+
// Force a non-one-byte string path through UTF-8 conversion.
103+
const largeUnicode = '\u2603'.repeat(2048);
104+
105+
106+
const res = update.run(largeAscii);
107+
t.assert.strictEqual(res.changes, 1);
108+
109+
110+
t.assert.strictEqual(
111+
db.prepare('SELECT text FROM data WHERE key = 1').get().text,
112+
largeAscii,
113+
);
114+
115+
t.assert.strictEqual(update.run(largeUnicode).changes, 1);
116+
t.assert.strictEqual(
117+
db.prepare('SELECT text FROM data WHERE key = 1').get().text,
118+
largeUnicode,
119+
);
120+
});
121+
85122
test('unsupported data types', (t) => {
86123
const db = new DatabaseSync(nextDb());
87124
t.after(() => { db.close(); });

0 commit comments

Comments
 (0)