Merge pull request #11264 from alumni/fix-transaction-issues

This commit is contained in:
Lucian Mocanu 2025-03-03 20:30:50 +01:00 committed by GitHub
commit 673b6ceb88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 748 additions and 955 deletions

View File

@ -2,7 +2,6 @@ version: "3"
services:
# mysql
mysql:
platform: linux/amd64
image: "mysql:5.7.37"
container_name: "typeorm-mysql"
ports:

View File

@ -99,12 +99,11 @@ export class AuroraMysqlQueryRunner
}
if (this.transactionDepth === 0) {
this.transactionDepth += 1
await this.client.startTransaction()
} else {
this.transactionDepth += 1
await this.query(`SAVEPOINT typeorm_${this.transactionDepth - 1}`)
await this.query(`SAVEPOINT typeorm_${this.transactionDepth}`)
}
this.transactionDepth += 1
await this.broadcaster.broadcast("AfterTransactionStart")
}
@ -119,15 +118,14 @@ export class AuroraMysqlQueryRunner
await this.broadcaster.broadcast("BeforeTransactionCommit")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`RELEASE SAVEPOINT typeorm_${this.transactionDepth}`,
`RELEASE SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.client.commitTransaction()
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionCommit")
}
@ -142,15 +140,14 @@ export class AuroraMysqlQueryRunner
await this.broadcaster.broadcast("BeforeTransactionRollback")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth}`,
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.client.rollbackTransaction()
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionRollback")
}

View File

@ -110,12 +110,11 @@ export class AuroraPostgresQueryRunner
}
if (this.transactionDepth === 0) {
this.transactionDepth += 1
await this.client.startTransaction()
} else {
this.transactionDepth += 1
await this.query(`SAVEPOINT typeorm_${this.transactionDepth} - 1`)
await this.query(`SAVEPOINT typeorm_${this.transactionDepth}`)
}
this.transactionDepth += 1
await this.broadcaster.broadcast("AfterTransactionStart")
}
@ -130,15 +129,14 @@ export class AuroraPostgresQueryRunner
await this.broadcaster.broadcast("BeforeTransactionCommit")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`RELEASE SAVEPOINT typeorm_${this.transactionDepth}`,
`RELEASE SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.client.commitTransaction()
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionCommit")
}
@ -153,15 +151,14 @@ export class AuroraPostgresQueryRunner
await this.broadcaster.broadcast("BeforeTransactionRollback")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth}`,
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.client.rollbackTransaction()
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionRollback")
}

View File

@ -190,7 +190,6 @@ export class CockroachQueryRunner
}
if (this.transactionDepth === 0) {
this.transactionDepth += 1
await this.query("START TRANSACTION")
await this.query("SAVEPOINT cockroach_restart")
if (isolationLevel) {
@ -199,10 +198,10 @@ export class CockroachQueryRunner
)
}
} else {
this.transactionDepth += 1
await this.query(`SAVEPOINT typeorm_${this.transactionDepth - 1}`)
await this.query(`SAVEPOINT typeorm_${this.transactionDepth}`)
}
this.transactionDepth += 1
this.storeQueries = true
await this.broadcaster.broadcast("AfterTransactionStart")
@ -218,20 +217,18 @@ export class CockroachQueryRunner
await this.broadcaster.broadcast("BeforeTransactionCommit")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`RELEASE SAVEPOINT typeorm_${this.transactionDepth}`,
`RELEASE SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
this.transactionDepth -= 1
} else {
this.storeQueries = false
this.transactionDepth -= 1
// This was disabled because it failed tests after update to CRDB 24.2
// https://github.com/typeorm/typeorm/pull/11190
// await this.query("RELEASE SAVEPOINT cockroach_restart")
await this.query("RELEASE SAVEPOINT cockroach_restart")
await this.query("COMMIT")
this.queries = []
this.isTransactionActive = false
this.transactionRetries = 0
this.transactionDepth -= 1
}
await this.broadcaster.broadcast("AfterTransactionCommit")
@ -247,18 +244,17 @@ export class CockroachQueryRunner
await this.broadcaster.broadcast("BeforeTransactionRollback")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth}`,
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.storeQueries = false
this.transactionDepth -= 1
await this.query("ROLLBACK")
this.queries = []
this.isTransactionActive = false
this.transactionRetries = 0
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionRollback")
}

View File

@ -119,7 +119,6 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
throw err
}
if (this.transactionDepth === 0) {
this.transactionDepth += 1
if (isolationLevel) {
await this.query(
"SET TRANSACTION ISOLATION LEVEL " + isolationLevel,
@ -127,9 +126,9 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
}
await this.query("START TRANSACTION")
} else {
this.transactionDepth += 1
await this.query(`SAVEPOINT typeorm_${this.transactionDepth - 1}`)
await this.query(`SAVEPOINT typeorm_${this.transactionDepth}`)
}
this.transactionDepth += 1
await this.broadcaster.broadcast("AfterTransactionStart")
}
@ -144,15 +143,14 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
await this.broadcaster.broadcast("BeforeTransactionCommit")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`RELEASE SAVEPOINT typeorm_${this.transactionDepth}`,
`RELEASE SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.query("COMMIT")
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionCommit")
}
@ -167,15 +165,14 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
await this.broadcaster.broadcast("BeforeTransactionRollback")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth}`,
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.query("ROLLBACK")
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionRollback")
}

View File

