Skip to content

Commit 03d6b67

Browse files
author
Oscar Franco
authored
Merge pull request #112 from pbbadenhorst/fix/serialize-multiple-transactions
2 parents 905667c + e8abfb3 commit 03d6b67

2 files changed

Lines changed: 184 additions & 90 deletions

File tree

example/src/tests/rawQueries.spec.ts

Lines changed: 115 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -288,13 +288,13 @@ export function registerBaseTests() {
288288
});
289289
});
290290

291-
it('Async transaction, auto commit', done => {
291+
it('Async transaction, auto commit', async () => {
292292
const id = chance.integer();
293293
const name = chance.name();
294294
const age = chance.integer();
295295
const networth = chance.floating();
296296

297-
db.transactionAsync(async tx => {
297+
await db.transactionAsync(async tx => {
298298
const res = await tx.executeAsync(
299299
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
300300
[id, name, age, networth],
@@ -308,87 +308,156 @@ export function registerBaseTests() {
308308
expect(res.rows.item).to.be.a('function');
309309
});
310310

311-
setTimeout(() => {
312-
const res = db.execute('SELECT * FROM User');
313-
expect(res.rows?._array).to.eql([
314-
{
315-
id,
316-
name,
317-
age,
318-
networth,
319-
},
320-
]);
321-
done();
322-
}, 200);
311+
const res = db.execute('SELECT * FROM User');
312+
expect(res.rows?._array).to.eql([
313+
{
314+
id,
315+
name,
316+
age,
317+
networth,
318+
},
319+
]);
323320
});
324321

325-
it('Async transaction, auto rollback', done => {
326-
const id = chance.string();
322+
it('Async transaction, auto rollback', async () => {
323+
const id = chance.string(); // Causes error because it should be an integer
327324
const name = chance.name();
328325
const age = chance.integer();
329326
const networth = chance.floating();
330327

331-
db.transactionAsync(async tx => {
332-
await tx.executeAsync(
333-
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
334-
[id, name, age, networth],
335-
);
336-
});
328+
try {
329+
await db.transactionAsync(async tx => {
330+
await tx.executeAsync(
331+
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
332+
[id, name, age, networth],
333+
);
334+
});
335+
} catch (error) {
336+
expect(error).to.be.instanceOf(Error)
337+
expect((error as Error).message)
338+
.to.include('SQL execution error')
339+
.and
340+
.to.include('cannot store TEXT value in INT column User.id');
337341

338-
setTimeout(() => {
339342
const res = db.execute('SELECT * FROM User');
340343
expect(res.rows?._array).to.eql([]);
341-
done();
342-
}, 200);
344+
}
343345
});
344346

345-
it('Async transaction, manual commit', done => {
347+
it('Async transaction, manual commit', async () => {
346348
const id = chance.integer();
347349
const name = chance.name();
348350
const age = chance.integer();
349351
const networth = chance.floating();
350352

351-
db.transactionAsync(async tx => {
353+
await db.transactionAsync(async tx => {
352354
await tx.executeAsync(
353355
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
354356
[id, name, age, networth],
355357
);
356358
tx.commit();
357359
});
358360

359-
setTimeout(() => {
360-
const res = db.execute('SELECT * FROM User');
361-
expect(res.rows?._array).to.eql([
362-
{
363-
id,
364-
name,
365-
age,
366-
networth,
367-
},
368-
]);
369-
done();
370-
}, 1000);
361+
const res = db.execute('SELECT * FROM User');
362+
expect(res.rows?._array).to.eql([
363+
{
364+
id,
365+
name,
366+
age,
367+
networth,
368+
},
369+
]);
371370
});
372371

373-
it('Async transaction, manual rollback', done => {
372+
it('Async transaction, manual rollback', async () => {
374373
const id = chance.integer();
375374
const name = chance.name();
376375
const age = chance.integer();
377376
const networth = chance.floating();
378377

379-
db.transactionAsync(async tx => {
378+
await db.transactionAsync(async tx => {
380379
await tx.executeAsync(
381380
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
382381
[id, name, age, networth],
383382
);
384383
tx.rollback();
385384
});
386385

387-
setTimeout(() => {
388-
const res = db.execute('SELECT * FROM User');
389-
expect(res.rows?._array).to.eql([]);
390-
done();
391-
}, 1000);
386+
const res = db.execute('SELECT * FROM User');
387+
expect(res.rows?._array).to.eql([]);
388+
});
389+
390+
it('Async transaction, upsert and select', async () => {
391+
// ARRANGE: Setup for multiple transactions
392+
const iterations = 10;
393+
const actual = new Set();
394+
395+
// ARRANGE: Generate expected data
396+
const id = chance.integer();
397+
const name = chance.name();
398+
const age = chance.integer();
399+
400+
// ACT: Start multiple async transactions to upsert and select the same record
401+
const promises = [];
402+
for (let iteration = 1; iteration <= iterations; iteration++) {
403+
const promised = db.transactionAsync(async (tx) => {
404+
// ACT: Upsert statement to create record / increment the value
405+
await tx.executeAsync(`
406+
INSERT OR REPLACE INTO [User] ([id], [name], [age], [networth])
407+
SELECT ?, ?, ?,
408+
IFNULL((
409+
SELECT [networth] + 1000
410+
FROM [User]
411+
WHERE [id] = ?
412+
), 1000)
413+
`, [id, name, age, id]);
414+
415+
// ACT: Select statement to get incremented value and store it for checking later
416+
const results = await tx.executeAsync('SELECT [networth] FROM [User] WHERE [id] = ?', [id]);
417+
418+
actual.add(results.rows._array[0].networth);
419+
})
420+
421+
promises.push(promised);
422+
}
423+
424+
// ACT: Wait for all transactions to complete
425+
await Promise.all(promises);
426+
427+
// ASSERT: That the expected values where returned
428+
expect(actual.size).to.equal(iterations, 'Each transaction should read a different value');
429+
});
430+
431+
it('Async transaction, rejects on callback error', async () => {
432+
const promised = db.transactionAsync(async (tx) => {
433+
throw new Error('Error from callback');
434+
});
435+
436+
// ASSERT: should return a promise that eventually rejects
437+
expect(promised).to.have.property('then').that.is.a('function');
438+
try {
439+
await promised;
440+
expect.fail('Should not resolve');
441+
} catch (e) {
442+
expect(e).to.be.a.instanceof(Error);
443+
expect((e as Error)?.message).to.equal('Error from callback');
444+
}
445+
});
446+
447+
it('Async transaction, rejects on invalid query', async () => {
448+
const promised = db.transactionAsync(async (tx) => {
449+
await tx.executeAsync('SELECT * FROM [tableThatDoesNotExist];');
450+
})
451+
452+
// ASSERT: should return a promise that eventually rejects
453+
expect(promised).to.have.property('then').that.is.a('function');
454+
try {
455+
await promised;
456+
expect.fail('Should not resolve');
457+
} catch (e) {
458+
expect(e).to.be.a.instanceof(Error);
459+
expect((e as Error)?.message).to.include('no such table: tableThatDoesNotExist');
460+
}
392461
});
393462

394463
it('Batch execute', () => {

src/index.ts

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ interface ISQLite {
140140
transactionAsync: (
141141
dbName: string,
142142
fn: (tx: TransactionAsync) => Promise<any>
143-
) => void;
143+
) => Promise<void>;
144144
transaction: (dbName: string, fn: (tx: Transaction) => void) => void;
145145
execute: (dbName: string, query: string, params?: any[]) => QueryResult;
146146
executeAsync: (
@@ -240,12 +240,22 @@ QuickSQLite.transaction = (
240240
};
241241

242242
const commit = () => {
243+
if (isFinalized) {
244+
throw Error(
245+
`Quick SQLite Error: Cannot execute commit on finalized transaction: ${dbName}`
246+
);
247+
}
243248
const result = QuickSQLite.execute(dbName, 'COMMIT');
244249
isFinalized = true;
245250
return result;
246251
};
247252

248253
const rollback = () => {
254+
if (isFinalized) {
255+
throw Error(
256+
`Quick SQLite Error: Cannot execute rollback on finalized transaction: ${dbName}`
257+
);
258+
}
249259
const result = QuickSQLite.execute(dbName, 'ROLLBACK');
250260
isFinalized = true;
251261
return result;
@@ -276,7 +286,7 @@ QuickSQLite.transaction = (
276286
startNextTransaction(dbName);
277287
};
278288

279-
QuickSQLite.transactionAsync = (
289+
QuickSQLite.transactionAsync = async (
280290
dbName: string,
281291
callback: (tx: TransactionAsync) => Promise<any>
282292
) => {
@@ -306,69 +316,84 @@ QuickSQLite.transactionAsync = (
306316
};
307317

308318
const commit = () => {
319+
if (isFinalized) {
320+
throw Error(
321+
`Quick SQLite Error: Cannot execute commit on finalized transaction: ${dbName}`
322+
);
323+
}
309324
const result = QuickSQLite.execute(dbName, 'COMMIT');
310325
isFinalized = true;
311326
return result;
312327
};
313328

314329
const rollback = () => {
330+
if (isFinalized) {
331+
throw Error(
332+
`Quick SQLite Error: Cannot execute rollback on finalized transaction: ${dbName}`
333+
);
334+
}
315335
const result = QuickSQLite.execute(dbName, 'ROLLBACK');
316336
isFinalized = true;
317337
return result;
318338
};
319339

320-
const tx: PendingTransaction = {
321-
start: async () => {
322-
try {
323-
QuickSQLite.execute(dbName, 'BEGIN TRANSACTION');
324-
await callback({
325-
commit,
326-
execute,
327-
executeAsync,
328-
rollback,
329-
});
330-
331-
if (!isFinalized) {
332-
commit();
333-
}
334-
} catch (e: any) {
335-
if (!isFinalized) {
336-
rollback();
337-
}
340+
return await new Promise((resolve, reject) => {
341+
const tx: PendingTransaction = {
342+
start: async () => {
343+
try {
344+
QuickSQLite.execute(dbName, 'BEGIN TRANSACTION');
345+
await callback({
346+
commit,
347+
execute,
348+
executeAsync,
349+
rollback,
350+
});
351+
352+
if (!isFinalized) {
353+
commit();
354+
}
338355

339-
// Do not throw an error, because the transaction is executed with a setImmediate call
340-
// This errors are uncatchable
341-
// https://stackoverflow.com/questions/51081892/nodejs-asynchronous-exceptions-are-uncatchable
356+
resolve();
357+
} catch (e) {
358+
if (!isFinalized) {
359+
try {
360+
rollback();
361+
} catch (rollbackError) {
362+
reject(rollbackError);
363+
}
364+
}
342365

343-
// throw e;
344-
} finally {
345-
locks[dbName].inProgress = false;
346-
isFinalized = false;
347-
startNextTransaction(dbName);
348-
}
349-
},
350-
};
366+
reject(e);
367+
} finally {
368+
locks[dbName].inProgress = false;
369+
isFinalized = false;
370+
startNextTransaction(dbName);
371+
}
372+
},
373+
};
351374

352-
locks[dbName].queue.push(tx);
353-
startNextTransaction(dbName);
375+
locks[dbName].queue.push(tx);
376+
startNextTransaction(dbName);
377+
});
354378
};
355379

356380
const startNextTransaction = (dbName: string) => {
381+
if (!locks[dbName]) {
382+
throw Error(`Lock not found for db: ${dbName}`);
383+
}
384+
357385
if (locks[dbName].inProgress) {
358386
// Transaction is already in process bail out
359387
return;
360388
}
361389

362-
setImmediate(() => {
363-
if (!locks[dbName]) {
364-
throw Error(`Lock not found for db: ${dbName}`);
365-
}
366-
367-
if (locks[dbName].queue.length) {
368-
locks[dbName].inProgress = true;
369-
locks[dbName].queue.shift().start();
370-
}
371-
});
390+
if (locks[dbName].queue.length) {
391+
locks[dbName].inProgress = true;
392+
const tx = locks[dbName].queue.shift();
393+
setImmediate(() => {
394+
tx.start();
395+
});
396+
}
372397
};
373398

374399
// _________ _______ ______ ____ _____ __ __ _____ _____
@@ -413,7 +438,7 @@ export const typeORMDriver = {
413438
fail(e);
414439
}
415440
},
416-
transaction: (fn: (tx: Transaction) => Promise<void>): void => {
441+
transaction: (fn: (tx: Transaction) => Promise<void>): Promise<void> => {
417442
return QuickSQLite.transactionAsync(options.name, fn);
418443
},
419444
close: (ok: any, fail: any) => {

0 commit comments

Comments
 (0)