Skip to content

Commit 5683cab

Browse files
committed
fix: resolve flaky sqlite each snapshot tests
`each(rowCallback?, complete?)` was called via `promisify` without an explicit row callback, so promisify's callback landed in the row slot instead of the completion slot. The sql_query return event was then recorded in a later event loop tick, racing with subsequent events and producing non-deterministic event IDs in the snapshot. Fix by passing `undefined` as a row callback placeholder so promisify's callback correctly binds to `complete`. Update the sqlite hook to recognise null/undefined in the row callback position accordingly.
1 parent 511789a commit 5683cab

3 files changed

Lines changed: 43 additions & 23 deletions

File tree

src/hooks/sqlite.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,15 @@ function createRecordingProxy<T extends RecordingProxyTarget>(
6060
const lastArgIsaFunction = () =>
6161
argArray.length > 0 && typeof argArray[argArray.length - 1] === "function";
6262
if (lastArgIsaFunction()) functionArgs.unshift(argArray.pop());
63-
if (needsRowCallback && lastArgIsaFunction()) functionArgs.unshift(argArray.pop());
63+
// Also accept undefined/null as an explicit "no row callback" placeholder.
64+
const lastArgIsRowCallback = () =>
65+
argArray.length > 0 &&
66+
(typeof argArray[argArray.length - 1] === "function" ||
67+
argArray[argArray.length - 1] == null);
68+
if (needsRowCallback && lastArgIsRowCallback()) functionArgs.unshift(argArray.pop());
6469

6570
// if needsRowCallback:
66-
// functionArgs is [] or [rowCallback] or [rowCallback, completionCallback]
71+
// functionArgs is [] or [rowCallback] or [null, completionCallback] or [rowCallback, completionCallback]
6772
// otherwise:
6873
// functionArgs is [] or [completionCallback]
6974

test/__snapshots__/sqlite.test.ts.snap

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ exports[`mapping Sqlite tests 1`] = `
215215
{
216216
"class": "Function",
217217
"name": "method",
218-
"value": "[Function (anonymous)]",
218+
"value": "[Function: exec]",
219219
},
220220
{
221221
"class": "Database",
@@ -225,7 +225,7 @@ exports[`mapping Sqlite tests 1`] = `
225225
},
226226
{
227227
"class": "String",
228-
"value": ""SELECT 'Database.each'"",
228+
"value": ""SELECT 'Database.exec'"",
229229
},
230230
],
231231
"path": "index.js",
@@ -237,7 +237,7 @@ exports[`mapping Sqlite tests 1`] = `
237237
"id": 16,
238238
"sql_query": {
239239
"database_type": "sqlite",
240-
"sql": "SELECT 'Database.each'",
240+
"sql": "SELECT 'Database.exec'",
241241
},
242242
"thread_id": 0,
243243
},
@@ -247,12 +247,19 @@ exports[`mapping Sqlite tests 1`] = `
247247
"id": 17,
248248
"parent_id": 15,
249249
"return_value": {
250-
"class": "Promise<Object>",
250+
"class": "Promise<undefined>",
251251
"object_id": 6,
252-
"value": "Promise { { "'Database.each'": 'Database.each' } }",
252+
"value": "Promise { undefined }",
253253
},
254254
"thread_id": 0,
255255
},
256+
{
257+
"elapsed": 31.337,
258+
"event": "return",
259+
"id": 18,
260+
"parent_id": 16,
261+
"thread_id": 0,
262+
},
256263
{
257264
"defined_class": "index",
258265
"event": "call",
@@ -263,7 +270,7 @@ exports[`mapping Sqlite tests 1`] = `
263270
{
264271
"class": "Function",
265272
"name": "method",
266-
"value": "[Function: exec]",
273+
"value": "[Function (anonymous)]",
267274
},
268275
{
269276
"class": "Database",
@@ -273,7 +280,11 @@ exports[`mapping Sqlite tests 1`] = `
273280
},
274281
{
275282
"class": "String",
276-
"value": ""SELECT 'Database.exec'"",
283+
"value": ""SELECT 'Database.each'"",
284+
},
285+
{
286+
"class": "undefined",
287+
"value": "undefined",
277288
},
278289
],
279290
"path": "index.js",
@@ -285,7 +296,7 @@ exports[`mapping Sqlite tests 1`] = `
285296
"id": 20,
286297
"sql_query": {
287298
"database_type": "sqlite",
288-
"sql": "SELECT 'Database.exec'",
299+
"sql": "SELECT 'Database.each'",
289300
},
290301
"thread_id": 0,
291302
},
@@ -295,19 +306,12 @@ exports[`mapping Sqlite tests 1`] = `
295306
"id": 21,
296307
"parent_id": 19,
297308
"return_value": {
298-
"class": "Promise<undefined>",
309+
"class": "Promise<Object>",
299310
"object_id": 7,
300-
"value": "Promise { undefined }",
311+
"value": "Promise { { "'Database.each'": 'Database.each' } }",
301312
},
302313
"thread_id": 0,
303314
},
304-
{
305-
"elapsed": 31.337,
306-
"event": "return",
307-
"id": 22,
308-
"parent_id": 20,
309-
"thread_id": 0,
310-
},
311315
{
312316
"defined_class": "index",
313317
"event": "call",
@@ -664,6 +668,10 @@ exports[`mapping Sqlite tests 1`] = `
664668
"object_id": 18,
665669
"value": "Statement {}",
666670
},
671+
{
672+
"class": "undefined",
673+
"value": "undefined",
674+
},
667675
],
668676
"path": "index.js",
669677
"static": true,
@@ -839,8 +847,8 @@ exports[`mapping Sqlite tests 1`] = `
839847
{
840848
"elapsed": 31.337,
841849
"event": "return",
842-
"id": 18,
843-
"parent_id": 16,
850+
"id": 22,
851+
"parent_id": 20,
844852
"thread_id": 0,
845853
},
846854
{

test/sqlite/index.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ async function main() {
1616
[sqlite.Database.prototype.run, "run"],
1717
[sqlite.Database.prototype.all, "all"],
1818
[sqlite.Database.prototype.get, "get"],
19-
[sqlite.Database.prototype.each, "each"],
2019
[sqlite.Database.prototype.exec, "exec"],
2120
]) {
2221
const [method, name] = item;
2322
await promisify(method, db, `SELECT 'Database.${name}'`);
2423
}
2524

25+
// each() takes an optional row callback followed by a completion callback.
26+
// Pass undefined for the row callback so promisify's callback lands in the
27+
// completion slot, ensuring functionReturn is recorded before the promise resolves.
28+
await promisify(sqlite.Database.prototype.each, db, `SELECT 'Database.each'`, undefined);
29+
2630
await promisify(
2731
sqlite.Database.prototype.each,
2832
db,
@@ -50,14 +54,17 @@ async function main() {
5054
[sqlite.Statement.prototype.run, "run"],
5155
[sqlite.Statement.prototype.all, "all"],
5256
[sqlite.Statement.prototype.get, "get"],
53-
[sqlite.Statement.prototype.each, "each"],
5457
]) {
5558
const [method, name] = item;
5659
const st = db.prepare(`SELECT 'Statement.${name}'`);
5760
await promisify(method, st);
5861
st.finalize();
5962
}
6063

64+
const stEach = db.prepare(`SELECT 'Statement.each'`);
65+
await promisify(sqlite.Statement.prototype.each, stEach, undefined);
66+
stEach.finalize();
67+
6168
// Same statement twice should produce two sql events in appmap.
6269
const st = db.prepare(`SELECT 'Statement.run - prepare once run twice'`);
6370
await promisify(sqlite.Statement.prototype.run, st);

0 commit comments

Comments
 (0)