@ -136,14 +136,13 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
}
if (this.transactionDepth === 0) {
this.transactionDepth += 1
await this.query(
"SET TRANSACTION ISOLATION LEVEL " + isolationLevel,
)
} else {
this.transactionDepth += 1
await this.query(`SAVEPOINT typeorm_${this.transactionDepth - 1}`)
await this.query(`SAVEPOINT typeorm_${this.transactionDepth}`)
}
this.transactionDepth += 1
await this.broadcaster.broadcast("AfterTransactionStart")
}
@ -176,15 +175,14 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
await this.broadcaster.broadcast("BeforeTransactionRollback")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth}`,
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.query("ROLLBACK")
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionRollback")
}

View File

@ -174,7 +174,6 @@ export class PostgresQueryRunner
}
if (this.transactionDepth === 0) {
this.transactionDepth += 1
await this.query("START TRANSACTION")
if (isolationLevel) {
await this.query(
@ -182,9 +181,9 @@ export class PostgresQueryRunner
)
}
} else {
this.transactionDepth += 1
await this.query(`SAVEPOINT typeorm_${this.transactionDepth - 1}`)
await this.query(`SAVEPOINT typeorm_${this.transactionDepth}`)
}
this.transactionDepth += 1
await this.broadcaster.broadcast("AfterTransactionStart")
}
@ -199,15 +198,14 @@ export class PostgresQueryRunner
await this.broadcaster.broadcast("BeforeTransactionCommit")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`RELEASE SAVEPOINT typeorm_${this.transactionDepth}`,
`RELEASE SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.query("COMMIT")
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionCommit")
}
@ -222,15 +220,14 @@ export class PostgresQueryRunner
await this.broadcaster.broadcast("BeforeTransactionRollback")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth}`,
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.query("ROLLBACK")
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionRollback")
}

View File

@ -101,7 +101,6 @@ export abstract class AbstractSqliteQueryRunner
}
if (this.transactionDepth === 0) {
this.transactionDepth += 1
if (isolationLevel) {
if (isolationLevel === "READ UNCOMMITTED") {
await this.query("PRAGMA read_uncommitted = true")
@ -111,9 +110,9 @@ export abstract class AbstractSqliteQueryRunner
}
await this.query("BEGIN TRANSACTION")
} else {
this.transactionDepth += 1
await this.query(`SAVEPOINT typeorm_${this.transactionDepth - 1}`)
await this.query(`SAVEPOINT typeorm_${this.transactionDepth}`)
}
this.transactionDepth += 1
await this.broadcaster.broadcast("AfterTransactionStart")
}
@ -128,15 +127,14 @@ export abstract class AbstractSqliteQueryRunner
await this.broadcaster.broadcast("BeforeTransactionCommit")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`RELEASE SAVEPOINT typeorm_${this.transactionDepth}`,
`RELEASE SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.query("COMMIT")
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionCommit")
}
@ -151,15 +149,14 @@ export abstract class AbstractSqliteQueryRunner
await this.broadcaster.broadcast("BeforeTransactionRollback")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth}`,
`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth - 1}`,
)
} else {
this.transactionDepth -= 1
await this.query("ROLLBACK")
this.isTransactionActive = false
}
this.transactionDepth -= 1
await this.broadcaster.broadcast("AfterTransactionRollback")
}

View File

@ -107,7 +107,6 @@ export class SqlServerQueryRunner
}
if (this.transactionDepth === 0) {
this.transactionDepth += 1
const pool = await (this.mode === "slave"
? this.driver.obtainSlaveConnection()
: this.driver.obtainMasterConnection())
@ -125,12 +124,12 @@ export class SqlServerQueryRunner
this.databaseConnection.begin(transactionCallback)
}
} else {
this.transactionDepth += 1
await this.query(
`SAVE TRANSACTION typeorm_${this.transactionDepth - 1}`,
`SAVE TRANSACTION typeorm_${this.transactionDepth}`,
)
ok()
}
this.transactionDepth += 1
})
await this.broadcaster.broadcast("AfterTransactionStart")
@ -149,7 +148,6 @@ export class SqlServerQueryRunner
if (this.transactionDepth === 1) {
return new Promise<void>((ok, fail) => {
this.transactionDepth -= 1
this.databaseConnection.commit(async (err: any) => {
if (err) return fail(err)
this.isTransactionActive = false
@ -159,6 +157,7 @@ export class SqlServerQueryRunner
ok()
this.connection.logger.logQuery("COMMIT")
this.transactionDepth -= 1
})
})
}
@ -177,13 +176,12 @@ export class SqlServerQueryRunner
await this.broadcaster.broadcast("BeforeTransactionRollback")
if (this.transactionDepth > 1) {
this.transactionDepth -= 1
await this.query(
`ROLLBACK TRANSACTION typeorm_${this.transactionDepth}`,
`ROLLBACK TRANSACTION typeorm_${this.transactionDepth - 1}`,
)
this.transactionDepth -= 1
} else {
return new Promise<void>((ok, fail) => {
this.transactionDepth -= 1
this.databaseConnection.rollback(async (err: any) => {
if (err) return fail(err)
this.isTransactionActive = false
@ -193,6 +191,7 @@ export class SqlServerQueryRunner
ok()
this.connection.logger.logQuery("ROLLBACK")
this.transactionDepth -= 1
})
})
}

View File

@ -581,6 +581,7 @@ export class RelationIdLoader {
entities: ObjectLiteral[],
relatedEntities?: ObjectLiteral[],
) {
const originalRelation = relation
relation = relation.inverseRelation!
if (
@ -611,7 +612,7 @@ export class RelationIdLoader {
const primaryColumnName =
joinColumn.entityMetadata.name +
"_" +
relation.inverseRelation!.propertyPath.replace(
originalRelation.propertyPath.replace(
".",
"_",
) +
@ -636,7 +637,7 @@ export class RelationIdLoader {
undefined,
primaryColumn.entityMetadata.name +
"_" +
relation.inverseRelation!.propertyPath.replace(".", "_") +
originalRelation.propertyPath.replace(".", "_") +
"_" +
primaryColumn.propertyPath.replace(".", "_"),
)

View File

@ -1,15 +1,17 @@
import "reflect-metadata"
import { expect } from "chai"
import { DataSource } from "../../../src/data-source/DataSource"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { DataSource } from "../../../src/data-source/DataSource"
import { Company } from "./entity/Company"
import { Office } from "./entity/Office"
import { expect } from "chai"
describe("deferrable uq constraints should be check at the end of transaction", () => {
describe("deferrable unique constraint", () => {
let connections: DataSource[]
before(
async () =>
@ -21,7 +23,7 @@ describe("deferrable uq constraints should be check at the end of transaction",
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
it("use initially deferred deferrable uq constraints", () =>
it("initially deferred unique should be validated at the end of transaction", () =>
Promise.all(
connections.map(async (connection) => {
await connection.manager.transaction(async (entityManager) => {
@ -63,7 +65,7 @@ describe("deferrable uq constraints should be check at the end of transaction",
}),
))
it("use initially immediated deferrable uq constraints", () =>
it("initially immediate unique should be validated at the end at transaction with deferred check time", () =>
Promise.all(
connections.map(async (connection) => {
await connection.manager.transaction(async (entityManager) => {

View File

@ -1,28 +1,30 @@
import "reflect-metadata"
import { expect } from "chai"
import { DataSource } from "../../../src/data-source/DataSource"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { DataSource } from "../../../src/data-source/DataSource"
import { Company } from "./entity/Company"
import { Office } from "./entity/Office"
import { User } from "./entity/User"
import { expect } from "chai"
describe("deferrable fk constraints should be check at the end of transaction (#2191)", () => {
describe("deferrable foreign key constraint", () => {
let connections: DataSource[]
before(
async () =>
(connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["postgres"],
enabledDrivers: ["better-sqlite3", "postgres", "sap", "sqlite"],
})),
)
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
it("use initially deferred deferrable fk constraints", () =>
it("initially deferred fk should be validated at the end of transaction", () =>
Promise.all(
connections.map(async (connection) => {
await connection.manager.transaction(async (entityManager) => {
@ -40,7 +42,7 @@ describe("deferrable fk constraints should be check at the end of transaction (#
company.name = "Acme"
await entityManager.save(company)
})
}).should.not.be.rejected
// now check
const user = await connection.manager.findOne(User, {
@ -48,9 +50,7 @@ describe("deferrable fk constraints should be check at the end of transaction (#
where: { id: 1 },
})
expect(user).not.to.be.null
user!.should.be.eql({
expect(user).to.deep.equal({
id: 1,
name: "Bob",
company: {
@ -61,9 +61,12 @@ describe("deferrable fk constraints should be check at the end of transaction (#
}),
))
it("use initially immediated deferrable fk constraints", () =>
it("initially immediate fk should be validated at the end at transaction with deferred check time", () =>
Promise.all(
connections.map(async (connection) => {
// changing the constraint check time is only supported on postgres
if (connection.driver.options.type !== "postgres") return
await connection.manager.transaction(async (entityManager) => {
// first set constraints deferred manually
await entityManager.query("SET CONSTRAINTS ALL DEFERRED")
@ -82,7 +85,7 @@ describe("deferrable fk constraints should be check at the end of transaction (#
company.name = "Emca"
await entityManager.save(company)
})
}).should.not.be.rejected
// now check
const office = await connection.manager.findOne(Office, {
@ -90,9 +93,7 @@ describe("deferrable fk constraints should be check at the end of transaction (#
where: { id: 2 },
})
expect(office).not.to.be.null
office!.should.be.eql({
expect(office).to.deep.equal({
id: 2,
name: "Barcelona",
company: {

View File

@ -1,6 +1,6 @@
import { Entity } from "../../../../src/decorator/entity/Entity"
import { Column } from "../../../../src/decorator/columns/Column"
import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn"
import { Entity } from "../../../../src/decorator/entity/Entity"
import { Unique } from "../../../../src/decorator/Unique"
@Entity()

View File

@ -1,8 +1,9 @@
import { Entity } from "../../../../src/decorator/entity/Entity"
import { Column } from "../../../../src/decorator/columns/Column"
import { ManyToOne } from "../../../../src/decorator/relations/ManyToOne"
import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn"
import { Entity } from "../../../../src/decorator/entity/Entity"
import { ManyToOne } from "../../../../src/decorator/relations/ManyToOne"
import { Unique } from "../../../../src/decorator/Unique"
import { Company } from "./Company"
@Entity()
@ -14,8 +15,6 @@ export class Office {
@Column()
name: string
@ManyToOne((type) => Company, (company) => company.id, {
deferrable: "INITIALLY IMMEDIATE",
})
@ManyToOne(() => Company, { deferrable: "INITIALLY IMMEDIATE" })
company: Company
}

View File

@ -1,7 +1,8 @@
import { Entity } from "../../../../src/decorator/entity/Entity"
import { Column } from "../../../../src/decorator/columns/Column"
import { ManyToOne } from "../../../../src/decorator/relations/ManyToOne"
import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn"
import { Entity } from "../../../../src/decorator/entity/Entity"
import { ManyToOne } from "../../../../src/decorator/relations/ManyToOne"
import { Company } from "./Company"
@Entity()
@ -12,8 +13,6 @@ export class User {
@Column()
name: string
@ManyToOne((type) => Company, (company) => company.id, {
deferrable: "INITIALLY DEFERRED",
})
@ManyToOne(() => Company, { deferrable: "INITIALLY DEFERRED" })
company: Company
}

View File

@ -39,21 +39,22 @@ describe("repository > find options > locking", () => {
DriverUtils.isSQLiteFamily(connection.driver) ||
connection.driver.options.type === "sap" ||
connection.driver.options.type === "spanner"
)
) {
return
}
if (connection.driver.options.type === "cockroachdb") {
return Promise.all([
connection
.getRepository(PostWithVersion)
.findOne({
where: { id: 1 },
lock: { mode: "pessimistic_write" },
})
.should.be.rejectedWith(
PessimisticLockTransactionRequiredError,
),
])
await connection
.getRepository(PostWithVersion)
.findOne({
where: { id: 1 },
lock: { mode: "pessimistic_write" },
})
.should.be.rejectedWith(
PessimisticLockTransactionRequiredError,
)
return
}
return Promise.all([
@ -91,20 +92,16 @@ describe("repository > find options > locking", () => {
return
if (connection.driver.options.type === "cockroachdb") {
return connection.manager.transaction((entityManager) => {
return Promise.all([
entityManager
.getRepository(PostWithVersion)
.findOne({
where: { id: 1 },
lock: { mode: "pessimistic_write" },
}).should.not.be.rejected,
])
})
return connection.manager.transaction((entityManager) =>
entityManager.getRepository(PostWithVersion).findOne({
where: { id: 1 },
lock: { mode: "pessimistic_write" },
}),
).should.not.be.rejected
}
return connection.manager.transaction((entityManager) => {
return Promise.all([
return connection.manager.transaction((entityManager) =>
Promise.all([
entityManager.getRepository(PostWithVersion).find({
where: { id: 1 },
lock: { mode: "pessimistic_read" },
@ -114,8 +111,8 @@ describe("repository > find options > locking", () => {
where: { id: 1 },
lock: { mode: "pessimistic_write" },
}).should.not.be.rejected,
])
})
]),
)
}),
))
@ -150,19 +147,15 @@ describe("repository > find options > locking", () => {
})
if (DriverUtils.isMySQLFamily(connection.driver)) {
expect(executedSql[0].indexOf("LOCK IN SHARE MODE") !== -1)
.to.be.true
expect(executedSql[0]).to.contain("LOCK IN SHARE MODE")
} else if (connection.driver.options.type === "postgres") {
expect(executedSql[0].indexOf("FOR SHARE") !== -1).to.be
.true
expect(executedSql[0]).to.contain("FOR SHARE")
} else if (connection.driver.options.type === "oracle") {
expect(executedSql[0].indexOf("FOR UPDATE") !== -1).to.be
.true
expect(executedSql[0]).to.contain("FOR UPDATE")
} else if (connection.driver.options.type === "mssql") {
expect(
executedSql[0].indexOf("WITH (HOLDLOCK, ROWLOCK)") !==
-1,
).to.be.true
expect(executedSql[0]).to.contain(
"WITH (HOLDLOCK, ROWLOCK)",
)
}
}),
))
@ -170,7 +163,9 @@ describe("repository > find options > locking", () => {
it("should attach for no key update lock statement on query if locking enabled", () =>
Promise.all(
connections.map(async (connection) => {
if (!(connection.driver.options.type === "postgres")) return
if (connection.driver.options.type !== "postgres") {
return
}
const executedSql: string[] = []
@ -193,8 +188,7 @@ describe("repository > find options > locking", () => {
})
})
expect(executedSql.join(" ").includes("FOR NO KEY UPDATE")).to
.be.true
expect(executedSql.join(" ")).to.contain("FOR NO KEY UPDATE")
}),
))
@ -224,8 +218,7 @@ describe("repository > find options > locking", () => {
})
})
expect(executedSql.join(" ").includes("FOR KEY SHARE")).to.be
.true
expect(executedSql.join(" ")).to.contain("FOR KEY SHARE")
}),
))
@ -241,8 +234,9 @@ describe("repository > find options > locking", () => {
"8.0.0",
))
)
)
) {
return
}
const executedSql: string[] = []
@ -268,8 +262,9 @@ describe("repository > find options > locking", () => {
})
})
expect(executedSql.join(" ").includes("FOR SHARE SKIP LOCKED"))
.to.be.true
expect(executedSql.join(" ")).to.contain(
"FOR SHARE SKIP LOCKED",
)
}),
))
@ -285,8 +280,9 @@ describe("repository > find options > locking", () => {
"8.0.0",
))
)
)
) {
return
}
const executedSql: string[] = []
@ -312,8 +308,7 @@ describe("repository > find options > locking", () => {
})
})
expect(executedSql.join(" ").includes("FOR UPDATE NOWAIT")).to
.be.true
expect(executedSql.join(" ")).to.contain("FOR UPDATE NOWAIT")
}),
))
@ -324,8 +319,9 @@ describe("repository > find options > locking", () => {
DriverUtils.isSQLiteFamily(connection.driver) ||
connection.driver.options.type === "sap" ||
connection.driver.options.type === "spanner"
)
) {
return
}
const executedSql: string[] = []
@ -351,13 +347,9 @@ describe("repository > find options > locking", () => {
connection.driver.options.type === "postgres" ||
connection.driver.options.type === "oracle"
) {
expect(executedSql[0].indexOf("FOR UPDATE") !== -1).to.be
.true
expect(executedSql[0]).to.contain("FOR UPDATE")
} else if (connection.driver.options.type === "mssql") {
expect(
executedSql[0].indexOf("WITH (UPDLOCK, ROWLOCK)") !==
-1,
).to.be.true
expect(executedSql[0]).to.contain("WITH (UPDLOCK, ROWLOCK)")
}
}),
))
@ -388,15 +380,14 @@ describe("repository > find options > locking", () => {
})
})
expect(executedSql[0].indexOf("WITH (NOLOCK)") !== -1).to.be
.true
expect(executedSql[0]).to.contain("WITH (NOLOCK)")
}),
))
it("should throw error if optimistic lock used with `find` method", () =>
Promise.all(
connections.map(async (connection) => {
return connection
await connection
.getRepository(PostWithVersion)
.find({ lock: { mode: "optimistic", version: 1 } })
.should.be.rejectedWith(OptimisticLockCanNotBeUsedError)
@ -406,7 +397,7 @@ describe("repository > find options > locking", () => {
it("should not throw error if optimistic lock used with `findOne` method", () =>
Promise.all(
connections.map(async (connection) => {
return connection.getRepository(PostWithVersion).findOne({
await connection.getRepository(PostWithVersion).findOne({
where: { id: 1 },
lock: { mode: "optimistic", version: 1 },
}).should.not.be.rejected
@ -420,7 +411,7 @@ describe("repository > find options > locking", () => {
post.title = "New post"
await connection.manager.save(post)
return connection
await connection
.getRepository(PostWithoutVersionAndUpdateDate)
.findOne({
where: { id: 1 },
@ -437,7 +428,7 @@ describe("repository > find options > locking", () => {
post.title = "New post"
await connection.manager.save(post)
return connection
await connection
.getRepository(PostWithVersion)
.findOne({
where: { id: 1 },
@ -454,7 +445,7 @@ describe("repository > find options > locking", () => {
post.title = "New post"
await connection.manager.save(post)
return connection.getRepository(PostWithVersion).findOne({
await connection.getRepository(PostWithVersion).findOne({
where: { id: 1 },
lock: { mode: "optimistic", version: 1 },
}).should.not.be.rejected
@ -465,13 +456,15 @@ describe("repository > find options > locking", () => {
Promise.all(
connections.map(async (connection) => {
// skipped because inserted milliseconds are not always equal to what we say it to insert, unskip when needed
if (connection.driver.options.type === "mssql") return
if (connection.driver.options.type === "mssql") {
return
}
const post = new PostWithUpdateDate()
post.title = "New post"
await connection.manager.save(post)
return connection
await connection
.getRepository(PostWithUpdateDate)
.findOne({
where: { id: 1 },
@ -488,13 +481,15 @@ describe("repository > find options > locking", () => {
Promise.all(
connections.map(async (connection) => {
// skipped because inserted milliseconds are not always equal to what we say it to insert, unskip when needed
if (connection.driver.options.type === "mssql") return
if (connection.driver.options.type === "mssql") {
return
}
const post = new PostWithUpdateDate()
post.title = "New post"
await connection.manager.save(post)
return connection.getRepository(PostWithUpdateDate).findOne({
await connection.getRepository(PostWithUpdateDate).findOne({
where: { id: 1 },
lock: { mode: "optimistic", version: post.updateDate },
}).should.not.be.rejected
@ -505,13 +500,15 @@ describe("repository > find options > locking", () => {
Promise.all(
connections.map(async (connection) => {
// skipped because inserted milliseconds are not always equal to what we say it to insert, unskip when needed
if (connection.driver.options.type === "mssql") return
if (connection.driver.options.type === "mssql") {
return
}
const post = new PostWithVersionAndUpdatedDate()
post.title = "New post"
await connection.manager.save(post)
return Promise.all([
await Promise.all([
connection
.getRepository(PostWithVersionAndUpdatedDate)
.findOne({
@ -538,29 +535,26 @@ describe("repository > find options > locking", () => {
DriverUtils.isSQLiteFamily(connection.driver) ||
connection.driver.options.type === "sap"
)
return connection.manager.transaction((entityManager) => {
return Promise.all([
entityManager
.getRepository(PostWithVersion)
.findOne({
where: { id: 1 },
lock: { mode: "pessimistic_read" },
})
.should.be.rejectedWith(
LockNotSupportedOnGivenDriverError,
),
entityManager
.getRepository(PostWithVersion)
.findOne({
where: { id: 1 },
lock: { mode: "pessimistic_write" },
})
.should.be.rejectedWith(
LockNotSupportedOnGivenDriverError,
),
])
})
await connection.manager
.transaction((entityManager) =>
Promise.all([
entityManager
.getRepository(PostWithVersion)
.findOne({
where: { id: 1 },
lock: { mode: "pessimistic_read" },
}),
entityManager
.getRepository(PostWithVersion)
.findOne({
where: { id: 1 },
lock: { mode: "pessimistic_write" },
}),
]),
)
.should.be.rejectedWith(
LockNotSupportedOnGivenDriverError,
)
return
}),
@ -570,26 +564,22 @@ describe("repository > find options > locking", () => {
Promise.all(
connections.map(async (connection) => {
if (
!(
connection.driver.options.type === "postgres" ||
connection.driver.options.type === "cockroachdb"
)
)
connection.driver.options.type !== "postgres" &&
connection.driver.options.type !== "cockroachdb"
) {
return
}
return connection.manager.transaction((entityManager) => {
return Promise.all([
entityManager
.getRepository(Post)
.findOne({
where: { id: 1 },
lock: { mode: "pessimistic_write", tables: [] },
})
.should.be.rejectedWith(
"lockTables cannot be an empty array",
),
])
})
await connection.manager
.transaction((entityManager) =>
entityManager.getRepository(Post).findOne({
where: { id: 1 },
lock: { mode: "pessimistic_write", tables: [] },
}),
)
.should.be.rejectedWith(
"lockTables cannot be an empty array",
)
}),
))
@ -597,39 +587,40 @@ describe("repository > find options > locking", () => {
Promise.all(
connections.map(async (connection) => {
if (
!(
connection.driver.options.type === "postgres" ||
connection.driver.options.type === "cockroachdb"
)
)
connection.driver.options.type !== "postgres" &&
connection.driver.options.type !== "cockroachdb"
) {
return
}
return connection.manager.transaction((entityManager) => {
return Promise.all([
entityManager
.getRepository(Post)
.findOne({
where: { id: 1 },
relations: { author: true },
lock: {
mode: "pessimistic_write",
tables: ["img"],
},
})
.should.be.rejectedWith(
'"img" is not part of this query',
),
])
})
await connection.manager
.transaction((entityManager) =>
entityManager.getRepository(Post).findOne({
where: { id: 1 },
relations: { author: true },
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.options.type === "cockroachdb") {
return connection.manager.transaction((entityManager) => {
return Promise.all([
if (
connection.driver.options.type !== "postgres" &&
connection.driver.options.type !== "cockroachdb"
) {
return
}
await connection.manager
.transaction((entityManager) =>
Promise.all([
entityManager.getRepository(Post).findOne({
where: { id: 1 },
relations: { author: true },
@ -638,56 +629,28 @@ describe("repository > find options > locking", () => {
tables: ["post"],
},
}),
entityManager
.getRepository(Post)
.findOne({
where: { id: 1 },
relations: { author: true },
lock: { mode: "pessimistic_write" },
})
.should.be.rejectedWith(
"FOR UPDATE cannot be applied to the nullable side of an outer join",
),
])
})
}
if (connection.driver.options.type === "postgres") {
return connection.manager.transaction((entityManager) => {
return Promise.all([
entityManager.getRepository(Post).findOne({
where: { id: 1 },
relations: { author: true },
lock: {
mode: "pessimistic_write",
tables: ["post"],
},
lock: { mode: "pessimistic_write" },
}),
entityManager
.getRepository(Post)
.findOne({
where: { id: 1 },
relations: { author: true },
lock: { mode: "pessimistic_write" },
})
.should.be.rejectedWith(
"FOR UPDATE cannot be applied to the nullable side of an outer join",
),
])
})
}
return
]),
)
.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.options.type === "postgres")) return
if (connection.driver.options.type !== "postgres") {
return
}
return connection.manager.transaction((entityManager) => {
return Promise.all([
await connection.manager.transaction((entityManager) =>
Promise.all([
entityManager.getRepository(Post).findOne({
where: { id: 1 },
relations: { author: true },
@ -736,8 +699,8 @@ describe("repository > find options > locking", () => {
tables: ["post"],
},
}),
])
})
]),
)
}),
))
@ -745,31 +708,28 @@ describe("repository > find options > locking", () => {
Promise.all(
connections.map(async (connection) => {
if (
!(
connection.driver.options.type === "postgres" ||
connection.driver.options.type === "cockroachdb"
)
)
connection.driver.options.type !== "postgres" &&
connection.driver.options.type !== "cockroachdb"
) {
return
}
return connection.manager.transaction((entityManager) => {
return Promise.all([
entityManager.getRepository(Post).findOne({
where: { id: 1 },
join: {
alias: "post",
innerJoinAndSelect: {
categorys: "post.categories",
images: "categorys.images",
},
await connection.manager.transaction((entityManager) =>
entityManager.getRepository(Post).findOne({
where: { id: 1 },
join: {
alias: "post",
innerJoinAndSelect: {
categorys: "post.categories",
images: "categorys.images",
},
lock: {
mode: "pessimistic_write",
tables: ["image"],
},
}),
])
})
},
lock: {
mode: "pessimistic_write",
tables: ["image"],
},
}),
)
}),
))
})

View File

@ -28,8 +28,8 @@ describe("transaction > nested transaction", () => {
shouldExist: boolean
}[] = []
// Spanner does not support nested transactions
if (connection.driver.options.type === "spanner") return
// SAP HANA, Spanner etc. do not support nested transactions
if (connection.driver.transactionSupport !== "nested") return
await connection.manager.transaction(async (em0) => {
const post = new Post()
@ -37,29 +37,28 @@ describe("transaction > nested transaction", () => {
await em0.save(post)
conditions.push({ ...post, shouldExist: true })
try {
await em0.transaction(async (em1) => {
const post = new Post()
post.title = "Post #2"
await em1.save(post)
conditions.push({ ...post, shouldExist: false })
await em0.transaction(async (em1) => {
const post = new Post()
post.title = "Post #2"
await em1.save(post)
conditions.push({ ...post, shouldExist: false })
await em1.transaction(async (em2) => {
const post = new Post()
post.title = "Post #3"
await em2.save(post)
conditions.push({ ...post, shouldExist: false })
})
throw new Error("")
})
} catch (_) {}
await em1.transaction(async (em2) => {
const post = new Post()
post.title = "Post #3"
await em2.save(post)
conditions.push({ ...post, shouldExist: false })
}).should.not.be.rejected
throw new Error("")
}).should.be.rejected
await em0.transaction(async (em1) => {
const post = new Post()
post.title = "Post #4"
await em1.save(post)
conditions.push({ ...post, shouldExist: true })
})
}).should.not.be.rejected
await em0.transaction(async (em1) => {
const post = new Post()
@ -67,32 +66,31 @@ describe("transaction > nested transaction", () => {
await em1.save(post)
conditions.push({ ...post, shouldExist: true })
try {
await em1.transaction(async (em2) => {
const post = new Post()
post.title = "Post #6"
await em2.save(post)
conditions.push({ ...post, shouldExist: false })
await em1.transaction(async (em2) => {
const post = new Post()
post.title = "Post #6"
await em2.save(post)
conditions.push({ ...post, shouldExist: false })
await em2.transaction(async (em3) => {
const post = new Post()
post.title = "Post #7"
await em3.save(post)
conditions.push({
...post,
shouldExist: false,
})
await em2.transaction(async (em3) => {
const post = new Post()
post.title = "Post #7"
await em3.save(post)
conditions.push({
...post,
shouldExist: false,
})
throw new Error("")
})
} catch (_) {}
}).should.not.be.rejected
throw new Error("")
}).should.be.rejected
await em1.transaction(async (em2) => {
const post = new Post()
post.title = "Post #8"
await em2.save(post)
conditions.push({ ...post, shouldExist: true })
})
}).should.not.be.rejected
await em1.transaction(async (em2) => {
const post = new Post()
@ -100,38 +98,37 @@ describe("transaction > nested transaction", () => {
await em2.save(post)
conditions.push({ ...post, shouldExist: true })
try {
await em2.transaction(async (em3) => {
await em2.transaction(async (em3) => {
const post = new Post()
post.title = "Post #10"
await em3.save(post)
conditions.push({
...post,
shouldExist: false,
})
await em3.transaction(async (em4) => {
const post = new Post()
post.title = "Post #10"
await em3.save(post)
post.title = "Post #11"
await em4.save(post)
conditions.push({
...post,
shouldExist: false,
})
}).should.not.be.rejected
await em3.transaction(async (em4) => {
const post = new Post()
post.title = "Post #11"
await em4.save(post)
conditions.push({
...post,
shouldExist: false,
})
})
throw new Error("")
})
} catch (_) {}
throw new Error("")
}).should.be.rejected
await em2.transaction(async (em3) => {
const post = new Post()
post.title = "Post #12"
await em3.save(post)
conditions.push({ ...post, shouldExist: true })
})
})
})
})
}).should.not.be.rejected
}).should.not.be.rejected
}).should.not.be.rejected
}).should.not.be.rejected
for (const condition of conditions) {
const post = await connection.manager.findOne(Post, {
@ -155,59 +152,55 @@ describe("transaction > nested transaction", () => {
connections.map(async (connection) => {
const conditions: { id: number; title: string }[] = []
try {
await connection.manager.transaction(async (em0) => {
await connection.manager.transaction(async (em0) => {
const post = new Post()
post.title = "Post #1"
await em0.save(post)
conditions.push({ ...post })
await em0.transaction(async (em1) => {
const post = new Post()
post.title = "Post #1"
await em0.save(post)
post.title = "Post #2"
await em1.save(post)
conditions.push({ ...post })
try {
await em0.transaction(async (em1) => {
const post = new Post()
post.title = "Post #2"
await em1.save(post)
conditions.push({ ...post })
throw new Error("")
})
} catch (_) {}
throw new Error("")
}).should.be.rejected
await em0.transaction(async (em1) => {
await em0.transaction(async (em1) => {
const post = new Post()
post.title = "Post #3"
await em1.save(post)
conditions.push({ ...post })
await em1.transaction(async (em2) => {
const post = new Post()
post.title = "Post #3"
await em1.save(post)
post.title = "Post #4"
await em2.save(post)
conditions.push({ ...post })
try {
await em1.transaction(async (em2) => {
const post = new Post()
post.title = "Post #4"
await em2.save(post)
conditions.push({ ...post })
throw new Error("")
})
} catch (_) {}
throw new Error("")
}).should.be.rejected
await em1.transaction(async (em2) => {
await em1.transaction(async (em2) => {
const post = new Post()
post.title = "Post #5"
await em2.save(post)
conditions.push({ ...post })
await em2.transaction(async (em3) => {
const post = new Post()
post.title = "Post #5"
await em2.save(post)
post.title = "Post #6"
await em3.save(post)
conditions.push({ ...post })
try {
await em2.transaction(async (em3) => {
const post = new Post()
post.title = "Post #6"
await em3.save(post)
conditions.push({ ...post })
throw new Error("")
})
} catch (_) {}
})
})
throw new Error("")
})
} catch (_) {}
throw new Error("")
}).should.be.rejected
}).should.not.be.rejected
}).should.not.be.rejected
throw new Error("")
}).should.be.rejected
for (const condition of conditions) {
const post = await connection.manager.findOne(Post, {

View File

@ -1,44 +0,0 @@
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "../../../../src"
import { ConfigurationEntity } from "./configuration"
export enum AssetStatus {
new = 0,
deleted = -999,
}
@Entity("assets")
export class AssetEntity {
@PrimaryGeneratedColumn("uuid")
id!: string
@Column({ length: 255 })
name!: string
@Column({ type: "uuid" })
configuration_id!: string
@Column()
status!: AssetStatus
@CreateDateColumn()
created_at!: Date
@UpdateDateColumn()
updated_at!: Date
@DeleteDateColumn()
deleted_at!: Date | null
@ManyToOne(() => ConfigurationEntity, { nullable: false })
@JoinColumn({ name: "configuration_id" })
configuration!: ConfigurationEntity
}

View File

@ -1,50 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "../../../../src"
import { AssetEntity } from "./asset"
import { LocationEntity } from "./location"
export enum ConfigurationStatus {
deleted = -999,
new = 0,
}
@Entity("configurations")
export class ConfigurationEntity {
@PrimaryGeneratedColumn("uuid")
id!: string
@Column({ length: 255 })
name!: string
@Column()
status!: ConfigurationStatus
@Column({ type: "uuid", nullable: false })
location_id!: string
@ManyToOne(() => LocationEntity, { nullable: false })
@JoinColumn({ name: "location_id" })
location!: LocationEntity
@Column({ default: true })
active!: boolean
@OneToMany(() => AssetEntity, (asset) => asset.configuration, {
cascade: true,
})
assets!: AssetEntity[]
@CreateDateColumn()
created_at!: Date
@UpdateDateColumn()
updated_at!: Date
}

View File

@ -1,34 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "../../../../src"
import { ConfigurationEntity } from "./configuration"
@Entity("locations")
export class LocationEntity {
@PrimaryGeneratedColumn("uuid")
id!: string
@Column({ length: 255 })
name!: string
@Column({ default: true })
active!: boolean
@CreateDateColumn()
created_at!: Date
@UpdateDateColumn()
updated_at!: Date
@OneToMany(
() => ConfigurationEntity,
(configuration) => configuration.location,
{ cascade: true },
)
configurations!: ConfigurationEntity[]
}

View File

@ -1,90 +0,0 @@
import "reflect-metadata"
import {
createTestingConnections,
closeTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { DataSource } from "../../../src/data-source/DataSource"
import { expect } from "chai"
import { LocationEntity } from "./entity/location"
import {
ConfigurationEntity,
ConfigurationStatus,
} from "./entity/configuration"
import { AssetEntity, AssetStatus } from "./entity/asset"
describe("github issues > #10209", () => {
let dataSources: DataSource[]
before(
async () =>
(dataSources = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchema: true,
})),
)
beforeEach(() => reloadTestingDatabases(dataSources))
after(() => closeTestingConnections(dataSources))
it("should not fail to run multiple nested transactions in parallel", function () {
this.retries(3) // Fix for SQLite
return Promise.all(
dataSources.map(async (dataSource) => {
const manager = dataSource.createEntityManager()
await manager.transaction(async (txManager) => {
const location = txManager.create(LocationEntity)
location.name = "location-0"
location.configurations = []
for (let c = 0; c < 3; c++) {
const config = txManager.create(ConfigurationEntity)
config.name = `config-${c}`
config.status = ConfigurationStatus.new
config.assets = []
for (let a = 0; a < 5; a++) {
const asset = txManager.create(AssetEntity)
asset.name = `asset-${c}-${a}`
asset.status = AssetStatus.new
config.assets.push(asset)
}
location.configurations.push(config)
}
await txManager.save(location)
})
const location =
(await manager.findOne(LocationEntity, {
where: {
name: "location-0",
},
relations: ["configurations", "configurations.assets"],
})) || ({} as LocationEntity)
await manager.transaction(async (txManager) => {
return Promise.all(
location.configurations.map(async (config) => {
await txManager.transaction(async (txManager2) => {
await Promise.all(
config.assets.map(async (asset) => {
asset.status = AssetStatus.deleted
await txManager2.save(asset)
await txManager2.softDelete(
AssetEntity,
asset,
)
}),
)
})
config.status = ConfigurationStatus.deleted
return await txManager.save(config)
}),
)
})
// We only care that the transaction above didn't fail
expect(true).to.be.true
}),
)
})
})

View File

@ -0,0 +1,14 @@
import { Column } from "../../../../src/decorator/columns/Column"
import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn"
import { Entity } from "../../../../src/decorator/entity/Entity"
import { Unique } from "../../../../src/decorator/Unique"
@Entity()
@Unique(["name"], { deferrable: "INITIALLY DEFERRED" })
export class Company {
@PrimaryColumn()
id: number
@Column()
name?: string
}

View File

@ -0,0 +1,18 @@
import { Column } from "../../../../src/decorator/columns/Column"
import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn"
import { Entity } from "../../../../src/decorator/entity/Entity"
import { ManyToOne } from "../../../../src/decorator/relations/ManyToOne"
import { Company } from "./Company"
@Entity()
export class User {
@PrimaryColumn()
id: number
@Column()
name: string
@ManyToOne(() => Company, { deferrable: "INITIALLY DEFERRED" })
company: Company
}

View File

@ -0,0 +1,93 @@
import "reflect-metadata"
import { expect } from "chai"
import { DataSource } from "../../../src/data-source/DataSource"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { EntityManager } from "../../../src"
import { BaseQueryRunner } from "../../../src/query-runner/BaseQueryRunner"
import { Company } from "./entity/Company"
import { User } from "./entity/User"
describe("github issues > #10626 Regression in transactionDepth handling", () => {
let connections: DataSource[]
before(
async () =>
(connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["better-sqlite3", "postgres", "sqlite"],
})),
)
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
it("transactionDepth should be updated correctly when commit fails", () =>
Promise.all(
connections.map(async (connection) => {
const queryRunner = connection.createQueryRunner()
const transactionDepths: Record<string, number> = {}
const recordDepth = (mark: string) => {
transactionDepths[mark] = (
queryRunner as unknown as BaseQueryRunner
)["transactionDepth"]
}
recordDepth("initial")
await queryRunner.startTransaction()
recordDepth("startTransaction")
const runInTransaction = async (
entityManager: EntityManager,
) => {
// first save user
const user = new User()
user.id = 1
user.company = { id: 100 }
user.name = "Bob"
await entityManager.save(user)
// then save company
const company = new Company()
company.id = 200
company.name = "Acme"
await entityManager.save(company)
}
await runInTransaction(queryRunner.manager).should.not.rejected
recordDepth("afterStatements")
await queryRunner.commitTransaction().should.be.rejected
recordDepth("afterCommit")
await queryRunner.rollbackTransaction().should.not.be.rejected
recordDepth("afterRollback")
await queryRunner.release()
recordDepth("afterRelease")
expect(transactionDepths).to.deep.equal({
initial: 0,
startTransaction: 1,
afterStatements: 1,
afterCommit: 1,
afterRollback: 0,
afterRelease: 0,
})
// check data
const user = await connection.manager.findOneBy(User, { id: 1 })
expect(user).to.equal(null)
const company = await connection.manager.findOneBy(Company, {
id: 200,
})
expect(company).to.equal(null)
}),
))
})