added locking functionality in query builder;

fixed issue #312;
This commit is contained in:
Zotov Dmitry 2017-03-02 18:23:09 +05:00
parent 540ffffafc
commit abf62fe753
17 changed files with 635 additions and 75 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ node_modules/
npm-debug.log
ormconfig.json
.vscode
.idea/

View File

@ -1,3 +1,7 @@
///<reference path="node_modules/@types/node/index.d.ts"/>
///<reference path="node_modules/@types/chai/index.d.ts"/>
///<reference path="node_modules/@types/mocha/index.d.ts"/>
import {Gulpclass, Task, SequenceTask, MergedTask} from "gulpclass";
const gulp = require("gulp");
@ -301,15 +305,16 @@ export class Gulpfile {
/**
* Runs tests the quick way.
*/
@Task("ts-node-tests")
@Task()
quickTests() {
chai.should();
chai.use(require("sinon-chai"));
chai.use(require("chai-as-promised"));
return gulp.src(["./test/**/*.ts"])
return gulp.src(["./build/compiled/test/**/*.js"])
.pipe(mocha({
timeout: 10000
bail: true,
timeout: 15000
}));
}
@ -321,13 +326,21 @@ export class Gulpfile {
}
/**
* Compiles the code and runs tests.
* Compiles the code and runs tests + makes coverage report.
*/
@SequenceTask()
tests() {
return ["compile", "tslint", "coveragePost", "coverageRemap"];
}
/**
* Compiles the code and runs only mocha tests.
*/
@SequenceTask()
mocha() {
return ["compile", "quickTests"];
}
// -------------------------------------------------------------------------
// CI tasks
// -------------------------------------------------------------------------

View File

@ -16,6 +16,16 @@ import {PersistOptions} from "../repository/PersistOptions";
*/
export class EntityManager extends BaseEntityManager {
// -------------------------------------------------------------------------
// Private properties
// -------------------------------------------------------------------------
/**
* Stores temporarily user data.
* Useful for sharing data with subscribers.
*/
private data: ObjectLiteral = {};
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -28,6 +38,20 @@ export class EntityManager extends BaseEntityManager {
// Public Methods
// -------------------------------------------------------------------------
/**
* Gets user data by a given key.
*/
getData(key: string): any {
return this.data[key];
}
/**
* Sets value for the given key in user data.
*/
setData(key: string, value: any) {
this.data[key] = value;
}
/**
* Persists (saves) all given entities in the database.
* If entities do not exist in the database then inserts, otherwise updates.

View File

@ -5,11 +5,9 @@ import {QueryRunner} from "../query-runner/QueryRunner";
import {Subject, JunctionInsert, JunctionRemove} from "./Subject";
import {OrmUtils} from "../util/OrmUtils";
import {QueryRunnerProvider} from "../query-runner/QueryRunnerProvider";
import {RelationMetadata} from "../metadata/RelationMetadata";
import {EntityManager} from "../entity-manager/EntityManager";
import {PromiseUtils} from "../util/PromiseUtils";
import {MongoDriver} from "../driver/mongodb/MongoDriver";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {EmbeddedMetadata} from "../metadata/EmbeddedMetadata";
/**
@ -581,7 +579,7 @@ export class SubjectOperationExecutor {
const value = this.connection.driver.preparePersistentValue(1, metadata.versionColumn);
columnNames.push(metadata.versionColumn.fullName);
columnValues.push(value);
columnsAndValuesMap[metadata.updateDateColumn.fullName] = value;
columnsAndValuesMap[metadata.versionColumn.fullName] = value;
}
// add special column and value - discriminator value (for tables using table inheritance)
@ -1053,7 +1051,7 @@ export class SubjectOperationExecutor {
if (subject.metadata.hasCreateDateColumn)
subject.entity[subject.metadata.createDateColumn.propertyName] = subject.date;
if (subject.metadata.hasVersionColumn)
subject.entity[subject.metadata.versionColumn.propertyName]++;
subject.entity[subject.metadata.versionColumn.propertyName] = 1;
if (subject.metadata.hasTreeLevelColumn) {
// const parentEntity = insertOperation.entity[metadata.treeParentMetadata.propertyName];
// const parentLevel = parentEntity ? (parentEntity[metadata.treeLevelColumn.propertyName] || 0) : 0;

View File

@ -9,6 +9,13 @@ import {OrderByCondition} from "../find-options/OrderByCondition";
import {Connection} from "../connection/Connection";
import {JoinOptions} from "./JoinOptions";
import {QueryRunnerProvider} from "../query-runner/QueryRunnerProvider";
import {PessimisticLockTransactionRequiredError} from "./error/PessimisticLockTransactionRequiredError";
import {NoVersionOrUpdateDateColumnError} from "./error/NoVersionOrUpdateDateColumnError";
import {OptimisticLockVersionMismatchError} from "./error/OptimisticLockVersionMismatchError";
import {OptimisticLockCanNotBeUsedError} from "./error/OptimisticLockCanNotBeUsedError";
import {PostgresDriver} from "../driver/postgres/PostgresDriver";
import {MysqlDriver} from "../driver/mysql/MysqlDriver";
import {LockNotSupportedOnGivenDriverError} from "./error/LockNotSupportedOnGivenDriverError";
/**
*/
@ -76,6 +83,8 @@ export class QueryBuilder<Entity> {
protected parameters: ObjectLiteral = {};
protected limit: number;
protected offset: number;
protected lockMode: "optimistic"|"pessimistic_read"|"pessimistic_write";
protected lockVersion?: number|Date;
protected skipNumber: number;
protected takeNumber: number;
protected ignoreParentTablesJoins: boolean = false;
@ -221,6 +230,15 @@ export class QueryBuilder<Entity> {
return this;
}
setLock(lockMode: "optimistic", lockVersion: number): this;
setLock(lockMode: "optimistic", lockVersion: Date): this;
setLock(lockMode: "pessimistic_read"|"pessimistic_write"): this;
setLock(lockMode: "optimistic"|"pessimistic_read"|"pessimistic_write", lockVersion?: number|Date): this {
this.lockMode = lockMode;
this.lockVersion = lockVersion;
return this;
}
/**
* Specifies FROM which entity's table select/update/delete will be executed.
* Also sets a main string alias of the selection data.
@ -841,6 +859,7 @@ export class QueryBuilder<Entity> {
sql += this.createOrderByExpression();
sql += this.createLimitExpression();
sql += this.createOffsetExpression();
sql += this.createLockExpression();
[sql] = this.connection.driver.escapeQueryWithParameters(sql, this.parameters);
return sql;
}
@ -860,6 +879,7 @@ export class QueryBuilder<Entity> {
sql += this.createOrderByExpression();
sql += this.createLimitExpression();
sql += this.createOffsetExpression();
sql += this.createLockExpression();
return sql;
}
@ -879,6 +899,7 @@ export class QueryBuilder<Entity> {
sql += this.createOrderByExpression();
sql += this.createLimitExpression();
sql += this.createOffsetExpression();
sql += this.createLockExpression();
return this.connection.driver.escapeQueryWithParameters(sql, this.getParameters());
}
@ -887,6 +908,7 @@ export class QueryBuilder<Entity> {
*/
async execute(): Promise<any> {
const queryRunner = await this.getQueryRunner();
const [sql, parameters] = this.getSqlWithParameters();
try {
return await queryRunner.query(sql, parameters); // await is needed here because we are using finally
@ -901,57 +923,66 @@ export class QueryBuilder<Entity> {
* Executes sql generated by query builder and returns object with raw results and entities created from them.
*/
async getEntitiesAndRawResults(): Promise<{ entities: Entity[], rawResults: any[] }> {
if (!this.aliasMap.hasMainAlias)
throw new Error(`Alias is not set. Looks like nothing is selected. Use select*, delete, update method to set an alias.`);
const queryRunner = await this.getQueryRunner();
const mainAliasName = this.fromTableName ? this.fromTableName : this.aliasMap.mainAlias.name;
let rawResults: any[];
if (this.skipNumber || this.takeNumber) {
// we are skipping order by here because its not working in subqueries anyway
// to make order by working we need to apply it on a distinct query
const [sql, parameters] = this.getSqlWithParameters({ skipOrderBy: true });
const [selects, orderBys] = this.createOrderByCombinedWithSelectExpression("distinctAlias");
try {
if (!this.aliasMap.hasMainAlias)
throw new Error(`Alias is not set. Looks like nothing is selected. Use select*, delete, update method to set an alias.`);
const distinctAlias = this.connection.driver.escapeTableName("distinctAlias");
const metadata = this.connection.getMetadata(this.fromEntity.alias.target);
let idsQuery = `SELECT `;
idsQuery += metadata.primaryColumns.map((primaryColumn, index) => {
const propertyName = this.connection.driver.escapeAliasName(mainAliasName + "_" + primaryColumn.fullName);
if (index === 0) {
return `DISTINCT(${distinctAlias}.${propertyName}) as ids_${primaryColumn.fullName}`;
if ((this.lockMode === "pessimistic_read" || this.lockMode === "pessimistic_write") && !queryRunner.isTransactionActive())
throw new PessimisticLockTransactionRequiredError();
if (this.lockMode === "optimistic") {
const metadata = this.connection.getMetadata(this.aliasMap.mainAlias.target);
if (!metadata.hasVersionColumn && !metadata.hasUpdateDateColumn)
throw new NoVersionOrUpdateDateColumnError(metadata.name);
}
const mainAliasName = this.fromTableName ? this.fromTableName : this.aliasMap.mainAlias.name;
let rawResults: any[];
if (this.skipNumber || this.takeNumber) {
// we are skipping order by here because its not working in subqueries anyway
// to make order by working we need to apply it on a distinct query
const [sql, parameters] = this.getSqlWithParameters({ skipOrderBy: true });
const [selects, orderBys] = this.createOrderByCombinedWithSelectExpression("distinctAlias");
const distinctAlias = this.connection.driver.escapeTableName("distinctAlias");
const metadata = this.connection.getMetadata(this.fromEntity.alias.target);
let idsQuery = `SELECT `;
idsQuery += metadata.primaryColumns.map((primaryColumn, index) => {
const propertyName = this.connection.driver.escapeAliasName(mainAliasName + "_" + primaryColumn.fullName);
if (index === 0) {
return `DISTINCT(${distinctAlias}.${propertyName}) as ids_${primaryColumn.fullName}`;
} else {
return `${distinctAlias}.${propertyName}) as ids_${primaryColumn.fullName}`;
}
}).join(", ");
if (selects.length > 0)
idsQuery += ", " + selects;
idsQuery += ` FROM (${sql}) ${distinctAlias}`; // TODO: WHAT TO DO WITH PARAMETERS HERE? DO THEY WORK?
if (orderBys.length > 0) {
idsQuery += " ORDER BY " + orderBys;
} else {
return `${distinctAlias}.${propertyName}) as ids_${primaryColumn.fullName}`;
idsQuery += ` ORDER BY "ids_${metadata.firstPrimaryColumn.fullName}"`; // this is required for mssql driver if firstResult is used. Other drivers don't care about it
}
}).join(", ");
if (selects.length > 0)
idsQuery += ", " + selects;
idsQuery += ` FROM (${sql}) ${distinctAlias}`; // TODO: WHAT TO DO WITH PARAMETERS HERE? DO THEY WORK?
if (this.connection.driver instanceof SqlServerDriver) { // todo: temporary. need to refactor and make a proper abstraction
if (orderBys.length > 0) {
idsQuery += " ORDER BY " + orderBys;
} else {
idsQuery += ` ORDER BY "ids_${metadata.firstPrimaryColumn.fullName}"`; // this is required for mssql driver if firstResult is used. Other drivers don't care about it
}
if (this.skipNumber || this.takeNumber) {
idsQuery += ` OFFSET ${this.skipNumber || 0} ROWS`;
if (this.takeNumber)
idsQuery += " FETCH NEXT " + this.takeNumber + " ROWS ONLY";
}
} else {
if (this.connection.driver instanceof SqlServerDriver) { // todo: temporary. need to refactor and make a proper abstraction
if (this.skipNumber || this.takeNumber) {
idsQuery += ` OFFSET ${this.skipNumber || 0} ROWS`;
if (this.takeNumber)
idsQuery += " FETCH NEXT " + this.takeNumber + " ROWS ONLY";
idsQuery += " LIMIT " + this.takeNumber;
if (this.skipNumber)
idsQuery += " OFFSET " + this.skipNumber;
}
} else {
if (this.takeNumber)
idsQuery += " LIMIT " + this.takeNumber;
if (this.skipNumber)
idsQuery += " OFFSET " + this.skipNumber;
}
try {
return await queryRunner.query(idsQuery, parameters)
.then((results: any[]) => {
rawResults = results;
@ -1007,17 +1038,10 @@ export class QueryBuilder<Entity> {
};
});
} finally {
if (this.hasOwnQueryRunner()) // means we created our own query runner
await queryRunner.release();
}
} else {
} else {
const [sql, parameters] = this.getSqlWithParameters();
const [sql, parameters] = this.getSqlWithParameters();
try {
// console.log(sql);
return await queryRunner.query(sql, parameters)
.then(results => {
rawResults = results;
@ -1045,11 +1069,11 @@ export class QueryBuilder<Entity> {
rawResults: rawResults
};
});
} finally {
if (this.hasOwnQueryRunner()) // means we created our own query runner
await queryRunner.release();
}
} finally {
if (this.hasOwnQueryRunner()) // means we created our own query runner
await queryRunner.release();
}
}
@ -1058,6 +1082,8 @@ export class QueryBuilder<Entity> {
* Count excludes all limitations set by setFirstResult and setMaxResults methods call.
*/
async getCount(): Promise<number> {
if (this.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError();
const queryRunner = await this.getQueryRunner();
@ -1102,24 +1128,34 @@ export class QueryBuilder<Entity> {
/**
* Gets all raw results returned by execution of generated query builder sql.
*/
getRawMany(): Promise<any[]> { // todo: rename to getRawMany
async getRawMany(): Promise<any[]> {
if (this.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError();
return this.execute();
}
/**
* Gets first raw result returned by execution of generated query builder sql.
*/
getRawOne(): Promise<any> { // todo: rename to getRawOne
return this.getRawMany().then(results => results[0]);
async getRawOne(): Promise<any> {
if (this.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError();
const results = await this.execute();
return results[0];
}
/**
* Gets entities and count returned by execution of generated query builder sql.
*/
getManyAndCount(): Promise<[Entity[], number]> {
async getManyAndCount(): Promise<[Entity[], number]> {
if (this.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError();
// todo: share database connection and counter
return Promise.all<any>([
return Promise.all([
this.getMany(),
this.getCount()
]);
@ -1128,17 +1164,38 @@ export class QueryBuilder<Entity> {
/**
* Gets entities returned by execution of generated query builder sql.
*/
getMany(): Promise<Entity[]> {
return this.getEntitiesAndRawResults().then(results => {
return results.entities;
});
async getMany(): Promise<Entity[]> {
if (this.lockMode === "optimistic")
throw new OptimisticLockCanNotBeUsedError();
const results = await this.getEntitiesAndRawResults();
return results.entities;
}
/**
* Gets single entity returned by execution of generated query builder sql.
*/
getOne(): Promise<Entity|undefined> {
return this.getMany().then(entities => entities[0]);
async getOne(): Promise<Entity|undefined> {
const results = await this.getEntitiesAndRawResults();
const result = results.entities[0] as any;
if (result && this.lockMode === "optimistic" && this.lockVersion) {
const metadata = this.connection.getMetadata(this.fromEntity.alias.target);
if (this.lockVersion instanceof Date) {
const actualVersion = result[metadata.updateDateColumn.propertyName];
this.lockVersion.setMilliseconds(0);
if (actualVersion.getTime() !== this.lockVersion.getTime())
throw new OptimisticLockVersionMismatchError(metadata.name, this.lockVersion, actualVersion);
} else {
const actualVersion = result[metadata.versionColumn.propertyName];
if (actualVersion !== this.lockVersion)
throw new OptimisticLockVersionMismatchError(metadata.name, this.lockVersion, actualVersion);
}
}
return result;
}
/**
@ -1447,10 +1504,22 @@ export class QueryBuilder<Entity> {
if (allSelects.length === 0)
allSelects.push("*");
let lock: string = "";
if (this.connection.driver instanceof SqlServerDriver) {
switch (this.lockMode) {
case "pessimistic_read":
lock = " WITH (HOLDLOCK, ROWLOCK)";
break;
case "pessimistic_write":
lock = " WITH (UPDLOCK, ROWLOCK)";
break;
}
}
// create a selection query
switch (this.type) {
case "select":
return "SELECT " + allSelects.join(", ") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + this.connection.driver.escapeAliasName(alias);
return "SELECT " + allSelects.join(", ") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + this.connection.driver.escapeAliasName(alias) + lock;
case "delete":
return "DELETE FROM " + this.connection.driver.escapeTableName(tableName);
// return "DELETE " + (alias ? this.connection.driver.escapeAliasName(alias) : "") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + (alias ? this.connection.driver.escapeAliasName(alias) : ""); // TODO: only mysql supports aliasing, so what to do with aliases in DELETE queries? right now aliases are used however we are relaying that they will always match a table names
@ -1730,16 +1799,46 @@ export class QueryBuilder<Entity> {
return "";
}
protected createLimitExpression() {
protected createLimitExpression(): string {
if (!this.limit) return "";
return " LIMIT " + this.limit;
}
protected createOffsetExpression() {
protected createOffsetExpression(): string {
if (!this.offset) return "";
return " OFFSET " + this.offset;
}
protected createLockExpression(): string {
switch (this.lockMode) {
case "pessimistic_read":
if (this.connection.driver instanceof MysqlDriver) {
return " LOCK IN SHARE MODE";
} else if (this.connection.driver instanceof PostgresDriver) {
return " FOR SHARE";
} else if (this.connection.driver instanceof SqlServerDriver) {
return "";
} else {
throw new LockNotSupportedOnGivenDriverError();
}
case "pessimistic_write":
if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof PostgresDriver) {
return " FOR UPDATE";
} else if (this.connection.driver instanceof SqlServerDriver) {
return "";
} else {
throw new LockNotSupportedOnGivenDriverError();
}
default:
return "";
}
}
private extractJoinMappings(): JoinMapping[] {
const mappings: JoinMapping[] = [];
this.joins

View File

@ -0,0 +1,13 @@
/**
* Thrown when selected sql driver does not supports locking.
*/
export class LockNotSupportedOnGivenDriverError extends Error {
name = "LockNotSupportedOnGivenDriverError";
constructor() {
super();
Object.setPrototypeOf(this, LockNotSupportedOnGivenDriverError.prototype);
this.message = `Locking not supported on giver driver.`;
}
}

View File

@ -0,0 +1,13 @@
/**
* Thrown when an entity does not have no version and no update date column.
*/
export class NoVersionOrUpdateDateColumnError extends Error {
name = "NoVersionOrUpdateDateColumnError";
constructor(entity: string) {
super();
Object.setPrototypeOf(this, NoVersionOrUpdateDateColumnError.prototype);
this.message = `Entity ${entity} does not have version or update date columns.`;
}
}

View File

@ -0,0 +1,13 @@
/**
* Thrown when an optimistic lock cannot be used in query builder.
*/
export class OptimisticLockCanNotBeUsedError extends Error {
name = "OptimisticLockCanNotBeUsedError";
constructor() {
super();
Object.setPrototypeOf(this, OptimisticLockCanNotBeUsedError.prototype);
this.message = `The optimistic lock can be used only with getOne() method.`;
}
}

View File

@ -0,0 +1,13 @@
/**
* Thrown when a version check on an object that uses optimistic locking through a version field fails.
*/
export class OptimisticLockVersionMismatchError extends Error {
name = "OptimisticLockVersionMismatchError";
constructor(entity: string, expectedVersion: number|Date, actualVersion: number|Date) {
super();
Object.setPrototypeOf(this, OptimisticLockVersionMismatchError.prototype);
this.message = `The optimistic lock on entity ${entity} failed, version ${expectedVersion} was expected, but is actually ${actualVersion}.`;
}
}

View File

@ -0,0 +1,13 @@
/**
* Thrown when a transaction is required for the current operation, but there is none open.
*/
export class PessimisticLockTransactionRequiredError extends Error {
name = "PessimisticLockTransactionRequiredError";
constructor() {
super();
Object.setPrototypeOf(this, PessimisticLockTransactionRequiredError.prototype);
this.message = `An open transaction is required for pessimistic lock.`;
}
}

View File

@ -163,6 +163,7 @@ export class Repository<Entity extends ObjectLiteral> {
const queryRunnerProvider = this.queryRunnerProvider || new QueryRunnerProvider(this.connection.driver, true);
try {
const transactionEntityManager = this.connection.createEntityManagerWithSingleDatabaseConnection(queryRunnerProvider);
// transactionEntityManager.data =
const databaseEntityLoader = new SubjectBuilder(this.connection, queryRunnerProvider);
await databaseEntityLoader.persist(entityOrEntities, this.metadata);

View File

@ -0,0 +1,18 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../../src/decorator/columns/Column";
import {UpdateDateColumn} from "../../../../../src/decorator/columns/UpdateDateColumn";
@Entity()
export class PostWithUpdateDate {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@UpdateDateColumn()
updateDate: Date;
}

View File

@ -0,0 +1,18 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../../src/decorator/columns/Column";
import {VersionColumn} from "../../../../../src/decorator/columns/VersionColumn";
@Entity()
export class PostWithVersion {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@VersionColumn()
version: number;
}

View File

@ -0,0 +1,22 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../../src/decorator/columns/Column";
import {VersionColumn} from "../../../../../src/decorator/columns/VersionColumn";
import {UpdateDateColumn} from "../../../../../src/decorator/columns/UpdateDateColumn";
@Entity()
export class PostWithVersionAndUpdatedDate {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@VersionColumn()
version: number;
@UpdateDateColumn()
updateDate: Date;
}

View File

@ -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 PostWithoutVersionAndUpdateDate {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
}

View File

@ -0,0 +1,280 @@
import "reflect-metadata";
import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Connection} from "../../../../src/connection/Connection";
import {PostWithVersion} from "./entity/PostWithVersion";
import {expect} from "chai";
import {PostWithoutVersionAndUpdateDate} from "./entity/PostWithoutVersionAndUpdateDate";
import {PostWithUpdateDate} from "./entity/PostWithUpdateDate";
import {PostWithVersionAndUpdatedDate} from "./entity/PostWithVersionAndUpdatedDate";
import {OptimisticLockVersionMismatchError} from "../../../../src/query-builder/error/OptimisticLockVersionMismatchError";
import {OptimisticLockCanNotBeUsedError} from "../../../../src/query-builder/error/OptimisticLockCanNotBeUsedError";
import {NoVersionOrUpdateDateColumnError} from "../../../../src/query-builder/error/NoVersionOrUpdateDateColumnError";
import {PessimisticLockTransactionRequiredError} from "../../../../src/query-builder/error/PessimisticLockTransactionRequiredError";
import {MysqlDriver} from "../../../../src/driver/mysql/MysqlDriver";
import {PostgresDriver} from "../../../../src/driver/postgres/PostgresDriver";
import {SqlServerDriver} from "../../../../src/driver/sqlserver/SqlServerDriver";
import {SqliteDriver} from "../../../../src/driver/sqlite/SqliteDriver";
import {OracleDriver} from "../../../../src/driver/oracle/OracleDriver";
import {LockNotSupportedOnGivenDriverError} from "../../../../src/query-builder/error/LockNotSupportedOnGivenDriverError";
describe.only("query builder > locking", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchemaOnConnection: true,
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("should not attach pessimistic read lock statement on query if locking is not used", () => Promise.all(connections.map(async connection => {
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
return;
const sql = connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.where("post.id = :id", { id: 1 })
.getSql();
if (connection.driver instanceof MysqlDriver) {
expect(sql.indexOf("LOCK IN SHARE MODE") === -1).to.be.true;
} else if (connection.driver instanceof PostgresDriver) {
expect(sql.indexOf("FOR SHARE") === -1).to.be.true;
} else if (connection.driver instanceof SqlServerDriver) {
expect(sql.indexOf("WITH (HOLDLOCK, ROWLOCK)") === -1).to.be.true;
}
})));
it("should throw error if pessimistic lock used without transaction", () => Promise.all(connections.map(async connection => {
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
return;
return Promise.all([
connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("pessimistic_read")
.where("post.id = :id", { id: 1 })
.getOne().should.be.rejectedWith(PessimisticLockTransactionRequiredError),
connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("pessimistic_write")
.where("post.id = :id", { id: 1 })
.getOne().should.be.rejectedWith(PessimisticLockTransactionRequiredError)
]);
})));
it("should not throw error if pessimistic lock used with transaction", () => Promise.all(connections.map(async connection => {
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
return;
return connection.entityManager.transaction(entityManager => {
return Promise.all([
entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("pessimistic_read")
.where("post.id = :id", { id: 1 })
.getOne().should.not.be.rejected,
entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("pessimistic_write")
.where("post.id = :id", { id: 1 })
.getOne().should.not.be.rejected
]);
});
})));
it("should attach pessimistic read lock statement on query if locking enabled", () => Promise.all(connections.map(async connection => {
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
return;
const sql = connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("pessimistic_read")
.where("post.id = :id", { id: 1 })
.getSql();
if (connection.driver instanceof MysqlDriver) {
expect(sql.indexOf("LOCK IN SHARE MODE") !== -1).to.be.true;
} else if (connection.driver instanceof PostgresDriver) {
expect(sql.indexOf("FOR SHARE") !== -1).to.be.true;
} else if (connection.driver instanceof SqlServerDriver) {
expect(sql.indexOf("WITH (HOLDLOCK, ROWLOCK)") !== -1).to.be.true;
}
})));
it("should not attach pessimistic write lock statement on query if locking is not used", () => Promise.all(connections.map(async connection => {
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
return;
const sql = connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.where("post.id = :id", { id: 1 })
.getSql();
if (connection.driver instanceof MysqlDriver || connection.driver instanceof PostgresDriver) {
expect(sql.indexOf("FOR UPDATE") === -1).to.be.true;
} else if (connection.driver instanceof SqlServerDriver) {
expect(sql.indexOf("WITH (UPDLOCK, ROWLOCK)") === -1).to.be.true;
}
})));
it("should attach pessimistic write lock statement on query if locking enabled", () => Promise.all(connections.map(async connection => {
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
return;
const sql = connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("pessimistic_write")
.where("post.id = :id", { id: 1 })
.getSql();
if (connection.driver instanceof MysqlDriver || connection.driver instanceof PostgresDriver) {
expect(sql.indexOf("FOR UPDATE") !== -1).to.be.true;
} else if (connection.driver instanceof SqlServerDriver) {
expect(sql.indexOf("WITH (UPDLOCK, ROWLOCK)") !== -1).to.be.true;
}
})));
it("should throw error if optimistic lock used with getMany method", () => Promise.all(connections.map(async connection => {
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("optimistic", 1)
.getMany().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
})));
it("should throw error if optimistic lock used with getCount method", () => Promise.all(connections.map(async connection => {
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("optimistic", 1)
.getCount().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
})));
it("should throw error if optimistic lock used with getManyAndCount method", () => Promise.all(connections.map(async connection => {
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("optimistic", 1)
.getManyAndCount().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
})));
it("should throw error if optimistic lock used with getRawMany method", () => Promise.all(connections.map(async connection => {
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("optimistic", 1)
.getRawMany().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
})));
it("should throw error if optimistic lock used with getRawOne method", () => Promise.all(connections.map(async connection => {
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("optimistic", 1)
.where("post.id = :id", { id: 1 })
.getRawOne().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
})));
it("should not throw error if optimistic lock used with getOne method", () => Promise.all(connections.map(async connection => {
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("optimistic", 1)
.where("post.id = :id", { id: 1 })
.getOne().should.not.be.rejected;
})));
it("should throw error if entity does not have version and update date columns", () => Promise.all(connections.map(async connection => {
const post = new PostWithoutVersionAndUpdateDate();
post.title = "New post";
await connection.entityManager.persist(post);
return connection.entityManager.createQueryBuilder(PostWithoutVersionAndUpdateDate, "post")
.setLock("optimistic", 1)
.where("post.id = :id", { id: 1 })
.getOne().should.be.rejectedWith(NoVersionOrUpdateDateColumnError);
})));
it("should throw error if actual version does not equal expected version", () => Promise.all(connections.map(async connection => {
const post = new PostWithVersion();
post.title = "New post";
await connection.entityManager.persist(post);
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("optimistic", 2)
.where("post.id = :id", { id: 1 })
.getOne().should.be.rejectedWith(OptimisticLockVersionMismatchError);
})));
it("should not throw error if actual version and expected versions are equal", () => Promise.all(connections.map(async connection => {
const post = new PostWithVersion();
post.title = "New post";
await connection.entityManager.persist(post);
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("optimistic", 1)
.where("post.id = :id", { id: 1 })
.getOne().should.not.be.rejected;
})));
it("should throw error if actual updated date does not equal expected updated date", () => Promise.all(connections.map(async connection => {
const post = new PostWithUpdateDate();
post.title = "New post";
await connection.entityManager.persist(post);
return connection.entityManager.createQueryBuilder(PostWithUpdateDate, "post")
.setLock("optimistic", new Date(2017, 1, 1))
.where("post.id = :id", { id: 1 })
.getOne().should.be.rejectedWith(OptimisticLockVersionMismatchError);
})));
it("should not throw error if actual updated date and expected updated date are equal", () => Promise.all(connections.map(async connection => {
const post = new PostWithUpdateDate();
post.title = "New post";
await connection.entityManager.persist(post);
return connection.entityManager.createQueryBuilder(PostWithUpdateDate, "post")
.setLock("optimistic", post.updateDate)
.where("post.id = :id", { id: 1 })
.getOne().should.not.be.rejected;
})));
it("should work if both version and update date columns applied", () => Promise.all(connections.map(async connection => {
const post = new PostWithVersionAndUpdatedDate();
post.title = "New post";
await connection.entityManager.persist(post);
return Promise.all([
connection.entityManager.createQueryBuilder(PostWithVersionAndUpdatedDate, "post")
.setLock("optimistic", post.updateDate)
.where("post.id = :id", { id: 1 })
.getOne().should.not.be.rejected,
connection.entityManager.createQueryBuilder(PostWithVersionAndUpdatedDate, "post")
.setLock("optimistic", 1)
.where("post.id = :id", { id: 1 })
.getOne().should.not.be.rejected
]);
})));
it("should throw error if pessimistic locking not supported by given driver", () => Promise.all(connections.map(async connection => {
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
return connection.entityManager.transaction(entityManager => {
return Promise.all([
entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("pessimistic_read")
.where("post.id = :id", { id: 1 })
.getOne().should.be.rejectedWith(LockNotSupportedOnGivenDriverError),
entityManager.createQueryBuilder(PostWithVersion, "post")
.setLock("pessimistic_write")
.where("post.id = :id", { id: 1 })
.getOne().should.be.rejectedWith(LockNotSupportedOnGivenDriverError)
]);
});
})));
});

View File

@ -42,6 +42,11 @@ export interface TestingOptions {
*/
entities?: string[]|Function[];
/**
* Subscribers needs to be included in the connection for the given test suite.
*/
subscribers?: string[]|Function[];
/**
* Entity schemas needs to be included in the connection for the given test suite.
*/
@ -73,6 +78,7 @@ export function setupSingleTestingConnection(driverType: DriverType, options: Te
const testingConnections = setupTestingConnections({
name: options.name ? options.name : undefined,
entities: options.entities ? options.entities : [],
subscribers: options.subscribers ? options.subscribers : [],
entitySchemas: options.entitySchemas ? options.entitySchemas : [],
dropSchemaOnConnection: options.dropSchemaOnConnection ? options.dropSchemaOnConnection : false,
schemaCreate: options.schemaCreate ? options.schemaCreate : false,
@ -133,6 +139,7 @@ export function setupTestingConnections(options?: TestingOptions) {
const newConnectionOptions = Object.assign({}, connectionOptions as ConnectionOptions, {
name: options && options.name ? options.name : connectionOptions.name,
entities: options && options.entities ? options.entities : [],
subscribers: options && options.subscribers ? options.subscribers : [],
entitySchemas: options && options.entitySchemas ? options.entitySchemas : [],
autoSchemaSync: options && options.entities ? options.schemaCreate : false,
dropSchemaOnConnection: options && options.entities ? options.dropSchemaOnConnection : false,