Skip to content

Commit

Permalink
feat: add for_key_share ("FOR KEY SHARE") lock mode for postgres dr…
Browse files Browse the repository at this point in the history
…iver (#8879)

Adds support for new lock mode `for_key_share` - generating FOR KEY SHARE lock. Postgres specific.

Closes: #8878
  • Loading branch information
urossmolnik committed Apr 12, 2022
1 parent dfd0585 commit 4687be8
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 14 deletions.
19 changes: 10 additions & 9 deletions docs/find-options.md
Expand Up @@ -221,7 +221,8 @@ or
"dirty_read" |
"pessimistic_partial_write" |
"pessimistic_write_or_fail" |
"for_no_key_update"
"for_no_key_update" |
"for_key_share"
}
```

Expand All @@ -239,14 +240,14 @@ userRepository.findOne({
Support of lock modes, and SQL statements they translate to, are listed in the table below (blank cell denotes unsupported). When specified lock mode is not supported, a `LockNotSupportedOnGivenDriverError` error will be thrown.

```text
| | pessimistic_read | pessimistic_write | dirty_read | pessimistic_partial_write | pessimistic_write_or_fail | for_no_key_update |
| --------------- | -------------------- | ----------------------- | ------------- | --------------------------- | --------------------------- | ------------------- |
| MySQL | LOCK IN SHARE MODE | FOR UPDATE | (nothing) | FOR UPDATE SKIP LOCKED | FOR UPDATE NOWAIT | |
| Postgres | FOR SHARE | FOR UPDATE | (nothing) | FOR UPDATE SKIP LOCKED | FOR UPDATE NOWAIT | FOR NO KEY UPDATE |
| Oracle | FOR UPDATE | FOR UPDATE | (nothing) | | | |
| SQL Server | WITH (HOLDLOCK, ROWLOCK) | WITH (UPDLOCK, ROWLOCK) | WITH (NOLOCK) | | | |
| AuroraDataApi | LOCK IN SHARE MODE | FOR UPDATE | (nothing) | | | |
| CockroachDB | | FOR UPDATE | (nothing) | | FOR UPDATE NOWAIT | FOR NO KEY UPDATE |
| | pessimistic_read | pessimistic_write | dirty_read | pessimistic_partial_write | pessimistic_write_or_fail | for_no_key_update | for_key_share |
| --------------- | -------------------- | ----------------------- | ------------- | --------------------------- | --------------------------- | ------------------- | ------------- |
| MySQL | LOCK IN SHARE MODE | FOR UPDATE | (nothing) | FOR UPDATE SKIP LOCKED | FOR UPDATE NOWAIT | | |
| Postgres | FOR SHARE | FOR UPDATE | (nothing) | FOR UPDATE SKIP LOCKED | FOR UPDATE NOWAIT | FOR NO KEY UPDATE | FOR KEY SHARE |
| Oracle | FOR UPDATE | FOR UPDATE | (nothing) | | | | |
| SQL Server | WITH (HOLDLOCK, ROWLOCK) | WITH (UPDLOCK, ROWLOCK) | WITH (NOLOCK) | | | | |
| AuroraDataApi | LOCK IN SHARE MODE | FOR UPDATE | (nothing) | | | | |
| CockroachDB | | FOR UPDATE | (nothing) | | FOR UPDATE NOWAIT | FOR NO KEY UPDATE | |
```

Expand Down
1 change: 1 addition & 0 deletions src/find-options/FindOneOptions.ts
Expand Up @@ -77,6 +77,7 @@ export interface FindOneOptions<Entity = any> {
| "pessimistic_partial_write"
| "pessimistic_write_or_fail"
| "for_no_key_update"
| "for_key_share"
tables?: string[]
}

Expand Down
3 changes: 2 additions & 1 deletion src/find-options/FindOptionsUtils.ts
Expand Up @@ -181,7 +181,8 @@ export class FindOptionsUtils {
options.lock.mode === "dirty_read" ||
options.lock.mode === "pessimistic_partial_write" ||
options.lock.mode === "pessimistic_write_or_fail" ||
options.lock.mode === "for_no_key_update"
options.lock.mode === "for_no_key_update" ||
options.lock.mode === "for_key_share"
) {
const tableNames = options.lock.tables ? options.lock.tables.map((table) => {
const tableAlias = qb.expressionMap.aliases.find((alias) => {
Expand Down
1 change: 1 addition & 0 deletions src/query-builder/QueryExpressionMap.ts
Expand Up @@ -189,6 +189,7 @@ export class QueryExpressionMap {
| "pessimistic_partial_write"
| "pessimistic_write_or_fail"
| "for_no_key_update"
| "for_key_share"

/**
* Current version of the entity, used for locking.
Expand Down
20 changes: 16 additions & 4 deletions src/query-builder/SelectQueryBuilder.ts
Expand Up @@ -1487,7 +1487,8 @@ export class SelectQueryBuilder<Entity>
| "dirty_read"
| "pessimistic_partial_write"
| "pessimistic_write_or_fail"
| "for_no_key_update",
| "for_no_key_update"
| "for_key_share",
lockVersion?: undefined,
lockTables?: string[],
): this
Expand All @@ -1503,7 +1504,8 @@ export class SelectQueryBuilder<Entity>
| "dirty_read"
| "pessimistic_partial_write"
| "pessimistic_write_or_fail"
| "for_no_key_update",
| "for_no_key_update"
| "for_key_share",
lockVersion?: number | Date,
lockTables?: string[],
): this {
Expand Down Expand Up @@ -2564,6 +2566,14 @@ export class SelectQueryBuilder<Entity>
} else {
throw new LockNotSupportedOnGivenDriverError()
}

case "for_key_share":
if (driver.options.type === "postgres") {
return " FOR KEY SHARE" + lockTablesClause
} else {
throw new LockNotSupportedOnGivenDriverError()
}

default:
return ""
}
Expand Down Expand Up @@ -3061,7 +3071,8 @@ export class SelectQueryBuilder<Entity>
"pessimistic_partial_write" ||
this.findOptions.lock.mode ===
"pessimistic_write_or_fail" ||
this.findOptions.lock.mode === "for_no_key_update"
this.findOptions.lock.mode === "for_no_key_update" ||
this.findOptions.lock.mode === "for_key_share"
) {
const tableNames = this.findOptions.lock.tables
? this.findOptions.lock.tables.map((table) => {
Expand Down Expand Up @@ -3140,7 +3151,8 @@ export class SelectQueryBuilder<Entity>
this.expressionMap.lockMode === "pessimistic_write" ||
this.expressionMap.lockMode === "pessimistic_partial_write" ||
this.expressionMap.lockMode === "pessimistic_write_or_fail" ||
this.expressionMap.lockMode === "for_no_key_update") &&
this.expressionMap.lockMode === "for_no_key_update" ||
this.expressionMap.lockMode === "for_key_share") &&
!queryRunner.isTransactionActive
)
throw new PessimisticLockTransactionRequiredError()
Expand Down
88 changes: 88 additions & 0 deletions test/functional/query-builder/locking/query-builder-locking.ts
Expand Up @@ -162,6 +162,41 @@ describe("query builder > locking", () => {
}),
))

it("should throw error if for key share lock used without transaction", () =>
Promise.all(
connections.map(async (connection) => {
if (connection.driver.options.type === "postgres") {
return connection
.createQueryBuilder(PostWithVersion, "post")
.setLock("for_key_share")
.where("post.id = :id", { id: 1 })
.getOne()
.should.be.rejectedWith(
PessimisticLockTransactionRequiredError,
)
}
return
}),
))

it("should not throw error if for key share lock used with transaction", () =>
Promise.all(
connections.map(async (connection) => {
if (connection.driver.options.type === "postgres") {
return connection.manager.transaction((entityManager) => {
return Promise.all([
entityManager
.createQueryBuilder(PostWithVersion, "post")
.setLock("for_key_share")
.where("post.id = :id", { id: 1 })
.getOne().should.not.be.rejected,
])
})
}
return
}),
))

it("should throw error if pessimistic_partial_write lock used without transaction", () =>
Promise.all(
connections.map(async (connection) => {
Expand Down Expand Up @@ -459,6 +494,37 @@ describe("query builder > locking", () => {
}),
))

it("should not attach for key share lock statement on query if locking is not used", () =>
Promise.all(
connections.map(async (connection) => {
if (connection.driver.options.type === "postgres") {
const sql = connection
.createQueryBuilder(PostWithVersion, "post")
.where("post.id = :id", { id: 1 })
.getSql()

expect(sql.indexOf("FOR KEY SHARE") === -1).to.be.true
}
return
}),
))

it("should attach for key share lock statement on query if locking enabled", () =>
Promise.all(
connections.map(async (connection) => {
if (connection.driver.options.type === "postgres") {
const sql = connection
.createQueryBuilder(PostWithVersion, "post")
.setLock("for_key_share")
.where("post.id = :id", { id: 1 })
.getSql()

expect(sql.indexOf("FOR KEY SHARE") !== -1).to.be.true
}
return
}),
))

it("should not attach pessimistic_partial_write lock statement on query if locking is not used", () =>
Promise.all(
connections.map(async (connection) => {
Expand Down Expand Up @@ -792,6 +858,28 @@ describe("query builder > locking", () => {
}),
))

it("should throw error if for key share locking not supported by given driver", () =>
Promise.all(
connections.map(async (connection) => {
if (!(connection.driver.options.type === "postgres")) {
return connection.manager.transaction((entityManager) => {
return Promise.all([
entityManager
.createQueryBuilder(PostWithVersion, "post")
.setLock("for_key_share")
.where("post.id = :id", { id: 1 })
.getOne()
.should.be.rejectedWith(
LockNotSupportedOnGivenDriverError,
),
])
})
}

return
}),
))

it("should only specify locked tables in FOR UPDATE OF clause if argument is given", () =>
Promise.all(
connections.map(async (connection) => {
Expand Down
Expand Up @@ -190,6 +190,35 @@ describe("repository > find options > locking", () => {
}),
))

it("should attach for key share lock statement on query if locking enabled", () =>
Promise.all(
connections.map(async (connection) => {
if (!(connection.driver.options.type === "postgres")) return

const executedSql: string[] = []

await connection.manager.transaction((entityManager) => {
const originalQuery = entityManager.queryRunner!.query.bind(
entityManager.queryRunner,
)
entityManager.queryRunner!.query = (...args: any[]) => {
executedSql.push(args[0])
return originalQuery(...args)
}

return entityManager
.getRepository(PostWithVersion)
.findOne({
where: { id: 1 },
lock: { mode: "for_key_share" },
})
})

expect(executedSql.join(" ").includes("FOR KEY SHARE")).to.be
.true
}),
))

it("should attach pessimistic write lock statement on query if locking enabled", () =>
Promise.all(
connections.map(async (connection) => {
Expand Down Expand Up @@ -606,6 +635,14 @@ describe("repository > find options > locking", () => {
tables: ["post"],
},
}),
entityManager.getRepository(Post).findOne({
where: { id: 1 },
relations: ["author"],
lock: {
mode: "for_key_share",
tables: ["post"],
},
}),
])
})
}),
Expand Down

0 comments on commit 4687be8

Please sign in to comment.