Skip to content

Commit 3fa671b

Browse files
committed
perf: cache column names per statement for object rows
get() and iterate() internalized every column name again for each row, and all() did so once per call. Statements now keep the names as persistent handles and refresh them only when SQLite re-prepares the statement (SQLITE_STMTSTATUS_REPREPARE) or the column count changes, which is the only time names can change, e.g. a schema change behind SELECT *.
1 parent 6a0a30f commit 3fa671b

9 files changed

Lines changed: 126 additions & 13 deletions

‎src/better_sqlite3.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Backup;
2525
#include "util/data-converter.cpp"
2626
#include "util/data.cpp"
2727
#if defined(NODE_MODULE_VERSION) && NODE_MODULE_VERSION >= 127
28+
#include "util/column-keys.cpp"
2829
#include "util/row-builder.cpp"
2930
#endif
3031

‎src/objects/statement-iterator.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ void StatementIterator::Next(NODE_ARGUMENTS info) {
4040
UseIsolate;
4141
UseContext;
4242
info.GetReturnValue().Set(
43-
NewRecord(isolate, ctx, Data::GetRowJS(isolate, ctx, handle, safe_ints, mode), db_state->addon, false)
43+
NewRecord(isolate, ctx, stmt->GetRowJS(isolate, ctx, safe_ints, mode), db_state->addon, false)
4444
);
4545
} else {
4646
if (status == SQLITE_DONE) Return(info);

‎src/objects/statement.cpp‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@ BindMap* Statement::GetBindMap(v8::Isolate* isolate) {
5252
Statement::Extras::Extras(sqlite3_uint64 id)
5353
: bind_map(0), id(id) {}
5454

55+
#if defined(NODE_MODULE_VERSION) && NODE_MODULE_VERSION >= 127
56+
RowBuilder Statement::NewRowBuilder(v8::Isolate* isolate, bool safe_ints) {
57+
return RowBuilder(isolate, handle, safe_ints, &extras->column_keys);
58+
}
59+
#endif
60+
61+
v8::Local<v8::Value> Statement::GetRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, bool safe_ints, char mode) {
62+
#if defined(NODE_MODULE_VERSION) && NODE_MODULE_VERSION >= 127
63+
if (mode == Data::FLAT) {
64+
return NewRowBuilder(isolate, safe_ints).GetRowJS();
65+
}
66+
#endif
67+
return Data::GetRowJS(isolate, ctx, handle, safe_ints, mode);
68+
}
69+
5570
INIT(Statement::Init) {
5671
v8::Local<v8::FunctionTemplate> t = NewConstructorTemplate(isolate, data, JS_new, "Statement");
5772
SetPrototypeMethod(isolate, data, t, "run", JS_run);
@@ -169,7 +184,7 @@ NODE_METHOD(Statement::JS_get) {
169184
STATEMENT_START(REQUIRE_STATEMENT_RETURNS_DATA, DOES_NOT_MUTATE);
170185
int status = sqlite3_step(handle);
171186
if (status == SQLITE_ROW) {
172-
v8::Local<v8::Value> result = Data::GetRowJS(isolate, OnlyContext, handle, stmt->safe_ints, stmt->mode);
187+
v8::Local<v8::Value> result = stmt->GetRowJS(isolate, OnlyContext, stmt->safe_ints, stmt->mode);
173188
sqlite3_reset(handle);
174189
STATEMENT_RETURN(result);
175190
} else if (status == SQLITE_DONE) {
@@ -206,7 +221,7 @@ NODE_METHOD(Statement::JS_all) {
206221
rows.reserve(8);
207222

208223
if (mode == Data::FLAT) {
209-
RowBuilder rowBuilder(isolate, handle, safe_ints);
224+
RowBuilder rowBuilder = stmt->NewRowBuilder(isolate, safe_ints);
210225
while (sqlite3_step(handle) == SQLITE_ROW) {
211226
rows.emplace_back(rowBuilder.GetRowJS());
212227
}
@@ -285,7 +300,7 @@ NODE_METHOD(Statement::JS_explainQueryPlan) {
285300
rows.reserve(8);
286301

287302
if (mode == Data::FLAT) {
288-
RowBuilder rowBuilder(isolate, handle, safe_ints);
303+
RowBuilder rowBuilder = stmt->NewRowBuilder(isolate, safe_ints);
289304
while (sqlite3_step(handle) == SQLITE_ROW) {
290305
rows.emplace_back(rowBuilder.GetRowJS());
291306
}

‎src/objects/statement.hpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ class Statement : public node::ObjectWrap { friend class StatementIterator;
1414
// Returns the Statement's bind map (creates it upon first execution).
1515
BindMap* GetBindMap(v8::Isolate* isolate);
1616

17+
#if defined(NODE_MODULE_VERSION) && NODE_MODULE_VERSION >= 127
18+
// Returns a builder of object rows that reuses the Statement's cached
19+
// column names.
20+
RowBuilder NewRowBuilder(v8::Isolate* isolate, bool safe_ints);
21+
#endif
22+
23+
// Converts the current row to JavaScript, reusing the cached column names
24+
// for object rows.
25+
v8::Local<v8::Value> GetRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, bool safe_ints, char mode);
26+
1727
static INIT(Init);
1828

1929
private:
@@ -23,6 +33,9 @@ class Statement : public node::ObjectWrap { friend class StatementIterator;
2333
explicit Extras(sqlite3_uint64 id);
2434
BindMap bind_map;
2535
const sqlite3_uint64 id;
36+
#if defined(NODE_MODULE_VERSION) && NODE_MODULE_VERSION >= 127
37+
ColumnKeys column_keys;
38+
#endif
2639
};
2740

2841
explicit Statement(

‎src/util/column-keys.cpp‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// The column names of a statement as internalized strings, cached across calls
2+
// so that building object rows doesn't re-internalize them for every row.
3+
// Names only change when SQLite re-prepares the statement (e.g. after a schema
4+
// change behind `SELECT *`), so the cache is refreshed when the re-prepare
5+
// count or the column count changes.
6+
class ColumnKeys {
7+
public:
8+
9+
explicit ColumnKeys() : reprepares(-1) {}
10+
11+
// Must be called after sqlite3_step(), which is where re-preparing happens.
12+
void Get(v8::Isolate* isolate, sqlite3_stmt* handle, v8::LocalVector<v8::Name>& out) {
13+
const int count = sqlite3_column_count(handle);
14+
const int current = sqlite3_stmt_status(handle, SQLITE_STMTSTATUS_REPREPARE, 0);
15+
if (current != reprepares || static_cast<int>(keys.size()) != count) {
16+
keys.clear();
17+
keys.reserve(count);
18+
for (int i = 0; i < count; ++i) {
19+
keys.emplace_back(isolate,
20+
InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1).As<v8::Name>()
21+
);
22+
}
23+
reprepares = current;
24+
}
25+
out.clear();
26+
out.reserve(count);
27+
for (const v8::Global<v8::Name>& key : keys) {
28+
out.emplace_back(key.Get(isolate));
29+
}
30+
}
31+
32+
private:
33+
std::vector<v8::Global<v8::Name>> keys;
34+
int reprepares;
35+
};

‎src/util/row-builder.cpp‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,20 @@ class RowBuilder {
44
explicit RowBuilder(
55
v8::Isolate* isolate,
66
sqlite3_stmt* handle,
7-
bool safe_ints
7+
bool safe_ints,
8+
ColumnKeys* column_keys
89
) :
910
isolate(isolate),
1011
handle(handle),
1112
column_count(-1),
1213
safe_ints(safe_ints),
14+
column_keys(column_keys),
1315
keys(isolate) {}
1416

1517
v8::Local<v8::Value> GetRowJS() {
1618
if (column_count < 0) {
17-
column_count = sqlite3_column_count(handle);
18-
keys.reserve(column_count);
19-
for (int i = 0; i < column_count; ++i) {
20-
keys.emplace_back(
21-
InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1)
22-
.As<v8::Name>()
23-
);
24-
}
19+
column_keys->Get(isolate, handle, keys);
20+
column_count = static_cast<int>(keys.size());
2521
}
2622

2723
v8::LocalVector<v8::Value> values(isolate);
@@ -45,5 +41,6 @@ class RowBuilder {
4541
sqlite3_stmt* handle;
4642
int column_count;
4743
const bool safe_ints;
44+
ColumnKeys* const column_keys;
4845
v8::LocalVector<v8::Name> keys;
4946
};

‎test/21.statement.get.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,18 @@ describe('Statement#get()', function () {
106106
this.db.prepare(SQL2).get({})
107107
).to.throw(RangeError);
108108
});
109+
it('should use the new column names after the schema changes', function () {
110+
const stmt = this.db.prepare('SELECT * FROM entries ORDER BY rowid');
111+
// Several calls, so later ones reuse the cached column names.
112+
const get = () => {
113+
const rows = Array.from({ length: 100 }, () => stmt.get());
114+
for (const row of rows) expect(row).to.deep.equal(rows[0]);
115+
return rows[0];
116+
};
117+
expect(get()).to.deep.equal({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null });
118+
this.db.prepare('ALTER TABLE entries RENAME COLUMN a TO z').run();
119+
expect(get()).to.deep.equal({ z: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null });
120+
this.db.prepare('ALTER TABLE entries ADD COLUMN f INTEGER').run();
121+
expect(get()).to.deep.equal({ z: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null, f: null });
122+
});
109123
});

‎test/22.statement.all.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,23 @@ describe('Statement#all()', function () {
126126
this.db.prepare(SQL2).all({})
127127
).to.throw(RangeError);
128128
});
129+
it('should use the new column names after the schema changes', function () {
130+
// Enough rows that the change also shows up past the first ones.
131+
this.db.prepare('INSERT INTO entries (a, b) SELECT a, b + 10 FROM entries').run();
132+
this.db.prepare('INSERT INTO entries (a, b) SELECT a, b + 20 FROM entries').run();
133+
this.db.prepare('INSERT INTO entries (a, b) SELECT a, b + 40 FROM entries').run();
134+
const stmt = this.db.prepare('SELECT * FROM entries ORDER BY rowid');
135+
const keys = () => {
136+
const rows = stmt.all();
137+
expect(rows).to.have.lengthOf(80);
138+
const result = Object.keys(rows[0]);
139+
for (const row of rows) expect(Object.keys(row)).to.deep.equal(result);
140+
return result;
141+
};
142+
expect(keys()).to.deep.equal(['a', 'b', 'c', 'd', 'e']);
143+
this.db.prepare('ALTER TABLE entries RENAME COLUMN a TO z').run();
144+
expect(keys()).to.deep.equal(['z', 'b', 'c', 'd', 'e']);
145+
this.db.prepare('ALTER TABLE entries ADD COLUMN f INTEGER').run();
146+
expect(keys()).to.deep.equal(['z', 'b', 'c', 'd', 'e', 'f']);
147+
});
129148
});

‎test/23.statement.iterate.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,23 @@ describe('Statement#iterate()', function () {
228228
this.db.prepare(SQL1).iterate('foo', 1, new (function(){})(), Buffer.alloc(4).fill(0xdd), null)
229229
).to.throw(TypeError);
230230
});
231+
it('should use the new column names after the schema changes', function () {
232+
// Enough rows that the change also shows up past the first ones.
233+
this.db.prepare('INSERT INTO entries (a, b) SELECT a, b + 10 FROM entries').run();
234+
this.db.prepare('INSERT INTO entries (a, b) SELECT a, b + 20 FROM entries').run();
235+
this.db.prepare('INSERT INTO entries (a, b) SELECT a, b + 40 FROM entries').run();
236+
const stmt = this.db.prepare('SELECT * FROM entries ORDER BY rowid');
237+
const keys = () => {
238+
const rows = [...stmt.iterate()];
239+
expect(rows).to.have.lengthOf(80);
240+
const result = Object.keys(rows[0]);
241+
for (const row of rows) expect(Object.keys(row)).to.deep.equal(result);
242+
return result;
243+
};
244+
expect(keys()).to.deep.equal(['a', 'b', 'c', 'd', 'e']);
245+
this.db.prepare('ALTER TABLE entries RENAME COLUMN a TO z').run();
246+
expect(keys()).to.deep.equal(['z', 'b', 'c', 'd', 'e']);
247+
this.db.prepare('ALTER TABLE entries ADD COLUMN f INTEGER').run();
248+
expect(keys()).to.deep.equal(['z', 'b', 'c', 'd', 'e', 'f']);
249+
});
231250
});

0 commit comments

Comments
 (0)