upsert.test.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { DataTypes, literal } from '@sequelize/core';
  2. import { expect } from 'chai';
  3. import sinon from 'sinon';
  4. import { beforeAll2, expectsql, sequelize } from '../../support';
  5. const dialectName = sequelize.dialect.name;
  6. describe('QueryInterface#upsert', () => {
  7. if (!sequelize.dialect.supports.upserts) {
  8. return;
  9. }
  10. const vars = beforeAll2(() => {
  11. const User = sequelize.define(
  12. 'User',
  13. {
  14. firstName: DataTypes.STRING,
  15. },
  16. { timestamps: false },
  17. );
  18. return { User };
  19. });
  20. afterEach(() => {
  21. sinon.restore();
  22. });
  23. // you'll find more replacement tests in query-generator tests
  24. it('does not parse replacements outside of raw sql', async () => {
  25. const { User } = vars;
  26. const stub = sinon.stub(sequelize, 'queryRaw');
  27. await sequelize.queryInterface.upsert(
  28. User.tableName,
  29. { firstName: ':name' },
  30. { firstName: ':name' },
  31. { id: ':id' },
  32. {
  33. model: User,
  34. replacements: {
  35. name: 'Zoe',
  36. data: 'abc',
  37. },
  38. },
  39. );
  40. expect(stub.callCount).to.eq(1);
  41. const firstCall = stub.getCall(0);
  42. expectsql(firstCall.args[0], {
  43. default:
  44. 'INSERT INTO [Users] ([firstName]) VALUES ($sequelize_1) ON CONFLICT ([id]) DO UPDATE SET [firstName]=EXCLUDED.[firstName];',
  45. 'mariadb mysql':
  46. 'INSERT INTO `Users` (`firstName`) VALUES ($sequelize_1) ON DUPLICATE KEY UPDATE `firstName`=$sequelize_1;',
  47. mssql: `
  48. MERGE INTO [Users] WITH(HOLDLOCK)
  49. AS [Users_target]
  50. USING (VALUES(N':name')) AS [Users_source]([firstName])
  51. ON [Users_target].[id] = [Users_source].[id]
  52. WHEN MATCHED THEN
  53. UPDATE SET [Users_target].[firstName] = N':name'
  54. WHEN NOT MATCHED THEN
  55. INSERT ([firstName]) VALUES(N':name') OUTPUT $action, INSERTED.*;
  56. `,
  57. db2: `
  58. MERGE INTO "Users"
  59. AS "Users_target"
  60. USING (VALUES(':name')) AS "Users_source"("firstName")
  61. ON "Users_target"."id" = "Users_source"."id"
  62. WHEN MATCHED THEN
  63. UPDATE SET "Users_target"."firstName" = ':name'
  64. WHEN NOT MATCHED THEN
  65. INSERT ("firstName") VALUES(':name');
  66. `,
  67. });
  68. if (dialectName === 'mssql' || dialectName === 'db2') {
  69. expect(firstCall.args[1]?.bind).to.be.undefined;
  70. } else {
  71. expect(firstCall.args[1]?.bind).to.deep.eq({
  72. sequelize_1: ':name',
  73. });
  74. }
  75. });
  76. it('throws if a bind parameter name starts with the reserved "sequelize_" prefix', async () => {
  77. const { User } = vars;
  78. sinon.stub(sequelize, 'queryRaw');
  79. await expect(
  80. sequelize.queryInterface.upsert(
  81. User.tableName,
  82. { firstName: literal('$sequelize_test') },
  83. { firstName: ':name' },
  84. { id: ':id' },
  85. {
  86. model: User,
  87. bind: {
  88. sequelize_test: 'test',
  89. },
  90. },
  91. ),
  92. ).to.be.rejectedWith(
  93. 'Bind parameters cannot start with "sequelize_", these bind parameters are reserved by Sequelize.',
  94. );
  95. });
  96. it('merges user-provided bind parameters with sequelize-generated bind parameters (object bind)', async () => {
  97. const { User } = vars;
  98. const stub = sinon.stub(sequelize, 'queryRaw');
  99. await sequelize.queryInterface.upsert(
  100. User.tableName,
  101. {
  102. firstName: literal('$firstName'),
  103. lastName: 'Doe',
  104. },
  105. {},
  106. // TODO: weird mssql/db2 specific behavior that should be unified
  107. dialectName === 'mssql' || dialectName === 'db2' ? { id: 1 } : {},
  108. {
  109. model: User,
  110. bind: {
  111. firstName: 'John',
  112. },
  113. },
  114. );
  115. expect(stub.callCount).to.eq(1);
  116. const firstCall = stub.getCall(0);
  117. expectsql(firstCall.args[0], {
  118. default:
  119. 'INSERT INTO [Users] ([firstName],[lastName]) VALUES ($firstName,$sequelize_1) ON CONFLICT ([id]) DO NOTHING;',
  120. 'mariadb mysql':
  121. 'INSERT INTO `Users` (`firstName`,`lastName`) VALUES ($firstName,$sequelize_1) ON DUPLICATE KEY UPDATE `id`=`id`;',
  122. mssql: `
  123. MERGE INTO [Users] WITH(HOLDLOCK) AS [Users_target]
  124. USING (VALUES($firstName, N'Doe')) AS [Users_source]([firstName], [lastName])
  125. ON [Users_target].[id] = [Users_source].[id]
  126. WHEN NOT MATCHED THEN
  127. INSERT ([firstName], [lastName]) VALUES($firstName, N'Doe')
  128. OUTPUT $action, INSERTED.*;
  129. `,
  130. db2: `
  131. MERGE INTO "Users" AS "Users_target"
  132. USING (VALUES($firstName, 'Doe')) AS "Users_source"("firstName", "lastName")
  133. ON "Users_target"."id" = "Users_source"."id"
  134. WHEN NOT MATCHED THEN
  135. INSERT ("firstName", "lastName") VALUES($firstName, 'Doe');
  136. `,
  137. });
  138. if (dialectName === 'mssql' || dialectName === 'db2') {
  139. expect(firstCall.args[1]?.bind).to.deep.eq({
  140. firstName: 'John',
  141. });
  142. } else {
  143. expect(firstCall.args[1]?.bind).to.deep.eq({
  144. firstName: 'John',
  145. sequelize_1: 'Doe',
  146. });
  147. }
  148. });
  149. it('merges user-provided bind parameters with sequelize-generated bind parameters (array bind)', async () => {
  150. const { User } = vars;
  151. const stub = sinon.stub(sequelize, 'queryRaw');
  152. await sequelize.queryInterface.upsert(
  153. User.tableName,
  154. {
  155. firstName: literal('$1'),
  156. lastName: 'Doe',
  157. },
  158. {},
  159. // TODO: weird mssql/db2 specific behavior that should be unified
  160. dialectName === 'mssql' || dialectName === 'db2' ? { id: 1 } : {},
  161. {
  162. model: User,
  163. bind: ['John'],
  164. },
  165. );
  166. expect(stub.callCount).to.eq(1);
  167. const firstCall = stub.getCall(0);
  168. expectsql(firstCall.args[0], {
  169. default:
  170. 'INSERT INTO [Users] ([firstName],[lastName]) VALUES ($1,$sequelize_1) ON CONFLICT ([id]) DO NOTHING;',
  171. 'mariadb mysql':
  172. 'INSERT INTO `Users` (`firstName`,`lastName`) VALUES ($1,$sequelize_1) ON DUPLICATE KEY UPDATE `id`=`id`;',
  173. mssql: `
  174. MERGE INTO [Users] WITH(HOLDLOCK) AS [Users_target]
  175. USING (VALUES($1, N'Doe')) AS [Users_source]([firstName], [lastName])
  176. ON [Users_target].[id] = [Users_source].[id]
  177. WHEN NOT MATCHED THEN
  178. INSERT ([firstName], [lastName]) VALUES($1, N'Doe')
  179. OUTPUT $action, INSERTED.*;
  180. `,
  181. db2: `
  182. MERGE INTO "Users" AS "Users_target"
  183. USING (VALUES($1, 'Doe')) AS "Users_source"("firstName", "lastName")
  184. ON "Users_target"."id" = "Users_source"."id"
  185. WHEN NOT MATCHED THEN
  186. INSERT ("firstName", "lastName") VALUES($1, 'Doe');
  187. `,
  188. });
  189. // mssql does not generate any bind parameter
  190. if (dialectName === 'mssql' || dialectName === 'db2') {
  191. expect(firstCall.args[1]?.bind).to.deep.eq(['John']);
  192. } else {
  193. expect(firstCall.args[1]?.bind).to.deep.eq({
  194. 1: 'John',
  195. sequelize_1: 'Doe',
  196. });
  197. }
  198. });
  199. it('binds parameters if they are literals', async () => {
  200. const { User } = vars;
  201. const stub = sinon.stub(sequelize, 'queryRaw');
  202. await sequelize.queryInterface.upsert(
  203. User.tableName,
  204. {
  205. firstName: 'Jonh',
  206. counter: literal('`counter` + 1'),
  207. },
  208. {
  209. counter: literal('`counter` + 1'),
  210. },
  211. // TODO: weird mssql/db2 specific behavior that should be unified
  212. dialectName === 'mssql' || dialectName === 'db2' ? { id: 1 } : {},
  213. {
  214. model: User,
  215. },
  216. );
  217. expect(stub.callCount).to.eq(1);
  218. const firstCall = stub.getCall(0);
  219. expectsql(firstCall.args[0], {
  220. default:
  221. 'INSERT INTO `Users` (`firstName`,`counter`) VALUES ($sequelize_1,`counter` + 1) ON DUPLICATE KEY UPDATE `counter`=`counter` + 1;',
  222. postgres:
  223. 'INSERT INTO "Users" ("firstName","counter") VALUES ($sequelize_1,`counter` + 1) ON CONFLICT ("id") DO UPDATE SET "counter"=EXCLUDED."counter";',
  224. mssql: `
  225. MERGE INTO [Users] WITH(HOLDLOCK) AS [Users_target]
  226. USING (VALUES(N'Jonh', \`counter\` + 1)) AS [Users_source]([firstName], [counter])
  227. ON [Users_target].[id] = [Users_source].[id] WHEN MATCHED THEN UPDATE SET [Users_target].[counter] = \`counter\` + 1
  228. WHEN NOT MATCHED THEN INSERT ([firstName], [counter]) VALUES(N'Jonh', \`counter\` + 1) OUTPUT $action, INSERTED.*;
  229. `,
  230. sqlite3:
  231. 'INSERT INTO `Users` (`firstName`,`counter`) VALUES ($sequelize_1,`counter` + 1) ON CONFLICT (`id`) DO UPDATE SET `counter`=EXCLUDED.`counter`;',
  232. db2: `
  233. MERGE INTO "Users" AS "Users_target"
  234. USING (VALUES('Jonh', \`counter\` + 1)) AS "Users_source"("firstName", "counter")
  235. ON "Users_target"."id" = "Users_source"."id" WHEN MATCHED THEN UPDATE SET "Users_target"."counter" = \`counter\` + 1
  236. WHEN NOT MATCHED THEN INSERT ("firstName", "counter") VALUES('Jonh', \`counter\` + 1);
  237. `,
  238. });
  239. });
  240. });