123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- import type { InferAttributes, InferCreationAttributes } from '@sequelize/core';
- import {
- ConstraintChecking,
- DataTypes,
- IsolationLevel,
- Model,
- Transaction,
- TransactionNestMode,
- TransactionType,
- } from '@sequelize/core';
- import { Attribute, NotNull } from '@sequelize/core/decorators-legacy';
- import { assert, expect } from 'chai';
- import delay from 'delay';
- import type { SinonStub } from 'sinon';
- import sinon from 'sinon';
- import {
- beforeAll2,
- createMultiTransactionalTestSequelizeInstance,
- createSingleTransactionalTestSequelizeInstance,
- getTestDialect,
- getTestDialectTeaser,
- sequelize,
- setResetMode,
- } from '../support';
- const dialectName = sequelize.dialect.name;
- describe(getTestDialectTeaser('Sequelize#transaction'), () => {
- if (!sequelize.dialect.supports.transactions) {
- return;
- }
- let stubs: SinonStub[] = [];
- afterEach(() => {
- for (const stub of stubs) {
- stub.restore();
- }
- stubs = [];
- });
- describe('nested managed transactions', () => {
- it('reuses the parent transaction by default', async () => {
- await sequelize.transaction(async transaction1 => {
- await sequelize.transaction({ transaction: transaction1 }, async transaction2 => {
- expect(transaction1 === transaction2).to.equal(
- true,
- 'transaction1 and transaction2 should be the same',
- );
- });
- });
- });
- it('requires compatible options if nestMode is set to "reuse"', async () => {
- await sequelize.transaction(async transaction1 => {
- if (sequelize.dialect.supports.startTransaction.transactionType) {
- await expect(
- sequelize.transaction(
- { transaction: transaction1, type: TransactionType.EXCLUSIVE },
- async () => {
- /* noop */
- },
- ),
- ).to.be.rejectedWith(
- 'Requested transaction type (EXCLUSIVE) is not compatible with the one of the existing transaction (DEFERRED)',
- );
- } else {
- await expect(
- sequelize.transaction(
- { transaction: transaction1, type: TransactionType.EXCLUSIVE },
- async () => {
- /* noop */
- },
- ),
- ).to.be.rejectedWith(
- `The ${sequelize.dialect.name} dialect does not support transaction types.`,
- );
- }
- await expect(
- sequelize.transaction(
- { transaction: transaction1, isolationLevel: IsolationLevel.READ_UNCOMMITTED },
- async () => {
- /* noop */
- },
- ),
- ).to.be.rejectedWith(
- 'Requested isolation level (READ UNCOMMITTED) is not compatible with the one of the existing transaction (unspecified)',
- );
- await expect(
- sequelize.transaction(
- { transaction: transaction1, constraintChecking: ConstraintChecking.IMMEDIATE },
- async () => {
- /* noop */
- },
- ),
- ).to.be.rejectedWith(
- 'Requested transaction constraintChecking (IMMEDIATE) is not compatible with the one of the existing transaction (none)',
- );
- await expect(
- sequelize.transaction({ transaction: transaction1, readOnly: true }, async () => {
- /* noop */
- }),
- ).to.be.rejectedWith(
- 'Requested a transaction in read-only mode, which is not compatible with the existing read/write transaction',
- );
- });
- });
- it('creates a savepoint if nestMode is set to "savepoint"', async () => {
- await sequelize.transaction(async transaction1 => {
- await sequelize.transaction(
- { transaction: transaction1, nestMode: TransactionNestMode.savepoint },
- async transaction2 => {
- expect(transaction1 === transaction2).to.equal(
- false,
- 'transaction1 and transaction2 should not be the same',
- );
- expect(transaction2.parent === transaction1).to.equal(
- true,
- 'transaction2.parent should be transaction1',
- );
- },
- );
- });
- });
- it('requires compatible options if nestMode is set to "savepoint"', async () => {
- await sequelize.transaction(async transaction1 => {
- const commonOptions = {
- transaction: transaction1,
- nestMode: TransactionNestMode.savepoint,
- };
- if (sequelize.dialect.supports.startTransaction.transactionType) {
- await expect(
- sequelize.transaction(
- { ...commonOptions, type: TransactionType.EXCLUSIVE },
- async () => {
- /* noop */
- },
- ),
- ).to.be.rejectedWith(
- 'Requested transaction type (EXCLUSIVE) is not compatible with the one of the existing transaction (DEFERRED)',
- );
- } else {
- await expect(
- sequelize.transaction(
- { ...commonOptions, type: TransactionType.EXCLUSIVE },
- async () => {
- /* noop */
- },
- ),
- ).to.be.rejectedWith(
- `The ${sequelize.dialect.name} dialect does not support transaction types.`,
- );
- }
- await expect(
- sequelize.transaction(
- { ...commonOptions, isolationLevel: IsolationLevel.READ_UNCOMMITTED },
- async () => {
- /* noop */
- },
- ),
- ).to.be.rejectedWith(
- 'Requested isolation level (READ UNCOMMITTED) is not compatible with the one of the existing transaction (unspecified)',
- );
- await expect(
- sequelize.transaction(
- { ...commonOptions, constraintChecking: ConstraintChecking.IMMEDIATE },
- async () => {
- /* noop */
- },
- ),
- ).to.be.rejectedWith(
- 'Requested transaction constraintChecking (IMMEDIATE) is not compatible with the one of the existing transaction (none)',
- );
- await expect(
- sequelize.transaction({ ...commonOptions, readOnly: true }, async () => {
- /* noop */
- }),
- ).to.be.rejectedWith(
- 'Requested a transaction in read-only mode, which is not compatible with the existing read/write transaction',
- );
- });
- });
- // sqlite cannot have more than one transaction at the same time, so separate is not available.
- if (dialectName !== 'sqlite3') {
- it('creates a new transaction if nestMode is set to "separate"', async () => {
- await sequelize.transaction(async transaction1 => {
- await sequelize.transaction(
- { transaction: transaction1, nestMode: TransactionNestMode.separate },
- async transaction2 => {
- expect(transaction1 === transaction2).to.equal(
- false,
- 'transaction1 and transaction2 should not be the same',
- );
- expect(transaction1.parent === null).to.equal(
- true,
- 'transaction1.parent should be null',
- );
- expect(transaction2.parent === null).to.equal(
- true,
- 'transaction2.parent should be null',
- );
- },
- );
- });
- });
- it('does not care about option compatibility when nestMode is set to "separate"', async () => {
- await sequelize.transaction(async transaction1 => {
- await sequelize.transaction(
- {
- transaction: transaction1,
- nestMode: TransactionNestMode.separate,
- type: sequelize.dialect.supports.startTransaction.transactionType
- ? TransactionType.EXCLUSIVE
- : undefined,
- isolationLevel: IsolationLevel.READ_UNCOMMITTED,
- constraintChecking: sequelize.dialect.supports.constraints.deferrable
- ? ConstraintChecking.DEFERRED
- : undefined,
- readOnly: true,
- },
- async () => {
- /* noop */
- },
- );
- });
- });
- }
- it(`defaults nestMode to sequelize's defaultTransactionNestMode option`, async () => {
- const customSequelize = await createSingleTransactionalTestSequelizeInstance(sequelize, {
- defaultTransactionNestMode: TransactionNestMode.savepoint,
- });
- await customSequelize.transaction(async transaction1 => {
- await customSequelize.transaction({ transaction: transaction1 }, async transaction2 => {
- expect(transaction1 === transaction2).to.equal(
- false,
- 'transaction1 and transaction2 should not be the same',
- );
- expect(transaction2.parent === transaction1).to.equal(
- true,
- 'transaction2.parent should be transaction1',
- );
- });
- });
- });
- });
- describe('Isolation Levels', () => {
- setResetMode('truncate');
- const vars = beforeAll2(async () => {
- const transactionSequelize = await createMultiTransactionalTestSequelizeInstance(sequelize);
- class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
- @Attribute(DataTypes.STRING)
- @NotNull
- declare name: string;
- @Attribute(DataTypes.INTEGER)
- @NotNull
- declare age: number;
- }
- transactionSequelize.addModels([User]);
- await transactionSequelize.sync({ force: true });
- return { transactionSequelize, User };
- });
- after(async () => {
- return vars.transactionSequelize.close();
- });
- beforeEach(async () => {
- await vars.User.create({ name: 'John Doe', age: 21 });
- });
- if (sequelize.dialect.supports.settingIsolationLevelDuringTransaction) {
- it('should allow setting the isolation level during a transaction', async () => {
- const { User, transactionSequelize } = vars;
- await transactionSequelize.transaction(async transaction => {
- await transaction.setIsolationLevel(IsolationLevel.READ_UNCOMMITTED);
- await User.update({ age: 22 }, { where: { name: 'John Doe' }, transaction });
- });
- if (dialectName !== 'sqlite3') {
- await transactionSequelize.transaction(async transaction => {
- await transaction.setIsolationLevel(IsolationLevel.READ_COMMITTED);
- const johnDoe = await User.findOne({ where: { name: 'John Doe' }, transaction });
- assert(johnDoe, 'John Doe should exist');
- expect(johnDoe.age).to.equal(22);
- });
- await transactionSequelize.transaction(async transaction => {
- await transaction.setIsolationLevel(IsolationLevel.REPEATABLE_READ);
- const users = await User.findAll({ transaction });
- expect(users.length).to.equal(1);
- expect(users[0].name).to.equal('John Doe');
- expect(users[0].age).to.equal(22);
- });
- }
- await transactionSequelize.transaction(async transaction => {
- await transaction.setIsolationLevel(IsolationLevel.SERIALIZABLE);
- await User.create({ name: 'Jane Doe', age: 21 }, { transaction });
- });
- });
- }
- // SQLite only supports read uncommitted and serializable.
- if (dialectName !== 'sqlite3') {
- it('should read the most recent committed rows when using the READ COMMITTED isolation level', async () => {
- const { User, transactionSequelize } = vars;
- await transactionSequelize.transaction(
- { isolationLevel: IsolationLevel.READ_COMMITTED },
- async transaction => {
- const users0 = await User.findAll({ transaction });
- expect(users0).to.have.lengthOf(1);
- await User.create({ name: 'Jane Doe', age: 21 }); // Create a User outside of the transaction
- const users = await User.findAll({ transaction });
- expect(users).to.have.lengthOf(2); // We SHOULD see the created user inside the transaction
- },
- );
- });
- }
- // These dialects do not allow dirty reads with isolation level "READ UNCOMMITTED".
- if (!['postgres', 'sqlite3'].includes(dialectName)) {
- it('should allow dirty read with isolation level "READ UNCOMMITTED"', async () => {
- const { User, transactionSequelize } = vars;
- const t1 = await transactionSequelize.startUnmanagedTransaction({
- isolationLevel: IsolationLevel.READ_UNCOMMITTED,
- });
- try {
- await User.update({ age: 22 }, { where: { name: 'John Doe' }, transaction: t1 });
- await transactionSequelize.transaction(
- { isolationLevel: IsolationLevel.READ_UNCOMMITTED },
- async transaction => {
- const johnDoe = await User.findOne({ where: { name: 'John Doe' }, transaction });
- assert(johnDoe, 'John Doe should exist');
- expect(johnDoe.age).to.equal(22);
- },
- );
- } finally {
- await t1.rollback();
- const johnDoe = await User.findOne({ where: { name: 'John Doe' } });
- assert(johnDoe, 'John Doe should exist');
- expect(johnDoe.age).to.equal(21);
- }
- });
- }
- // SQLite only supports read uncommitted and serializable.
- if (dialectName !== 'sqlite3') {
- it('should prevent dirty read with isolation level "READ COMMITTED"', async () => {
- const { User, transactionSequelize } = vars;
- const t1 = await transactionSequelize.startUnmanagedTransaction({
- isolationLevel: IsolationLevel.READ_COMMITTED,
- });
- try {
- await User.update({ age: 22 }, { where: { name: 'John Doe' }, transaction: t1 });
- await transactionSequelize.transaction(
- { isolationLevel: IsolationLevel.READ_COMMITTED },
- async transaction => {
- const johnDoe = await User.findOne({ where: { name: 'John Doe' }, transaction });
- assert(johnDoe, 'John Doe should exist');
- expect(johnDoe.age).to.equal(21);
- },
- );
- } finally {
- await t1.rollback();
- const johnDoe = await User.findOne({ where: { name: 'John Doe' } });
- assert(johnDoe, 'John Doe should exist');
- expect(johnDoe.age).to.equal(21);
- }
- });
- }
- // SQLite only supports read uncommitted and serializable.
- if (dialectName !== 'sqlite3') {
- it('should allow non-repeatable read with isolation level "READ COMMITTED"', async () => {
- const { User, transactionSequelize } = vars;
- const t1 = await transactionSequelize.startUnmanagedTransaction({
- isolationLevel: IsolationLevel.READ_COMMITTED,
- });
- try {
- const johnDoe = await User.findOne({ where: { name: 'John Doe' }, transaction: t1 });
- assert(johnDoe, 'John Doe should exist');
- expect(johnDoe.name).to.equal('John Doe');
- expect(johnDoe.age).to.equal(21);
- await transactionSequelize.transaction(
- { isolationLevel: IsolationLevel.READ_COMMITTED },
- async transaction => {
- await User.update({ age: 22 }, { where: { name: 'John Doe' }, transaction });
- },
- );
- const johnDoe1 = await User.findOne({ where: { name: 'John Doe' }, transaction: t1 });
- assert(johnDoe1, 'John Doe should exist');
- expect(johnDoe1.name).to.equal('John Doe');
- expect(johnDoe1.age).to.equal(22);
- await t1.commit();
- } catch (error) {
- await t1.rollback();
- throw error;
- }
- });
- }
- // These dialects do not allow phantom reads with isolation level "REPEATABLE READ" as they use snapshot rather than locking.
- if (['mariadb', 'mysql', 'postgres'].includes(dialectName)) {
- it('should not read newly committed rows when using the REPEATABLE READ isolation level', async () => {
- const { User, transactionSequelize } = vars;
- await transactionSequelize.transaction(
- { isolationLevel: IsolationLevel.REPEATABLE_READ },
- async transaction => {
- const users0 = await User.findAll({ transaction });
- expect(users0).to.have.lengthOf(1);
- await User.create({ name: 'Jane Doe', age: 21 }); // Create a User outside of the transaction
- const users = await User.findAll({ transaction });
- expect(users).to.have.lengthOf(1); // We SHOULD NOT see the created user inside the transaction
- },
- );
- });
- // SQLite only supports read uncommitted and serializable.
- } else if (dialectName !== 'sqlite3') {
- it('should allow phantom read with isolation level "REPEATABLE READ"', async () => {
- const { User, transactionSequelize } = vars;
- const t1 = await transactionSequelize.startUnmanagedTransaction({
- isolationLevel: IsolationLevel.REPEATABLE_READ,
- });
- try {
- const users = await User.findAll({ transaction: t1 });
- expect(users.length).to.equal(1);
- expect(users[0].name).to.equal('John Doe');
- expect(users[0].age).to.equal(21);
- await transactionSequelize.transaction(
- { isolationLevel: IsolationLevel.REPEATABLE_READ },
- async transaction => {
- await User.create({ name: 'Jane Doe', age: 21 }, { transaction });
- },
- );
- const users2 = await User.findAll({ transaction: t1 });
- expect(users2.length).to.equal(2);
- expect(users2[0].name).to.equal('John Doe');
- expect(users2[0].age).to.equal(21);
- expect(users2[1].name).to.equal('Jane Doe');
- expect(users2[1].age).to.equal(21);
- await t1.commit();
- } catch (error) {
- await t1.rollback();
- throw error;
- }
- });
- }
- // PostgreSQL is excluded because it detects Serialization Failure on commit instead of acquiring locks on the read rows
- if (!['postgres'].includes(dialectName)) {
- it('should block updates after reading a row using SERIALIZABLE', async () => {
- const { User, transactionSequelize } = vars;
- const transactionSpy = sinon.spy();
- const transaction = await transactionSequelize.startUnmanagedTransaction({
- isolationLevel: IsolationLevel.SERIALIZABLE,
- });
- await User.findAll({ transaction });
- await Promise.all([
- // Update should not succeed before transaction has committed
- User.update({ age: 25 }, { where: { name: 'John Doe' } }).then(() => {
- expect(transactionSpy).to.have.been.called;
- expect(transaction.finished).to.equal('commit');
- }),
- delay(4000)
- .then(transactionSpy)
- .then(async () => transaction.commit()),
- ]);
- });
- }
- });
- describe('Transaction#commit', () => {
- it('returns a promise that resolves once the transaction has been committed', async () => {
- const t = await sequelize.startUnmanagedTransaction();
- await expect(t.commit()).to.eventually.equal(undefined);
- });
- // we cannot close a sqlite connection, but there also cannot be a network error with sqlite.
- // so this test is not necessary for that dialect.
- if (dialectName !== 'sqlite3') {
- it('does not pollute the pool with broken connections if commit fails', async () => {
- const initialPoolSize = sequelize.pool.size;
- stubs.push(
- sinon
- .stub(sequelize.queryInterface, '_commitTransaction')
- .rejects(new Error('Oh no, an error!')),
- );
- const t = await sequelize.startUnmanagedTransaction();
- await expect(t.commit()).to.be.rejectedWith('Oh no, an error!');
- // connection should have been destroyed
- expect(sequelize.pool.size).to.eq(Math.max(0, initialPoolSize - 1));
- });
- }
- });
- describe('Transaction#rollback', () => {
- it('returns a promise that resolves once the transaction has been rolled back', async () => {
- const t = await sequelize.startUnmanagedTransaction();
- await expect(t.rollback()).to.eventually.equal(undefined);
- });
- // we cannot close a sqlite connection, but there also cannot be a network error with sqlite.
- // so this test is not necessary for that dialect.
- if (dialectName !== 'sqlite3') {
- it('does not pollute the pool with broken connections if the rollback fails', async () => {
- const initialPoolSize = sequelize.pool.size;
- stubs.push(
- sinon
- .stub(sequelize.queryInterface, '_rollbackTransaction')
- .rejects(new Error('Oh no, an error!')),
- );
- const t = await sequelize.startUnmanagedTransaction();
- await expect(t.rollback()).to.be.rejectedWith('Oh no, an error!');
- // connection should have been destroyed
- expect(sequelize.pool.size).to.eq(Math.max(0, initialPoolSize - 1));
- });
- }
- });
- if (getTestDialect() !== 'sqlite3' && getTestDialect() !== 'db2') {
- it('works for long running transactions', async () => {
- const sequelize2 = await createSingleTransactionalTestSequelizeInstance(sequelize);
- interface IUser extends Model<InferAttributes<IUser>, InferCreationAttributes<IUser>> {
- name: string | null;
- }
- const User = sequelize2.define<IUser>(
- 'User',
- {
- name: DataTypes.STRING,
- },
- { timestamps: false },
- );
- await sequelize2.sync({ force: true });
- const t = await sequelize2.startUnmanagedTransaction();
- let query: string;
- switch (getTestDialect()) {
- case 'postgres':
- query = 'select pg_sleep(2);';
- break;
- case 'sqlite3':
- query = 'select sqlite3_sleep(2000);';
- break;
- case 'mssql':
- query = "WAITFOR DELAY '00:00:02';";
- break;
- default:
- query = 'select sleep(2);';
- break;
- }
- await sequelize2.query(query, { transaction: t });
- await User.create({ name: 'foo' });
- await sequelize2.query(query, { transaction: t });
- await t.commit();
- const users = await User.findAll();
- expect(users.length).to.equal(1);
- expect(users[0].name).to.equal('foo');
- });
- }
- describe('complex long running example', () => {
- it('works with promise syntax', async () => {
- const sequelize2 = await createSingleTransactionalTestSequelizeInstance(sequelize);
- const Test = sequelize2.define('Test', {
- id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
- name: { type: DataTypes.STRING },
- });
- await sequelize2.sync({ force: true });
- const transaction = await sequelize2.startUnmanagedTransaction();
- expect(transaction).to.be.instanceOf(Transaction);
- await Test.create({ name: 'Peter' }, { transaction });
- await delay(1000);
- await transaction.commit();
- const count = await Test.count();
- expect(count).to.equal(1);
- });
- });
- describe('concurrency: having tables with uniqueness constraints', () => {
- it('triggers the error event for the second transactions', async () => {
- const sequelize2 = await createSingleTransactionalTestSequelizeInstance(sequelize);
- const User = sequelize2.define(
- 'User',
- {
- name: { type: DataTypes.STRING, unique: true },
- },
- {
- timestamps: false,
- },
- );
- await User.sync({ force: true });
- const t1 = await sequelize2.startUnmanagedTransaction();
- const t2 = await sequelize2.startUnmanagedTransaction();
- await User.create({ name: 'omnom' }, { transaction: t1 });
- await Promise.all([
- (async () => {
- try {
- return await User.create({ name: 'omnom' }, { transaction: t2 });
- } catch (error) {
- expect(error).to.be.ok;
- return t2.rollback();
- }
- })(),
- delay(100).then(async () => {
- return t1.commit();
- }),
- ]);
- });
- });
- });
|