Skip to content

Commit

Permalink
feat: add NOWAIT and SKIP LOCKED lock support for MySQL (#7236)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lacunadream committed Jan 11, 2021
1 parent ea3bc35 commit 9407507
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 2 deletions.
4 changes: 2 additions & 2 deletions src/query-builder/SelectQueryBuilder.ts
Expand Up @@ -1686,14 +1686,14 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> 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();
Expand Down
151 changes: 151 additions & 0 deletions test/functional/query-builder/locking/query-builder-locking.ts
Expand Up @@ -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", () => {

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 9407507

Please sign in to comment.