WindowExpressionTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The Open Group Test Suite License
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 4.1.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\Database\Expression;
  16. use Cake\Database\Expression\AggregateExpression;
  17. use Cake\Database\Expression\IdentifierExpression;
  18. use Cake\Database\Expression\OrderByExpression;
  19. use Cake\Database\Expression\OrderClauseExpression;
  20. use Cake\Database\Expression\QueryExpression;
  21. use Cake\Database\Expression\WindowExpression;
  22. use Cake\Database\ValueBinder;
  23. use Cake\TestSuite\TestCase;
  24. /**
  25. * Tests WindowExpression class
  26. */
  27. class WindowExpressionTest extends TestCase
  28. {
  29. /**
  30. * Tests an empty window expression
  31. */
  32. public function testEmptyWindow(): void
  33. {
  34. $w = new WindowExpression();
  35. $this->assertSame('', $w->sql(new ValueBinder()));
  36. $w->partition('')->order([]);
  37. $this->assertSame('', $w->sql(new ValueBinder()));
  38. }
  39. /**
  40. * Tests windows with partitions
  41. */
  42. public function testPartitions(): void
  43. {
  44. $w = (new WindowExpression())->partition('test');
  45. $this->assertEqualsSql(
  46. 'PARTITION BY test',
  47. $w->sql(new ValueBinder())
  48. );
  49. $w->partition(new IdentifierExpression('identifier'));
  50. $this->assertEqualsSql(
  51. 'PARTITION BY test, identifier',
  52. $w->sql(new ValueBinder())
  53. );
  54. $w = (new WindowExpression())->partition(new AggregateExpression('MyAggregate', ['param']));
  55. $this->assertEqualsSql(
  56. 'PARTITION BY MyAggregate(:param0)',
  57. $w->sql(new ValueBinder())
  58. );
  59. $w = (new WindowExpression())->partition(function (QueryExpression $expr) {
  60. return $expr->add(new AggregateExpression('MyAggregate', ['param']));
  61. });
  62. $this->assertEqualsSql(
  63. 'PARTITION BY MyAggregate(:param0)',
  64. $w->sql(new ValueBinder())
  65. );
  66. }
  67. /**
  68. * Tests windows with order by
  69. */
  70. public function testOrder(): void
  71. {
  72. $w = (new WindowExpression())->order('test');
  73. $this->assertEqualsSql(
  74. 'ORDER BY test',
  75. $w->sql(new ValueBinder())
  76. );
  77. $w->order(['test2' => 'DESC']);
  78. $this->assertEqualsSql(
  79. 'ORDER BY test, test2 DESC',
  80. $w->sql(new ValueBinder())
  81. );
  82. $w->partition('test');
  83. $this->assertEqualsSql(
  84. 'PARTITION BY test ORDER BY test, test2 DESC',
  85. $w->sql(new ValueBinder())
  86. );
  87. $w = (new WindowExpression())
  88. ->order(function () {
  89. return 'test';
  90. })
  91. ->order(function (QueryExpression $expr) {
  92. return [$expr->add('test2'), new OrderClauseExpression(new IdentifierExpression('test3'), 'DESC')];
  93. });
  94. $this->assertEqualsSql(
  95. 'ORDER BY test, test2, test3 DESC',
  96. $w->sql(new ValueBinder())
  97. );
  98. }
  99. /**
  100. * Tests windows with range frames
  101. */
  102. public function testRange(): void
  103. {
  104. $w = (new WindowExpression())->range(null);
  105. $this->assertEqualsSql(
  106. 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW',
  107. $w->sql(new ValueBinder())
  108. );
  109. $w = (new WindowExpression())->range(0);
  110. $this->assertEqualsSql(
  111. 'RANGE BETWEEN CURRENT ROW AND CURRENT ROW',
  112. $w->sql(new ValueBinder())
  113. );
  114. $w = (new WindowExpression())->range(2);
  115. $this->assertEqualsSql(
  116. 'RANGE BETWEEN 2 PRECEDING AND CURRENT ROW',
  117. $w->sql(new ValueBinder())
  118. );
  119. $w = (new WindowExpression())->range(null, null);
  120. $this->assertEqualsSql(
  121. 'RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING',
  122. $w->sql(new ValueBinder())
  123. );
  124. $w = (new WindowExpression())->range(0, null);
  125. $this->assertEqualsSql(
  126. 'RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING',
  127. $w->sql(new ValueBinder())
  128. );
  129. $w = (new WindowExpression())->range(0, 0);
  130. $this->assertEqualsSql(
  131. 'RANGE BETWEEN CURRENT ROW AND CURRENT ROW',
  132. $w->sql(new ValueBinder())
  133. );
  134. $w = (new WindowExpression())->range(1, 2);
  135. $this->assertEqualsSql(
  136. 'RANGE BETWEEN 1 PRECEDING AND 2 FOLLOWING',
  137. $w->sql(new ValueBinder())
  138. );
  139. $w = (new WindowExpression())->range("'1 day'", "'10 days'");
  140. $this->assertRegExpSql(
  141. "RANGE BETWEEN '1 day' PRECEDING AND '10 days' FOLLOWING",
  142. $w->sql(new ValueBinder())
  143. );
  144. $w = (new WindowExpression())->range(new QueryExpression("'1 day'"), new QueryExpression("'10 days'"));
  145. $this->assertRegExpSql(
  146. "RANGE BETWEEN '1 day' PRECEDING AND '10 days' FOLLOWING",
  147. $w->sql(new ValueBinder())
  148. );
  149. $w = (new WindowExpression())->frame(
  150. WindowExpression::RANGE,
  151. 2,
  152. WindowExpression::PRECEDING,
  153. 1,
  154. WindowExpression::PRECEDING
  155. );
  156. $this->assertEqualsSql(
  157. 'RANGE BETWEEN 2 PRECEDING AND 1 PRECEDING',
  158. $w->sql(new ValueBinder())
  159. );
  160. }
  161. /**
  162. * Tests windows with rows frames
  163. */
  164. public function testRows(): void
  165. {
  166. $w = (new WindowExpression())->rows(null);
  167. $this->assertEqualsSql(
  168. 'ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW',
  169. $w->sql(new ValueBinder())
  170. );
  171. $w = (new WindowExpression())->rows(0);
  172. $this->assertEqualsSql(
  173. 'ROWS BETWEEN CURRENT ROW AND CURRENT ROW',
  174. $w->sql(new ValueBinder())
  175. );
  176. $w = (new WindowExpression())->rows(2);
  177. $this->assertEqualsSql(
  178. 'ROWS BETWEEN 2 PRECEDING AND CURRENT ROW',
  179. $w->sql(new ValueBinder())
  180. );
  181. $w = (new WindowExpression())->rows(null, null);
  182. $this->assertEqualsSql(
  183. 'ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING',
  184. $w->sql(new ValueBinder())
  185. );
  186. $w = (new WindowExpression())->rows(0, null);
  187. $this->assertEqualsSql(
  188. 'ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING',
  189. $w->sql(new ValueBinder())
  190. );
  191. $w = (new WindowExpression())->rows(0, 0);
  192. $this->assertEqualsSql(
  193. 'ROWS BETWEEN CURRENT ROW AND CURRENT ROW',
  194. $w->sql(new ValueBinder())
  195. );
  196. $w = (new WindowExpression())->rows(1, 2);
  197. $this->assertEqualsSql(
  198. 'ROWS BETWEEN 1 PRECEDING AND 2 FOLLOWING',
  199. $w->sql(new ValueBinder())
  200. );
  201. $w = (new WindowExpression())->frame(
  202. WindowExpression::ROWS,
  203. 2,
  204. WindowExpression::PRECEDING,
  205. 1,
  206. WindowExpression::PRECEDING
  207. );
  208. $b = new ValueBinder();
  209. $this->assertEqualsSql(
  210. 'ROWS BETWEEN 2 PRECEDING AND 1 PRECEDING',
  211. $w->sql($b)
  212. );
  213. }
  214. /**
  215. * Tests windows with groups frames
  216. */
  217. public function testGroups(): void
  218. {
  219. $w = (new WindowExpression())->groups(null);
  220. $this->assertEqualsSql(
  221. 'GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW',
  222. $w->sql(new ValueBinder())
  223. );
  224. $w = (new WindowExpression())->groups(0);
  225. $this->assertEqualsSql(
  226. 'GROUPS BETWEEN CURRENT ROW AND CURRENT ROW',
  227. $w->sql(new ValueBinder())
  228. );
  229. $w = (new WindowExpression())->groups(2);
  230. $this->assertEqualsSql(
  231. 'GROUPS BETWEEN 2 PRECEDING AND CURRENT ROW',
  232. $w->sql(new ValueBinder())
  233. );
  234. $w = (new WindowExpression())->groups(null, null);
  235. $this->assertEqualsSql(
  236. 'GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING',
  237. $w->sql(new ValueBinder())
  238. );
  239. $w = (new WindowExpression())->groups(0, null);
  240. $this->assertEqualsSql(
  241. 'GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING',
  242. $w->sql(new ValueBinder())
  243. );
  244. $w = (new WindowExpression())->groups(0, 0);
  245. $this->assertEqualsSql(
  246. 'GROUPS BETWEEN CURRENT ROW AND CURRENT ROW',
  247. $w->sql(new ValueBinder())
  248. );
  249. $w = (new WindowExpression())->groups(1, 2);
  250. $this->assertEqualsSql(
  251. 'GROUPS BETWEEN 1 PRECEDING AND 2 FOLLOWING',
  252. $w->sql(new ValueBinder())
  253. );
  254. $w = (new WindowExpression())->frame(
  255. WindowExpression::GROUPS,
  256. 2,
  257. WindowExpression::PRECEDING,
  258. 1,
  259. WindowExpression::PRECEDING
  260. );
  261. $b = new ValueBinder();
  262. $this->assertEqualsSql(
  263. 'GROUPS BETWEEN 2 PRECEDING AND 1 PRECEDING',
  264. $w->sql($b)
  265. );
  266. }
  267. /**
  268. * Tests windows with frame exclusion
  269. */
  270. public function testExclusion(): void
  271. {
  272. $w = (new WindowExpression())->excludeCurrent();
  273. $this->assertEqualsSql(
  274. '',
  275. $w->sql(new ValueBinder())
  276. );
  277. $w = (new WindowExpression())->range(null)->excludeCurrent();
  278. $this->assertEqualsSql(
  279. 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW',
  280. $w->sql(new ValueBinder())
  281. );
  282. $w = (new WindowExpression())->range(null)->excludeGroup();
  283. $this->assertEqualsSql(
  284. 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE GROUP',
  285. $w->sql(new ValueBinder())
  286. );
  287. $w = (new WindowExpression())->range(null)->excludeTies();
  288. $this->assertEqualsSql(
  289. 'RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE TIES',
  290. $w->sql(new ValueBinder())
  291. );
  292. }
  293. /**
  294. * Tests windows with partition, order and frames
  295. */
  296. public function testCombined(): void
  297. {
  298. $w = (new WindowExpression())->partition('test')->range(null);
  299. $this->assertEqualsSql(
  300. 'PARTITION BY test RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW',
  301. $w->sql(new ValueBinder())
  302. );
  303. $w = (new WindowExpression())->order('test')->range(null);
  304. $this->assertEqualsSql(
  305. 'ORDER BY test RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW',
  306. $w->sql(new ValueBinder())
  307. );
  308. $w = (new WindowExpression())->partition('test')->order('test')->range(null);
  309. $this->assertEqualsSql(
  310. 'PARTITION BY test ORDER BY test RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW',
  311. $w->sql(new ValueBinder())
  312. );
  313. $w = (new WindowExpression())->partition('test')->order('test')->range(null)->excludeCurrent();
  314. $this->assertEqualsSql(
  315. 'PARTITION BY test ORDER BY test RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW',
  316. $w->sql(new ValueBinder())
  317. );
  318. }
  319. /**
  320. * Tests named windows
  321. */
  322. public function testNamedWindow(): void
  323. {
  324. $w = new WindowExpression();
  325. $this->assertFalse($w->isNamedOnly());
  326. $w->name('name');
  327. $this->assertTrue($w->isNamedOnly());
  328. $this->assertEqualsSql(
  329. 'name',
  330. $w->sql(new ValueBinder())
  331. );
  332. $w->name('new_name');
  333. $this->assertEqualsSql(
  334. 'new_name',
  335. $w->sql(new ValueBinder())
  336. );
  337. $w->order('test');
  338. $this->assertFalse($w->isNamedOnly());
  339. $this->assertEqualsSql(
  340. 'new_name ORDER BY test',
  341. $w->sql(new ValueBinder())
  342. );
  343. }
  344. /**
  345. * Tests traversing window expressions.
  346. */
  347. public function testTraverse(): void
  348. {
  349. $w = (new WindowExpression('test1'))
  350. ->partition('test2')
  351. ->order('test3')
  352. ->range(new QueryExpression("'1 day'"));
  353. $expressions = [];
  354. $w->traverse(function ($expression) use (&$expressions): void {
  355. $expressions[] = $expression;
  356. });
  357. $this->assertEquals(new IdentifierExpression('test1'), $expressions[0]);
  358. $this->assertEquals(new IdentifierExpression('test2'), $expressions[1]);
  359. $this->assertEquals((new OrderByExpression())->add('test3'), $expressions[2]);
  360. $this->assertEquals(new QueryExpression("'1 day'"), $expressions[3]);
  361. $w->range(new QueryExpression("'1 day'"), new QueryExpression("'10 days'"));
  362. $expressions = [];
  363. $w->traverse(function ($expression) use (&$expressions): void {
  364. $expressions[] = $expression;
  365. });
  366. $this->assertEquals(new QueryExpression("'1 day'"), $expressions[3]);
  367. $this->assertEquals(new QueryExpression("'10 days'"), $expressions[4]);
  368. }
  369. /**
  370. * Tests cloning window expressions
  371. */
  372. public function testCloning(): void
  373. {
  374. $w1 = (new WindowExpression())->name('test');
  375. $w2 = (clone $w1)->name('test2');
  376. $this->assertNotSame($w1->sql(new ValueBinder()), $w2->sql(new ValueBinder()));
  377. $w1 = (new WindowExpression())->partition('test');
  378. $w2 = (clone $w1)->partition('new');
  379. $this->assertNotSame($w1->sql(new ValueBinder()), $w2->sql(new ValueBinder()));
  380. $w1 = (new WindowExpression())->order('test');
  381. $w2 = (clone $w1)->order('new');
  382. $this->assertNotSame($w1->sql(new ValueBinder()), $w2->sql(new ValueBinder()));
  383. $w1 = (new WindowExpression())->rows(0, null);
  384. $w2 = (clone $w1)->rows(0, 0);
  385. $this->assertNotSame($w1->sql(new ValueBinder()), $w2->sql(new ValueBinder()));
  386. }
  387. }