Skip to content

Commit 0a61be6

Browse files
AntiMoronAntiMoron
andauthored
feat: add unlock/lock tables (ali-sdk#97)
Co-authored-by: AntiMoron <boyuan.gao@anker.com>
1 parent 9799993 commit 0a61be6

File tree

3 files changed

+163
-1
lines changed

3 files changed

+163
-1
lines changed

lib/client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class RDSClient extends Operator {
104104

105105
/**
106106
* doomed to be rollbacked after transaction scope
107-
* useful on writing test that depend on database
107+
* useful on writing tests which are related with database
108108
*
109109
* @param {Function} scope - scope with code
110110
* @param {Object} [ctx] - transaction env context, like koa's ctx.

lib/operator.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,98 @@ class Operator {
367367
}
368368
return ' LIMIT ' + offset + ', ' + limit;
369369
}
370+
371+
/**
372+
* Lock tables.
373+
* @param {object[]} tables table lock descriptions.
374+
* @description
375+
* LOCK TABLES
376+
* tbl_name [[AS] alias] lock_type
377+
* [, tbl_name [[AS] alias] lock_type] ...
378+
* lock_type: {
379+
* READ [LOCAL]
380+
* | [LOW_PRIORITY] WRITE
381+
* }
382+
* For more details:
383+
* https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html
384+
* @example
385+
* await locks([{ tableName: 'posts', lockType: 'READ', tableAlias: 't' }]);
386+
*/
387+
async locks(tables) {
388+
const sql = this.#locks(tables);
389+
debug('lock tables \n=> %j', sql);
390+
return await this.query(sql);
391+
}
392+
393+
/**
394+
* Lock a single table.
395+
* @param {string} tableName
396+
* @param {string} lockType
397+
* @param {string} tableAlias
398+
* @description
399+
* LOCK TABLES
400+
* tbl_name [[AS] alias] lock_type
401+
* [, tbl_name [[AS] alias] lock_type] ...
402+
* lock_type: {
403+
* READ [LOCAL]
404+
* | [LOW_PRIORITY] WRITE
405+
* }
406+
* For more details:
407+
* https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html
408+
* @example
409+
* await lockOne('posts_table', 'READ', 't'); // LOCK TABLS 'posts_table' AS t READ
410+
*/
411+
async lockOne(tableName, lockType, tableAlias) {
412+
const sql = this.#locks([{
413+
tableName,
414+
lockType,
415+
tableAlias,
416+
}]);
417+
debug('lock one table \n=> %j', sql);
418+
return await this.query(sql);
419+
}
420+
421+
#locks(tableLocks) {
422+
if (tableLocks.length === 0) {
423+
throw new Error('Cannot lock empty tables.');
424+
}
425+
let sql = 'LOCK TABLES ';
426+
for (let i = 0; i < tableLocks.length; i++) {
427+
const table = tableLocks[i];
428+
const { tableName, lockType, tableAlias } = table;
429+
if (!tableName) {
430+
throw new Error('No table_name provided while trying to lock table');
431+
}
432+
if (!lockType) {
433+
throw new Error('No lock_type provided while trying to lock table `' + tableName + '`');
434+
}
435+
if ([ 'READ', 'WRITE', 'READ LOCAL', 'LOW_PRIORITY WRITE' ].indexOf(lockType.toUpperCase()) < 0) {
436+
throw new Error('lock_type provided while trying to lock table `' + tableName +
437+
'` must be one of the following(CASE INSENSITIVE):\n`READ` | `WRITE` | `READ LOCAL` | `LOW_PRIORITY WRITE`');
438+
}
439+
if (i > 0) {
440+
sql += ', ';
441+
}
442+
sql += ' ' + this.escapeId(tableName) + ' ';
443+
if (tableAlias) {
444+
sql += ' AS ' + this.escapeId(tableAlias) + ' ';
445+
}
446+
sql += ' ' + lockType;
447+
}
448+
return sql + ';';
449+
}
450+
451+
/**
452+
* To unlock all tables locked in current session.
453+
* For more details:
454+
* https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html
455+
* @example
456+
* await unlock(); // unlock all tables.
457+
*/
458+
async unlock() {
459+
debug('unlock tables');
460+
return await this.query('UNLOCK TABLES;');
461+
}
370462
}
371463

