bulk-destroy.test.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import type {
  2. CreationOptional,
  3. InferAttributes,
  4. InferCreationAttributes,
  5. NonAttribute,
  6. } from '@sequelize/core';
  7. import { DataTypes, ManualOnDelete, Model } from '@sequelize/core';
  8. import { Attribute, BelongsTo, NotNull } from '@sequelize/core/decorators-legacy';
  9. import sinon from 'sinon';
  10. import { beforeAll2, expectPerDialect, sequelize, toMatchSql } from '../../support';
  11. import { setResetMode } from '../support';
  12. describe('ModelRepository#_UNSTABLE_bulkDestroy', () => {
  13. setResetMode('destroy');
  14. const vars = beforeAll2(async () => {
  15. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  16. declare id: CreationOptional<number>;
  17. }
  18. class Project extends Model<InferAttributes<Project>, InferCreationAttributes<Project>> {
  19. declare id: CreationOptional<number>;
  20. @NotNull
  21. @Attribute(DataTypes.INTEGER)
  22. declare ownerId: number;
  23. @BelongsTo(() => User, 'ownerId')
  24. declare owner: NonAttribute<User>;
  25. }
  26. class Task extends Model<InferAttributes<Task>, InferCreationAttributes<Task>> {
  27. declare id: CreationOptional<number>;
  28. @NotNull
  29. @Attribute(DataTypes.INTEGER)
  30. declare projectId: number;
  31. @BelongsTo(() => Project, 'projectId')
  32. declare project: NonAttribute<Project>;
  33. }
  34. sequelize.addModels([User, Project, Task]);
  35. await sequelize.sync({ force: true });
  36. return { User, Project, Task };
  37. });
  38. afterEach(() => {
  39. sinon.restore();
  40. });
  41. describe('with "manualOnDelete" = "all"', () => {
  42. it('cascade deletes in JavaScript', async () => {
  43. const { User, Project, Task } = vars;
  44. const user = await User.create({ id: 1 });
  45. const project = await Project.create({ id: 1, ownerId: user.id });
  46. await Task.create({ id: 1, projectId: project.id });
  47. const spy = sinon.spy(sequelize, 'queryRaw');
  48. await User.modelRepository._UNSTABLE_bulkDestroy({
  49. where: { id: user.id },
  50. manualOnDelete: ManualOnDelete.all,
  51. });
  52. const calls = spy.getCalls().map(call => call.args[0]);
  53. expectPerDialect(() => calls, {
  54. default: toMatchSql([
  55. 'START TRANSACTION',
  56. 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;',
  57. 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);',
  58. 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);',
  59. 'DELETE FROM [Tasks] WHERE [id] = 1',
  60. 'DELETE FROM [Projects] WHERE [id] = 1',
  61. 'DELETE FROM [Users] WHERE [id] = 1',
  62. 'COMMIT',
  63. ]),
  64. mssql: toMatchSql([
  65. // mssql transactions don't go through .queryRaw, they are called on the connection object
  66. // 'BEGIN TRANSACTION;',
  67. 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;',
  68. 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);',
  69. 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);',
  70. 'DELETE FROM [Tasks] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;',
  71. 'DELETE FROM [Projects] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;',
  72. 'DELETE FROM [Users] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;',
  73. // 'COMMIT TRANSACTION;',
  74. ]),
  75. db2: toMatchSql(
  76. [
  77. // db2 transactions don't go through .queryRaw, they are called on the connection object
  78. // 'BEGIN TRANSACTION;',
  79. 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;',
  80. 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);',
  81. 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);',
  82. 'DELETE FROM [Tasks] WHERE [id] = 1',
  83. 'DELETE FROM [Projects] WHERE [id] = 1',
  84. 'DELETE FROM [Users] WHERE [id] = 1',
  85. // 'COMMIT TRANSACTION;',
  86. ],
  87. { genericQuotes: true },
  88. ),
  89. sqlite3: toMatchSql(
  90. [
  91. 'BEGIN DEFERRED TRANSACTION',
  92. 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;',
  93. 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);',
  94. 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);',
  95. 'DELETE FROM [Tasks] WHERE [id] = 1',
  96. 'DELETE FROM [Projects] WHERE [id] = 1',
  97. 'DELETE FROM [Users] WHERE [id] = 1',
  98. 'COMMIT',
  99. ],
  100. { genericQuotes: true },
  101. ),
  102. });
  103. });
  104. it('does not start a transaction if one is already started', async () => {
  105. const { User, Project, Task } = vars;
  106. const user = await User.create({ id: 1 });
  107. const project = await Project.create({ id: 1, ownerId: user.id });
  108. await Task.create({ id: 1, projectId: project.id });
  109. const calls = await sequelize.transaction(async transaction => {
  110. const spy = sinon.spy(sequelize, 'queryRaw');
  111. await User.modelRepository._UNSTABLE_bulkDestroy({
  112. where: { id: user.id },
  113. manualOnDelete: ManualOnDelete.all,
  114. transaction,
  115. });
  116. return spy.getCalls().map(call => call.args[0]);
  117. });
  118. expectPerDialect(() => calls, {
  119. default: toMatchSql([
  120. 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;',
  121. 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);',
  122. 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);',
  123. 'DELETE FROM [Tasks] WHERE [id] = 1',
  124. 'DELETE FROM [Projects] WHERE [id] = 1',
  125. 'DELETE FROM [Users] WHERE [id] = 1',
  126. ]),
  127. mssql: toMatchSql([
  128. 'SELECT [id], [createdAt], [updatedAt] FROM [Users] AS [User] WHERE [User].[id] = 1;',
  129. 'SELECT [id], [ownerId], [createdAt], [updatedAt] FROM [Projects] AS [Project] WHERE [Project].[ownerId] IN (1);',
  130. 'SELECT [id], [projectId], [createdAt], [updatedAt] FROM [Tasks] AS [Task] WHERE [Task].[projectId] IN (1);',
  131. 'DELETE FROM [Tasks] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;',
  132. 'DELETE FROM [Projects] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;',
  133. 'DELETE FROM [Users] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;',
  134. ]),
  135. });
  136. });
  137. });
  138. });