Skip to content

Commit

Permalink
feat: added support for NOWAIT & SKIP LOCKED in Postgres (#5927)
Browse files Browse the repository at this point in the history
* Added support for NOWAIT & SKIP LOCKED in Postgres

* fix merge typo

Co-authored-by: Umed Khudoiberdiev <pleerock.me@gmail.com>
  • Loading branch information
Nopik and pleerock committed May 16, 2020
1 parent 90d31a0 commit 2c90e1c
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 39 deletions.
64 changes: 33 additions & 31 deletions docs/find-options.md
Expand Up @@ -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: {
Expand All @@ -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" } } });
Expand All @@ -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')
Expand All @@ -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"
Expand All @@ -79,28 +79,28 @@ userRepository.find({
* `skip` - offset (paginated) from where entities should be taken.

```typescript
userRepository.find({
userRepository.find({
skip: 5
});
```

* `take` - limit (paginated) - max number of entities that should be taken.

```typescript
userRepository.find({
userRepository.find({
take: 10
});
```

** 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
})
```

Expand All @@ -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:
Expand All @@ -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",
Expand All @@ -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'
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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'
Expand All @@ -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 #%'
Expand All @@ -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
Expand All @@ -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')
Expand All @@ -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'])
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions src/find-options/FindOneOptions.ts
Expand Up @@ -40,11 +40,11 @@ export interface FindOneOptions<Entity = any> {
/**
* 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;

/**
Expand Down
4 changes: 2 additions & 2 deletions src/find-options/FindOptionsUtils.ts
Expand Up @@ -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();
}
Expand Down
2 changes: 1 addition & 1 deletion src/query-builder/QueryExpressionMap.ts
Expand Up @@ -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.
Expand Down
20 changes: 17 additions & 3 deletions src/query-builder/SelectQueryBuilder.ts
Expand Up @@ -967,12 +967,12 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> 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;
Expand Down Expand Up @@ -1672,6 +1672,20 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> 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";
Expand Down Expand Up @@ -1812,7 +1826,7 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> 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") {
Expand Down

0 comments on commit 2c90e1c

Please sign in to comment.