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