diff --git a/src/find-options/FindOneOptions.ts b/src/find-options/FindOneOptions.ts index dcb016a474..d1855bdf3a 100644 --- a/src/find-options/FindOneOptions.ts +++ b/src/find-options/FindOneOptions.ts @@ -40,8 +40,10 @@ export interface FindOneOptions { /** * Indicates what locking mode should be used. + * + * Note: For lock tables, you must specify the table names and not the relation names */ - lock?: { mode: "optimistic", version: number|Date } | { mode: "pessimistic_read"|"pessimistic_write"|"dirty_read"|"pessimistic_partial_write"|"pessimistic_write_or_fail" }; + lock?: { mode: "optimistic", version: number|Date } | { mode: "pessimistic_read"|"pessimistic_write"|"dirty_read"|"pessimistic_partial_write"|"pessimistic_write_or_fail"|"for_no_key_update", tables?: string[] }; /** * Indicates if soft-deleted rows should be included in entity result. diff --git a/src/find-options/FindOptionsUtils.ts b/src/find-options/FindOptionsUtils.ts index 210d75e23e..c4fb9e26ed 100644 --- a/src/find-options/FindOptionsUtils.ts +++ b/src/find-options/FindOptionsUtils.ts @@ -180,9 +180,18 @@ export class FindOptionsUtils { if (options.lock) { if (options.lock.mode === "optimistic") { - qb.setLock(options.lock.mode, options.lock.version as any); + qb.setLock(options.lock.mode, options.lock.version); } 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); + const tableNames = options.lock.tables ? options.lock.tables.map((table) => { + const tableAlias = qb.expressionMap.aliases.find((alias) => { + return alias.metadata.tableNameWithoutPrefix === table; + }); + if (!tableAlias) { + throw new Error(`"${table}" is not part of this query`); + } + return qb.escape(tableAlias.name); + }) : undefined; + qb.setLock(options.lock.mode, undefined, tableNames); } } diff --git a/src/find-options/JoinOptions.ts b/src/find-options/JoinOptions.ts index 17b0002d8b..338b618616 100644 --- a/src/find-options/JoinOptions.ts +++ b/src/find-options/JoinOptions.ts @@ -38,22 +38,34 @@ export interface JoinOptions { alias: string; /** - * Array of columns to LEFT JOIN. + * Object where each key represents the LEFT JOIN alias, + * and the corresponding value represents the relation path. + * + * The columns of the joined table are included in the selection. */ leftJoinAndSelect?: { [key: string]: string }; /** - * Array of columns to INNER JOIN. + * Object where each key represents the INNER JOIN alias, + * and the corresponding value represents the relation path. + * + * The columns of the joined table are included in the selection. */ innerJoinAndSelect?: { [key: string]: string }; /** - * Array of columns to LEFT JOIN. + * Object where each key represents the LEFT JOIN alias, + * and the corresponding value represents the relation path. + * + * This method does not select the columns of the joined table. */ leftJoin?: { [key: string]: string }; /** - * Array of columns to INNER JOIN. + * Object where each key represents the INNER JOIN alias, + * and the corresponding value represents the relation path. + * + * This method does not select the columns of the joined table. */ innerJoin?: { [key: string]: string }; diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index e57cc43526..a703bf15e6 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -688,7 +688,7 @@ export abstract class QueryBuilder { } if (!conditionsArray.length) { - return " "; + return ""; } else if (conditionsArray.length === 1) { return ` WHERE ${conditionsArray[0]}`; } else { diff --git a/src/query-builder/QueryExpressionMap.ts b/src/query-builder/QueryExpressionMap.ts index 93df26a5b2..1190b19590 100644 --- a/src/query-builder/QueryExpressionMap.ts +++ b/src/query-builder/QueryExpressionMap.ts @@ -157,6 +157,11 @@ export class QueryExpressionMap { */ lockVersion?: number|Date; + /** + * Tables to be specified in the "FOR UPDATE OF" clause, referred by their alias + */ + lockTables?: string[]; + /** * Indicates if soft-deleted rows should be included in entity result. * By default the soft-deleted rows are not included. @@ -416,6 +421,7 @@ export class QueryExpressionMap { map.take = this.take; map.lockMode = this.lockMode; map.lockVersion = this.lockVersion; + map.lockTables = this.lockTables; map.withDeleted = this.withDeleted; map.parameters = Object.assign({}, this.parameters); map.disableEscaping = this.disableEscaping; diff --git a/src/query-builder/SelectQueryBuilder.ts b/src/query-builder/SelectQueryBuilder.ts index cdf4ed3a2e..67b68a2117 100644 --- a/src/query-builder/SelectQueryBuilder.ts +++ b/src/query-builder/SelectQueryBuilder.ts @@ -960,26 +960,21 @@ export class SelectQueryBuilder extends QueryBuilder implements /** * Sets locking mode. */ - setLock(lockMode: "optimistic", lockVersion: number): this; + setLock(lockMode: "optimistic", lockVersion: number | Date): this; /** * Sets locking mode. */ - setLock(lockMode: "optimistic", lockVersion: Date): this; + setLock(lockMode: "pessimistic_read"|"pessimistic_write"|"dirty_read"|"pessimistic_partial_write"|"pessimistic_write_or_fail"|"for_no_key_update", lockVersion?: undefined, lockTables?: string[]): this; /** * Sets locking mode. */ - 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"|"pessimistic_partial_write"|"pessimistic_write_or_fail"|"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, lockTables?: string[]): this { this.expressionMap.lockMode = lockMode; this.expressionMap.lockVersion = lockVersion; + this.expressionMap.lockTables = lockTables; return this; - } /** @@ -1664,13 +1659,26 @@ export class SelectQueryBuilder extends QueryBuilder implements */ protected createLockExpression(): string { const driver = this.connection.driver; + + let lockTablesClause = ""; + + if (this.expressionMap.lockTables) { + if (!(driver instanceof PostgresDriver)) { + throw new Error("Lock tables not supported in selected driver"); + } + if (this.expressionMap.lockTables.length < 1) { + throw new Error("lockTables cannot be an empty array"); + } + lockTablesClause = " OF " + this.expressionMap.lockTables.join(", "); + } + switch (this.expressionMap.lockMode) { case "pessimistic_read": if (driver instanceof MysqlDriver || driver instanceof AuroraDataApiDriver) { return " LOCK IN SHARE MODE"; } else if (driver instanceof PostgresDriver) { - return " FOR SHARE"; + return " FOR SHARE" + lockTablesClause; } else if (driver instanceof OracleDriver) { return " FOR UPDATE"; @@ -1682,9 +1690,13 @@ export class SelectQueryBuilder extends QueryBuilder implements throw new LockNotSupportedOnGivenDriverError(); } case "pessimistic_write": - if (driver instanceof MysqlDriver || driver instanceof AuroraDataApiDriver || driver instanceof PostgresDriver || driver instanceof OracleDriver) { + if (driver instanceof MysqlDriver || driver instanceof AuroraDataApiDriver || driver instanceof OracleDriver) { return " FOR UPDATE"; + } + else if (driver instanceof PostgresDriver ) { + return " FOR UPDATE" + lockTablesClause; + } else if (driver instanceof SqlServerDriver) { return ""; @@ -1692,22 +1704,29 @@ export class SelectQueryBuilder extends QueryBuilder implements throw new LockNotSupportedOnGivenDriverError(); } case "pessimistic_partial_write": - if (driver instanceof PostgresDriver || driver instanceof MysqlDriver) { + if (driver instanceof PostgresDriver) { + return " FOR UPDATE" + lockTablesClause + " SKIP LOCKED"; + + } else if (driver instanceof MysqlDriver) { return " FOR UPDATE SKIP LOCKED"; } else { throw new LockNotSupportedOnGivenDriverError(); } case "pessimistic_write_or_fail": - if (driver instanceof PostgresDriver || driver instanceof MysqlDriver) { + if (driver instanceof PostgresDriver) { + return " FOR UPDATE" + lockTablesClause + " NOWAIT"; + + } else if (driver instanceof MysqlDriver) { return " FOR UPDATE NOWAIT"; + } else { throw new LockNotSupportedOnGivenDriverError(); } case "for_no_key_update": if (driver instanceof PostgresDriver) { - return " FOR NO KEY UPDATE"; + return " FOR NO KEY UPDATE" + lockTablesClause; } else { throw new LockNotSupportedOnGivenDriverError(); } diff --git a/test/functional/query-builder/locking/entity/Category.ts b/test/functional/query-builder/locking/entity/Category.ts new file mode 100644 index 0000000000..25b92db3ab --- /dev/null +++ b/test/functional/query-builder/locking/entity/Category.ts @@ -0,0 +1,32 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; +import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany"; +import {JoinTable} from "../../../../../src/decorator/relations/JoinTable"; +import {Post} from "./Post"; +import {Image} from "./Image"; + +@Entity() +export class Category { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + isRemoved: boolean = false; + + @ManyToMany(type => Post, post => post.categories) + posts: Post[]; + + @ManyToMany(type => Image) + @JoinTable() + images: Image[]; + + titleImage: Image; + + removedImages: Image[]; + +} \ No newline at end of file diff --git a/test/functional/query-builder/locking/entity/Image.ts b/test/functional/query-builder/locking/entity/Image.ts new file mode 100644 index 0000000000..0abf5cc804 --- /dev/null +++ b/test/functional/query-builder/locking/entity/Image.ts @@ -0,0 +1,17 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class Image { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + isRemoved: boolean = false; + +} \ No newline at end of file diff --git a/test/functional/query-builder/locking/entity/Post.ts b/test/functional/query-builder/locking/entity/Post.ts new file mode 100644 index 0000000000..9e4075fef1 --- /dev/null +++ b/test/functional/query-builder/locking/entity/Post.ts @@ -0,0 +1,40 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; +import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne"; +import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany"; +import {JoinTable} from "../../../../../src/decorator/relations/JoinTable"; +import {OneToOne} from "../../../../../src/decorator/relations/OneToOne"; +import {JoinColumn} from "../../../../../src/decorator/relations/JoinColumn"; +import {User} from "./User"; +import {Category} from "./Category"; +import {Tag} from "./Tag"; +import {Image} from "./Image"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + + @ManyToOne(type => Tag) + tag: Tag; + + @OneToOne(type => User) + @JoinColumn() + author: User; + + @ManyToMany(type => Category, category => category.posts) + @JoinTable() + categories: Category[]; + + subcategories: Category[]; + + removedCategories: Category[]; + + images: Image[]; + +} \ No newline at end of file diff --git a/test/functional/query-builder/locking/entity/Tag.ts b/test/functional/query-builder/locking/entity/Tag.ts new file mode 100644 index 0000000000..649ed21d41 --- /dev/null +++ b/test/functional/query-builder/locking/entity/Tag.ts @@ -0,0 +1,14 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class Tag { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + +} \ No newline at end of file diff --git a/test/functional/query-builder/locking/entity/User.ts b/test/functional/query-builder/locking/entity/User.ts new file mode 100644 index 0000000000..0e019207a4 --- /dev/null +++ b/test/functional/query-builder/locking/entity/User.ts @@ -0,0 +1,14 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class User { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + +} \ No newline at end of file diff --git a/test/functional/query-builder/locking/query-builder-locking.ts b/test/functional/query-builder/locking/query-builder-locking.ts index bfa5d1583b..18e49ea4c9 100644 --- a/test/functional/query-builder/locking/query-builder-locking.ts +++ b/test/functional/query-builder/locking/query-builder-locking.ts @@ -4,6 +4,7 @@ import {SapDriver} from "../../../../src/driver/sap/SapDriver"; import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils"; import {Connection} from "../../../../src/connection/Connection"; import {PostWithVersion} from "./entity/PostWithVersion"; +import {Post} from './entity/Post'; import {expect} from "chai"; import {PostWithoutVersionAndUpdateDate} from "./entity/PostWithoutVersionAndUpdateDate"; import {PostWithUpdateDate} from "./entity/PostWithUpdateDate"; @@ -503,4 +504,116 @@ describe("query builder > locking", () => { return; }))); + it("should only specify locked tables in FOR UPDATE OF clause if argument is given", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + const sql = connection.createQueryBuilder(Post, "post") + .innerJoin("post.author", "user") + .setLock('pessimistic_write', undefined, ["user"]) + .getSql(); + + expect(sql).to.match(/FOR UPDATE OF user$/); + + const sql2 = connection.createQueryBuilder(Post, "post") + .innerJoin("post.author", "user") + .setLock('pessimistic_write', undefined, ["post","user"]) + .getSql(); + + expect(sql2).to.match(/FOR UPDATE OF post, user$/); + }))); + + it("should not allow empty array for lockTables", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + return Promise.all([ + entityManager.createQueryBuilder(Post, "post") + .innerJoin("post.author", "user") + .setLock('pessimistic_write', undefined, []) + .getOne().should.be.rejectedWith('lockTables cannot be an empty array'), + ]); + }); + }))); + + it("should throw error when specifying a table that is not part of the query", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + return Promise.all([ + entityManager.createQueryBuilder(Post, "post") + .innerJoin("post.author", "user") + .setLock('pessimistic_write', undefined, ["img"]) + .getOne().should.be.rejectedWith('relation "img" in FOR UPDATE clause not found in FROM clause'), + ]); + }); + }))); + + it("should allow on a left join", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + + return Promise.all([ + entityManager.createQueryBuilder(Post, "post") + .leftJoin("post.author", "user") + .setLock('pessimistic_write', undefined, ["post"]) + .getOne(), + entityManager.createQueryBuilder(Post, "post") + .leftJoin("post.author", "user") + .setLock('pessimistic_write') + .getOne().should.be.rejectedWith('FOR UPDATE cannot be applied to the nullable side of an outer join'), + ]); + }); + }))); + + it("should allow using lockTables on all types of locking", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + + return Promise.all([ + entityManager.createQueryBuilder(Post, "post") + .leftJoin("post.author", "user") + .setLock('pessimistic_read', undefined, ["post"]) + .getOne(), + entityManager.createQueryBuilder(Post, "post") + .leftJoin("post.author", "user") + .setLock('pessimistic_write', undefined, ["post"]) + .getOne(), + entityManager.createQueryBuilder(Post, "post") + .leftJoin("post.author", "user") + .setLock('pessimistic_partial_write', undefined, ["post"]) + .getOne(), + entityManager.createQueryBuilder(Post, "post") + .leftJoin("post.author", "user") + .setLock('pessimistic_write_or_fail', undefined, ["post"]) + .getOne(), + entityManager.createQueryBuilder(Post, "post") + .leftJoin("post.author", "user") + .setLock('for_no_key_update', undefined, ["post"]) + .getOne(), + ]); + }); + }))); + + it("should allow locking a relation of a relation", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + + return Promise.all([ + entityManager.createQueryBuilder(Post, "post") + .innerJoin("post.categories", "cat") + .innerJoin("cat.images", "img") + .setLock('pessimistic_write', undefined, ["img"]) + .getOne() + ]); + }); + }))); }); diff --git a/test/functional/repository/find-options-locking/entity/Category.ts b/test/functional/repository/find-options-locking/entity/Category.ts new file mode 100644 index 0000000000..25b92db3ab --- /dev/null +++ b/test/functional/repository/find-options-locking/entity/Category.ts @@ -0,0 +1,32 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; +import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany"; +import {JoinTable} from "../../../../../src/decorator/relations/JoinTable"; +import {Post} from "./Post"; +import {Image} from "./Image"; + +@Entity() +export class Category { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + isRemoved: boolean = false; + + @ManyToMany(type => Post, post => post.categories) + posts: Post[]; + + @ManyToMany(type => Image) + @JoinTable() + images: Image[]; + + titleImage: Image; + + removedImages: Image[]; + +} \ No newline at end of file diff --git a/test/functional/repository/find-options-locking/entity/Image.ts b/test/functional/repository/find-options-locking/entity/Image.ts new file mode 100644 index 0000000000..0abf5cc804 --- /dev/null +++ b/test/functional/repository/find-options-locking/entity/Image.ts @@ -0,0 +1,17 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class Image { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + isRemoved: boolean = false; + +} \ No newline at end of file diff --git a/test/functional/repository/find-options-locking/entity/Post.ts b/test/functional/repository/find-options-locking/entity/Post.ts new file mode 100644 index 0000000000..9e4075fef1 --- /dev/null +++ b/test/functional/repository/find-options-locking/entity/Post.ts @@ -0,0 +1,40 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; +import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne"; +import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany"; +import {JoinTable} from "../../../../../src/decorator/relations/JoinTable"; +import {OneToOne} from "../../../../../src/decorator/relations/OneToOne"; +import {JoinColumn} from "../../../../../src/decorator/relations/JoinColumn"; +import {User} from "./User"; +import {Category} from "./Category"; +import {Tag} from "./Tag"; +import {Image} from "./Image"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + + @ManyToOne(type => Tag) + tag: Tag; + + @OneToOne(type => User) + @JoinColumn() + author: User; + + @ManyToMany(type => Category, category => category.posts) + @JoinTable() + categories: Category[]; + + subcategories: Category[]; + + removedCategories: Category[]; + + images: Image[]; + +} \ No newline at end of file diff --git a/test/functional/repository/find-options-locking/entity/Tag.ts b/test/functional/repository/find-options-locking/entity/Tag.ts new file mode 100644 index 0000000000..649ed21d41 --- /dev/null +++ b/test/functional/repository/find-options-locking/entity/Tag.ts @@ -0,0 +1,14 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class Tag { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + +} \ No newline at end of file diff --git a/test/functional/repository/find-options-locking/entity/User.ts b/test/functional/repository/find-options-locking/entity/User.ts new file mode 100644 index 0000000000..0e019207a4 --- /dev/null +++ b/test/functional/repository/find-options-locking/entity/User.ts @@ -0,0 +1,14 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class User { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + +} \ No newline at end of file diff --git a/test/functional/repository/find-options-locking/find-options-locking.ts b/test/functional/repository/find-options-locking/find-options-locking.ts index 876c67ff2e..532489583d 100644 --- a/test/functional/repository/find-options-locking/find-options-locking.ts +++ b/test/functional/repository/find-options-locking/find-options-locking.ts @@ -8,6 +8,7 @@ import {expect} from "chai"; import {PostWithoutVersionAndUpdateDate} from "./entity/PostWithoutVersionAndUpdateDate"; import {PostWithUpdateDate} from "./entity/PostWithUpdateDate"; import {PostWithVersionAndUpdatedDate} from "./entity/PostWithVersionAndUpdatedDate"; +import {Post} from './entity/Post'; import {OptimisticLockVersionMismatchError} from "../../../../src/error/OptimisticLockVersionMismatchError"; import {OptimisticLockCanNotBeUsedError} from "../../../../src/error/OptimisticLockCanNotBeUsedError"; import {NoVersionOrUpdateDateColumnError} from "../../../../src/error/NoVersionOrUpdateDateColumnError"; @@ -268,4 +269,103 @@ describe("repository > find options > locking", () => { return; }))); + it("should not allow empty array for lockTables", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + return Promise.all([ + entityManager.getRepository(Post) + .findOne({ + lock: {mode: 'pessimistic_write', tables: []} + }).should.be.rejectedWith('lockTables cannot be an empty array'), + ]); + }); + }))); + + it("should throw error when specifying a table that is not part of the query", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + return Promise.all([ + entityManager.getRepository(Post) + .findOne({ + relations: ['author'], + lock: {mode: 'pessimistic_write', tables: ['img']} + }).should.be.rejectedWith('"img" is not part of this query') + ]); + }); + }))); + + it("should allow on a left join", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + return Promise.all([ + entityManager.getRepository(Post).findOne({ + relations: ['author'], + lock: {mode: 'pessimistic_write', tables: ['post']} + }), + entityManager.getRepository(Post).findOne({ + relations: ['author'], + lock: {mode: 'pessimistic_write'} + }).should.be.rejectedWith('FOR UPDATE cannot be applied to the nullable side of an outer join') + ]); + }); + }))); + + it("should allow using lockTables on all types of locking", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + + return Promise.all([ + entityManager.getRepository(Post).findOne({ + relations: ['author'], + lock: {mode: 'pessimistic_read', tables: ['post']} + }), + entityManager.getRepository(Post).findOne({ + relations: ['author'], + lock: {mode: 'pessimistic_write', tables: ['post']} + }), + entityManager.getRepository(Post).findOne({ + relations: ['author'], + lock: {mode: 'pessimistic_partial_write', tables: ['post']} + }), + entityManager.getRepository(Post).findOne({ + relations: ['author'], + lock: {mode: 'pessimistic_write_or_fail', tables: ['post']} + }), + entityManager.getRepository(Post).findOne({ + relations: ['author'], + lock: {mode: 'for_no_key_update', tables: ['post']} + }), + ]); + }); + }))); + + it("should allow locking a relation of a relation", () => Promise.all(connections.map(async connection => { + if (!(connection.driver instanceof PostgresDriver)) + return; + + return connection.manager.transaction(entityManager => { + + return Promise.all([ + entityManager.getRepository(Post).findOne({ + join: { + alias: 'post', + innerJoinAndSelect: { + categorys: 'post.categories', + images: 'categorys.images' + } + }, + lock: {mode: 'pessimistic_write', tables: ['image']} + }), + ]); + }); + }))); + });