[BUGFIX] Allow cloning of the QueryBuilder
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Unit / Database / Query / QueryBuilderTest.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Core\Tests\Unit\Database\Query;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Doctrine\DBAL\Platforms\AbstractPlatform;
19 use Doctrine\DBAL\Platforms\MySqlPlatform;
20 use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
21 use Doctrine\DBAL\Platforms\SQLServerPlatform;
22 use Prophecy\Argument;
23 use TYPO3\CMS\Core\Database\Connection;
24 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
25 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
26 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27 use TYPO3\CMS\Core\Tests\Unit\Database\Mocks\MockPlatform;
28 use TYPO3\CMS\Core\Utility\GeneralUtility;
29 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
30
31 class QueryBuilderTest extends UnitTestCase
32 {
33 /**
34 * @var Connection|\Prophecy\Prophecy\ObjectProphecy
35 */
36 protected $connection;
37
38 /**
39 * @var AbstractPlatform
40 */
41 protected $platform;
42
43 /**
44 * @var QueryBuilder
45 */
46 protected $subject;
47
48 /**
49 * @var \Doctrine\DBAL\Query\QueryBuilder|\Prophecy\Prophecy\ObjectProphecy
50 */
51 protected $concreteQueryBuilder;
52
53 /**
54 * Create a new database connection mock object for every test.
55 */
56 protected function setUp()
57 {
58 parent::setUp();
59
60 $this->concreteQueryBuilder = $this->prophesize(\Doctrine\DBAL\Query\QueryBuilder::class);
61
62 $this->connection = $this->prophesize(Connection::class);
63 $this->connection->getDatabasePlatform()->willReturn(new MockPlatform());
64
65 $this->subject = GeneralUtility::makeInstance(
66 QueryBuilder::class,
67 $this->connection->reveal(),
68 null,
69 $this->concreteQueryBuilder->reveal()
70 );
71 }
72
73 /**
74 * @test
75 */
76 public function exprReturnsExpressionBuilderForConnection()
77 {
78 $this->connection->getExpressionBuilder()
79 ->shouldBeCalled()
80 ->willReturn(GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal()));
81
82 $this->subject->expr();
83 }
84
85 /**
86 * @test
87 */
88 public function getTypeDelegatesToConcreteQueryBuilder()
89 {
90 $this->concreteQueryBuilder->getType()
91 ->shouldBeCalled()
92 ->willReturn(\Doctrine\DBAL\Query\QueryBuilder::INSERT);
93
94 $this->subject->getType();
95 }
96
97 /**
98 * @test
99 */
100 public function getStateDelegatesToConcreteQueryBuilder()
101 {
102 $this->concreteQueryBuilder->getState()
103 ->shouldBeCalled()
104 ->willReturn(\Doctrine\DBAL\Query\QueryBuilder::STATE_CLEAN);
105
106 $this->subject->getState();
107 }
108
109 /**
110 * @test
111 */
112 public function getSQLDelegatesToConcreteQueryBuilder()
113 {
114 $this->concreteQueryBuilder->getSQL()
115 ->shouldBeCalled()
116 ->willReturn('UPDATE aTable SET pid = 7');
117 $this->concreteQueryBuilder->getType()
118 ->willReturn(2); // Update Type
119
120 $this->subject->getSQL();
121 }
122
123 /**
124 * @test
125 */
126 public function setParameterDelegatesToConcreteQueryBuilder()
127 {
128 $this->concreteQueryBuilder->setParameter(Argument::exact('aField'), Argument::exact(5), Argument::cetera())
129 ->shouldBeCalled()
130 ->willReturn($this->subject);
131
132 $this->subject->setParameter('aField', 5);
133 }
134
135 /**
136 * @test
137 */
138 public function setParametersDelegatesToConcreteQueryBuilder()
139 {
140 $this->concreteQueryBuilder->setParameters(Argument::exact(['aField' => 'aValue']), Argument::exact([]))
141 ->shouldBeCalled()
142 ->willReturn($this->subject);
143
144 $this->subject->setParameters(['aField' => 'aValue']);
145 }
146
147 /**
148 * @test
149 */
150 public function getParametersDelegatesToConcreteQueryBuilder()
151 {
152 $this->concreteQueryBuilder->getParameters()
153 ->shouldBeCalled()
154 ->willReturn(['aField' => 'aValue']);
155
156 $this->subject->getParameters();
157 }
158
159 /**
160 * @test
161 */
162 public function getParameterDelegatesToConcreteQueryBuilder()
163 {
164 $this->concreteQueryBuilder->getParameter(Argument::exact('aField'))
165 ->shouldBeCalled()
166 ->willReturn('aValue');
167
168 $this->subject->getParameter('aField');
169 }
170
171 /**
172 * @test
173 */
174 public function getParameterTypesDelegatesToConcreteQueryBuilder()
175 {
176 $this->concreteQueryBuilder->getParameterTypes()
177 ->shouldBeCalled()
178 ->willReturn([]);
179
180 $this->subject->getParameterTypes();
181 }
182
183 /**
184 * @test
185 */
186 public function getParameterTypeDelegatesToConcreteQueryBuilder()
187 {
188 $this->concreteQueryBuilder->getParameterType(Argument::exact('aField'))
189 ->shouldBeCalled()
190 ->willReturn(Connection::PARAM_STR);
191
192 $this->subject->getParameterType('aField');
193 }
194
195 /**
196 * @test
197 */
198 public function setFirstResultDelegatesToConcreteQueryBuilder()
199 {
200 $this->concreteQueryBuilder->setFirstResult(Argument::cetera())
201 ->shouldBeCalled()
202 ->willReturn($this->subject);
203
204 $this->subject->setFirstResult(1);
205 }
206
207 /**
208 * @test
209 */
210 public function getFirstResultDelegatesToConcreteQueryBuilder()
211 {
212 $this->concreteQueryBuilder->getFirstResult()
213 ->shouldBeCalled()
214 ->willReturn(1);
215
216 $this->subject->getFirstResult();
217 }
218
219 /**
220 * @test
221 */
222 public function setMaxResultsDelegatesToConcreteQueryBuilder()
223 {
224 $this->concreteQueryBuilder->setMaxResults(Argument::cetera())
225 ->shouldBeCalled()
226 ->willReturn($this->subject);
227
228 $this->subject->setMaxResults(1);
229 }
230
231 /**
232 * @test
233 */
234 public function getMaxResultsDelegatesToConcreteQueryBuilder()
235 {
236 $this->concreteQueryBuilder->getMaxResults()
237 ->shouldBeCalled()
238 ->willReturn(1);
239
240 $this->subject->getMaxResults();
241 }
242
243 /**
244 * @test
245 */
246 public function addDelegatesToConcreteQueryBuilder()
247 {
248 $this->concreteQueryBuilder->add(Argument::exact('select'), Argument::exact('aField'), Argument::cetera())
249 ->shouldBeCalled()
250 ->willReturn($this->subject);
251
252 $this->subject->add('select', 'aField');
253 }
254
255 /**
256 * @test
257 */
258 public function countBuildsExpressionAndCallsSelect()
259 {
260 $this->concreteQueryBuilder->select(Argument::exact('COUNT(*)'))
261 ->shouldBeCalled()
262 ->willReturn($this->subject);
263
264 $this->subject->count('*');
265 }
266
267 /**
268 * @test
269 */
270 public function selectQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
271 {
272 $this->connection->quoteIdentifier('aField')
273 ->shouldBeCalled()
274 ->willReturnArgument(0);
275 $this->connection->quoteIdentifier('anotherField')
276 ->shouldBeCalled()
277 ->willReturnArgument(0);
278 $this->concreteQueryBuilder->select(Argument::exact('aField'), Argument::exact('anotherField'))
279 ->shouldBeCalled()
280 ->willReturn($this->subject);
281
282 $this->subject->select('aField', 'anotherField');
283 }
284
285 public function quoteIdentifiersForSelectDataProvider()
286 {
287 return [
288 'fieldName' => [
289 'fieldName',
290 '"fieldName"',
291 ],
292 'tableName.fieldName' => [
293 'tableName.fieldName',
294 '"tableName"."fieldName"',
295 ],
296 'tableName.*' => [
297 'tableName.*',
298 '"tableName".*',
299 ],
300 '*' => [
301 '*',
302 '*',
303 ],
304 'fieldName AS anotherFieldName' => [
305 'fieldName AS anotherFieldName',
306 '"fieldName" AS "anotherFieldName"',
307 ],
308 'tableName.fieldName AS anotherFieldName' => [
309 'tableName.fieldName AS anotherFieldName',
310 '"tableName"."fieldName" AS "anotherFieldName"',
311 ],
312 'tableName.fieldName AS anotherTable.anotherFieldName' => [
313 'tableName.fieldName AS anotherTable.anotherFieldName',
314 '"tableName"."fieldName" AS "anotherTable"."anotherFieldName"',
315 ],
316 ];
317 }
318
319 /**
320 * @test
321 * @dataProvider quoteIdentifiersForSelectDataProvider
322 * @param string $identifier
323 * @param string $expectedResult
324 */
325 public function quoteIdentifiersForSelect($identifier, $expectedResult)
326 {
327 $this->connection->quoteIdentifier(Argument::cetera())->will(
328 function ($args) {
329 $platform = new MockPlatform();
330
331 return $platform->quoteIdentifier($args[0]);
332 }
333 );
334
335 $this->assertSame([$expectedResult], $this->subject->quoteIdentifiersForSelect([$identifier]));
336 }
337
338 /**
339 * @test
340 */
341 public function quoteIdentifiersForSelectWithInvalidAlias()
342 {
343 $this->expectException(\InvalidArgumentException::class);
344 $this->expectExceptionCode(1461170686);
345
346 $this->connection->quoteIdentifier(Argument::cetera())->will(
347 function ($args) {
348 $platform = new MockPlatform();
349
350 return $platform->quoteIdentifier($args[0]);
351 }
352 );
353 $this->subject->quoteIdentifiersForSelect(['aField AS anotherField,someField AS someThing']);
354 }
355
356 /**
357 * @test
358 */
359 public function selectDoesNotQuoteStarPlaceholder()
360 {
361 $this->connection->quoteIdentifier('aField')
362 ->shouldBeCalled()
363 ->willReturnArgument(0);
364 $this->connection->quoteIdentifier('*')
365 ->shouldNotBeCalled();
366 $this->concreteQueryBuilder->select(Argument::exact('aField'), Argument::exact('*'))
367 ->shouldBeCalled()
368 ->willReturn($this->subject);
369
370 $this->subject->select('aField', '*');
371 }
372
373 /**
374 * @test
375 */
376 public function addSelectQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
377 {
378 $this->connection->quoteIdentifier('aField')
379 ->shouldBeCalled()
380 ->willReturnArgument(0);
381 $this->connection->quoteIdentifier('anotherField')
382 ->shouldBeCalled()
383 ->willReturnArgument(0);
384 $this->concreteQueryBuilder->addSelect(Argument::exact('aField'), Argument::exact('anotherField'))
385 ->shouldBeCalled()
386 ->willReturn($this->subject);
387
388 $this->subject->addSelect('aField', 'anotherField');
389 }
390
391 /**
392 * @test
393 */
394 public function addSelectDoesNotQuoteStarPlaceholder()
395 {
396 $this->connection->quoteIdentifier('aField')
397 ->shouldBeCalled()
398 ->willReturnArgument(0);
399 $this->connection->quoteIdentifier('*')
400 ->shouldNotBeCalled();
401 $this->concreteQueryBuilder->addSelect(Argument::exact('aField'), Argument::exact('*'))
402 ->shouldBeCalled()
403 ->willReturn($this->subject);
404
405 $this->subject->addSelect('aField', '*');
406 }
407
408 /**
409 * @test
410 */
411 public function selectLiteralDirectlyDelegatesToConcreteQueryBuilder()
412 {
413 $this->connection->quoteIdentifier(Argument::cetera())
414 ->shouldNotBeCalled();
415 $this->concreteQueryBuilder->select(Argument::exact('MAX(aField) AS anAlias'))
416 ->shouldBeCalled()
417 ->willReturn($this->subject);
418
419 $this->subject->selectLiteral('MAX(aField) AS anAlias');
420 }
421
422 /**
423 * @test
424 */
425 public function addSelectLiteralDirectlyDelegatesToConcreteQueryBuilder()
426 {
427 $this->connection->quoteIdentifier(Argument::cetera())
428 ->shouldNotBeCalled();
429 $this->concreteQueryBuilder->addSelect(Argument::exact('MAX(aField) AS anAlias'))
430 ->shouldBeCalled()
431 ->willReturn($this->subject);
432
433 $this->subject->addSelectLiteral('MAX(aField) AS anAlias');
434 }
435
436 /**
437 * @test
438 * @todo: Test with alias
439 */
440 public function deleteQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
441 {
442 $this->connection->quoteIdentifier('aTable')
443 ->shouldBeCalled()
444 ->willReturnArgument(0);
445 $this->concreteQueryBuilder->delete(Argument::exact('aTable'), Argument::cetera())
446 ->shouldBeCalled()
447 ->willReturn($this->subject);
448
449 $this->subject->delete('aTable');
450 }
451
452 /**
453 * @test
454 * @todo: Test with alias
455 */
456 public function updateQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
457 {
458 $this->connection->quoteIdentifier('aTable')
459 ->shouldBeCalled()
460 ->willReturnArgument(0);
461 $this->concreteQueryBuilder->update(Argument::exact('aTable'), Argument::cetera())
462 ->shouldBeCalled()
463 ->willReturn($this->subject);
464
465 $this->subject->update('aTable');
466 }
467
468 /**
469 * @test
470 */
471 public function insertQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
472 {
473 $this->connection->quoteIdentifier('aTable')
474 ->shouldBeCalled()
475 ->willReturnArgument(0);
476 $this->concreteQueryBuilder->insert(Argument::exact('aTable'))
477 ->shouldBeCalled()
478 ->willReturn($this->subject);
479
480 $this->subject->insert('aTable');
481 }
482
483 /**
484 * @test
485 * @todo: Test with alias
486 */
487 public function fromQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
488 {
489 $this->connection->quoteIdentifier('aTable')
490 ->shouldBeCalled()
491 ->willReturnArgument(0);
492 $this->concreteQueryBuilder->from(Argument::exact('aTable'), Argument::cetera())
493 ->shouldBeCalled()
494 ->willReturn($this->subject);
495
496 $this->subject->from('aTable');
497 }
498
499 /**
500 * @test
501 */
502 public function joinQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
503 {
504 $this->connection->quoteIdentifier('fromAlias')
505 ->shouldBeCalled()
506 ->willReturnArgument(0);
507 $this->connection->quoteIdentifier('join')
508 ->shouldBeCalled()
509 ->willReturnArgument(0);
510 $this->connection->quoteIdentifier('alias')
511 ->shouldBeCalled()
512 ->willReturnArgument(0);
513 $this->concreteQueryBuilder->innerJoin('fromAlias', 'join', 'alias', null)
514 ->shouldBeCalled()
515 ->willReturn($this->subject);
516
517 $this->subject->join('fromAlias', 'join', 'alias');
518 }
519
520 /**
521 * @test
522 */
523 public function innerJoinQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
524 {
525 $this->connection->quoteIdentifier('fromAlias')
526 ->shouldBeCalled()
527 ->willReturnArgument(0);
528 $this->connection->quoteIdentifier('join')
529 ->shouldBeCalled()
530 ->willReturnArgument(0);
531 $this->connection->quoteIdentifier('alias')
532 ->shouldBeCalled()
533 ->willReturnArgument(0);
534 $this->concreteQueryBuilder->innerJoin('fromAlias', 'join', 'alias', null)
535 ->shouldBeCalled()
536 ->willReturn($this->subject);
537
538 $this->subject->innerJoin('fromAlias', 'join', 'alias');
539 }
540
541 /**
542 * @test
543 */
544 public function leftJoinQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
545 {
546 $this->connection->quoteIdentifier('fromAlias')
547 ->shouldBeCalled()
548 ->willReturnArgument(0);
549 $this->connection->quoteIdentifier('join')
550 ->shouldBeCalled()
551 ->willReturnArgument(0);
552 $this->connection->quoteIdentifier('alias')
553 ->shouldBeCalled()
554 ->willReturnArgument(0);
555 $this->concreteQueryBuilder->leftJoin('fromAlias', 'join', 'alias', null)
556 ->shouldBeCalled()
557 ->willReturn($this->subject);
558
559 $this->subject->leftJoin('fromAlias', 'join', 'alias');
560 }
561
562 /**
563 * @test
564 */
565 public function rightJoinQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
566 {
567 $this->connection->quoteIdentifier('fromAlias')
568 ->shouldBeCalled()
569 ->willReturnArgument(0);
570 $this->connection->quoteIdentifier('join')
571 ->shouldBeCalled()
572 ->willReturnArgument(0);
573 $this->connection->quoteIdentifier('alias')
574 ->shouldBeCalled()
575 ->willReturnArgument(0);
576 $this->concreteQueryBuilder->rightJoin('fromAlias', 'join', 'alias', null)
577 ->shouldBeCalled()
578 ->willReturn($this->subject);
579
580 $this->subject->rightJoin('fromAlias', 'join', 'alias');
581 }
582
583 /**
584 * @test
585 */
586 public function setQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
587 {
588 $this->connection->quoteIdentifier('aField')
589 ->shouldBeCalled()
590 ->willReturnArgument(0);
591 $this->concreteQueryBuilder->createNamedParameter('aValue', Argument::cetera())
592 ->shouldBeCalled()
593 ->willReturn(':dcValue1');
594 $this->concreteQueryBuilder->set('aField', ':dcValue1')
595 ->shouldBeCalled()
596 ->willReturn($this->subject);
597
598 $this->subject->set('aField', 'aValue');
599 }
600
601 /**
602 * @test
603 */
604 public function setWithoutNamedParameterQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
605 {
606 $this->connection->quoteIdentifier('aField')
607 ->shouldBeCalled()
608 ->willReturnArgument(0);
609 $this->concreteQueryBuilder->createNamedParameter(Argument::cetera())->shouldNotBeCalled();
610 $this->concreteQueryBuilder->set('aField', 'aValue')
611 ->shouldBeCalled()
612 ->willReturn($this->subject);
613
614 $this->subject->set('aField', 'aValue', false);
615 }
616
617 /**
618 * @test
619 */
620 public function whereDelegatesToConcreteQueryBuilder()
621 {
622 $this->concreteQueryBuilder->where('uid=1', 'type=9')
623 ->shouldBeCalled()
624 ->willReturn($this->subject);
625
626 $this->subject->where('uid=1', 'type=9');
627 }
628
629 /**
630 * @test
631 */
632 public function andWhereDelegatesToConcreteQueryBuilder()
633 {
634 $this->concreteQueryBuilder->andWhere('uid=1', 'type=9')
635 ->shouldBeCalled()
636 ->willReturn($this->subject);
637
638 $this->subject->andWhere('uid=1', 'type=9');
639 }
640
641 /**
642 * @test
643 */
644 public function orWhereDelegatesToConcreteQueryBuilder()
645 {
646 $this->concreteQueryBuilder->orWhere('uid=1', 'type=9')
647 ->shouldBeCalled()
648 ->willReturn($this->subject);
649
650 $this->subject->orWhere('uid=1', 'type=9');
651 }
652
653 /**
654 * @test
655 */
656 public function groupByQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
657 {
658 $this->connection->quoteIdentifiers(['aField', 'anotherField'])
659 ->shouldBeCalled()
660 ->willReturnArgument(0);
661 $this->concreteQueryBuilder->groupBy('aField', 'anotherField')
662 ->shouldBeCalled()
663 ->willReturn($this->subject);
664
665 $this->subject->groupBy('aField', 'anotherField');
666 }
667
668 /**
669 * @test
670 */
671 public function addGroupByQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
672 {
673 $this->connection->quoteIdentifiers(['aField', 'anotherField'])
674 ->shouldBeCalled()
675 ->willReturnArgument(0);
676 $this->concreteQueryBuilder->addGroupBy('aField', 'anotherField')
677 ->shouldBeCalled()
678 ->willReturn($this->subject);
679
680 $this->subject->addGroupBy('aField', 'anotherField');
681 }
682
683 /**
684 * @test
685 */
686 public function setValueQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
687 {
688 $this->connection->quoteIdentifier('aField')
689 ->shouldBeCalled()
690 ->willReturnArgument(0);
691 $this->concreteQueryBuilder->createNamedParameter('aValue', Argument::cetera())
692 ->shouldBeCalled()
693 ->willReturn(':dcValue1');
694 $this->concreteQueryBuilder->setValue('aField', ':dcValue1')
695 ->shouldBeCalled()
696 ->willReturn($this->subject);
697
698 $this->subject->setValue('aField', 'aValue');
699 }
700
701 /**
702 * @test
703 */
704 public function setValueWithoudNamedParameterQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
705 {
706 $this->connection->quoteIdentifier('aField')
707 ->shouldBeCalled()
708 ->willReturnArgument(0);
709 $this->concreteQueryBuilder->setValue('aField', 'aValue')
710 ->shouldBeCalled()
711 ->willReturn($this->subject);
712
713 $this->subject->setValue('aField', 'aValue', false);
714 }
715
716 /**
717 * @test
718 */
719 public function valuesQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
720 {
721 $this->connection->quoteColumnValuePairs(['aField' => ':dcValue1', 'aValue' => ':dcValue2'])
722 ->shouldBeCalled()
723 ->willReturnArgument(0);
724 $this->concreteQueryBuilder->createNamedParameter(1, Argument::cetera())
725 ->shouldBeCalled()
726 ->willReturn(':dcValue1');
727 $this->concreteQueryBuilder->createNamedParameter(2, Argument::cetera())
728 ->shouldBeCalled()
729 ->willReturn(':dcValue2');
730 $this->concreteQueryBuilder->values(['aField' => ':dcValue1', 'aValue' => ':dcValue2'])
731 ->shouldBeCalled()
732 ->willReturn($this->subject);
733
734 $this->subject->values(['aField' => 1, 'aValue' => 2]);
735 }
736
737 /**
738 * @test
739 */
740 public function valuesWithoutNamedParametersQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
741 {
742 $this->connection->quoteColumnValuePairs(['aField' => 1, 'aValue' => 2])
743 ->shouldBeCalled()
744 ->willReturnArgument(0);
745 $this->concreteQueryBuilder->values(['aField' => 1, 'aValue' => 2])
746 ->shouldBeCalled()
747 ->willReturn($this->subject);
748
749 $this->subject->values(['aField' => 1, 'aValue' => 2], false);
750 }
751
752 /**
753 * @test
754 */
755 public function havingDelegatesToConcreteQueryBuilder()
756 {
757 $this->concreteQueryBuilder->having('uid=1', 'type=9')
758 ->shouldBeCalled()
759 ->willReturn($this->subject);
760
761 $this->subject->having('uid=1', 'type=9');
762 }
763
764 /**
765 * @test
766 */
767 public function andHavingDelegatesToConcreteQueryBuilder()
768 {
769 $this->concreteQueryBuilder->andHaving('uid=1', 'type=9')
770 ->shouldBeCalled()
771 ->willReturn($this->subject);
772
773 $this->subject->andHaving('uid=1', 'type=9');
774 }
775
776 /**
777 * @test
778 */
779 public function orHavingDelegatesToConcreteQueryBuilder()
780 {
781 $this->concreteQueryBuilder->orHaving('uid=1', 'type=9')
782 ->shouldBeCalled()
783 ->willReturn($this->subject);
784
785 $this->subject->orHaving('uid=1', 'type=9');
786 }
787
788 /**
789 * @test
790 */
791 public function orderByQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
792 {
793 $this->connection->quoteIdentifier('aField')
794 ->shouldBeCalled()
795 ->willReturnArgument(0);
796 $this->concreteQueryBuilder->orderBy('aField', null)
797 ->shouldBeCalled()
798 ->willReturn($this->subject);
799
800 $this->subject->orderBy('aField');
801 }
802
803 /**
804 * @test
805 */
806 public function addOrderByQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
807 {
808 $this->connection->quoteIdentifier('aField')
809 ->shouldBeCalled()
810 ->willReturnArgument(0);
811 $this->concreteQueryBuilder->addOrderBy('aField', 'DESC')
812 ->shouldBeCalled()
813 ->willReturn($this->subject);
814
815 $this->subject->addOrderBy('aField', 'DESC');
816 }
817
818 /**
819 * @test
820 */
821 public function getQueryPartDelegatesToConcreteQueryBuilder()
822 {
823 $this->concreteQueryBuilder->getQueryPart('from')
824 ->shouldBeCalled()
825 ->willReturn('aTable');
826
827 $this->subject->getQueryPart('from');
828 }
829
830 /**
831 * @test
832 */
833 public function getQueryPartsDelegatesToConcreteQueryBuilder()
834 {
835 $this->concreteQueryBuilder->getQueryParts()
836 ->shouldBeCalled()
837 ->willReturn([]);
838
839 $this->subject->getQueryParts();
840 }
841
842 /**
843 * @test
844 */
845 public function resetQueryPartsDelegatesToConcreteQueryBuilder()
846 {
847 $this->concreteQueryBuilder->resetQueryParts(['select', 'from'])
848 ->shouldBeCalled()
849 ->willReturn($this->subject);
850
851 $this->subject->resetQueryParts(['select', 'from']);
852 }
853
854 /**
855 * @test
856 */
857 public function resetQueryPartDelegatesToConcreteQueryBuilder()
858 {
859 $this->concreteQueryBuilder->resetQueryPart('select')
860 ->shouldBeCalled()
861 ->willReturn($this->subject);
862
863 $this->subject->resetQueryPart('select');
864 }
865
866 /**
867 * @test
868 */
869 public function createNamedParameterDelegatesToConcreteQueryBuilder()
870 {
871 $this->concreteQueryBuilder->createNamedParameter(5, Argument::cetera())
872 ->shouldBeCalled()
873 ->willReturn(':dcValue1');
874
875 $this->subject->createNamedParameter(5);
876 }
877
878 /**
879 * @test
880 */
881 public function createPositionalParameterDelegatesToConcreteQueryBuilder()
882 {
883 $this->concreteQueryBuilder->createPositionalParameter(5, Argument::cetera())
884 ->shouldBeCalled()
885 ->willReturn('?');
886
887 $this->subject->createPositionalParameter(5);
888 }
889
890 /**
891 * @test
892 */
893 public function queryRestrictionsAreAddedForSelectOnExecute()
894 {
895 $GLOBALS['TCA']['pages']['ctrl'] = [
896 'tstamp' => 'tstamp',
897 'versioningWS' => true,
898 'delete' => 'deleted',
899 'crdate' => 'crdate',
900 'enablecolumns' => [
901 'disabled' => 'hidden',
902 ],
903 ];
904
905 $this->connection->quoteIdentifier(Argument::cetera())
906 ->willReturnArgument(0);
907 $this->connection->quoteIdentifiers(Argument::cetera())
908 ->willReturnArgument(0);
909
910 $connectionBuilder = GeneralUtility::makeInstance(
911 \Doctrine\DBAL\Query\QueryBuilder::class,
912 $this->connection->reveal()
913 );
914
915 $expressionBuilder = GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal());
916 $this->connection->getExpressionBuilder()
917 ->willReturn($expressionBuilder);
918
919 $subject = GeneralUtility::makeInstance(
920 QueryBuilder::class,
921 $this->connection->reveal(),
922 null,
923 $connectionBuilder
924 );
925
926 $subject->select('*')
927 ->from('pages')
928 ->where('uid=1');
929
930 $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
931 $this->connection->executeQuery($expectedSQL, Argument::cetera())
932 ->shouldBeCalled();
933
934 $subject->execute();
935 }
936
937 /**
938 * @test
939 */
940 public function queryRestrictionsAreAddedForCountOnExecute()
941 {
942 $GLOBALS['TCA']['pages']['ctrl'] = [
943 'tstamp' => 'tstamp',
944 'versioningWS' => true,
945 'delete' => 'deleted',
946 'crdate' => 'crdate',
947 'enablecolumns' => [
948 'disabled' => 'hidden',
949 ],
950 ];
951
952 $this->connection->quoteIdentifier(Argument::cetera())
953 ->willReturnArgument(0);
954 $this->connection->quoteIdentifiers(Argument::cetera())
955 ->willReturnArgument(0);
956
957 $connectionBuilder = GeneralUtility::makeInstance(
958 \Doctrine\DBAL\Query\QueryBuilder::class,
959 $this->connection->reveal()
960 );
961
962 $expressionBuilder = GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal());
963 $this->connection->getExpressionBuilder()
964 ->willReturn($expressionBuilder);
965
966 $subject = GeneralUtility::makeInstance(
967 QueryBuilder::class,
968 $this->connection->reveal(),
969 null,
970 $connectionBuilder
971 );
972
973 $subject->count('uid')
974 ->from('pages')
975 ->where('uid=1');
976
977 $expectedSQL = 'SELECT COUNT(uid) FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
978 $this->connection->executeQuery($expectedSQL, Argument::cetera())
979 ->shouldBeCalled();
980
981 $subject->execute();
982 }
983
984 /**
985 * @test
986 */
987 public function queryRestrictionsAreReevaluatedOnSettingsChangeForGetSQL()
988 {
989 $GLOBALS['TCA']['pages']['ctrl'] = [
990 'tstamp' => 'tstamp',
991 'versioningWS' => true,
992 'delete' => 'deleted',
993 'crdate' => 'crdate',
994 'enablecolumns' => [
995 'disabled' => 'hidden',
996 ],
997 ];
998
999 $this->connection->quoteIdentifier(Argument::cetera())
1000 ->willReturnArgument(0);
1001 $this->connection->quoteIdentifiers(Argument::cetera())
1002 ->willReturnArgument(0);
1003 $this->connection->getExpressionBuilder()
1004 ->willReturn(GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal()));
1005
1006 $concreteQueryBuilder = GeneralUtility::makeInstance(
1007 \Doctrine\DBAL\Query\QueryBuilder::class,
1008 $this->connection->reveal()
1009 );
1010
1011 $subject = GeneralUtility::makeInstance(
1012 QueryBuilder::class,
1013 $this->connection->reveal(),
1014 null,
1015 $concreteQueryBuilder
1016 );
1017
1018 $subject->select('*')
1019 ->from('pages')
1020 ->where('uid=1');
1021
1022 $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
1023 $this->assertSame($expectedSQL, $subject->getSQL());
1024
1025 $subject->getRestrictions()->removeAll()->add(new DeletedRestriction());
1026
1027 $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND (pages.deleted = 0)';
1028 $this->assertSame($expectedSQL, $subject->getSQL());
1029 }
1030
1031 /**
1032 * @test
1033 */
1034 public function queryRestrictionsAreReevaluatedOnSettingsChangeForExecute()
1035 {
1036 $GLOBALS['TCA']['pages']['ctrl'] = [
1037 'tstamp' => 'tstamp',
1038 'versioningWS' => true,
1039 'delete' => 'deleted',
1040 'crdate' => 'crdate',
1041 'enablecolumns' => [
1042 'disabled' => 'hidden',
1043 ],
1044 ];
1045
1046 $this->connection->quoteIdentifier(Argument::cetera())
1047 ->willReturnArgument(0);
1048 $this->connection->quoteIdentifiers(Argument::cetera())
1049 ->willReturnArgument(0);
1050 $this->connection->getExpressionBuilder()
1051 ->willReturn(GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal()));
1052
1053 $concreteQueryBuilder = GeneralUtility::makeInstance(
1054 \Doctrine\DBAL\Query\QueryBuilder::class,
1055 $this->connection->reveal()
1056 );
1057
1058 $subject = GeneralUtility::makeInstance(
1059 QueryBuilder::class,
1060 $this->connection->reveal(),
1061 null,
1062 $concreteQueryBuilder
1063 );
1064
1065 $subject->select('*')
1066 ->from('pages')
1067 ->where('uid=1');
1068
1069 $subject->getRestrictions()->removeAll()->add(new DeletedRestriction());
1070
1071 $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND (pages.deleted = 0)';
1072 $this->connection->executeQuery($expectedSQL, Argument::cetera())
1073 ->shouldBeCalled();
1074
1075 $subject->execute();
1076
1077 $subject->resetRestrictions();
1078
1079 $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
1080 $this->connection->executeQuery($expectedSQL, Argument::cetera())
1081 ->shouldBeCalled();
1082
1083 $subject->execute();
1084 }
1085
1086 /**
1087 * @test
1088 */
1089 public function getQueriedTablesReturnsSameTableTwiceForInnerJoin()
1090 {
1091 $this->concreteQueryBuilder->getQueryPart('from')
1092 ->shouldBeCalled()
1093 ->willReturn([
1094 [
1095 'table' => 'aTable',
1096 ],
1097 ]);
1098 $this->concreteQueryBuilder->getQueryPart('join')
1099 ->shouldBeCalled()
1100 ->willReturn([
1101 'aTable' => [
1102 [
1103 'joinType' => 'inner',
1104 'joinTable' => 'aTable',
1105 'joinAlias' => 'aTable_alias'
1106 ]
1107 ]
1108 ]);
1109 $result = $this->callInaccessibleMethod($this->subject, 'getQueriedTables');
1110
1111 $expected = [
1112 'aTable' => 'aTable',
1113 'aTable_alias' => 'aTable'
1114 ];
1115 $this->assertEquals($expected, $result);
1116 }
1117
1118 /**
1119 * @return array
1120 */
1121 public function unquoteSingleIdentifierUnquotesCorrectlyOnDifferentPlatformsDataProvider()
1122 {
1123 return [
1124 'mysql' => [
1125 'platform' => MySqlPlatform::class,
1126 'quoteChar' => '`',
1127 'input' => '`anIdentifier`',
1128 'expected' => 'anIdentifier',
1129 ],
1130 'mysql with spaces' => [
1131 'platform' => MySqlPlatform::class,
1132 'quoteChar' => '`',
1133 'input' => ' `anIdentifier` ',
1134 'expected' => 'anIdentifier',
1135 ],
1136 'postgres' => [
1137 'platform' => PostgreSqlPlatform::class,
1138 'quoteChar' => '"',
1139 'input' => '"anIdentifier"',
1140 'expected' => 'anIdentifier',
1141 ],
1142 'mssql' => [
1143 'platform' => SQLServerPlatform::class,
1144 'quoteChar' => '', // no single quote character, but [ and ]
1145 'input' => '[anIdentifier]',
1146 'expected' => 'anIdentifier',
1147 ],
1148 ];
1149 }
1150
1151 /**
1152 * @test
1153 * @dataProvider unquoteSingleIdentifierUnquotesCorrectlyOnDifferentPlatformsDataProvider
1154 */
1155 public function unquoteSingleIdentifierUnquotesCorrectlyOnDifferentPlatforms($platform, $quoteChar, $input, $expected)
1156 {
1157 $connectionProphecy = $this->prophesize(Connection::class);
1158 $databasePlatformProphecy = $this->prophesize($platform);
1159 $databasePlatformProphecy->getIdentifierQuoteCharacter()->willReturn($quoteChar);
1160 $connectionProphecy->getDatabasePlatform()->willReturn($databasePlatformProphecy);
1161 $subject = GeneralUtility::makeInstance(QueryBuilder::class, $connectionProphecy->reveal());
1162 $result = $this->callInaccessibleMethod($subject, 'unquoteSingleIdentifier', $input);
1163 $this->assertEquals($expected, $result);
1164 }
1165
1166 /**
1167 * @test
1168 */
1169 public function cloningQueryBuilderClonesConcreteQueryBuilder()
1170 {
1171 $clonedQueryBuilder = clone $this->subject;
1172 self::assertNotSame($this->subject->getConcreteQueryBuilder(), $clonedQueryBuilder->getConcreteQueryBuilder());
1173 }
1174
1175 /**
1176 * @test
1177 */
1178 public function changingClonedQueryBuilderDoesNotInfluenceSourceOne()
1179 {
1180 $GLOBALS['TCA']['pages']['ctrl'] = [
1181 'tstamp' => 'tstamp',
1182 'versioningWS' => true,
1183 'delete' => 'deleted',
1184 'crdate' => 'crdate',
1185 'enablecolumns' => [
1186 'disabled' => 'hidden',
1187 ],
1188 ];
1189
1190 $this->connection->quoteIdentifier(Argument::cetera())
1191 ->willReturnArgument(0);
1192 $this->connection->quoteIdentifiers(Argument::cetera())
1193 ->willReturnArgument(0);
1194 $this->connection->getExpressionBuilder()
1195 ->willReturn(GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal()));
1196
1197 $concreteQueryBuilder = GeneralUtility::makeInstance(
1198 \Doctrine\DBAL\Query\QueryBuilder::class,
1199 $this->connection->reveal()
1200 );
1201
1202 $subject = GeneralUtility::makeInstance(
1203 QueryBuilder::class,
1204 $this->connection->reveal(),
1205 null,
1206 $concreteQueryBuilder
1207 );
1208
1209 $subject->select('*')
1210 ->from('pages')
1211 ->where('uid=1');
1212
1213 $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
1214 $this->assertSame($expectedSQL, $subject->getSQL());
1215
1216 $clonedQueryBuilder = clone $subject;
1217 //just after cloning both query builders should return the same sql
1218 $this->assertSame($expectedSQL, $clonedQueryBuilder->getSQL());
1219
1220 //change cloned QueryBuilder
1221 $clonedQueryBuilder->count('*');
1222 $expectedCountSQL = 'SELECT COUNT(*) FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
1223 $this->assertSame($expectedCountSQL, $clonedQueryBuilder->getSQL());
1224
1225 //check if the original QueryBuilder has not changed
1226 $this->assertSame($expectedSQL, $subject->getSQL());
1227
1228 //change restrictions in the original QueryBuilder and check if cloned has changed
1229 $subject->getRestrictions()->removeAll()->add(new DeletedRestriction());
1230 $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND (pages.deleted = 0)';
1231 $this->assertSame($expectedSQL, $subject->getSQL());
1232
1233 $this->assertSame($expectedCountSQL, $clonedQueryBuilder->getSQL());
1234 }
1235 }