Skip to content

Commit 8e9bcd5

Browse files
authored
Merge pull request #3117 from cloudflare/kenton/fix-sql-cache
Fix SQL statement caching of computed query strings.
2 parents 186655e + a1ce603 commit 8e9bcd5

File tree

5 files changed

+52
-2
lines changed

5 files changed

+52
-2
lines changed

src/workerd/api/sql-test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,25 @@ async function test(state) {
10121012
await state.blockConcurrencyWhile(async () => {
10131013
sql.exec(`PRAGMA foreign_keys = ON;`);
10141014
});
1015+
1016+
// Verify caching.
1017+
{
1018+
let isCached = (q) => {
1019+
let cursor = sql.exec(q);
1020+
cursor.toArray();
1021+
return cursor.reusedCachedQueryForTest;
1022+
};
1023+
1024+
// Query based on literal string is cached.
1025+
assert.equal(false, isCached('SELECT 179321'));
1026+
assert.equal(true, isCached('SELECT 179321'));
1027+
assert.equal(true, isCached('SELECT 179321'));
1028+
1029+
// Qeury based on computed string is cached.
1030+
assert.equal(false, isCached('SELECT "' + 'x'.repeat(4) + '"'));
1031+
assert.equal(true, isCached('SELECT "' + 'x'.repeat(4) + '"'));
1032+
assert.equal(true, isCached('SELECT "' + 'x'.repeat(4) + '"'));
1033+
}
10151034
}
10161035

10171036
async function testIoStats(storage) {

src/workerd/api/sql.c++

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ SqlStorage::~SqlStorage() {}
2525

2626
jsg::Ref<SqlStorage::Cursor> SqlStorage::exec(
2727
jsg::Lock& js, jsg::JsString querySql, jsg::Arguments<BindingValue> bindings) {
28+
// Internalize the string, so that the cache can be keyed by string identity rather than content.
29+
// Any string we put into the cache is expected to live there for a while anyway, so even if it
30+
// is a one-off, internalizing it (which moves it to the old generation) shouldn't hurt.
31+
querySql = querySql.internalize(js);
32+
2833
auto& db = getDb(js);
2934
auto& statementCache = *this->statementCache;
3035

@@ -159,6 +164,8 @@ void SqlStorage::Cursor::initColumnNames(jsg::Lock& js, State& stateRef) {
159164
// TODO(cleanup): Make `js.withinHandleScope` understand `jsg::JsValue` types in addition to
160165
// `v8::Local`.
161166
KJ_IF_SOME(cached, stateRef.cachedStatement) {
167+
reusedCachedQuery = cached->useCount++ > 0;
168+
162169
KJ_IF_SOME(names, cached->cachedColumnNames) {
163170
// Use cached copy.
164171
columnNames = names.addRef(js);

src/workerd/api/sql.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class SqlStorage final: public jsg::Object, private SqliteDatabase::Regulator {
8787
SqliteDatabase::Statement statement;
8888
kj::Maybe<jsg::JsRef<jsg::JsArray>> cachedColumnNames; // initialized on first exec
8989
kj::ListLink<CachedStatement> lruLink;
90+
uint useCount = 0;
9091

9192
CachedStatement(jsg::Lock& js,
9293
SqlStorage& sqlStorage,
@@ -190,7 +191,7 @@ class SqlStorage::Cursor final: public jsg::Object {
190191
double getRowsWritten();
191192

192193
jsg::JsArray getColumnNames(jsg::Lock& js);
193-
JSG_RESOURCE_TYPE(Cursor) {
194+
JSG_RESOURCE_TYPE(Cursor, CompatibilityFlags::Reader flags) {
194195
JSG_METHOD(next);
195196
JSG_METHOD(toArray);
196197
JSG_METHOD(one);
@@ -210,6 +211,10 @@ class SqlStorage::Cursor final: public jsg::Object {
210211
one(): T;
211212
columnNames: string[];
212213
});
214+
215+
if (flags.getWorkerdExperimental()) {
216+
JSG_READONLY_PROTOTYPE_PROPERTY(reusedCachedQueryForTest, getReusedCachedQueryForTest);
217+
}
213218
}
214219

215220
JSG_ITERATOR(RowIterator, rows, jsg::JsObject, jsg::Ref<Cursor>, rowIteratorNext);
@@ -226,6 +231,10 @@ class SqlStorage::Cursor final: public jsg::Object {
226231
tracker.trackField("columnNames", columnNames);
227232
}
228233

234+
bool getReusedCachedQueryForTest() {
235+
return reusedCachedQuery;
236+
}
237+
229238
private:
230239
struct State {
231240
kj::Maybe<kj::Rc<CachedStatement>> cachedStatement;
@@ -251,6 +260,9 @@ class SqlStorage::Cursor final: public jsg::Object {
251260
// flag an error if the application tries to reuse the cursor.
252261
bool canceled = false;
253262

263+
// Did we reuse a query from the query cache? Tracked for testing purposes.
264+
bool reusedCachedQuery = false;
265+
254266
// Reference to a weak reference that might point back to this object. If so, null it out at
255267
// destruction. Used by Statement to invalidate past cursors when the statement is
256268
// executed again.
@@ -295,7 +307,9 @@ class SqlStorage::Statement final: public jsg::Object {
295307
public:
296308
Statement(jsg::Lock& js, jsg::Ref<SqlStorage> sqlStorage, jsg::JsString query)
297309
: sqlStorage(kj::mv(sqlStorage)),
298-
query(js.v8Isolate, query) {}
310+
// Internalize the string before constructing the statement so that it doesn't have to
311+
// re-lookup the internalized string for every invocation.
312+
query(js.v8Isolate, query.internalize(js)) {}
299313

300314
jsg::Ref<Cursor> run(jsg::Lock& js, jsg::Arguments<BindingValue> bindings);
301315

src/workerd/jsg/jsvalue.c++

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ bool JsString::operator==(const JsString& other) const {
278278
return inner->StringEquals(other.inner);
279279
}
280280

281+
JsString JsString::internalize(Lock& js) const {
282+
return JsString(inner->InternalizeString(js.v8Isolate));
283+
}
284+
281285
JsString::WriteIntoStatus JsString::writeInto(
282286
Lock& js, kj::ArrayPtr<char> buffer, WriteOptions options) const {
283287
WriteIntoStatus result = {0, 0};

src/workerd/jsg/jsvalue.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,12 @@ class JsString final: public JsBase<v8::String, JsString> {
240240

241241
bool operator==(const JsString& other) const;
242242

243+
// "Internalize" the string. Returns a string with the same content but which is identity-equal
244+
// to all other internalized strings with the same content. If the string is already
245+
// internalized, this returns the same value. Note that strings originating from literals in the
246+
// code are always internalized.
247+
JsString internalize(Lock& js) const;
248+
243249
static JsString concat(Lock& js, const JsString& one, const JsString& two) KJ_WARN_UNUSED_RESULT;
244250

245251
enum WriteOptions {

0 commit comments

Comments
 (0)