select-query.test.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200
  1. import type {
  2. CreationOptional,
  3. InferAttributes,
  4. InferCreationAttributes,
  5. Model,
  6. } from '@sequelize/core';
  7. import { DataTypes, IndexHints, Op, TableHints, or, sql as sqlTag } from '@sequelize/core';
  8. import { _validateIncludedElements } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-internals.js';
  9. import { buildInvalidOptionReceivedError } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/check.js';
  10. import { expect } from 'chai';
  11. import { beforeAll2, expectsql, getTestDialect, sequelize } from '../../support';
  12. const { attribute, col, cast, where, fn, literal } = sqlTag;
  13. const dialectName = getTestDialect();
  14. describe('QueryGenerator#selectQuery', () => {
  15. const queryGenerator = sequelize.queryGenerator;
  16. const vars = beforeAll2(() => {
  17. interface TUser extends Model<InferAttributes<TUser>, InferCreationAttributes<TUser>> {
  18. id: CreationOptional<number>;
  19. username: string;
  20. }
  21. const User = sequelize.define<TUser>(
  22. 'User',
  23. {
  24. id: {
  25. type: DataTypes.INTEGER.UNSIGNED,
  26. autoIncrement: true,
  27. primaryKey: true,
  28. },
  29. username: DataTypes.STRING,
  30. },
  31. { timestamps: true },
  32. );
  33. interface TProject extends Model<InferAttributes<TProject>, InferCreationAttributes<TProject>> {
  34. id: CreationOptional<number>;
  35. duration: bigint;
  36. }
  37. const Project = sequelize.define<TProject>(
  38. 'Project',
  39. {
  40. id: {
  41. type: DataTypes.INTEGER.UNSIGNED,
  42. autoIncrement: true,
  43. primaryKey: true,
  44. },
  45. duration: DataTypes.INTEGER,
  46. },
  47. { timestamps: false },
  48. );
  49. const ProjectContributor = sequelize.define('ProjectContributor', {}, { timestamps: false });
  50. // project owners
  51. User.hasMany(Project, { as: 'projects' });
  52. Project.belongsTo(User, { as: 'owner' });
  53. // project contributors
  54. Project.belongsToMany(User, {
  55. through: ProjectContributor,
  56. as: 'contributors',
  57. inverse: 'contributedProjects',
  58. });
  59. return { User, Project, ProjectContributor };
  60. });
  61. describe('limit/offset', () => {
  62. it('supports offset without limit', () => {
  63. const { User } = vars;
  64. const sql = queryGenerator.selectQuery(
  65. User.table,
  66. {
  67. model: User,
  68. attributes: ['id'],
  69. offset: 1,
  70. },
  71. User,
  72. );
  73. expectsql(sql, {
  74. sqlite3: 'SELECT `id` FROM `Users` AS `User` ORDER BY `User`.`id` LIMIT -1 OFFSET 1;',
  75. postgres: 'SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" OFFSET 1;',
  76. snowflake: 'SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" LIMIT NULL OFFSET 1;',
  77. 'mariadb mysql':
  78. 'SELECT `id` FROM `Users` AS `User` ORDER BY `User`.`id` LIMIT 18446744073709551615 OFFSET 1;',
  79. 'db2 ibmi mssql': `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET 1 ROWS;`,
  80. });
  81. });
  82. it('support limit without offset', () => {
  83. const { User } = vars;
  84. const sql = queryGenerator.selectQuery(
  85. User.table,
  86. {
  87. model: User,
  88. attributes: ['id'],
  89. limit: 10,
  90. },
  91. User,
  92. );
  93. expectsql(sql, {
  94. default: 'SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT 10;',
  95. mssql: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;`,
  96. 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" FETCH NEXT 10 ROWS ONLY;`,
  97. });
  98. });
  99. it('supports offset and limit', () => {
  100. const { User } = vars;
  101. const sql = queryGenerator.selectQuery(
  102. User.table,
  103. {
  104. model: User,
  105. attributes: ['id'],
  106. offset: 1,
  107. limit: 10,
  108. },
  109. User,
  110. );
  111. expectsql(sql, {
  112. default: 'SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT 10 OFFSET 1;',
  113. 'db2 ibmi mssql': `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET 1 ROWS FETCH NEXT 10 ROWS ONLY;`,
  114. });
  115. });
  116. it('ignores 0 as offset with a limit', () => {
  117. const { User } = vars;
  118. const sql = queryGenerator.selectQuery(
  119. User.table,
  120. {
  121. model: User,
  122. attributes: ['id'],
  123. offset: 0,
  124. limit: 10,
  125. },
  126. User,
  127. );
  128. expectsql(sql, {
  129. default: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT 10;`,
  130. mssql: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;`,
  131. 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" FETCH NEXT 10 ROWS ONLY;`,
  132. });
  133. });
  134. it('ignores 0 as offset without a limit', () => {
  135. const { User } = vars;
  136. const sql = queryGenerator.selectQuery(
  137. User.table,
  138. {
  139. model: User,
  140. attributes: ['id'],
  141. offset: 0,
  142. },
  143. User,
  144. );
  145. expectsql(sql, {
  146. default: `SELECT [id] FROM [Users] AS [User];`,
  147. });
  148. });
  149. it('support 0 as limit', () => {
  150. const { User } = vars;
  151. expectsql(
  152. () =>
  153. queryGenerator.selectQuery(
  154. User.table,
  155. {
  156. model: User,
  157. attributes: ['id'],
  158. limit: 0,
  159. },
  160. User,
  161. ),
  162. {
  163. default: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT 0;`,
  164. mssql: new Error(`LIMIT 0 is not supported by ${dialectName} dialect.`),
  165. 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" FETCH NEXT 0 ROWS ONLY;`,
  166. },
  167. );
  168. });
  169. it('escapes limit', () => {
  170. const { User } = vars;
  171. const sql = queryGenerator.selectQuery(
  172. User.table,
  173. {
  174. model: User,
  175. attributes: ['id'],
  176. // @ts-expect-error -- testing invalid limit
  177. limit: `';DELETE FROM user`,
  178. },
  179. User,
  180. );
  181. expectsql(sql, {
  182. default: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT ''';DELETE FROM user';`,
  183. mssql: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET 0 ROWS FETCH NEXT N''';DELETE FROM user' ROWS ONLY;`,
  184. 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" FETCH NEXT ''';DELETE FROM user' ROWS ONLY;`,
  185. 'mariadb mysql':
  186. "SELECT `id` FROM `Users` AS `User` ORDER BY `User`.`id` LIMIT '\\';DELETE FROM user';",
  187. });
  188. });
  189. it('escapes offset', () => {
  190. const { User } = vars;
  191. const sql = queryGenerator.selectQuery(
  192. User.table,
  193. {
  194. model: User,
  195. attributes: ['id'],
  196. limit: 10,
  197. // @ts-expect-error -- testing invalid offset
  198. offset: `';DELETE FROM user`,
  199. },
  200. User,
  201. );
  202. expectsql(sql, {
  203. default: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT 10 OFFSET ''';DELETE FROM user';`,
  204. mssql: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET N''';DELETE FROM user' ROWS FETCH NEXT 10 ROWS ONLY;`,
  205. 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" OFFSET ''';DELETE FROM user' ROWS FETCH NEXT 10 ROWS ONLY;`,
  206. 'mariadb mysql':
  207. "SELECT `id` FROM `Users` AS `User` ORDER BY `User`.`id` LIMIT 10 OFFSET '\\';DELETE FROM user';",
  208. });
  209. });
  210. });
  211. it('supports querying for bigint values', () => {
  212. const { Project } = vars;
  213. const sql = queryGenerator.selectQuery(
  214. Project.table,
  215. {
  216. model: Project,
  217. attributes: ['id'],
  218. where: {
  219. duration: { [Op.eq]: 9_007_199_254_740_993n },
  220. },
  221. },
  222. Project,
  223. );
  224. expectsql(sql, {
  225. default: `SELECT [id] FROM [Projects] AS [Project] WHERE [Project].[duration] = 9007199254740993;`,
  226. });
  227. });
  228. it('supports cast in attributes', () => {
  229. const { User } = vars;
  230. const sql = queryGenerator.selectQuery(
  231. User.table,
  232. {
  233. model: User,
  234. attributes: ['id', [cast(col('createdAt'), 'varchar'), 'createdAt']],
  235. },
  236. User,
  237. );
  238. expectsql(sql, {
  239. default: `SELECT [id], CAST([createdAt] AS VARCHAR) AS [createdAt] FROM [Users] AS [User];`,
  240. });
  241. });
  242. it('supports empty where object', () => {
  243. const { User } = vars;
  244. const sql = queryGenerator.selectQuery(
  245. User.table,
  246. {
  247. model: User,
  248. attributes: ['id'],
  249. where: {},
  250. },
  251. User,
  252. );
  253. expectsql(sql, {
  254. default: `SELECT [id] FROM [Users] AS [User];`,
  255. });
  256. });
  257. it('escapes WHERE clause correctly', () => {
  258. const { User } = vars;
  259. const sql = queryGenerator.selectQuery(
  260. User.table,
  261. {
  262. model: User,
  263. attributes: ['id'],
  264. where: { username: "foo';DROP TABLE mySchema.myTable;" },
  265. },
  266. User,
  267. );
  268. expectsql(sql, {
  269. default: `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = 'foo'';DROP TABLE mySchema.myTable;';`,
  270. 'mysql mariadb': `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = 'foo\\';DROP TABLE mySchema.myTable;';`,
  271. mssql: `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = N'foo'';DROP TABLE mySchema.myTable;';`,
  272. });
  273. });
  274. if (
  275. sequelize.dialect.supports.jsonOperations &&
  276. sequelize.dialect.supports.jsonExtraction.quoted
  277. ) {
  278. it('accepts json paths in attributes', () => {
  279. const { User } = vars;
  280. const sql = queryGenerator.selectQuery(
  281. User.table,
  282. {
  283. model: User,
  284. attributes: [[attribute('data.email'), 'email']],
  285. },
  286. User,
  287. );
  288. expectsql(sql, {
  289. postgres: `SELECT "data"->'email' AS "email" FROM "Users" AS "User";`,
  290. mariadb: `SELECT json_compact(json_extract(\`data\`,'$.email')) AS \`email\` FROM \`Users\` AS \`User\`;`,
  291. 'sqlite3 mysql': `SELECT json_extract([data],'$.email') AS [email] FROM [Users] AS [User];`,
  292. });
  293. });
  294. }
  295. describe('replacements', () => {
  296. it('parses named replacements in literals', () => {
  297. const { User } = vars;
  298. // The goal of this test is to test that :replacements are parsed in literals in as many places as possible
  299. const sql = queryGenerator.selectQuery(
  300. User.table,
  301. {
  302. model: User,
  303. attributes: [[fn('uppercase', literal(':attr')), 'id'], literal(':attr2')],
  304. where: {
  305. username: or(
  306. { [Op.eq]: literal(':data') },
  307. where(fn('uppercase', cast(literal(':data'), 'string')), Op.eq, literal(':data')),
  308. ),
  309. },
  310. having: {
  311. username: {
  312. [Op.eq]: literal(':data'),
  313. },
  314. },
  315. order: literal(':order'),
  316. limit: literal(':limit'),
  317. offset: literal(':offset'),
  318. group: literal(':group'),
  319. replacements: {
  320. attr: 'id',
  321. attr2: 'id2',
  322. data: 'repl1',
  323. order: 'repl2',
  324. limit: 'repl3',
  325. offset: 'repl4',
  326. group: 'the group',
  327. },
  328. },
  329. User,
  330. );
  331. expectsql(sql, {
  332. default: `
  333. SELECT uppercase('id') AS [id], 'id2'
  334. FROM [Users] AS [User]
  335. WHERE [User].[username] = 'repl1' OR [User].[username] = (uppercase(CAST('repl1' AS STRING)) = 'repl1')
  336. GROUP BY 'the group'
  337. HAVING [User].[username] = 'repl1'
  338. ORDER BY 'repl2'
  339. LIMIT 'repl3'
  340. OFFSET 'repl4';
  341. `,
  342. mssql: `
  343. SELECT uppercase(N'id') AS [id], N'id2'
  344. FROM [Users] AS [User]
  345. WHERE [User].[username] = N'repl1' OR [User].[username] = (uppercase(CAST(N'repl1' AS STRING)) = N'repl1')
  346. GROUP BY N'the group'
  347. HAVING [User].[username] = N'repl1'
  348. ORDER BY N'repl2'
  349. OFFSET N'repl4' ROWS
  350. FETCH NEXT N'repl3' ROWS ONLY;
  351. `,
  352. 'db2 ibmi': `
  353. SELECT uppercase('id') AS "id", 'id2'
  354. FROM "Users" AS "User"
  355. WHERE "User"."username" = 'repl1' OR "User"."username" = (uppercase(CAST('repl1' AS STRING)) = 'repl1')
  356. GROUP BY 'the group'
  357. HAVING "User"."username" = 'repl1'
  358. ORDER BY 'repl2'
  359. OFFSET 'repl4' ROWS
  360. FETCH NEXT 'repl3' ROWS ONLY;
  361. `,
  362. });
  363. });
  364. // see the unit tests of 'injectReplacements' for more
  365. it('does not parse replacements in strings in literals', () => {
  366. const { User } = vars;
  367. // The goal of this test is to test that :replacements are parsed in literals in as many places as possible
  368. const sql = queryGenerator.selectQuery(
  369. User.table,
  370. {
  371. model: User,
  372. attributes: [literal('id')],
  373. where: literal(`id = ':id'`),
  374. replacements: {
  375. id: 1,
  376. },
  377. },
  378. User,
  379. );
  380. expectsql(sql, {
  381. default: `SELECT id FROM [Users] AS [User] WHERE id = ':id';`,
  382. });
  383. });
  384. it('parses named replacements in literals in includes', () => {
  385. const { User, Project } = vars;
  386. const sql = queryGenerator.selectQuery(
  387. User.table,
  388. {
  389. model: User,
  390. attributes: ['id'],
  391. include: _validateIncludedElements({
  392. model: User,
  393. include: [
  394. {
  395. association: User.associations.projects,
  396. attributes: [['id', 'id'], literal(':data'), [literal(':data'), 'id2']],
  397. on: literal(':on'),
  398. where: literal(':where'),
  399. include: [
  400. {
  401. association: Project.associations.owner,
  402. attributes: [literal(':data2')],
  403. },
  404. ],
  405. },
  406. ],
  407. }).include,
  408. replacements: {
  409. data: 'repl1',
  410. data2: 'repl2',
  411. on: 'on',
  412. where: 'where',
  413. },
  414. },
  415. User,
  416. );
  417. expectsql(sql, {
  418. default: `
  419. SELECT
  420. [User].[id],
  421. [projects].[id] AS [projects.id],
  422. 'repl1',
  423. 'repl1' AS [projects.id2],
  424. [projects->owner].[id] AS [projects.owner.id],
  425. 'repl2'
  426. FROM [Users] AS [User]
  427. INNER JOIN [Projects] AS [projects]
  428. ON 'on' AND 'where'
  429. LEFT OUTER JOIN [Users] AS [projects->owner]
  430. ON [projects].[ownerId] = [projects->owner].[id];
  431. `,
  432. mssql: `
  433. SELECT
  434. [User].[id],
  435. [projects].[id] AS [projects.id],
  436. N'repl1',
  437. N'repl1' AS [projects.id2],
  438. [projects->owner].[id] AS [projects.owner.id],
  439. N'repl2'
  440. FROM [Users] AS [User]
  441. INNER JOIN [Projects] AS [projects]
  442. ON N'on' AND N'where'
  443. LEFT OUTER JOIN [Users] AS [projects->owner]
  444. ON [projects].[ownerId] = [projects->owner].[id];
  445. `,
  446. ibmi: `
  447. SELECT
  448. "User"."id",
  449. "projects"."id" AS "projects.id",
  450. 'repl1',
  451. 'repl1' AS "projects.id2",
  452. "projects->owner"."id" AS "projects.owner.id",
  453. 'repl2'
  454. FROM "Users" AS "User"
  455. INNER JOIN "Projects" AS "projects"
  456. ON 'on' AND 'where'
  457. LEFT OUTER JOIN "Users" AS "projects->owner"
  458. ON "projects"."ownerId" = "projects->owner"."id"
  459. `,
  460. });
  461. });
  462. it(`parses named replacements in belongsToMany includes' through tables`, () => {
  463. const { Project } = vars;
  464. const sql = queryGenerator.selectQuery(
  465. Project.table,
  466. {
  467. model: Project,
  468. attributes: ['id'],
  469. include: _validateIncludedElements({
  470. model: Project,
  471. include: [
  472. {
  473. attributes: ['id'],
  474. association: Project.associations.contributors,
  475. through: {
  476. where: literal(':where'),
  477. },
  478. },
  479. ],
  480. }).include,
  481. replacements: {
  482. where: 'where',
  483. },
  484. },
  485. Project,
  486. );
  487. expectsql(sql, {
  488. default: `
  489. SELECT
  490. [Project].[id],
  491. [contributors].[id] AS [contributors.id],
  492. [contributors->ProjectContributor].[userId] AS [contributors.ProjectContributor.userId],
  493. [contributors->ProjectContributor].[projectId] AS [contributors.ProjectContributor.projectId]
  494. FROM [Projects] AS [Project]
  495. LEFT OUTER JOIN (
  496. [ProjectContributors] AS [contributors->ProjectContributor]
  497. INNER JOIN [Users] AS [contributors]
  498. ON [contributors].[id] = [contributors->ProjectContributor].[userId]
  499. AND 'where'
  500. )
  501. ON [Project].[id] = [contributors->ProjectContributor].[projectId];
  502. `,
  503. mssql: `
  504. SELECT
  505. [Project].[id],
  506. [contributors].[id] AS [contributors.id],
  507. [contributors->ProjectContributor].[userId] AS [contributors.ProjectContributor.userId],
  508. [contributors->ProjectContributor].[projectId] AS [contributors.ProjectContributor.projectId]
  509. FROM [Projects] AS [Project]
  510. LEFT OUTER JOIN (
  511. [ProjectContributors] AS [contributors->ProjectContributor]
  512. INNER JOIN [Users] AS [contributors]
  513. ON [contributors].[id] = [contributors->ProjectContributor].[userId]
  514. AND N'where'
  515. )
  516. ON [Project].[id] = [contributors->ProjectContributor].[projectId];
  517. `,
  518. });
  519. });
  520. it('parses named replacements in literals in includes (subQuery)', () => {
  521. const { User, Project } = vars;
  522. const sql = queryGenerator.selectQuery(
  523. User.table,
  524. {
  525. model: User,
  526. attributes: ['id'],
  527. include: _validateIncludedElements({
  528. model: User,
  529. include: [
  530. {
  531. association: User.associations.projects,
  532. attributes: [['id', 'id'], literal(':data'), [literal(':data'), 'id2']],
  533. on: literal(':on'),
  534. where: literal(':where'),
  535. include: [
  536. {
  537. association: Project.associations.owner,
  538. attributes: [literal(':data2')],
  539. },
  540. ],
  541. },
  542. ],
  543. }).include,
  544. limit: literal(':limit'),
  545. offset: literal(':offset'),
  546. order: literal(':order'),
  547. subQuery: true,
  548. replacements: {
  549. data: 'repl1',
  550. data2: 'repl2',
  551. on: 'on',
  552. where: 'where',
  553. limit: 'limit',
  554. offset: 'offset',
  555. order: 'order',
  556. },
  557. },
  558. User,
  559. );
  560. expectsql(sql, {
  561. default: `
  562. SELECT
  563. [User].*,
  564. [projects].[id] AS [projects.id],
  565. 'repl1',
  566. 'repl1' AS [projects.id2],
  567. [projects->owner].[id] AS [projects.owner.id],
  568. 'repl2'
  569. FROM (
  570. SELECT [User].[id]
  571. FROM [Users] AS [User]
  572. ORDER BY 'order'
  573. LIMIT 'limit'
  574. OFFSET 'offset'
  575. ) AS [User]
  576. INNER JOIN [Projects] AS [projects]
  577. ON 'on' AND 'where'
  578. LEFT OUTER JOIN [Users] AS [projects->owner]
  579. ON [projects].[ownerId] = [projects->owner].[id]
  580. ORDER BY 'order';
  581. `,
  582. mssql: `
  583. SELECT
  584. [User].*,
  585. [projects].[id] AS [projects.id],
  586. N'repl1',
  587. N'repl1' AS [projects.id2],
  588. [projects->owner].[id] AS [projects.owner.id],
  589. N'repl2'
  590. FROM (
  591. SELECT [User].[id]
  592. FROM [Users] AS [User]
  593. ORDER BY N'order'
  594. OFFSET N'offset' ROWS
  595. FETCH NEXT N'limit' ROWS ONLY
  596. ) AS [User]
  597. INNER JOIN [Projects] AS [projects]
  598. ON N'on' AND N'where'
  599. LEFT OUTER JOIN [Users] AS [projects->owner]
  600. ON [projects].[ownerId] = [projects->owner].[id]
  601. ORDER BY N'order';
  602. `,
  603. db2: `
  604. SELECT
  605. "User".*,
  606. "projects"."id" AS "projects.id",
  607. 'repl1',
  608. 'repl1' AS "projects.id2",
  609. "projects->owner"."id" AS "projects.owner.id",
  610. 'repl2' FROM (
  611. SELECT "User"."id"
  612. FROM "Users" AS "User"
  613. ORDER BY 'order'
  614. OFFSET 'offset' ROWS
  615. FETCH NEXT 'limit' ROWS ONLY
  616. ) AS "User"
  617. INNER JOIN "Projects" AS "projects"
  618. ON 'on' AND 'where'
  619. LEFT OUTER JOIN "Users" AS "projects->owner"
  620. ON "projects"."ownerId" = "projects->owner"."id"
  621. ORDER BY 'order';
  622. `,
  623. ibmi: `
  624. SELECT
  625. "User".*,
  626. "projects"."id" AS "projects.id",
  627. 'repl1',
  628. 'repl1' AS "projects.id2",
  629. "projects->owner"."id" AS "projects.owner.id",
  630. 'repl2' FROM (
  631. SELECT "User"."id"
  632. FROM "Users" AS "User"
  633. ORDER BY 'order'
  634. OFFSET 'offset' ROWS
  635. FETCH NEXT 'limit' ROWS ONLY
  636. ) AS "User"
  637. INNER JOIN "Projects" AS "projects"
  638. ON 'on' AND 'where'
  639. LEFT OUTER JOIN "Users" AS "projects->owner"
  640. ON "projects"."ownerId" = "projects->owner"."id"
  641. ORDER BY 'order'
  642. `,
  643. });
  644. });
  645. it('rejects positional replacements, because their execution order is hard to determine', () => {
  646. const { User } = vars;
  647. expect(() =>
  648. queryGenerator.selectQuery(
  649. User.table,
  650. {
  651. model: User,
  652. where: {
  653. username: {
  654. [Op.eq]: literal('?'),
  655. },
  656. },
  657. replacements: ['repl1', 'repl2', 'repl3'],
  658. },
  659. User,
  660. ),
  661. ).to.throwWithCause(`The following literal includes positional replacements (?).
  662. Only named replacements (:name) are allowed in literal() because we cannot guarantee the order in which they will be evaluated:
  663. ➜ literal("?")`);
  664. });
  665. it(`always escapes the attribute if it's provided as a string`, () => {
  666. const { User } = vars;
  667. const sql = queryGenerator.selectQuery(
  668. User.table,
  669. {
  670. model: User,
  671. attributes: [
  672. // these used to have special escaping logic, now they're always escaped like any other strings. col, fn, and literal can be used for advanced logic.
  673. ['count(*)', 'count'],
  674. // @ts-expect-error -- test against a vulnerability CVE-2023-22578
  675. '.*',
  676. // @ts-expect-error -- test against a vulnerability CVE-2023-22578
  677. '*',
  678. [literal('count(*)'), 'literal_count'],
  679. [fn('count', '*'), 'fn_count_str'],
  680. [fn('count', col('*')), 'fn_count_col'],
  681. [fn('count', literal('*')), 'fn_count_lit'],
  682. [col('a.b'), 'col_a_b'],
  683. [col('a.*'), 'col_a_all'],
  684. [col('*'), 'col_all'],
  685. ],
  686. },
  687. User,
  688. );
  689. expectsql(sql, {
  690. default: `
  691. SELECT
  692. [count(*)] AS [count],
  693. [.*],
  694. [*],
  695. count(*) AS [literal_count],
  696. count('*') AS [fn_count_str],
  697. count(*) AS [fn_count_col],
  698. count(*) AS [fn_count_lit],
  699. [a].[b] AS [col_a_b],
  700. [a].* AS [col_a_all],
  701. * AS [col_all]
  702. FROM [Users] AS [User];`,
  703. mssql: `
  704. SELECT
  705. [count(*)] AS [count],
  706. [.*],
  707. [*],
  708. count(*) AS [literal_count],
  709. count(N'*') AS [fn_count_str],
  710. count(*) AS [fn_count_col],
  711. count(*) AS [fn_count_lit],
  712. [a].[b] AS [col_a_b],
  713. [a].* AS [col_a_all],
  714. * AS [col_all]
  715. FROM [Users] AS [User];`,
  716. });
  717. });
  718. it('supports a "having" option', () => {
  719. const { User } = vars;
  720. const sql = queryGenerator.selectQuery(
  721. User.table,
  722. {
  723. model: User,
  724. attributes: [literal('*'), [fn('YEAR', col('createdAt')), 'creationYear']],
  725. group: ['creationYear', 'title'],
  726. having: { creationYear: { [Op.gt]: 2002 } },
  727. },
  728. User,
  729. );
  730. expectsql(sql, {
  731. default: `SELECT *, YEAR([createdAt]) AS [creationYear] FROM [Users] AS [User] GROUP BY [creationYear], [title] HAVING [User].[creationYear] > 2002;`,
  732. });
  733. });
  734. });
  735. describe('previously supported values', () => {
  736. it('raw replacements for where', () => {
  737. expect(() => {
  738. queryGenerator.selectQuery('User', {
  739. attributes: [[col('*'), 'col_all']],
  740. // @ts-expect-error -- this is not a valid value anymore
  741. where: ['name IN (?)', [1, 'test', 3, 'derp']],
  742. });
  743. }).to.throwWithCause(
  744. Error,
  745. `Invalid Query: expected a plain object, an array or a sequelize SQL method but got 'name IN (?)'`,
  746. );
  747. });
  748. it('raw replacements for nested where', () => {
  749. expect(() => {
  750. queryGenerator.selectQuery('User', {
  751. attributes: [[col('*'), 'col_all']],
  752. // @ts-expect-error -- this is not a valid value anymore
  753. where: [['name IN (?)', [1, 'test', 3, 'derp']]],
  754. });
  755. }).to.throwWithCause(
  756. Error,
  757. `Invalid Query: expected a plain object, an array or a sequelize SQL method but got 'name IN (?)'`,
  758. );
  759. });
  760. it('raw replacements for having', () => {
  761. expect(() => {
  762. queryGenerator.selectQuery('User', {
  763. attributes: [[col('*'), 'col_all']],
  764. // @ts-expect-error -- this is not a valid value anymore
  765. having: ['name IN (?)', [1, 'test', 3, 'derp']],
  766. });
  767. }).to.throwWithCause(
  768. Error,
  769. `Invalid Query: expected a plain object, an array or a sequelize SQL method but got 'name IN (?)'`,
  770. );
  771. });
  772. it('raw replacements for nested having', () => {
  773. expect(() => {
  774. queryGenerator.selectQuery('User', {
  775. attributes: [[col('*'), 'col_all']],
  776. // @ts-expect-error -- this is not a valid value anymore
  777. having: [['name IN (?)', [1, 'test', 3, 'derp']]],
  778. });
  779. }).to.throwWithCause(
  780. Error,
  781. `Invalid Query: expected a plain object, an array or a sequelize SQL method but got 'name IN (?)'`,
  782. );
  783. });
  784. it('raw string from where', () => {
  785. expect(() => {
  786. queryGenerator.selectQuery('User', {
  787. attributes: [[col('*'), 'col_all']],
  788. // @ts-expect-error -- this is not a valid value anymore
  789. where: `name = 'something'`,
  790. });
  791. }).to.throwWithCause(Error, "Support for `{ where: 'raw query' }` has been removed.");
  792. });
  793. it('raw string from having', () => {
  794. expect(() => {
  795. queryGenerator.selectQuery('User', {
  796. attributes: [[col('*'), 'col_all']],
  797. // @ts-expect-error -- this is not a valid value anymore
  798. having: `name = 'something'`,
  799. });
  800. }).to.throwWithCause(Error, "Support for `{ where: 'raw query' }` has been removed.");
  801. });
  802. it('rejects where: null', () => {
  803. expect(() => {
  804. queryGenerator.selectQuery('User', {
  805. attributes: [[col('*'), 'col_all']],
  806. // @ts-expect-error -- this is not a valid value anymore
  807. where: null,
  808. });
  809. }).to.throwWithCause(
  810. Error,
  811. `Invalid Query: expected a plain object, an array or a sequelize SQL method but got null`,
  812. );
  813. });
  814. it('rejects where: primitive', () => {
  815. expect(() => {
  816. queryGenerator.selectQuery('User', {
  817. attributes: [[col('*'), 'col_all']],
  818. // @ts-expect-error -- this is not a valid value anymore
  819. where: 1,
  820. });
  821. }).to.throwWithCause(
  822. Error,
  823. `Invalid Query: expected a plain object, an array or a sequelize SQL method but got 1`,
  824. );
  825. });
  826. it('rejects where: array of primitives', () => {
  827. expect(() => {
  828. queryGenerator.selectQuery('User', {
  829. attributes: [[col('*'), 'col_all']],
  830. // @ts-expect-error -- this is not a valid value anymore
  831. where: [''],
  832. });
  833. }).to.throwWithCause(
  834. Error,
  835. `Invalid Query: expected a plain object, an array or a sequelize SQL method but got ''`,
  836. );
  837. });
  838. });
  839. describe('minifyAliases', () => {
  840. it('minifies custom attributes', () => {
  841. const { User } = vars;
  842. const sql = queryGenerator.selectQuery(
  843. User.table,
  844. {
  845. minifyAliases: true,
  846. model: User,
  847. attributes: [[literal('1'), 'customAttr']],
  848. order: ['customAttr'],
  849. group: ['customAttr'],
  850. },
  851. User,
  852. );
  853. expectsql(sql, {
  854. default: `SELECT 1 AS [_0] FROM [Users] AS [User] GROUP BY [_0] ORDER BY [_0];`,
  855. });
  856. });
  857. });
  858. describe('optimizer hints', () => {
  859. it('max execution time hint', () => {
  860. const { User } = vars;
  861. const notSupportedError = new Error(
  862. `The maxExecutionTimeMs option is not supported by ${dialectName}`,
  863. );
  864. expectsql(
  865. () =>
  866. queryGenerator.selectQuery(
  867. User.tableName,
  868. {
  869. model: User,
  870. attributes: ['id'],
  871. maxExecutionTimeHintMs: 1000,
  872. },
  873. User,
  874. ),
  875. {
  876. default: notSupportedError,
  877. mysql: 'SELECT /*+ MAX_EXECUTION_TIME(1000) */ `id` FROM `Users` AS `User`;',
  878. },
  879. );
  880. });
  881. });
  882. describe('index hints', () => {
  883. it('should add an index hint', () => {
  884. const { User } = vars;
  885. expectsql(
  886. () =>
  887. queryGenerator.selectQuery(
  888. User.table,
  889. {
  890. model: User,
  891. attributes: ['id'],
  892. indexHints: [{ type: IndexHints.FORCE, values: ['index_project_on_name'] }],
  893. },
  894. User,
  895. ),
  896. {
  897. default: buildInvalidOptionReceivedError('quoteTable', sequelize.dialect.name, [
  898. 'indexHints',
  899. ]),
  900. 'mariadb mysql snowflake':
  901. 'SELECT [id] FROM [Users] AS [User] FORCE INDEX ([index_project_on_name]);',
  902. },
  903. );
  904. });
  905. it('should add an index hint with multiple values', () => {
  906. const { User } = vars;
  907. expectsql(
  908. () =>
  909. queryGenerator.selectQuery(
  910. User.table,
  911. {
  912. model: User,
  913. attributes: ['id'],
  914. indexHints: [
  915. {
  916. type: IndexHints.IGNORE,
  917. values: ['index_project_on_name', 'index_project_on_name_and_foo'],
  918. },
  919. ],
  920. },
  921. User,
  922. ),
  923. {
  924. default: buildInvalidOptionReceivedError('quoteTable', sequelize.dialect.name, [
  925. 'indexHints',
  926. ]),
  927. 'mariadb mysql snowflake':
  928. 'SELECT [id] FROM [Users] AS [User] IGNORE INDEX ([index_project_on_name],[index_project_on_name_and_foo]);',
  929. },
  930. );
  931. });
  932. it('should support index hints on queries with associations', () => {
  933. const { User } = vars;
  934. expectsql(
  935. () =>
  936. queryGenerator.selectQuery(
  937. User.table,
  938. {
  939. model: User,
  940. attributes: ['id'],
  941. indexHints: [{ type: IndexHints.FORCE, values: ['index_project_on_name'] }],
  942. include: _validateIncludedElements({
  943. model: User,
  944. include: [
  945. {
  946. association: User.associations.projects,
  947. attributes: ['id'],
  948. },
  949. ],
  950. }).include,
  951. },
  952. User,
  953. ),
  954. {
  955. default: buildInvalidOptionReceivedError('quoteTable', sequelize.dialect.name, [
  956. 'indexHints',
  957. ]),
  958. 'mariadb mysql snowflake':
  959. 'SELECT [User].[id], [projects].[id] AS [projects.id] FROM [Users] AS [User] FORCE INDEX ([index_project_on_name]) LEFT OUTER JOIN [Projects] AS [projects] ON [User].[id] = [projects].[userId];',
  960. },
  961. );
  962. });
  963. it('should throw an error if an index hint if the type is not valid', () => {
  964. const { User } = vars;
  965. expectsql(
  966. () =>
  967. queryGenerator.selectQuery(
  968. User.table,
  969. {
  970. model: User,
  971. attributes: ['id'],
  972. // @ts-expect-error -- we are testing invalid values
  973. indexHints: [{ type: 'INVALID', values: ['index_project_on_name'] }],
  974. },
  975. User,
  976. ),
  977. {
  978. default: buildInvalidOptionReceivedError('quoteTable', sequelize.dialect.name, [
  979. 'indexHints',
  980. ]),
  981. 'mariadb mysql snowflake': new Error(
  982. `The index hint type "INVALID" is invalid or not supported by dialect "${sequelize.dialect.name}".`,
  983. ),
  984. },
  985. );
  986. });
  987. });
  988. describe('table hints', () => {
  989. it('support an array of table hints', () => {
  990. const { User } = vars;
  991. expectsql(
  992. () =>
  993. queryGenerator.selectQuery(
  994. User.table,
  995. {
  996. model: User,
  997. attributes: ['id'],
  998. tableHints: [TableHints.UPDLOCK, TableHints.PAGLOCK],
  999. },
  1000. User,
  1001. ),
  1002. {
  1003. default: buildInvalidOptionReceivedError('quoteTable', sequelize.dialect.name, [
  1004. 'tableHints',
  1005. ]),
  1006. mssql: `SELECT [id] FROM [Users] AS [User] WITH (UPDLOCK, PAGLOCK);`,
  1007. },
  1008. );
  1009. });
  1010. it('should be able to use table hints on joins', () => {
  1011. const { User } = vars;
  1012. expectsql(
  1013. () =>
  1014. queryGenerator.selectQuery(
  1015. User.table,
  1016. {
  1017. model: User,
  1018. attributes: ['id'],
  1019. tableHints: [TableHints.NOLOCK],
  1020. include: _validateIncludedElements({
  1021. model: User,
  1022. include: [
  1023. {
  1024. association: User.associations.projects,
  1025. attributes: ['id'],
  1026. },
  1027. ],
  1028. }).include,
  1029. },
  1030. User,
  1031. ),
  1032. {
  1033. default: buildInvalidOptionReceivedError('quoteTable', sequelize.dialect.name, [
  1034. 'tableHints',
  1035. ]),
  1036. mssql: `SELECT [User].[id], [projects].[id] AS [projects.id] FROM [Users] AS [User] WITH (NOLOCK) LEFT OUTER JOIN [Projects] AS [projects] WITH (NOLOCK) ON [User].[id] = [projects].[userId];`,
  1037. },
  1038. );
  1039. });
  1040. it('should be able to use separate table hints on joins', () => {
  1041. const { User } = vars;
  1042. expectsql(
  1043. () =>
  1044. queryGenerator.selectQuery(
  1045. User.table,
  1046. {
  1047. model: User,
  1048. attributes: ['id'],
  1049. tableHints: [TableHints.NOLOCK],
  1050. include: _validateIncludedElements({
  1051. model: User,
  1052. include: [
  1053. {
  1054. association: User.associations.projects,
  1055. attributes: ['id'],
  1056. tableHints: [TableHints.READPAST],
  1057. },
  1058. ],
  1059. }).include,
  1060. },
  1061. User,
  1062. ),
  1063. {
  1064. default: buildInvalidOptionReceivedError('quoteTable', sequelize.dialect.name, [
  1065. 'tableHints',
  1066. ]),
  1067. mssql: `SELECT [User].[id], [projects].[id] AS [projects.id] FROM [Users] AS [User] WITH (NOLOCK) LEFT OUTER JOIN [Projects] AS [projects] WITH (READPAST) ON [User].[id] = [projects].[userId];`,
  1068. },
  1069. );
  1070. });
  1071. it('should throw an error if a table hint if the type is not valid', () => {
  1072. const { User } = vars;
  1073. expectsql(
  1074. () =>
  1075. queryGenerator.selectQuery(
  1076. User.table,
  1077. {
  1078. model: User,
  1079. attributes: ['id'],
  1080. // @ts-expect-error -- we are testing invalid values
  1081. tableHints: ['INVALID'],
  1082. },
  1083. User,
  1084. ),
  1085. {
  1086. default: buildInvalidOptionReceivedError('quoteTable', sequelize.dialect.name, [
  1087. 'tableHints',
  1088. ]),
  1089. mssql: new Error(
  1090. `The table hint "INVALID" is invalid or not supported by dialect "${sequelize.dialect.name}".`,
  1091. ),
  1092. },
  1093. );
  1094. });
  1095. });
  1096. });