From 2c90e1c05f9c1dfc66bc47510e44d953ae31566a Mon Sep 17 00:00:00 2001 From: Kamil Burzynski Date: Sat, 16 May 2020 13:08:06 +0200 Subject: [PATCH] feat: added support for NOWAIT & SKIP LOCKED in Postgres (#5927) * Added support for NOWAIT & SKIP LOCKED in Postgres * fix merge typo Co-authored-by: Umed Khudoiberdiev --- docs/find-options.md | 64 +++++++++++++------------ src/find-options/FindOneOptions.ts | 4 +- src/find-options/FindOptionsUtils.ts | 4 +- src/query-builder/QueryExpressionMap.ts | 2 +- src/query-builder/SelectQueryBuilder.ts | 20 ++++++-- 5 files changed, 55 insertions(+), 39 deletions(-) diff --git a/docs/find-options.md b/docs/find-options.md index 138a7a6680..f1df389d2e 100644 --- a/docs/find-options.md +++ b/docs/find-options.md @@ -23,7 +23,7 @@ userRepository.find({ relations: ["profile", "photos", "videos", "videos.video_a * `join` - joins needs to be performed for the entity. Extended version of "relations". ```typescript -userRepository.find({ +userRepository.find({ join: { alias: "user", leftJoinAndSelect: { @@ -40,7 +40,7 @@ userRepository.find({ ```typescript userRepository.find({ where: { firstName: "Timber", lastName: "Saw" } }); ``` -Querying a column from an embedded entity should be done with respect to the hierarchy in which it was defined. Example: +Querying a column from an embedded entity should be done with respect to the hierarchy in which it was defined. Example: ```typescript userRepository.find({ where: { name: { first: "Timber", last: "Saw" } } }); @@ -57,7 +57,7 @@ userRepository.find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "user" WHERE ("firstName" = 'Timber' AND "lastName" = 'Saw') OR ("firstName" = 'Stan' AND "lastName" = 'Lee') @@ -66,7 +66,7 @@ SELECT * FROM "user" WHERE ("firstName" = 'Timber' AND "lastName" = 'Saw') OR (" * `order` - selection order. ```typescript -userRepository.find({ +userRepository.find({ order: { name: "ASC", id: "DESC" @@ -79,7 +79,7 @@ userRepository.find({ * `skip` - offset (paginated) from where entities should be taken. ```typescript -userRepository.find({ +userRepository.find({ skip: 5 }); ``` @@ -87,7 +87,7 @@ userRepository.find({ * `take` - limit (paginated) - max number of entities that should be taken. ```typescript -userRepository.find({ +userRepository.find({ take: 10 }); ``` @@ -95,12 +95,12 @@ userRepository.find({ ** If you are using typeorm with MSSQL, and want to use `take` or `limit`, you need to use order as well or you will receive the following error: `'Invalid usage of the option NEXT in the FETCH statement.'` ```typescript -userRepository.find({ - order: { - columnName: 'ASC' - }, - skip: 0, - take: 10 +userRepository.find({ + order: { + columnName: 'ASC' + }, + skip: 0, + take: 10 }) ``` @@ -120,7 +120,7 @@ userRepository.find({ ``` or ```ts -{ mode: "pessimistic_read"|"pessimistic_write"|"dirty_read" } +{ mode: "pessimistic_read"|"pessimistic_write"|"dirty_read"|"pessimistic_partial_write"|"pessimistic_write_or_fail" } ``` for example: @@ -131,15 +131,17 @@ userRepository.findOne(1, { }) ``` +`pessimistic_partial_write` and `pessimistic_write_or_fail` are supported only on Postgres and are equivalents of `SELECT .. FOR UPDATE SKIP LOCKED` and `SELECT .. FOR UPDATE NOWAIT`, accordingly. + Complete example of find options: ```typescript -userRepository.find({ +userRepository.find({ select: ["firstName", "lastName"], relations: ["profile", "photos", "videos"], - where: { - firstName: "Timber", - lastName: "Saw" + where: { + firstName: "Timber", + lastName: "Saw" }, order: { name: "ASC", @@ -166,7 +168,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }) ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "title" != 'About #1' @@ -182,7 +184,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "likes" < 10 @@ -198,7 +200,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "likes" <= 10 @@ -214,7 +216,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "likes" > 10 @@ -230,7 +232,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "likes" >= 10 @@ -246,7 +248,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "title" = 'About #2' @@ -262,7 +264,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "title" LIKE '%out #%' @@ -278,7 +280,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "likes" BETWEEN 1 AND 10 @@ -294,7 +296,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "title" IN ('About #2','About #3') @@ -310,7 +312,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query (Postgres notation): +will execute following query (Postgres notation): ```sql SELECT * FROM "post" WHERE "title" = ANY(['About #2','About #3']) @@ -326,7 +328,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "title" IS NULL @@ -342,7 +344,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "likes" = "dislikes" - 4 @@ -359,7 +361,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE "currentDate" > NOW() @@ -379,7 +381,7 @@ const loadedPosts = await connection.getRepository(Post).find({ }); ``` -will execute following query: +will execute following query: ```sql SELECT * FROM "post" WHERE NOT("likes" > 10) AND NOT("title" = 'About #2') diff --git a/src/find-options/FindOneOptions.ts b/src/find-options/FindOneOptions.ts index 5f045c58f6..be55b1b2f2 100644 --- a/src/find-options/FindOneOptions.ts +++ b/src/find-options/FindOneOptions.ts @@ -40,11 +40,11 @@ export interface FindOneOptions { /** * Enables or disables query result caching. */ - lock?: { mode: "optimistic", version: number|Date } | { mode: "pessimistic_read"|"pessimistic_write"|"dirty_read" }; + lock?: { mode: "optimistic", version: number|Date } | { mode: "pessimistic_read"|"pessimistic_write"|"dirty_read"|"pessimistic_partial_write"|"pessimistic_write_or_fail" }; /** * Indicates if soft-deleted rows should be included in entity result. - */ + */ withDeleted?: boolean; /** diff --git a/src/find-options/FindOptionsUtils.ts b/src/find-options/FindOptionsUtils.ts index bce42618ac..4ecdcd7114 100644 --- a/src/find-options/FindOptionsUtils.ts +++ b/src/find-options/FindOptionsUtils.ts @@ -176,11 +176,11 @@ export class FindOptionsUtils { if (options.lock) { if (options.lock.mode === "optimistic") { qb.setLock(options.lock.mode, options.lock.version as any); - } else if (options.lock.mode === "pessimistic_read" || options.lock.mode === "pessimistic_write" || options.lock.mode === "dirty_read") { + } else if (options.lock.mode === "pessimistic_read" || options.lock.mode === "pessimistic_write" || options.lock.mode === "dirty_read" || options.lock.mode === "pessimistic_partial_write" || options.lock.mode === "pessimistic_write_or_fail") { qb.setLock(options.lock.mode); } } - + if (options.withDeleted) { qb.withDeleted(); } diff --git a/src/query-builder/QueryExpressionMap.ts b/src/query-builder/QueryExpressionMap.ts index 30f19c5d18..e4c638888a 100644 --- a/src/query-builder/QueryExpressionMap.ts +++ b/src/query-builder/QueryExpressionMap.ts @@ -150,7 +150,7 @@ export class QueryExpressionMap { /** * Locking mode. */ - lockMode?: "optimistic"|"pessimistic_read"|"pessimistic_write"|"dirty_read"|"for_no_key_update"; + lockMode?: "optimistic"|"pessimistic_read"|"pessimistic_write"|"dirty_read"|"pessimistic_partial_write"|"pessimistic_write_or_fail"|"for_no_key_update"; /** * Current version of the entity, used for locking. diff --git a/src/query-builder/SelectQueryBuilder.ts b/src/query-builder/SelectQueryBuilder.ts index 572e6b30cc..c1de0e2f25 100644 --- a/src/query-builder/SelectQueryBuilder.ts +++ b/src/query-builder/SelectQueryBuilder.ts @@ -967,12 +967,12 @@ export class SelectQueryBuilder extends QueryBuilder implements /** * Sets locking mode. */ - setLock(lockMode: "pessimistic_read"|"pessimistic_write"|"dirty_read"|"for_no_key_update"): this; + setLock(lockMode: "pessimistic_read"|"pessimistic_write"|"dirty_read"|"pessimistic_partial_write"|"pessimistic_write_or_fail"|"for_no_key_update"): this; /** * Sets locking mode. */ - setLock(lockMode: "optimistic"|"pessimistic_read"|"pessimistic_write"|"dirty_read"|"for_no_key_update", lockVersion?: number|Date): this { + setLock(lockMode: "optimistic"|"pessimistic_read"|"pessimistic_write"|"dirty_read"|"pessimistic_partial_write"|"pessimistic_write_or_fail"|"for_no_key_update", lockVersion?: number|Date): this { this.expressionMap.lockMode = lockMode; this.expressionMap.lockVersion = lockVersion; return this; @@ -1672,6 +1672,20 @@ export class SelectQueryBuilder extends QueryBuilder implements } else { throw new LockNotSupportedOnGivenDriverError(); } + case "pessimistic_partial_write": + if (driver instanceof PostgresDriver) { + return " FOR UPDATE SKIP LOCKED"; + + } else { + throw new LockNotSupportedOnGivenDriverError(); + } + case "pessimistic_write_or_fail": + if (driver instanceof PostgresDriver) { + return " FOR UPDATE NOWAIT"; + } else { + throw new LockNotSupportedOnGivenDriverError(); + } + case "for_no_key_update": if (driver instanceof PostgresDriver) { return " FOR NO KEY UPDATE"; @@ -1812,7 +1826,7 @@ export class SelectQueryBuilder extends QueryBuilder implements if (!this.expressionMap.mainAlias) throw new Error(`Alias is not set. Use "from" method to set an alias.`); - if ((this.expressionMap.lockMode === "pessimistic_read" || this.expressionMap.lockMode === "pessimistic_write" || this.expressionMap.lockMode === "for_no_key_update") && !queryRunner.isTransactionActive) + if ((this.expressionMap.lockMode === "pessimistic_read" || 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") && !queryRunner.isTransactionActive) throw new PessimisticLockTransactionRequiredError(); if (this.expressionMap.lockMode === "optimistic") {