From 9407507a742a3fe0ea2a836417d6851cad72e74c Mon Sep 17 00:00:00 2001 From: lacunadream <6987359+lacunadream@users.noreply.github.com> Date: Mon, 11 Jan 2021 18:56:12 +0800 Subject: [PATCH] feat: add NOWAIT and SKIP LOCKED lock support for MySQL (#7236) * feat: add mysql support for locks Add pessimistic_write_or_fail and pessimistic_partial_write support for mysql Closes: #6530 * test: add tests * fix: remove .only flags on tests * test: add db version check for tests --- src/query-builder/SelectQueryBuilder.ts | 4 +- .../locking/query-builder-locking.ts | 151 ++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) diff --git a/src/query-builder/SelectQueryBuilder.ts b/src/query-builder/SelectQueryBuilder.ts index 44d45041d0..8fcc1f652b 100644 --- a/src/query-builder/SelectQueryBuilder.ts +++ b/src/query-builder/SelectQueryBuilder.ts @@ -1686,14 +1686,14 @@ export class SelectQueryBuilder extends QueryBuilder implements throw new LockNotSupportedOnGivenDriverError(); } case "pessimistic_partial_write": - if (driver instanceof PostgresDriver) { + if (driver instanceof PostgresDriver || driver instanceof MysqlDriver) { return " FOR UPDATE SKIP LOCKED"; } else { throw new LockNotSupportedOnGivenDriverError(); } case "pessimistic_write_or_fail": - if (driver instanceof PostgresDriver) { + if (driver instanceof PostgresDriver || driver instanceof MysqlDriver) { return " FOR UPDATE NOWAIT"; } else { throw new LockNotSupportedOnGivenDriverError(); diff --git a/test/functional/query-builder/locking/query-builder-locking.ts b/test/functional/query-builder/locking/query-builder-locking.ts index adcf742087..bfa5d1583b 100644 --- a/test/functional/query-builder/locking/query-builder-locking.ts +++ b/test/functional/query-builder/locking/query-builder-locking.ts @@ -18,6 +18,7 @@ import {SqlServerDriver} from "../../../../src/driver/sqlserver/SqlServerDriver" import {AbstractSqliteDriver} from "../../../../src/driver/sqlite-abstract/AbstractSqliteDriver"; import {OracleDriver} from "../../../../src/driver/oracle/OracleDriver"; import {LockNotSupportedOnGivenDriverError} from "../../../../src/error/LockNotSupportedOnGivenDriverError"; +import { VersionUtils } from "../../../../src/util/VersionUtils"; describe("query builder > locking", () => { @@ -99,6 +100,108 @@ describe("query builder > locking", () => { return; }))); + it("should throw error if pessimistic_partial_write lock used without transaction", () => Promise.all(connections.map(async connection => { + if (connection.driver instanceof PostgresDriver) { + return connection.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_partial_write") + .where("post.id = :id", { id: 1 }) + .getOne().should.be.rejectedWith(PessimisticLockTransactionRequiredError); + } + + if (connection.driver instanceof MysqlDriver) { + let [{ version }] = await connection.query( + "SELECT VERSION() as version;" + ); + version = version.toLowerCase(); + if (version.includes('maria')) return; // not supported in mariadb + if (VersionUtils.isGreaterOrEqual(version, '8.0.0')) { + return connection.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_partial_write") + .where("post.id = :id", { id: 1 }) + .getOne().should.be.rejectedWith(PessimisticLockTransactionRequiredError); + } + } + return; + }))); + + it("should not throw error if pessimistic_partial_write lock used with transaction", () => Promise.all(connections.map(async connection => { + if (connection.driver instanceof PostgresDriver) { + return connection.manager.transaction(entityManager => { + return Promise.all([entityManager.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_partial_write") + .where("post.id = :id", { id: 1}) + .getOne().should.not.be.rejected]); + }); + } + + if (connection.driver instanceof MysqlDriver) { + let [{ version }] = await connection.query( + "SELECT VERSION() as version;" + ); + version = version.toLowerCase(); + if (version.includes('maria')) return; // not supported in mariadb + if (VersionUtils.isGreaterOrEqual(version, '8.0.0')) { + return connection.manager.transaction(entityManager => { + return Promise.all([entityManager.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_partial_write") + .where("post.id = :id", { id: 1}) + .getOne().should.not.be.rejected]); + }); + } + } + return; + }))); + + it("should throw error if pessimistic_write_or_fail lock used without transaction", () => Promise.all(connections.map(async connection => { + if (connection.driver instanceof PostgresDriver) { + return connection.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_write_or_fail") + .where("post.id = :id", { id: 1 }) + .getOne().should.be.rejectedWith(PessimisticLockTransactionRequiredError); + } + + if (connection.driver instanceof MysqlDriver) { + let [{ version }] = await connection.query( + "SELECT VERSION() as version;" + ); + version = version.toLowerCase(); + if ((version.includes('maria') && VersionUtils.isGreaterOrEqual(version, "10.3.0")) || !version.includes('maria') && VersionUtils.isGreaterOrEqual(version, '8.0.0')) { + return connection.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_write_or_fail") + .where("post.id = :id", { id: 1 }) + .getOne().should.be.rejectedWith(PessimisticLockTransactionRequiredError); + } + } + return; + }))); + + it("should not throw error if pessimistic_write_or_fail lock used with transaction", () => Promise.all(connections.map(async connection => { + if (connection.driver instanceof PostgresDriver) { + return connection.manager.transaction(entityManager => { + return Promise.all([entityManager.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_write_or_fail") + .where("post.id = :id", { id: 1}) + .getOne().should.not.be.rejected]); + }); + } + + if (connection.driver instanceof MysqlDriver) { + let [{ version }] = await connection.query( + "SELECT VERSION() as version;" + ); + version = version.toLowerCase(); + if ((version.includes('maria') && VersionUtils.isGreaterOrEqual(version, "10.3.0")) || !version.includes('maria') && VersionUtils.isGreaterOrEqual(version, '8.0.0')) { + return connection.manager.transaction(entityManager => { + return Promise.all([entityManager.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_write_or_fail") + .where("post.id = :id", { id: 1}) + .getOne().should.not.be.rejected]); + }); + } + } + return; + }))); + it("should attach pessimistic read lock statement on query if locking enabled", () => Promise.all(connections.map(async connection => { if (connection.driver instanceof AbstractSqliteDriver || connection.driver instanceof CockroachDriver || connection.driver instanceof SapDriver) return; @@ -187,6 +290,54 @@ describe("query builder > locking", () => { }))); + it("should not attach pessimistic_partial_write lock statement on query if locking is not used", () => Promise.all(connections.map(async connection => { + if (connection.driver instanceof PostgresDriver || connection.driver instanceof MysqlDriver) { + const sql = connection.createQueryBuilder(PostWithVersion, "post") + .where("post.id = :id", { id: 1 }) + .getSql(); + + expect(sql.indexOf("FOR UPDATE SKIP LOCKED") === -1).to.be.true; + } + return; + }))); + + it("should attach pessimistic_partial_write lock statement on query if locking enabled", () => Promise.all(connections.map(async connection => { + if (connection.driver instanceof PostgresDriver || connection.driver instanceof MysqlDriver) { + const sql = connection.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_partial_write") + .where("post.id = :id", { id: 1 }) + .getSql(); + + expect(sql.indexOf("FOR UPDATE SKIP LOCKED") !== -1).to.be.true; + } + return; + + }))); + + it("should not attach pessimistic_write_or_fail lock statement on query if locking is not used", () => Promise.all(connections.map(async connection => { + if (connection.driver instanceof PostgresDriver || connection.driver instanceof MysqlDriver) { + const sql = connection.createQueryBuilder(PostWithVersion, "post") + .where("post.id = :id", { id: 1 }) + .getSql(); + + expect(sql.indexOf("FOR UPDATE NOWAIT") === -1).to.be.true; + } + return; + }))); + + it("should attach pessimistic_write_or_fail lock statement on query if locking enabled", () => Promise.all(connections.map(async connection => { + if (connection.driver instanceof PostgresDriver || connection.driver instanceof MysqlDriver) { + const sql = connection.createQueryBuilder(PostWithVersion, "post") + .setLock("pessimistic_write_or_fail") + .where("post.id = :id", { id: 1 }) + .getSql(); + + expect(sql.indexOf("FOR UPDATE NOWAIT") !== -1).to.be.true; + } + return; + + }))); + it("should throw error if optimistic lock used with getMany method", () => Promise.all(connections.map(async connection => { return connection.createQueryBuilder(PostWithVersion, "post")