TupleComparisonQueryTest.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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\SelectQuery;
  23. use Cake\Database\StatementInterface;
  24. use Cake\Database\TypeMap;
  25. use Cake\Datasource\ConnectionManager;
  26. use Cake\TestSuite\TestCase;
  27. use InvalidArgumentException;
  28. use PDOException;
  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 array $fixtures = [
  44. 'core.Articles',
  45. ];
  46. /**
  47. * @var \Cake\Database\Connection
  48. */
  49. protected $connection;
  50. /**
  51. * @var \Cake\Database\SelectQuery
  52. */
  53. protected $query;
  54. public function setUp(): void
  55. {
  56. parent::setUp();
  57. $this->connection = ConnectionManager::get('test');
  58. $this->query = new SelectQuery($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(InvalidArgumentException::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. $this->connection->selectQuery(
  87. ['ArticlesAlias.id', 'ArticlesAlias.author_id'],
  88. ['ArticlesAlias' => 'articles']
  89. )
  90. ->where(['ArticlesAlias.author_id' => 1]),
  91. [],
  92. 'NOT IN'
  93. ),
  94. ])
  95. ->orderByAsc('articles.id')
  96. ->execute();
  97. }
  98. public function testInWithMultiResultSubquery(): void
  99. {
  100. $typeMap = new TypeMap([
  101. 'id' => 'integer',
  102. 'author_id' => 'integer',
  103. ]);
  104. $query = $this->query
  105. ->select(['articles.id', 'articles.author_id'])
  106. ->from('articles')
  107. ->where([
  108. new TupleComparison(
  109. ['articles.id', 'articles.author_id'],
  110. $this->connection->selectQuery(
  111. ['ArticlesAlias.id', 'ArticlesAlias.author_id'],
  112. ['ArticlesAlias' => 'articles']
  113. )
  114. ->where(['ArticlesAlias.author_id' => 1]),
  115. [],
  116. 'IN'
  117. ),
  118. ])
  119. ->orderByAsc('articles.id')
  120. ->setSelectTypeMap($typeMap);
  121. $expected = [
  122. [
  123. 'id' => 1,
  124. 'author_id' => 1,
  125. ],
  126. [
  127. 'id' => 3,
  128. 'author_id' => 1,
  129. ],
  130. ];
  131. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  132. }
  133. public function testInWithSingleResultSubquery(): void
  134. {
  135. $typeMap = new TypeMap([
  136. 'id' => 'integer',
  137. 'author_id' => 'integer',
  138. ]);
  139. $query = $this->query
  140. ->select(['articles.id', 'articles.author_id'])
  141. ->from('articles')
  142. ->where([
  143. new TupleComparison(
  144. ['articles.id', 'articles.author_id'],
  145. $this->connection->selectQuery(
  146. ['ArticlesAlias.id', 'ArticlesAlias.author_id'],
  147. ['ArticlesAlias' => 'articles']
  148. )
  149. ->where(['ArticlesAlias.id' => 1]),
  150. [],
  151. 'IN'
  152. ),
  153. ])
  154. ->setSelectTypeMap($typeMap);
  155. $expected = [
  156. [
  157. 'id' => 1,
  158. 'author_id' => 1,
  159. ],
  160. ];
  161. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  162. }
  163. public function testInWithMultiArrayValues(): void
  164. {
  165. $typeMap = new TypeMap([
  166. 'id' => 'integer',
  167. 'author_id' => 'integer',
  168. ]);
  169. $query = $this->query
  170. ->select(['articles.id', 'articles.author_id'])
  171. ->from('articles')
  172. ->where([
  173. new TupleComparison(
  174. ['articles.id', 'articles.author_id'],
  175. [[1, 1], [3, 1]],
  176. ['integer', 'integer'],
  177. 'IN'
  178. ),
  179. ])
  180. ->orderByAsc('articles.id')
  181. ->setSelectTypeMap($typeMap);
  182. $expected = [
  183. [
  184. 'id' => 1,
  185. 'author_id' => 1,
  186. ],
  187. [
  188. 'id' => 3,
  189. 'author_id' => 1,
  190. ],
  191. ];
  192. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  193. }
  194. public function testEqualWithMultiResultSubquery(): void
  195. {
  196. $driver = $this->connection->getDriver();
  197. if (
  198. $driver instanceof Mysql ||
  199. $driver instanceof Postgres
  200. ) {
  201. $this->expectException(PDOException::class);
  202. $this->expectExceptionMessageMatches('/cardinality violation/i');
  203. } else {
  204. // Due to the way tuple comparisons are being translated, the DBMS will
  205. // not run into a cardinality violation scenario.
  206. $this->markTestSkipped(
  207. 'Sqlite and Sqlserver currently do not fail with subqueries returning incompatible results.'
  208. );
  209. }
  210. $this->query
  211. ->select(['articles.id', 'articles.author_id'])
  212. ->from('articles')
  213. ->where([
  214. new TupleComparison(
  215. ['articles.id', 'articles.author_id'],
  216. $this->connection->selectQuery(
  217. ['ArticlesAlias.id', 'ArticlesAlias.author_id'],
  218. ['ArticlesAlias' => 'articles']
  219. )
  220. ->where(['ArticlesAlias.author_id' => 1]),
  221. [],
  222. '='
  223. ),
  224. ])
  225. ->orderByAsc('articles.id')
  226. ->execute();
  227. }
  228. public function testEqualWithSingleResultSubquery(): void
  229. {
  230. $typeMap = new TypeMap([
  231. 'id' => 'integer',
  232. 'author_id' => 'integer',
  233. ]);
  234. $query = $this->query
  235. ->select(['articles.id', 'articles.author_id'])
  236. ->from('articles')
  237. ->where([
  238. new TupleComparison(
  239. ['articles.id', 'articles.author_id'],
  240. $this->connection->selectQuery(
  241. fields: ['ArticlesAlias.id', 'ArticlesAlias.author_id'],
  242. table: ['ArticlesAlias' => 'articles']
  243. )
  244. ->where(['ArticlesAlias.id' => 1]),
  245. [],
  246. '='
  247. ),
  248. ])
  249. ->setSelectTypeMap($typeMap);
  250. $expected = [
  251. [
  252. 'id' => 1,
  253. 'author_id' => 1,
  254. ],
  255. ];
  256. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  257. }
  258. public function testEqualWithSingleArrayValue(): void
  259. {
  260. $typeMap = new TypeMap([
  261. 'id' => 'integer',
  262. 'author_id' => 'integer',
  263. ]);
  264. $query = $this->query
  265. ->select(['articles.id', 'articles.author_id'])
  266. ->from('articles')
  267. ->where([
  268. new TupleComparison(
  269. ['articles.id', 'articles.author_id'],
  270. [1, 1],
  271. ['integer', 'integer'],
  272. '='
  273. ),
  274. ])
  275. ->orderByAsc('articles.id')
  276. ->setSelectTypeMap($typeMap);
  277. $expected = [
  278. [
  279. 'id' => 1,
  280. 'author_id' => 1,
  281. ],
  282. ];
  283. $this->assertSame($expected, $query->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC));
  284. }
  285. }