update.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. import type { CreationOptional, InferAttributes, InferCreationAttributes } from '@sequelize/core';
  2. import { DataTypes, Model, sql } from '@sequelize/core';
  3. import { Attribute, ColumnName, PrimaryKey, Table } from '@sequelize/core/decorators-legacy';
  4. import chai from 'chai';
  5. import sinon from 'sinon';
  6. import {
  7. beforeAll2,
  8. createSingleTransactionalTestSequelizeInstance,
  9. expectsql,
  10. sequelize,
  11. setResetMode,
  12. } from '../support';
  13. const expect = chai.expect;
  14. const dialect = sequelize.dialect;
  15. const dialectName = dialect.name;
  16. describe('Model.update', () => {
  17. context('test-shared models', () => {
  18. setResetMode('destroy');
  19. const vars = beforeAll2(async () => {
  20. const clock = sinon.useFakeTimers();
  21. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  22. declare id: CreationOptional<number>;
  23. declare updatedAt: CreationOptional<Date>;
  24. declare createdAt: CreationOptional<Date>;
  25. @Attribute(DataTypes.STRING)
  26. declare username: string | null;
  27. @Attribute(DataTypes.STRING)
  28. declare email: string | null;
  29. }
  30. @Table({ paranoid: true })
  31. class ParanoidUser extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  32. declare id: CreationOptional<number>;
  33. declare updatedAt: CreationOptional<Date>;
  34. declare createdAt: CreationOptional<Date>;
  35. @Attribute(DataTypes.STRING)
  36. declare username: string | null;
  37. @Attribute(DataTypes.STRING)
  38. declare email: string | null;
  39. }
  40. sequelize.addModels([User, ParanoidUser]);
  41. await sequelize.sync({ force: true });
  42. return { User, ParanoidUser, clock };
  43. });
  44. afterEach(() => {
  45. vars.clock.reset();
  46. });
  47. after(() => {
  48. vars.clock.restore();
  49. });
  50. it('throws an error if no where clause is given', async () => {
  51. // @ts-expect-error -- testing that this fails
  52. await expect(vars.User.update({}, {})).to.be.rejectedWith(
  53. Error,
  54. 'Missing where attribute in the options parameter',
  55. );
  56. });
  57. it('only updates rows that match where', async () => {
  58. const { User } = vars;
  59. await User.bulkCreate([{ username: 'Peter' }, { username: 'Peter' }, { username: 'Bob' }]);
  60. await User.update({ username: 'John' }, { where: { username: 'Peter' } });
  61. const users = await User.findAll({ order: ['username'] });
  62. expect(users).to.have.lengthOf(3);
  63. expect(users[0].username).to.equal('Bob');
  64. expect(users[1].username).to.equal('John');
  65. expect(users[2].username).to.equal('John');
  66. });
  67. // TODO: rename "fields" -> "attributes"
  68. it('updates only attributes specified by "fields" option', async () => {
  69. const { User } = vars;
  70. const data = [{ username: 'Peter', email: 'first-email' }];
  71. await User.bulkCreate(data);
  72. await User.update(
  73. { username: 'Bill', email: 'second-email' },
  74. { where: { email: 'first-email' }, fields: ['username'] },
  75. );
  76. const users = await User.findAll();
  77. expect(users).to.have.lengthOf(1);
  78. expect(users[0].username).to.equal('Bill');
  79. expect(users[0].email).to.equal('first-email');
  80. });
  81. it('updates with casting', async () => {
  82. const { User } = vars;
  83. await User.create({ username: 'John' });
  84. await User.update(
  85. {
  86. // @ts-expect-error -- TODO: fix typing to allow this
  87. username: sql.cast('1', dialectName === 'mssql' ? 'nvarchar' : 'char'),
  88. },
  89. {
  90. where: { username: 'John' },
  91. },
  92. );
  93. expect((await User.findOne({ rejectOnEmpty: true })).username).to.equal('1');
  94. });
  95. it('updates with function and column value', async () => {
  96. const { User } = vars;
  97. await User.create({ username: 'John' });
  98. await User.update(
  99. {
  100. username: sql.fn('upper', sql.col('username')),
  101. },
  102. {
  103. where: { username: 'John' },
  104. },
  105. );
  106. expect((await User.findOne({ rejectOnEmpty: true })).username).to.equal('JOHN');
  107. });
  108. it('should properly set data when individualHooks are true', async () => {
  109. const { User } = vars;
  110. const unhook = User.hooks.addListener('beforeUpdate', instance => {
  111. instance.set('email', 'new email');
  112. });
  113. try {
  114. const user = await User.create({ username: 'Peter' });
  115. await User.update(
  116. { username: 'John' },
  117. {
  118. where: { id: user.id },
  119. individualHooks: true,
  120. },
  121. );
  122. expect((await User.findByPk(user.id, { rejectOnEmpty: true })).email).to.equal('new email');
  123. } finally {
  124. unhook();
  125. }
  126. });
  127. it('sets updatedAt to the current timestamp', async () => {
  128. const { User } = vars;
  129. await User.bulkCreate([{ username: 'Peter' }, { username: 'Paul' }, { username: 'Bob' }]);
  130. let users = await User.findAll({ order: ['id'] });
  131. const updatedAt = users[0].updatedAt;
  132. expect(updatedAt).to.be.ok;
  133. expect(updatedAt).to.equalTime(users[2].updatedAt); // All users should have the same updatedAt
  134. vars.clock.tick(1000);
  135. await User.update({ username: 'Bill' }, { where: { username: 'Bob' } });
  136. users = await User.findAll({ order: ['username'] });
  137. expect(users[0].username).to.equal('Bill');
  138. expect(users[1].username).to.equal('Paul');
  139. expect(users[2].username).to.equal('Peter');
  140. expect(users[0].updatedAt).to.be.afterTime(updatedAt);
  141. expect(users[1].updatedAt).to.equalTime(updatedAt);
  142. expect(users[2].updatedAt).to.equalTime(updatedAt);
  143. });
  144. it('does not update timestamps when passing silent=true in a bulk update', async () => {
  145. const { User, clock } = vars;
  146. await User.bulkCreate([{ username: 'Paul' }, { username: 'Peter' }]);
  147. const users0 = await User.findAll();
  148. const updatedAtPaul = users0[0].updatedAt;
  149. const updatedAtPeter = users0[1].updatedAt;
  150. clock.tick(150);
  151. await User.update({ username: 'John' }, { where: {}, silent: true });
  152. const users = await User.findAll();
  153. expect(users[0].updatedAt).to.equalTime(updatedAtPeter);
  154. expect(users[1].updatedAt).to.equalTime(updatedAtPaul);
  155. });
  156. it('returns the number of affected rows', async () => {
  157. const { User } = vars;
  158. await User.bulkCreate([{ username: 'Peter' }, { username: 'Paul' }, { username: 'Bob' }]);
  159. const [affectedRows] = await User.update({ username: 'Bill' }, { where: {} });
  160. expect(affectedRows).to.equal(3);
  161. });
  162. it('does not update soft deleted records when model is paranoid', async () => {
  163. const { ParanoidUser } = vars;
  164. await ParanoidUser.bulkCreate([{ username: 'user1' }, { username: 'user2' }]);
  165. await ParanoidUser.destroy({
  166. where: { username: 'user1' },
  167. });
  168. await ParanoidUser.update({ username: 'foo' }, { where: {} });
  169. const users = await ParanoidUser.findAll({
  170. paranoid: false,
  171. where: {
  172. username: 'foo',
  173. },
  174. });
  175. expect(users).to.have.lengthOf(1, 'should not update soft-deleted record');
  176. });
  177. it('updates soft deleted records when paranoid is overridden', async () => {
  178. const { ParanoidUser } = vars;
  179. await ParanoidUser.bulkCreate([{ username: 'user1' }, { username: 'user2' }]);
  180. await ParanoidUser.destroy({ where: { username: 'user1' } });
  181. await ParanoidUser.update(
  182. { username: 'foo' },
  183. {
  184. where: {},
  185. paranoid: false,
  186. },
  187. );
  188. const users = await ParanoidUser.findAll({
  189. paranoid: false,
  190. });
  191. expect(users).to.have.lengthOf(2);
  192. });
  193. it('calls update hook for soft deleted objects', async () => {
  194. const hookSpy = sinon.spy();
  195. const { ParanoidUser } = vars;
  196. const unhook = ParanoidUser.hooks.addListener('beforeUpdate', hookSpy);
  197. try {
  198. await ParanoidUser.bulkCreate([{ username: 'user1' }]);
  199. await ParanoidUser.destroy({
  200. where: {
  201. username: 'user1',
  202. },
  203. });
  204. await ParanoidUser.update(
  205. { username: 'updUser1' },
  206. {
  207. paranoid: false,
  208. where: { username: 'user1' },
  209. individualHooks: true,
  210. },
  211. );
  212. const user = await ParanoidUser.findOne({
  213. where: { username: 'updUser1' },
  214. rejectOnEmpty: true,
  215. paranoid: false,
  216. });
  217. expect(user.username).to.eq('updUser1');
  218. expect(hookSpy).to.have.been.called;
  219. } finally {
  220. unhook();
  221. }
  222. });
  223. if (dialect.supports['LIMIT ON UPDATE']) {
  224. it('supports limit clause', async () => {
  225. const { User } = vars;
  226. await User.bulkCreate([
  227. { username: 'Peter' },
  228. { username: 'Peter' },
  229. { username: 'Peter' },
  230. ]);
  231. const [affectedRows] = await User.update(
  232. { username: 'Bob' },
  233. {
  234. where: {},
  235. limit: 1,
  236. },
  237. );
  238. expect(affectedRows).to.equal(1);
  239. });
  240. }
  241. it('skips query if there is no data to update', async () => {
  242. const { User } = vars;
  243. const spy = sinon.spy();
  244. await User.create({});
  245. const result = await User.update(
  246. {
  247. // @ts-expect-error -- TODO: throw if trying to update non-existing attribute
  248. unknownField: 'haha',
  249. },
  250. {
  251. where: {},
  252. logging: spy,
  253. },
  254. );
  255. expect(result[0]).to.equal(0);
  256. expect(spy.called, 'Update query was issued when no data to update').to.be.false;
  257. });
  258. it('treats undefined like if the property were not set: The attribute is ignored', async () => {
  259. const { User } = vars;
  260. const account = await User.create({
  261. username: 'username 1',
  262. email: 'email 1',
  263. });
  264. await User.update(
  265. {
  266. username: 'username 2',
  267. email: undefined,
  268. },
  269. {
  270. where: {
  271. id: account.get('id'),
  272. },
  273. },
  274. );
  275. await account.reload();
  276. expect(account.email).to.equal('email 1');
  277. });
  278. if (sequelize.dialect.supports.returnValues) {
  279. it('should return the updated record', async () => {
  280. const { User } = vars;
  281. await User.create({ username: 'username 1', id: 5 });
  282. const [, accounts] = await User.update(
  283. { username: 'username 2' },
  284. {
  285. where: {},
  286. returning: true,
  287. },
  288. );
  289. const firstAcc = accounts[0];
  290. expect(firstAcc.username).to.equal('username 2');
  291. expect(firstAcc.id).to.equal(5);
  292. });
  293. }
  294. });
  295. context('test-specific models', () => {
  296. if (sequelize.dialect.supports.transactions) {
  297. it('supports transactions', async () => {
  298. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  299. @Attribute(DataTypes.STRING)
  300. declare username: string | null;
  301. }
  302. const transactionSequelize =
  303. await createSingleTransactionalTestSequelizeInstance(sequelize);
  304. transactionSequelize.addModels([User]);
  305. await User.sync({ force: true });
  306. await User.create({ username: 'foo' });
  307. const t = await transactionSequelize.startUnmanagedTransaction();
  308. await User.update(
  309. { username: 'bar' },
  310. {
  311. where: { username: 'foo' },
  312. transaction: t,
  313. },
  314. );
  315. const users1 = await User.findAll();
  316. const users2 = await User.findAll({ transaction: t });
  317. expect(users1[0].username).to.equal('foo');
  318. expect(users2[0].username).to.equal('bar');
  319. await t.rollback();
  320. });
  321. }
  322. it('should map the correct fields when saving instance (#10589)', async () => {
  323. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  324. @ColumnName('id2')
  325. @Attribute(DataTypes.INTEGER)
  326. declare id: number;
  327. @ColumnName('id3')
  328. @Attribute(DataTypes.INTEGER)
  329. declare id2: number;
  330. @ColumnName('id')
  331. @Attribute(DataTypes.INTEGER)
  332. @PrimaryKey
  333. declare id3: number;
  334. }
  335. sequelize.addModels([User]);
  336. await sequelize.sync({ force: true });
  337. await User.create({ id3: 94, id: 87, id2: 943 });
  338. const user = await User.findByPk(94, { rejectOnEmpty: true });
  339. await user.update({ id2: 8877 });
  340. expect((await User.findByPk(94, { rejectOnEmpty: true })).id2).to.equal(8877);
  341. });
  342. it('updates the attributes that we select only without updating createdAt', async () => {
  343. const User = sequelize.define(
  344. 'User1',
  345. {
  346. username: DataTypes.STRING,
  347. secretValue: DataTypes.STRING,
  348. },
  349. {
  350. paranoid: true,
  351. tableName: 'users1',
  352. },
  353. );
  354. let test = false;
  355. await User.sync({ force: true });
  356. const user = await User.create({ username: 'Peter', secretValue: '42' });
  357. await user.update(
  358. { secretValue: '43' },
  359. {
  360. fields: ['secretValue'],
  361. logging(sqlQuery: string) {
  362. test = true;
  363. expect(sqlQuery).to.match(/^Executing \(default\): /);
  364. sqlQuery = sqlQuery.slice(21);
  365. expectsql(sqlQuery, {
  366. default: `UPDATE [users1] SET [secretValue]=$sequelize_1,[updatedAt]=$sequelize_2 WHERE [id] = $sequelize_3`,
  367. sqlite3:
  368. 'UPDATE `users1` SET `secretValue`=$sequelize_1,`updatedAt`=$sequelize_2 WHERE `id` = $sequelize_3 RETURNING *',
  369. postgres: `UPDATE "users1" SET "secretValue"=$1,"updatedAt"=$2 WHERE "id" = $3 RETURNING *`,
  370. mysql: 'UPDATE `users1` SET `secretValue`=?,`updatedAt`=? WHERE `id` = ?',
  371. mariadb: 'UPDATE `users1` SET `secretValue`=?,`updatedAt`=? WHERE `id` = ?',
  372. mssql: `UPDATE [users1] SET [secretValue]=@sequelize_1,[updatedAt]=@sequelize_2 OUTPUT INSERTED.* WHERE [id] = @sequelize_3`,
  373. db2: `SELECT * FROM FINAL TABLE (UPDATE "users1" SET "secretValue"=?,"updatedAt"=? WHERE "id" = ?);`,
  374. ibmi: `UPDATE "users1" SET "secretValue"=?,"updatedAt"=? WHERE "id" = ?;`,
  375. });
  376. },
  377. returning: [sql.col('*')],
  378. },
  379. );
  380. expect(test).to.be.true;
  381. });
  382. it('allows sql logging of updated statements', async () => {
  383. const User = sequelize.define(
  384. 'User',
  385. {
  386. name: DataTypes.STRING,
  387. bio: DataTypes.TEXT,
  388. },
  389. {
  390. paranoid: true,
  391. },
  392. );
  393. let test = false;
  394. await User.sync({ force: true });
  395. const u = await User.create({ name: 'meg', bio: 'none' });
  396. expect(u).to.exist;
  397. await u.update(
  398. { name: 'brian' },
  399. {
  400. logging(sqlQuery) {
  401. test = true;
  402. expect(sqlQuery).to.exist;
  403. expect(sqlQuery.toUpperCase()).to.include('UPDATE');
  404. },
  405. },
  406. );
  407. expect(test).to.be.true;
  408. });
  409. it('does not update virtual attributes', async () => {
  410. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  411. @Attribute(DataTypes.STRING)
  412. declare username: string | null;
  413. // TODO: throw if a virtual attribute does not have either a getter or a setter
  414. @Attribute(DataTypes.VIRTUAL)
  415. declare virtual: CreationOptional<string>;
  416. }
  417. sequelize.addModels([User]);
  418. await User.sync();
  419. await User.create({ username: 'jan' });
  420. // TODO: Model.update should always throw an error if a virtual attributes are used (even if it has a setter, no access to it from static update)
  421. await User.update(
  422. {
  423. username: 'kurt',
  424. virtual: 'test',
  425. },
  426. {
  427. where: {
  428. username: 'jan',
  429. },
  430. },
  431. );
  432. const user = await User.findOne({ rejectOnEmpty: true });
  433. expect(user.username).to.equal('kurt');
  434. expect(user.virtual).to.not.equal('test');
  435. });
  436. it('updates attributes that are altered by virtual setters', async () => {
  437. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  438. @Attribute(DataTypes.STRING)
  439. declare username: string | null;
  440. @Attribute(DataTypes.STRING)
  441. declare illnessName: string;
  442. @Attribute(DataTypes.INTEGER)
  443. declare illnessPain: number;
  444. @Attribute(DataTypes.VIRTUAL)
  445. set illness(value: CreationOptional<{ pain: number; name: string }>) {
  446. this.set('illnessName', value.name);
  447. this.set('illnessPain', value.pain);
  448. }
  449. }
  450. sequelize.addModels([User]);
  451. await User.sync({ force: true });
  452. await User.create({
  453. username: 'Jan',
  454. illnessName: 'Headache',
  455. illnessPain: 5,
  456. });
  457. await User.update(
  458. {
  459. illness: { pain: 10, name: 'Backache' },
  460. },
  461. {
  462. where: {
  463. username: 'Jan',
  464. },
  465. },
  466. );
  467. expect((await User.findOne({ rejectOnEmpty: true })).illnessPain).to.equal(10);
  468. });
  469. it(`doesn't update attributes that are altered by virtual setters when "sideEffects" is false`, async () => {
  470. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  471. @Attribute(DataTypes.STRING)
  472. declare username: string | null;
  473. @Attribute(DataTypes.STRING)
  474. declare illnessName: string;
  475. @Attribute(DataTypes.INTEGER)
  476. declare illnessPain: number;
  477. @Attribute(DataTypes.VIRTUAL)
  478. set illness(value: CreationOptional<{ pain: number; name: string }>) {
  479. this.set('illnessName', value.name);
  480. this.set('illnessPain', value.pain);
  481. }
  482. }
  483. sequelize.addModels([User]);
  484. await User.sync({ force: true });
  485. await User.create({
  486. username: 'Jan',
  487. illnessName: 'Headache',
  488. illnessPain: 5,
  489. });
  490. await User.update(
  491. {
  492. illness: { pain: 10, name: 'Backache' },
  493. },
  494. {
  495. where: {
  496. username: 'Jan',
  497. },
  498. sideEffects: false,
  499. },
  500. );
  501. expect((await User.findOne({ rejectOnEmpty: true })).illnessPain).to.equal(5);
  502. });
  503. });
  504. });