TupleComparisonQueryTest.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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 MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 4.3.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Database\QueryTests;
  17. use Cake\Database\Driver\Mysql;
  18. use Cake\Database\Driver\Postgres;
  19. use Cake\Database\Driver\Sqlite;
  20. use Cake\Database\Driver\Sqlserver;
  21. use Cake\Database\Expression\TupleComparison;
  22. use Cake\Database\Query;
  23. use Cake\Database\StatementInterface;
  24. use Cake\Database\TypeMap;
  25. use Cake\Datasource\ConnectionManager;
  26. use Cake\TestSuite\TestCase;
  27. use PDOException;
  28. use RuntimeException;
  29. /**
  30. * Tuple comparison query tests.
  31. *
  32. * These tests are specifically relevant in the context of Sqlite and
  33. * Sqlserver, for which the tuple comparison will be transformed when
  34. * composite fields are used.
  35. *
  36. * @see \Cake\Database\Driver\TupleComparisonTranslatorTrait::_transformTupleComparison()
  37. */
  38. class TupleComparisonQueryTest extends TestCase
  39. {
  40. /**
  41. * @inheritDoc
  42. */
  43. protected $fixtures = [
  44. 'core.Articles',
  45. ];
  46. /**
  47. * @var \Cake\Database\Connection
  48. */
  49. protected $connection;
  50. /**
  51. * @var \Cake\Database\Query
  52. */
  53. protected $query;
  54. public function setUp(): void
  55. {
  56. parent::setUp();
  57. $this->connection = ConnectionManager::get('test');
  58. $this->query = new Query($this->connection);
  59. }
  60. public function tearDown(): void
  61. {
  62. parent::tearDown();
  63. unset($this->query);
  64. unset($this->connection);
  65. }
  66. public function testTransformWithInvalidOperator(): void
  67. {
  68. $driver = $this->connection->getDriver();
  69. if (
  70. $driver instanceof Sqlite ||
  71. $driver instanceof Sqlserver
  72. ) {
  73. $this->expectException(RuntimeException::class);
  74. $this->expectExceptionMessage(
  75. 'Tuple comparison transform only supports the `IN` and `=` operators, `NOT IN` given.'
  76. );
  77. } else {
  78. $this->markTestSkipped('Tuple comparisons are only being transformed for Sqlite and Sqlserver.');
  79. }
  80. $this->query
  81. ->select(['articles.id', 'articles.author_id'])
  82. ->from('articles')
  83. ->where([
  84. new TupleComparison(
  85. ['articles.id', 'articles.author_id'],
  86. (new Query($this->connection))
  87. ->select(['ArticlesAlias.id', 'ArticlesAlias.author_id'])
  88. ->from(['ArticlesAlias' => 'articles'])
  89. ->where(['ArticlesAlias.author_id' => 1]),
  90. [],
  91. 'NOT IN'
  92. ),
  93. ])
  94. ->orderAsc('articles.id')
  95. ->execute();
  96. }
  97. public function testInWithMultiResultSubquery(): void
  98. {
  99. $typeMap = new TypeMap([
  100. 'id' => 'integer',
  101. 'author_id' => 'integer',
  102. ]);
  103. $query = $this->query
  104. ->select(['articles.id', 'articles.author_id'])
  105. ->from('articles')
  106. ->where([
  107. new TupleComparison(
  108. ['articles.id', 'articles.author_id'],
  109. (new Query($this->connection))
  110. ->select(['ArticlesAlias.id', 'ArticlesAlias.author_id'])
  111. ->from(['ArticlesAlias' => 'articles'])
  112. ->where(['ArticlesAlias.author_id' => 1]),
  113. [],
  114. 'IN'
  115. ),
  116. ])
  117. ->orderAsc('articles.id')
  118. ->setSelectTypeMap($typeMap);
  119. $expected = [
  120. [
  121. 'id' => 1,
  122. 'author_id' => 1,
  123. ],
  124. [
  125. 'id' => 3,
  126. 'author_id' => 1,
  127. ],
  128. ];
  129. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  130. }
  131. public function testInWithSingleResultSubquery(): void
  132. {
  133. $typeMap = new TypeMap([
  134. 'id' => 'integer',
  135. 'author_id' => 'integer',
  136. ]);
  137. $query = $this->query
  138. ->select(['articles.id', 'articles.author_id'])
  139. ->from('articles')
  140. ->where([
  141. new TupleComparison(
  142. ['articles.id', 'articles.author_id'],
  143. (new Query($this->connection))
  144. ->select(['ArticlesAlias.id', 'ArticlesAlias.author_id'])
  145. ->from(['ArticlesAlias' => 'articles'])
  146. ->where(['ArticlesAlias.id' => 1]),
  147. [],
  148. 'IN'
  149. ),
  150. ])
  151. ->setSelectTypeMap($typeMap);
  152. $expected = [
  153. [
  154. 'id' => 1,
  155. 'author_id' => 1,
  156. ],
  157. ];
  158. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  159. }
  160. public function testInWithMultiArrayValues(): void
  161. {
  162. $typeMap = new TypeMap([
  163. 'id' => 'integer',
  164. 'author_id' => 'integer',
  165. ]);
  166. $query = $this->query
  167. ->select(['articles.id', 'articles.author_id'])
  168. ->from('articles')
  169. ->where([
  170. new TupleComparison(
  171. ['articles.id', 'articles.author_id'],
  172. [[1, 1], [3, 1]],
  173. ['integer', 'integer'],
  174. 'IN'
  175. ),
  176. ])
  177. ->orderAsc('articles.id')
  178. ->setSelectTypeMap($typeMap);
  179. $expected = [
  180. [
  181. 'id' => 1,
  182. 'author_id' => 1,
  183. ],
  184. [
  185. 'id' => 3,
  186. 'author_id' => 1,
  187. ],
  188. ];
  189. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  190. }
  191. public function testEqualWithMultiResultSubquery(): void
  192. {
  193. $driver = $this->connection->getDriver();
  194. if (
  195. $driver instanceof Mysql ||
  196. $driver instanceof Postgres
  197. ) {
  198. $this->expectException(PDOException::class);
  199. $this->expectExceptionMessageMatches('/cardinality violation/i');
  200. } else {
  201. // Due to the way tuple comparisons are being translated, the DBMS will
  202. // not run into a cardinality violation scenario.
  203. $this->markTestSkipped(
  204. 'Sqlite and Sqlserver currently do not fail with subqueries returning incompatible results.'
  205. );
  206. }
  207. $this->query
  208. ->select(['articles.id', 'articles.author_id'])
  209. ->from('articles')
  210. ->where([
  211. new TupleComparison(
  212. ['articles.id', 'articles.author_id'],
  213. (new Query($this->connection))
  214. ->select(['ArticlesAlias.id', 'ArticlesAlias.author_id'])
  215. ->from(['ArticlesAlias' => 'articles'])
  216. ->where(['ArticlesAlias.author_id' => 1]),
  217. [],
  218. '='
  219. ),
  220. ])
  221. ->orderAsc('articles.id')
  222. ->execute();
  223. }
  224. public function testEqualWithSingleResultSubquery(): void
  225. {
  226. $typeMap = new TypeMap([
  227. 'id' => 'integer',
  228. 'author_id' => 'integer',
  229. ]);
  230. $query = $this->query
  231. ->select(['articles.id', 'articles.author_id'])
  232. ->from('articles')
  233. ->where([
  234. new TupleComparison(
  235. ['articles.id', 'articles.author_id'],
  236. (new Query($this->connection))
  237. ->select(['ArticlesAlias.id', 'ArticlesAlias.author_id'])
  238. ->from(['ArticlesAlias' => 'articles'])
  239. ->where(['ArticlesAlias.id' => 1]),
  240. [],
  241. '='
  242. ),
  243. ])
  244. ->setSelectTypeMap($typeMap);
  245. $expected = [
  246. [
  247. 'id' => 1,
  248. 'author_id' => 1,
  249. ],
  250. ];
  251. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  252. }
  253. public function testEqualWithSingleArrayValue(): void
  254. {
  255. $typeMap = new TypeMap([
  256. 'id' => 'integer',
  257. 'author_id' => 'integer',
  258. ]);
  259. $query = $this->query
  260. ->select(['articles.id', 'articles.author_id'])
  261. ->from('articles')
  262. ->where([
  263. new TupleComparison(
  264. ['articles.id', 'articles.author_id'],
  265. [1, 1],
  266. ['integer', 'integer'],
  267. '='
  268. ),
  269. ])
  270. ->orderAsc('articles.id')
  271. ->setSelectTypeMap($typeMap);
  272. $expected = [
  273. [
  274. 'id' => 1,
  275. 'author_id' => 1,
  276. ],
  277. ];
  278. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  279. }
  280. }