372464
module.exports = Operator;

test/client.test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,58 @@ describe('test/client.test.js', () => {
101101
});
102102
});
103103

104+
describe('locks([...]), lockOne(name, lockType, alias), unlock()', () => {
105+
it('validate arguments', async () => {
106+
await assert.rejects(async () => {
107+
await db.locks([
108+
{ tableName: 'xxxx' },
109+
]);
110+
}, new Error('No lock_type provided while trying to lock table `xxxx`'));
111+
112+
await assert.rejects(async () => {
113+
await db.locks([
114+
{ lockType: 'READ' },
115+
]);
116+
}, new Error('No table_name provided while trying to lock table'));
117+
});
118+
119+
it('should lock a table', async () => {
120+
// assert.equal(sql.replace(/\s+/g, ' '), 'LOCK TABLES `posts` READ;');
121+
await assert.rejects(async () => {
122+
await db.locks([
123+
{ tableName: table, lockType: 'READ' },
124+
{ tableName: table, lockType: 'READ' },
125+
]);
126+
}, err => err.sql.includes('LOCK TABLES `' + table + '` READ, `' + table + '` READ;'));
127+
});
128+
129+
it('should lock multiple tables', async () => {
130+
// assert.equal(sql.replaceAll(/\s+/g, ' '), 'LOCK TABLES `posts` READ, `posts2` WRITE, `posts3` AS `t` WRITE;');
131+
await assert.rejects(async () => {
132+
await db.locks([
133+
{ tableName: table, lockType: 'READ' },
134+
{ tableName: table, lockType: 'WRITE' },
135+
{ tableName: table, lockType: 'WRITE', tableAlias: 't' },
136+
]);
137+
}, err => err.sql.includes('LOCK TABLES `' + table + '` READ, `' + table + '` WRITE, `' + table + '` AS `t` WRITE;'));
138+
await assert.rejects(async () => {
139+
await db.locks([
140+
{ tableName: 'xxxx' },
141+
]);
142+
}, new Error('No lock_type provided while trying to lock table `xxxx`'));
143+
});
144+
it('should unlock tables', async () => {
145+
await db.lockOne('ali-sdk-test-user', 'READ', 't');
146+
// error thrown: when table locked with alias, you can only query with the alias.
147+
await assert.rejects(async () => {
148+
await db.query('select * from `ali-sdk-test-user` limit 1;');
149+
});
150+
await db.unlock();
151+
// recovered after unlock.
152+
await db.query('select * from `ali-sdk-test-user` limit 1;');
153+
});
154+
});
155+
104156
describe('transactions', () => {
105157
it('should beginTransaction error', async () => {
106158
const failDB = new RDSClient({
@@ -217,6 +269,24 @@ describe('test/client.test.js', () => {
217269
assert.equal(rows[1].name, prefix + 'beginTransaction2');
218270
});
219271

272+
it('should lock & unlock table during transaction', async () => {
273+
const conn = await db.getConnection();
274+
try {
275+
await conn.beginTransaction();
276+
await conn.lockOne('ali-sdk-test-user', 'READ', 't');
277+
// error thrown: when table locked with alias, you can only query with the alias.
278+
await assert.rejects(async () => {
279+
await conn.query('select * from `ali-sdk-test-user` limit 1;');
280+
});
281+
await conn.unlock();
282+
// recovered after unlock.
283+
await conn.query('select * from `ali-sdk-test-user` limit 1;');
284+
} catch (err) {
285+
conn.release();
286+
throw err;
287+
}
288+
});
289+
220290
it('should rollback when query fail', async () => {
221291
const conn = await db.getConnection();
222292
try {

0 commit comments

Comments
 (0)