import type { CreationOptional, InferAttributes, InferCreationAttributes, NonAttribute, } from '@sequelize/core'; import { DataTypes, ManualOnDelete, Model } from '@sequelize/core'; import { Attribute, BelongsTo, NotNull } from '@sequelize/core/decorators-legacy'; import sinon from 'sinon'; import { beforeAll2, expectPerDialect, sequelize, toMatchSql } from '../../support'; import { setResetMode } from '../support'; describe('ModelRepository#_UNSTABLE_bulkDestroy', () => { setResetMode('destroy'); const vars = beforeAll2(async () => { class User extends Model, InferCreationAttributes> { declare id: CreationOptional; } class Project extends Model, InferCreationAttributes> { declare id: CreationOptional; @NotNull @Attribute(DataTypes.INTEGER) declare ownerId: number; @BelongsTo(() => User, 'ownerId') declare owner: NonAttribute; } class Task extends Model, InferCreationAttributes> { declare id: CreationOptional; @NotNull @Attribute(DataTypes.INTEGER) declare projectId: number; @BelongsTo(() => Project, 'projectId') declare project: NonAttribute; } sequelize.addModels([User, Project, Task]); await sequelize.sync({ force: true }); return { User, Project, Task }; }); afterEach(() => { sinon.restore(); }); describe('with "manualOnDelete" = "all"', () => { it('cascade deletes in JavaScript', async () => { const { User, Project, Task } = vars; const user = await User.create({ id: 1 }); const project = await Project.create({ id: 1, ownerId: user.id }); await Task.create({ id: 1, projectId: project.id }); const spy = sinon.spy(sequelize, 'queryRaw'); await User.modelRepository._UNSTABLE_bulkDestroy({ where: { id: user.id }, manualOnDelete: ManualOnDelete.all, }); const calls = spy.getCalls().map(call => call.args[0]); expectPerDialect(() => calls, { default: toMatchSql([ 'START TRANSACTION', 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;', 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);', 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);', 'DELETE FROM [Tasks] WHERE [id] = 1', 'DELETE FROM [Projects] WHERE [id] = 1', 'DELETE FROM [Users] WHERE [id] = 1', 'COMMIT', ]), mssql: toMatchSql([ // mssql transactions don't go through .queryRaw, they are called on the connection object // 'BEGIN TRANSACTION;', 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;', 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);', 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);', 'DELETE FROM [Tasks] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', 'DELETE FROM [Projects] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', 'DELETE FROM [Users] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', // 'COMMIT TRANSACTION;', ]), db2: toMatchSql( [ // db2 transactions don't go through .queryRaw, they are called on the connection object // 'BEGIN TRANSACTION;', 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;', 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);', 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);', 'DELETE FROM [Tasks] WHERE [id] = 1', 'DELETE FROM [Projects] WHERE [id] = 1', 'DELETE FROM [Users] WHERE [id] = 1', // 'COMMIT TRANSACTION;', ], { genericQuotes: true }, ), sqlite3: toMatchSql( [ 'BEGIN DEFERRED TRANSACTION', 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;', 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);', 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);', 'DELETE FROM [Tasks] WHERE [id] = 1', 'DELETE FROM [Projects] WHERE [id] = 1', 'DELETE FROM [Users] WHERE [id] = 1', 'COMMIT', ], { genericQuotes: true }, ), }); }); it('does not start a transaction if one is already started', async () => { const { User, Project, Task } = vars; const user = await User.create({ id: 1 }); const project = await Project.create({ id: 1, ownerId: user.id }); await Task.create({ id: 1, projectId: project.id }); const calls = await sequelize.transaction(async transaction => { const spy = sinon.spy(sequelize, 'queryRaw'); await User.modelRepository._UNSTABLE_bulkDestroy({ where: { id: user.id }, manualOnDelete: ManualOnDelete.all, transaction, }); return spy.getCalls().map(call => call.args[0]); }); expectPerDialect(() => calls, { default: toMatchSql([ 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;', 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);', 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);', 'DELETE FROM [Tasks] WHERE [id] = 1', 'DELETE FROM [Projects] WHERE [id] = 1', 'DELETE FROM [Users] WHERE [id] = 1', ]), mssql: toMatchSql([ 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;', 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);', 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);', 'DELETE FROM [Tasks] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', 'DELETE FROM [Projects] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', 'DELETE FROM [Users] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', ]), }); }); }); });