Skip to content

Commit 5aebc31

Browse files
committed
feat: add support for global transactions
1 parent 6780b44 commit 5aebc31

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed

adonis-typings/database.ts

+19
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,25 @@ declare module '@ioc:Adonis/Lucid/Database' {
688688
sql: string,
689689
bindings?: { [key: string]: StrictValuesWithoutRaw } | StrictValuesWithoutRaw[]
690690
): RawBuilderContract
691+
692+
/**
693+
* Begin a new global transaction. Multiple calls to this
694+
* method is a noop
695+
*/
696+
beginGlobalTransaction (
697+
connectionName?: string,
698+
options?: Exclude<DatabaseClientOptions, 'mode'>,
699+
): Promise<TransactionClientContract>
700+
701+
/**
702+
* Commit an existing global transaction
703+
*/
704+
commitGlobalTransaction (connectionName?: string): Promise<void>
705+
706+
/**
707+
* Rollback an existing global transaction
708+
*/
709+
rollbackGlobalTransaction (connectionName?: string): Promise<void>
691710
}
692711

693712
const Database: DatabaseContract

src/Database/index.ts

+84
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
DatabaseContract,
1818
DatabaseClientOptions,
1919
DatabaseConfigContract,
20+
TransactionClientContract,
2021
ConnectionManagerContract,
2122
} from '@ioc:Adonis/Lucid/Database'
2223

@@ -51,6 +52,11 @@ export class Database implements DatabaseContract {
5152
public InsertQueryBuilder = InsertQueryBuilder
5253
public ModelQueryBuilder = ModelQueryBuilder
5354

55+
/**
56+
* A store of global transactions
57+
*/
58+
public connectionGlobalTransactions: Map<string, TransactionClientContract> = new Map()
59+
5460
constructor (
5561
private config: DatabaseConfigContract,
5662
private logger: LoggerContract,
@@ -106,6 +112,16 @@ export class Database implements DatabaseContract {
106112
throw new Exception(`Invalid mode ${options.mode}. Must be read or write`)
107113
}
108114

115+
/**
116+
* Return the global transaction when it already exists.
117+
*/
118+
if (this.connectionGlobalTransactions.has(connection)) {
119+
this.logger.trace({ connection }, 'using pre-existing global transaction connection')
120+
const globalTransactionClient = this.connectionGlobalTransactions.get(connection)!
121+
globalTransactionClient.profiler = options.profiler
122+
return globalTransactionClient
123+
}
124+
109125
/**
110126
* Fetching connection for the given name
111127
*/
@@ -203,4 +219,72 @@ export class Database implements DatabaseContract {
203219
public raw (sql: string, bindings?: any) {
204220
return new RawBuilder(sql, bindings)
205221
}
222+
223+
/**
224+
* Begin a new global transaction
225+
*/
226+
public async beginGlobalTransaction (
227+
connectionName?: string,
228+
options?: Omit<DatabaseClientOptions, 'mode'>,
229+
) {
230+
connectionName = connectionName || this.primaryConnectionName
231+
232+
/**
233+
* Return global transaction as it is
234+
*/
235+
const globalTrx = this.connectionGlobalTransactions.get(connectionName)
236+
if (globalTrx) {
237+
return globalTrx
238+
}
239+
240+
/**
241+
* Create a new transaction and store a reference to it
242+
*/
243+
const trx = await this.connection(connectionName, options).transaction()
244+
this.connectionGlobalTransactions.set(trx.connectionName, trx)
245+
246+
/**
247+
* Listen for events to drop the reference when transaction
248+
* is over
249+
*/
250+
trx.on('commit', ($trx) => {
251+
this.connectionGlobalTransactions.delete($trx.connectionName)
252+
})
253+
254+
trx.on('rollback', ($trx) => {
255+
this.connectionGlobalTransactions.delete($trx.connectionName)
256+
})
257+
258+
return trx
259+
}
260+
261+
/**
262+
* Commit an existing global transaction
263+
*/
264+
public async commitGlobalTransaction (connectionName?: string) {
265+
connectionName = connectionName || this.primaryConnectionName
266+
const trx = this.connectionGlobalTransactions.get(connectionName)
267+
268+
if (!trx) {
269+
// eslint-disable-next-line max-len
270+
throw new Exception('Cannot commit a non-existing global transaction. Make sure you are not calling "commitGlobalTransaction" twice')
271+
}
272+
273+
await trx.commit()
274+
}
275+
276+
/**
277+
* Rollback an existing global transaction
278+
*/
279+
public async rollbackGlobalTransaction (connectionName?: string) {
280+
connectionName = connectionName || this.primaryConnectionName
281+
const trx = this.connectionGlobalTransactions.get(connectionName)
282+
283+
if (!trx) {
284+
// eslint-disable-next-line max-len
285+
throw new Exception('Cannot rollback a non-existing global transaction. Make sure you are not calling "commitGlobalTransaction" twice')
286+
}
287+
288+
await trx.rollback()
289+
}
206290
}

test/database/database.spec.ts

+73
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,76 @@ test.group('Database | extend', (group) => {
280280
await db.manager.closeAll()
281281
})
282282
})
283+
284+
test.group('Database | global transaction', (group) => {
285+
group.before(async () => {
286+
await setup()
287+
})
288+
289+
group.after(async () => {
290+
await cleanup()
291+
})
292+
293+
test('perform queries inside a global transaction', async (assert) => {
294+
const config = {
295+
connection: 'primary',
296+
connections: { primary: getConfig() },
297+
}
298+
299+
const db = new Database(config, getLogger(), getProfiler())
300+
await db.beginGlobalTransaction()
301+
302+
await db.table('users').insert({ username: 'virk' })
303+
await db.rollbackGlobalTransaction()
304+
305+
const users = await db.from('users')
306+
assert.lengthOf(users, 0)
307+
assert.equal(db.connectionGlobalTransactions.size, 0)
308+
309+
await db.manager.closeAll()
310+
})
311+
312+
test('create transactions inside a global transaction', async (assert) => {
313+
const config = {
314+
connection: 'primary',
315+
connections: { primary: getConfig() },
316+
}
317+
318+
const db = new Database(config, getLogger(), getProfiler())
319+
await db.beginGlobalTransaction()
320+
const trx = await db.transaction()
321+
322+
await trx.table('users').insert({ username: 'virk' })
323+
await trx.commit()
324+
325+
await db.rollbackGlobalTransaction()
326+
327+
const users = await db.from('users')
328+
assert.lengthOf(users, 0)
329+
assert.equal(db.connectionGlobalTransactions.size, 0)
330+
331+
await db.manager.closeAll()
332+
})
333+
334+
test('multiple calls to beginGlobalTransaction must be a noop', async (assert) => {
335+
const config = {
336+
connection: 'primary',
337+
connections: { primary: getConfig() },
338+
}
339+
340+
const db = new Database(config, getLogger(), getProfiler())
341+
await db.beginGlobalTransaction()
342+
await db.beginGlobalTransaction()
343+
await db.beginGlobalTransaction()
344+
345+
await db.table('users').insert({ username: 'virk' })
346+
347+
await db.rollbackGlobalTransaction()
348+
349+
const users = await db.from('users')
350+
assert.lengthOf(users, 0)
351+
assert.equal(db.connectionGlobalTransactions.size, 0)
352+
353+
await db.manager.closeAll()
354+
})
355+
})

0 commit comments

Comments
 (0)