Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Doctrine\DBAL\Query;
4
 
5
use Doctrine\DBAL\Cache\QueryCacheProfile;
6
use Doctrine\DBAL\Connection;
7
use Doctrine\DBAL\Exception;
8
use Doctrine\DBAL\ParameterType;
9
use Doctrine\DBAL\Query\Expression\CompositeExpression;
10
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
11
use Doctrine\DBAL\Result;
12
use Doctrine\DBAL\Statement;
13
use Doctrine\DBAL\Types\Type;
14
use Doctrine\Deprecations\Deprecation;
15
 
16
use function array_key_exists;
17
use function array_keys;
18
use function array_unshift;
19
use function count;
20
use function func_get_args;
21
use function func_num_args;
22
use function implode;
23
use function is_array;
24
use function is_object;
25
use function key;
26
use function strtoupper;
27
use function substr;
28
 
29
/**
30
 * QueryBuilder class is responsible to dynamically create SQL queries.
31
 *
32
 * Important: Verify that every feature you use will work with your database vendor.
33
 * SQL Query Builder does not attempt to validate the generated SQL at all.
34
 *
35
 * The query builder does no validation whatsoever if certain features even work with the
36
 * underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements
37
 * even if some vendors such as MySQL support it.
38
 */
39
class QueryBuilder
40
{
41
    /** @deprecated */
42
    public const SELECT = 0;
43
 
44
    /** @deprecated */
45
    public const DELETE = 1;
46
 
47
    /** @deprecated */
48
    public const UPDATE = 2;
49
 
50
    /** @deprecated */
51
    public const INSERT = 3;
52
 
53
    /** @deprecated */
54
    public const STATE_DIRTY = 0;
55
 
56
    /** @deprecated */
57
    public const STATE_CLEAN = 1;
58
 
59
    /**
60
     * The DBAL Connection.
61
     */
62
    private Connection $connection;
63
 
64
    /*
65
     * The default values of SQL parts collection
66
     */
67
    private const SQL_PARTS_DEFAULTS = [
68
        'select'   => [],
69
        'distinct' => false,
70
        'from'     => [],
71
        'join'     => [],
72
        'set'      => [],
73
        'where'    => null,
74
        'groupBy'  => [],
75
        'having'   => null,
76
        'orderBy'  => [],
77
        'values'   => [],
78
    ];
79
 
80
    /**
81
     * The array of SQL parts collected.
82
     *
83
     * @var mixed[]
84
     */
85
    private array $sqlParts = self::SQL_PARTS_DEFAULTS;
86
 
87
    /**
88
     * The complete SQL string for this query.
89
     */
90
    private ?string $sql = null;
91
 
92
    /**
93
     * The query parameters.
94
     *
95
     * @var list<mixed>|array<string, mixed>
96
     */
97
    private $params = [];
98
 
99
    /**
100
     * The parameter type map of this query.
101
     *
102
     * @var array<int, int|string|Type|null>|array<string, int|string|Type|null>
103
     */
104
    private array $paramTypes = [];
105
 
106
    /**
107
     * The type of query this is. Can be select, update or delete.
108
     */
109
    private int $type = self::SELECT;
110
 
111
    /**
112
     * The state of the query object. Can be dirty or clean.
113
     */
114
    private int $state = self::STATE_CLEAN;
115
 
116
    /**
117
     * The index of the first result to retrieve.
118
     */
119
    private int $firstResult = 0;
120
 
121
    /**
122
     * The maximum number of results to retrieve or NULL to retrieve all results.
123
     */
124
    private ?int $maxResults = null;
125
 
126
    /**
127
     * The counter of bound parameters used with {@see bindValue).
128
     */
129
    private int $boundCounter = 0;
130
 
131
    /**
132
     * The query cache profile used for caching results.
133
     */
134
    private ?QueryCacheProfile $resultCacheProfile = null;
135
 
136
    /**
137
     * Initializes a new <tt>QueryBuilder</tt>.
138
     *
139
     * @param Connection $connection The DBAL Connection.
140
     */
141
    public function __construct(Connection $connection)
142
    {
143
        $this->connection = $connection;
144
    }
145
 
146
    /**
147
     * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
148
     * This producer method is intended for convenient inline usage. Example:
149
     *
150
     * <code>
151
     *     $qb = $conn->createQueryBuilder()
152
     *         ->select('u')
153
     *         ->from('users', 'u')
154
     *         ->where($qb->expr()->eq('u.id', 1));
155
     * </code>
156
     *
157
     * For more complex expression construction, consider storing the expression
158
     * builder object in a local variable.
159
     *
160
     * @return ExpressionBuilder
161
     */
162
    public function expr()
163
    {
164
        return $this->connection->getExpressionBuilder();
165
    }
166
 
167
    /**
168
     * Gets the type of the currently built query.
169
     *
170
     * @deprecated If necessary, track the type of the query being built outside of the builder.
171
     *
172
     * @return int
173
     */
174
    public function getType()
175
    {
176
        Deprecation::trigger(
177
            'doctrine/dbal',
178
            'https://github.com/doctrine/dbal/pull/5551',
179
            'Relying on the type of the query being built is deprecated.'
180
                . ' If necessary, track the type of the query being built outside of the builder.',
181
        );
182
 
183
        return $this->type;
184
    }
185
 
186
    /**
187
     * Gets the associated DBAL Connection for this query builder.
188
     *
189
     * @deprecated Use the connection used to instantiate the builder instead.
190
     *
191
     * @return Connection
192
     */
193
    public function getConnection()
194
    {
195
        Deprecation::trigger(
196
            'doctrine/dbal',
197
            'https://github.com/doctrine/dbal/pull/5780',
198
            '%s is deprecated. Use the connection used to instantiate the builder instead.',
199
            __METHOD__,
200
        );
201
 
202
        return $this->connection;
203
    }
204
 
205
    /**
206
     * Gets the state of this query builder instance.
207
     *
208
     * @deprecated The builder state is an internal concern.
209
     *
210
     * @return int Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
211
     */
212
    public function getState()
213
    {
214
        Deprecation::trigger(
215
            'doctrine/dbal',
216
            'https://github.com/doctrine/dbal/pull/5551',
217
            'Relying on the query builder state is deprecated as it is an internal concern.',
218
        );
219
 
220
        return $this->state;
221
    }
222
 
223
    /**
224
     * Prepares and executes an SQL query and returns the first row of the result
225
     * as an associative array.
226
     *
227
     * @return array<string, mixed>|false False is returned if no rows are found.
228
     *
229
     * @throws Exception
230
     */
231
    public function fetchAssociative()
232
    {
233
        return $this->executeQuery()->fetchAssociative();
234
    }
235
 
236
    /**
237
     * Prepares and executes an SQL query and returns the first row of the result
238
     * as a numerically indexed array.
239
     *
240
     * @return array<int, mixed>|false False is returned if no rows are found.
241
     *
242
     * @throws Exception
243
     */
244
    public function fetchNumeric()
245
    {
246
        return $this->executeQuery()->fetchNumeric();
247
    }
248
 
249
    /**
250
     * Prepares and executes an SQL query and returns the value of a single column
251
     * of the first row of the result.
252
     *
253
     * @return mixed|false False is returned if no rows are found.
254
     *
255
     * @throws Exception
256
     */
257
    public function fetchOne()
258
    {
259
        return $this->executeQuery()->fetchOne();
260
    }
261
 
262
    /**
263
     * Prepares and executes an SQL query and returns the result as an array of numeric arrays.
264
     *
265
     * @return array<int,array<int,mixed>>
266
     *
267
     * @throws Exception
268
     */
269
    public function fetchAllNumeric(): array
270
    {
271
        return $this->executeQuery()->fetchAllNumeric();
272
    }
273
 
274
    /**
275
     * Prepares and executes an SQL query and returns the result as an array of associative arrays.
276
     *
277
     * @return array<int,array<string,mixed>>
278
     *
279
     * @throws Exception
280
     */
281
    public function fetchAllAssociative(): array
282
    {
283
        return $this->executeQuery()->fetchAllAssociative();
284
    }
285
 
286
    /**
287
     * Prepares and executes an SQL query and returns the result as an associative array with the keys
288
     * mapped to the first column and the values mapped to the second column.
289
     *
290
     * @return array<mixed,mixed>
291
     *
292
     * @throws Exception
293
     */
294
    public function fetchAllKeyValue(): array
295
    {
296
        return $this->executeQuery()->fetchAllKeyValue();
297
    }
298
 
299
    /**
300
     * Prepares and executes an SQL query and returns the result as an associative array with the keys mapped
301
     * to the first column and the values being an associative array representing the rest of the columns
302
     * and their values.
303
     *
304
     * @return array<mixed,array<string,mixed>>
305
     *
306
     * @throws Exception
307
     */
308
    public function fetchAllAssociativeIndexed(): array
309
    {
310
        return $this->executeQuery()->fetchAllAssociativeIndexed();
311
    }
312
 
313
    /**
314
     * Prepares and executes an SQL query and returns the result as an array of the first column values.
315
     *
316
     * @return array<int,mixed>
317
     *
318
     * @throws Exception
319
     */
320
    public function fetchFirstColumn(): array
321
    {
322
        return $this->executeQuery()->fetchFirstColumn();
323
    }
324
 
325
    /**
326
     * Executes an SQL query (SELECT) and returns a Result.
327
     *
328
     * @throws Exception
329
     */
330
    public function executeQuery(): Result
331
    {
332
        return $this->connection->executeQuery(
333
            $this->getSQL(),
334
            $this->params,
335
            $this->paramTypes,
336
            $this->resultCacheProfile,
337
        );
338
    }
339
 
340
    /**
341
     * Executes an SQL statement and returns the number of affected rows.
342
     *
343
     * Should be used for INSERT, UPDATE and DELETE
344
     *
345
     * @return int The number of affected rows.
346
     *
347
     * @throws Exception
348
     */
349
    public function executeStatement(): int
350
    {
351
        return $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes);
352
    }
353
 
354
    /**
355
     * Executes this query using the bound parameters and their types.
356
     *
357
     * @deprecated Use {@see executeQuery()} or {@see executeStatement()} instead.
358
     *
359
     * @return Result|int|string
360
     *
361
     * @throws Exception
362
     */
363
    public function execute()
364
    {
365
        if ($this->type === self::SELECT) {
366
            Deprecation::trigger(
367
                'doctrine/dbal',
368
                'https://github.com/doctrine/dbal/pull/4578',
369
                'QueryBuilder::execute() is deprecated, use QueryBuilder::executeQuery() for SQL queries instead.',
370
            );
371
 
372
            return $this->executeQuery();
373
        }
374
 
375
        Deprecation::trigger(
376
            'doctrine/dbal',
377
            'https://github.com/doctrine/dbal/pull/4578',
378
            'QueryBuilder::execute() is deprecated, use QueryBuilder::executeStatement() for SQL statements instead.',
379
        );
380
 
381
        return $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes);
382
    }
383
 
384
    /**
385
     * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
386
     *
387
     * <code>
388
     *     $qb = $em->createQueryBuilder()
389
     *         ->select('u')
390
     *         ->from('User', 'u')
391
     *     echo $qb->getSQL(); // SELECT u FROM User u
392
     * </code>
393
     *
394
     * @return string The SQL query string.
395
     */
396
    public function getSQL()
397
    {
398
        if ($this->sql !== null && $this->state === self::STATE_CLEAN) {
399
            return $this->sql;
400
        }
401
 
402
        switch ($this->type) {
403
            case self::INSERT:
404
                $sql = $this->getSQLForInsert();
405
                break;
406
 
407
            case self::DELETE:
408
                $sql = $this->getSQLForDelete();
409
                break;
410
 
411
            case self::UPDATE:
412
                $sql = $this->getSQLForUpdate();
413
                break;
414
 
415
            case self::SELECT:
416
            default:
417
                $sql = $this->getSQLForSelect();
418
                break;
419
        }
420
 
421
        $this->state = self::STATE_CLEAN;
422
        $this->sql   = $sql;
423
 
424
        return $sql;
425
    }
426
 
427
    /**
428
     * Sets a query parameter for the query being constructed.
429
     *
430
     * <code>
431
     *     $qb = $conn->createQueryBuilder()
432
     *         ->select('u')
433
     *         ->from('users', 'u')
434
     *         ->where('u.id = :user_id')
435
     *         ->setParameter('user_id', 1);
436
     * </code>
437
     *
438
     * @param int|string           $key   Parameter position or name
439
     * @param mixed                $value Parameter value
440
     * @param int|string|Type|null $type  Parameter type
441
     *
442
     * @return $this This QueryBuilder instance.
443
     */
444
    public function setParameter($key, $value, $type = ParameterType::STRING)
445
    {
446
        if ($type !== null) {
447
            $this->paramTypes[$key] = $type;
448
        } else {
449
            Deprecation::trigger(
450
                'doctrine/dbal',
451
                'https://github.com/doctrine/dbal/pull/5550',
452
                'Using NULL as prepared statement parameter type is deprecated.'
453
                    . 'Omit or use Parameter::STRING instead',
454
            );
455
        }
456
 
457
        $this->params[$key] = $value;
458
 
459
        return $this;
460
    }
461
 
462
    /**
463
     * Sets a collection of query parameters for the query being constructed.
464
     *
465
     * <code>
466
     *     $qb = $conn->createQueryBuilder()
467
     *         ->select('u')
468
     *         ->from('users', 'u')
469
     *         ->where('u.id = :user_id1 OR u.id = :user_id2')
470
     *         ->setParameters(array(
471
     *             'user_id1' => 1,
472
     *             'user_id2' => 2
473
     *         ));
474
     * </code>
475
     *
476
     * @param list<mixed>|array<string, mixed>                                     $params Parameters to set
477
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
478
     *
479
     * @return $this This QueryBuilder instance.
480
     */
481
    public function setParameters(array $params, array $types = [])
482
    {
483
        $this->paramTypes = $types;
484
        $this->params     = $params;
485
 
486
        return $this;
487
    }
488
 
489
    /**
490
     * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
491
     *
492
     * @return list<mixed>|array<string, mixed> The currently defined query parameters
493
     */
494
    public function getParameters()
495
    {
496
        return $this->params;
497
    }
498
 
499
    /**
500
     * Gets a (previously set) query parameter of the query being constructed.
501
     *
502
     * @param mixed $key The key (index or name) of the bound parameter.
503
     *
504
     * @return mixed The value of the bound parameter.
505
     */
506
    public function getParameter($key)
507
    {
508
        return $this->params[$key] ?? null;
509
    }
510
 
511
    /**
512
     * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
513
     *
514
     * @return array<int, int|string|Type|null>|array<string, int|string|Type|null> The currently defined
515
     *                                                                              query parameter types
516
     */
517
    public function getParameterTypes()
518
    {
519
        return $this->paramTypes;
520
    }
521
 
522
    /**
523
     * Gets a (previously set) query parameter type of the query being constructed.
524
     *
525
     * @param int|string $key The key of the bound parameter type
526
     *
527
     * @return int|string|Type The value of the bound parameter type
528
     */
529
    public function getParameterType($key)
530
    {
531
        return $this->paramTypes[$key] ?? ParameterType::STRING;
532
    }
533
 
534
    /**
535
     * Sets the position of the first result to retrieve (the "offset").
536
     *
537
     * @param int $firstResult The first result to return.
538
     *
539
     * @return $this This QueryBuilder instance.
540
     */
541
    public function setFirstResult($firstResult)
542
    {
543
        $this->state       = self::STATE_DIRTY;
544
        $this->firstResult = $firstResult;
545
 
546
        return $this;
547
    }
548
 
549
    /**
550
     * Gets the position of the first result the query object was set to retrieve (the "offset").
551
     *
552
     * @return int The position of the first result.
553
     */
554
    public function getFirstResult()
555
    {
556
        return $this->firstResult;
557
    }
558
 
559
    /**
560
     * Sets the maximum number of results to retrieve (the "limit").
561
     *
562
     * @param int|null $maxResults The maximum number of results to retrieve or NULL to retrieve all results.
563
     *
564
     * @return $this This QueryBuilder instance.
565
     */
566
    public function setMaxResults($maxResults)
567
    {
568
        $this->state      = self::STATE_DIRTY;
569
        $this->maxResults = $maxResults;
570
 
571
        return $this;
572
    }
573
 
574
    /**
575
     * Gets the maximum number of results the query object was set to retrieve (the "limit").
576
     * Returns NULL if all results will be returned.
577
     *
578
     * @return int|null The maximum number of results.
579
     */
580
    public function getMaxResults()
581
    {
582
        return $this->maxResults;
583
    }
584
 
585
    /**
586
     * Either appends to or replaces a single, generic query part.
587
     *
588
     * The available parts are: 'select', 'from', 'set', 'where',
589
     * 'groupBy', 'having' and 'orderBy'.
590
     *
591
     * @param string $sqlPartName
592
     * @param mixed  $sqlPart
593
     * @param bool   $append
594
     *
595
     * @return $this This QueryBuilder instance.
596
     */
597
    public function add($sqlPartName, $sqlPart, $append = false)
598
    {
599
        $isArray    = is_array($sqlPart);
600
        $isMultiple = is_array($this->sqlParts[$sqlPartName]);
601
 
602
        if ($isMultiple && ! $isArray) {
603
            $sqlPart = [$sqlPart];
604
        }
605
 
606
        $this->state = self::STATE_DIRTY;
607
 
608
        if ($append) {
609
            if (
610
                $sqlPartName === 'orderBy'
611
                || $sqlPartName === 'groupBy'
612
                || $sqlPartName === 'select'
613
                || $sqlPartName === 'set'
614
            ) {
615
                foreach ($sqlPart as $part) {
616
                    $this->sqlParts[$sqlPartName][] = $part;
617
                }
618
            } elseif ($isArray && is_array($sqlPart[key($sqlPart)])) {
619
                $key                                  = key($sqlPart);
620
                $this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key];
621
            } elseif ($isMultiple) {
622
                $this->sqlParts[$sqlPartName][] = $sqlPart;
623
            } else {
624
                $this->sqlParts[$sqlPartName] = $sqlPart;
625
            }
626
 
627
            return $this;
628
        }
629
 
630
        $this->sqlParts[$sqlPartName] = $sqlPart;
631
 
632
        return $this;
633
    }
634
 
635
    /**
636
     * Specifies an item that is to be returned in the query result.
637
     * Replaces any previously specified selections, if any.
638
     *
639
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
640
     *
641
     * <code>
642
     *     $qb = $conn->createQueryBuilder()
643
     *         ->select('u.id', 'p.id')
644
     *         ->from('users', 'u')
645
     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
646
     * </code>
647
     *
648
     * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED.
649
     *                                     Pass each value as an individual argument.
650
     *
651
     * @return $this This QueryBuilder instance.
652
     */
653
    public function select($select = null/*, string ...$selects*/)
654
    {
655
        $this->type = self::SELECT;
656
 
657
        if ($select === null) {
658
            return $this;
659
        }
660
 
661
        if (is_array($select)) {
662
            Deprecation::trigger(
663
                'doctrine/dbal',
664
                'https://github.com/doctrine/dbal/issues/3837',
665
                'Passing an array for the first argument to QueryBuilder::select() is deprecated, ' .
666
                'pass each value as an individual variadic argument instead.',
667
            );
668
        }
669
 
670
        $selects = is_array($select) ? $select : func_get_args();
671
 
672
        return $this->add('select', $selects);
673
    }
674
 
675
    /**
676
     * Adds DISTINCT to the query.
677
     *
678
     * <code>
679
     *     $qb = $conn->createQueryBuilder()
680
     *         ->select('u.id')
681
     *         ->distinct()
682
     *         ->from('users', 'u')
683
     * </code>
684
     *
685
     * @return $this This QueryBuilder instance.
686
     */
687
    public function distinct(): self
688
    {
689
        $this->sqlParts['distinct'] = true;
690
 
691
        return $this;
692
    }
693
 
694
    /**
695
     * Adds an item that is to be returned in the query result.
696
     *
697
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
698
     *
699
     * <code>
700
     *     $qb = $conn->createQueryBuilder()
701
     *         ->select('u.id')
702
     *         ->addSelect('p.id')
703
     *         ->from('users', 'u')
704
     *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
705
     * </code>
706
     *
707
     * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED.
708
     *                                     Pass each value as an individual argument.
709
     *
710
     * @return $this This QueryBuilder instance.
711
     */
712
    public function addSelect($select = null/*, string ...$selects*/)
713
    {
714
        $this->type = self::SELECT;
715
 
716
        if ($select === null) {
717
            return $this;
718
        }
719
 
720
        if (is_array($select)) {
721
            Deprecation::trigger(
722
                'doctrine/dbal',
723
                'https://github.com/doctrine/dbal/issues/3837',
724
                'Passing an array for the first argument to QueryBuilder::addSelect() is deprecated, ' .
725
                'pass each value as an individual variadic argument instead.',
726
            );
727
        }
728
 
729
        $selects = is_array($select) ? $select : func_get_args();
730
 
731
        return $this->add('select', $selects, true);
732
    }
733
 
734
    /**
735
     * Turns the query being built into a bulk delete query that ranges over
736
     * a certain table.
737
     *
738
     * <code>
739
     *     $qb = $conn->createQueryBuilder()
740
     *         ->delete('users', 'u')
741
     *         ->where('u.id = :user_id')
742
     *         ->setParameter(':user_id', 1);
743
     * </code>
744
     *
745
     * @param string $delete The table whose rows are subject to the deletion.
746
     * @param string $alias  The table alias used in the constructed query.
747
     *
748
     * @return $this This QueryBuilder instance.
749
     */
750
    public function delete($delete = null, $alias = null)
751
    {
752
        $this->type = self::DELETE;
753
 
754
        if ($delete === null) {
755
            return $this;
756
        }
757
 
758
        return $this->add('from', [
759
            'table' => $delete,
760
            'alias' => $alias,
761
        ]);
762
    }
763
 
764
    /**
765
     * Turns the query being built into a bulk update query that ranges over
766
     * a certain table
767
     *
768
     * <code>
769
     *     $qb = $conn->createQueryBuilder()
770
     *         ->update('counters', 'c')
771
     *         ->set('c.value', 'c.value + 1')
772
     *         ->where('c.id = ?');
773
     * </code>
774
     *
775
     * @param string $update The table whose rows are subject to the update.
776
     * @param string $alias  The table alias used in the constructed query.
777
     *
778
     * @return $this This QueryBuilder instance.
779
     */
780
    public function update($update = null, $alias = null)
781
    {
782
        $this->type = self::UPDATE;
783
 
784
        if ($update === null) {
785
            return $this;
786
        }
787
 
788
        return $this->add('from', [
789
            'table' => $update,
790
            'alias' => $alias,
791
        ]);
792
    }
793
 
794
    /**
795
     * Turns the query being built into an insert query that inserts into
796
     * a certain table
797
     *
798
     * <code>
799
     *     $qb = $conn->createQueryBuilder()
800
     *         ->insert('users')
801
     *         ->values(
802
     *             array(
803
     *                 'name' => '?',
804
     *                 'password' => '?'
805
     *             )
806
     *         );
807
     * </code>
808
     *
809
     * @param string $insert The table into which the rows should be inserted.
810
     *
811
     * @return $this This QueryBuilder instance.
812
     */
813
    public function insert($insert = null)
814
    {
815
        $this->type = self::INSERT;
816
 
817
        if ($insert === null) {
818
            return $this;
819
        }
820
 
821
        return $this->add('from', ['table' => $insert]);
822
    }
823
 
824
    /**
825
     * Creates and adds a query root corresponding to the table identified by the
826
     * given alias, forming a cartesian product with any existing query roots.
827
     *
828
     * <code>
829
     *     $qb = $conn->createQueryBuilder()
830
     *         ->select('u.id')
831
     *         ->from('users', 'u')
832
     * </code>
833
     *
834
     * @param string      $from  The table.
835
     * @param string|null $alias The alias of the table.
836
     *
837
     * @return $this This QueryBuilder instance.
838
     */
839
    public function from($from, $alias = null)
840
    {
841
        return $this->add('from', [
842
            'table' => $from,
843
            'alias' => $alias,
844
        ], true);
845
    }
846
 
847
    /**
848
     * Creates and adds a join to the query.
849
     *
850
     * <code>
851
     *     $qb = $conn->createQueryBuilder()
852
     *         ->select('u.name')
853
     *         ->from('users', 'u')
854
     *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
855
     * </code>
856
     *
857
     * @param string $fromAlias The alias that points to a from clause.
858
     * @param string $join      The table name to join.
859
     * @param string $alias     The alias of the join table.
860
     * @param string $condition The condition for the join.
861
     *
862
     * @return $this This QueryBuilder instance.
863
     */
864
    public function join($fromAlias, $join, $alias, $condition = null)
865
    {
866
        return $this->innerJoin($fromAlias, $join, $alias, $condition);
867
    }
868
 
869
    /**
870
     * Creates and adds a join to the query.
871
     *
872
     * <code>
873
     *     $qb = $conn->createQueryBuilder()
874
     *         ->select('u.name')
875
     *         ->from('users', 'u')
876
     *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
877
     * </code>
878
     *
879
     * @param string $fromAlias The alias that points to a from clause.
880
     * @param string $join      The table name to join.
881
     * @param string $alias     The alias of the join table.
882
     * @param string $condition The condition for the join.
883
     *
884
     * @return $this This QueryBuilder instance.
885
     */
886
    public function innerJoin($fromAlias, $join, $alias, $condition = null)
887
    {
888
        return $this->add('join', [
889
            $fromAlias => [
890
                'joinType'      => 'inner',
891
                'joinTable'     => $join,
892
                'joinAlias'     => $alias,
893
                'joinCondition' => $condition,
894
            ],
895
        ], true);
896
    }
897
 
898
    /**
899
     * Creates and adds a left join to the query.
900
     *
901
     * <code>
902
     *     $qb = $conn->createQueryBuilder()
903
     *         ->select('u.name')
904
     *         ->from('users', 'u')
905
     *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
906
     * </code>
907
     *
908
     * @param string $fromAlias The alias that points to a from clause.
909
     * @param string $join      The table name to join.
910
     * @param string $alias     The alias of the join table.
911
     * @param string $condition The condition for the join.
912
     *
913
     * @return $this This QueryBuilder instance.
914
     */
915
    public function leftJoin($fromAlias, $join, $alias, $condition = null)
916
    {
917
        return $this->add('join', [
918
            $fromAlias => [
919
                'joinType'      => 'left',
920
                'joinTable'     => $join,
921
                'joinAlias'     => $alias,
922
                'joinCondition' => $condition,
923
            ],
924
        ], true);
925
    }
926
 
927
    /**
928
     * Creates and adds a right join to the query.
929
     *
930
     * <code>
931
     *     $qb = $conn->createQueryBuilder()
932
     *         ->select('u.name')
933
     *         ->from('users', 'u')
934
     *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
935
     * </code>
936
     *
937
     * @param string $fromAlias The alias that points to a from clause.
938
     * @param string $join      The table name to join.
939
     * @param string $alias     The alias of the join table.
940
     * @param string $condition The condition for the join.
941
     *
942
     * @return $this This QueryBuilder instance.
943
     */
944
    public function rightJoin($fromAlias, $join, $alias, $condition = null)
945
    {
946
        return $this->add('join', [
947
            $fromAlias => [
948
                'joinType'      => 'right',
949
                'joinTable'     => $join,
950
                'joinAlias'     => $alias,
951
                'joinCondition' => $condition,
952
            ],
953
        ], true);
954
    }
955
 
956
    /**
957
     * Sets a new value for a column in a bulk update query.
958
     *
959
     * <code>
960
     *     $qb = $conn->createQueryBuilder()
961
     *         ->update('counters', 'c')
962
     *         ->set('c.value', 'c.value + 1')
963
     *         ->where('c.id = ?');
964
     * </code>
965
     *
966
     * @param string $key   The column to set.
967
     * @param string $value The value, expression, placeholder, etc.
968
     *
969
     * @return $this This QueryBuilder instance.
970
     */
971
    public function set($key, $value)
972
    {
973
        return $this->add('set', $key . ' = ' . $value, true);
974
    }
975
 
976
    /**
977
     * Specifies one or more restrictions to the query result.
978
     * Replaces any previously specified restrictions, if any.
979
     *
980
     * <code>
981
     *     $qb = $conn->createQueryBuilder()
982
     *         ->select('c.value')
983
     *         ->from('counters', 'c')
984
     *         ->where('c.id = ?');
985
     *
986
     *     // You can optionally programmatically build and/or expressions
987
     *     $qb = $conn->createQueryBuilder();
988
     *
989
     *     $or = $qb->expr()->orx();
990
     *     $or->add($qb->expr()->eq('c.id', 1));
991
     *     $or->add($qb->expr()->eq('c.id', 2));
992
     *
993
     *     $qb->update('counters', 'c')
994
     *         ->set('c.value', 'c.value + 1')
995
     *         ->where($or);
996
     * </code>
997
     *
998
     * @param mixed $predicates The restriction predicates.
999
     *
1000
     * @return $this This QueryBuilder instance.
1001
     */
1002
    public function where($predicates)
1003
    {
1004
        if (! (func_num_args() === 1 && $predicates instanceof CompositeExpression)) {
1005
            $predicates = CompositeExpression::and(...func_get_args());
1006
        }
1007
 
1008
        return $this->add('where', $predicates);
1009
    }
1010
 
1011
    /**
1012
     * Adds one or more restrictions to the query results, forming a logical
1013
     * conjunction with any previously specified restrictions.
1014
     *
1015
     * <code>
1016
     *     $qb = $conn->createQueryBuilder()
1017
     *         ->select('u')
1018
     *         ->from('users', 'u')
1019
     *         ->where('u.username LIKE ?')
1020
     *         ->andWhere('u.is_active = 1');
1021
     * </code>
1022
     *
1023
     * @see where()
1024
     *
1025
     * @param mixed $where The query restrictions.
1026
     *
1027
     * @return $this This QueryBuilder instance.
1028
     */
1029
    public function andWhere($where)
1030
    {
1031
        $args  = func_get_args();
1032
        $where = $this->getQueryPart('where');
1033
 
1034
        if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_AND) {
1035
            $where = $where->with(...$args);
1036
        } else {
1037
            array_unshift($args, $where);
1038
            $where = CompositeExpression::and(...$args);
1039
        }
1040
 
1041
        return $this->add('where', $where, true);
1042
    }
1043
 
1044
    /**
1045
     * Adds one or more restrictions to the query results, forming a logical
1046
     * disjunction with any previously specified restrictions.
1047
     *
1048
     * <code>
1049
     *     $qb = $em->createQueryBuilder()
1050
     *         ->select('u.name')
1051
     *         ->from('users', 'u')
1052
     *         ->where('u.id = 1')
1053
     *         ->orWhere('u.id = 2');
1054
     * </code>
1055
     *
1056
     * @see where()
1057
     *
1058
     * @param mixed $where The WHERE statement.
1059
     *
1060
     * @return $this This QueryBuilder instance.
1061
     */
1062
    public function orWhere($where)
1063
    {
1064
        $args  = func_get_args();
1065
        $where = $this->getQueryPart('where');
1066
 
1067
        if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_OR) {
1068
            $where = $where->with(...$args);
1069
        } else {
1070
            array_unshift($args, $where);
1071
            $where = CompositeExpression::or(...$args);
1072
        }
1073
 
1074
        return $this->add('where', $where, true);
1075
    }
1076
 
1077
    /**
1078
     * Specifies a grouping over the results of the query.
1079
     * Replaces any previously specified groupings, if any.
1080
     *
1081
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
1082
     *
1083
     * <code>
1084
     *     $qb = $conn->createQueryBuilder()
1085
     *         ->select('u.name')
1086
     *         ->from('users', 'u')
1087
     *         ->groupBy('u.id');
1088
     * </code>
1089
     *
1090
     * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED.
1091
     *                                 Pass each value as an individual argument.
1092
     *
1093
     * @return $this This QueryBuilder instance.
1094
     */
1095
    public function groupBy($groupBy/*, string ...$groupBys*/)
1096
    {
1097
        if (is_array($groupBy) && count($groupBy) === 0) {
1098
            return $this;
1099
        }
1100
 
1101
        if (is_array($groupBy)) {
1102
            Deprecation::trigger(
1103
                'doctrine/dbal',
1104
                'https://github.com/doctrine/dbal/issues/3837',
1105
                'Passing an array for the first argument to QueryBuilder::groupBy() is deprecated, ' .
1106
                'pass each value as an individual variadic argument instead.',
1107
            );
1108
        }
1109
 
1110
        $groupBy = is_array($groupBy) ? $groupBy : func_get_args();
1111
 
1112
        return $this->add('groupBy', $groupBy, false);
1113
    }
1114
 
1115
    /**
1116
     * Adds a grouping expression to the query.
1117
     *
1118
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
1119
     *
1120
     * <code>
1121
     *     $qb = $conn->createQueryBuilder()
1122
     *         ->select('u.name')
1123
     *         ->from('users', 'u')
1124
     *         ->groupBy('u.lastLogin')
1125
     *         ->addGroupBy('u.createdAt');
1126
     * </code>
1127
     *
1128
     * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED.
1129
     *                                 Pass each value as an individual argument.
1130
     *
1131
     * @return $this This QueryBuilder instance.
1132
     */
1133
    public function addGroupBy($groupBy/*, string ...$groupBys*/)
1134
    {
1135
        if (is_array($groupBy) && count($groupBy) === 0) {
1136
            return $this;
1137
        }
1138
 
1139
        if (is_array($groupBy)) {
1140
            Deprecation::trigger(
1141
                'doctrine/dbal',
1142
                'https://github.com/doctrine/dbal/issues/3837',
1143
                'Passing an array for the first argument to QueryBuilder::addGroupBy() is deprecated, ' .
1144
                'pass each value as an individual variadic argument instead.',
1145
            );
1146
        }
1147
 
1148
        $groupBy = is_array($groupBy) ? $groupBy : func_get_args();
1149
 
1150
        return $this->add('groupBy', $groupBy, true);
1151
    }
1152
 
1153
    /**
1154
     * Sets a value for a column in an insert query.
1155
     *
1156
     * <code>
1157
     *     $qb = $conn->createQueryBuilder()
1158
     *         ->insert('users')
1159
     *         ->values(
1160
     *             array(
1161
     *                 'name' => '?'
1162
     *             )
1163
     *         )
1164
     *         ->setValue('password', '?');
1165
     * </code>
1166
     *
1167
     * @param string $column The column into which the value should be inserted.
1168
     * @param string $value  The value that should be inserted into the column.
1169
     *
1170
     * @return $this This QueryBuilder instance.
1171
     */
1172
    public function setValue($column, $value)
1173
    {
1174
        $this->sqlParts['values'][$column] = $value;
1175
 
1176
        return $this;
1177
    }
1178
 
1179
    /**
1180
     * Specifies values for an insert query indexed by column names.
1181
     * Replaces any previous values, if any.
1182
     *
1183
     * <code>
1184
     *     $qb = $conn->createQueryBuilder()
1185
     *         ->insert('users')
1186
     *         ->values(
1187
     *             array(
1188
     *                 'name' => '?',
1189
     *                 'password' => '?'
1190
     *             )
1191
     *         );
1192
     * </code>
1193
     *
1194
     * @param mixed[] $values The values to specify for the insert query indexed by column names.
1195
     *
1196
     * @return $this This QueryBuilder instance.
1197
     */
1198
    public function values(array $values)
1199
    {
1200
        return $this->add('values', $values);
1201
    }
1202
 
1203
    /**
1204
     * Specifies a restriction over the groups of the query.
1205
     * Replaces any previous having restrictions, if any.
1206
     *
1207
     * @param mixed $having The restriction over the groups.
1208
     *
1209
     * @return $this This QueryBuilder instance.
1210
     */
1211
    public function having($having)
1212
    {
1213
        if (! (func_num_args() === 1 && $having instanceof CompositeExpression)) {
1214
            $having = CompositeExpression::and(...func_get_args());
1215
        }
1216
 
1217
        return $this->add('having', $having);
1218
    }
1219
 
1220
    /**
1221
     * Adds a restriction over the groups of the query, forming a logical
1222
     * conjunction with any existing having restrictions.
1223
     *
1224
     * @param mixed $having The restriction to append.
1225
     *
1226
     * @return $this This QueryBuilder instance.
1227
     */
1228
    public function andHaving($having)
1229
    {
1230
        $args   = func_get_args();
1231
        $having = $this->getQueryPart('having');
1232
 
1233
        if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_AND) {
1234
            $having = $having->with(...$args);
1235
        } else {
1236
            array_unshift($args, $having);
1237
            $having = CompositeExpression::and(...$args);
1238
        }
1239
 
1240
        return $this->add('having', $having);
1241
    }
1242
 
1243
    /**
1244
     * Adds a restriction over the groups of the query, forming a logical
1245
     * disjunction with any existing having restrictions.
1246
     *
1247
     * @param mixed $having The restriction to add.
1248
     *
1249
     * @return $this This QueryBuilder instance.
1250
     */
1251
    public function orHaving($having)
1252
    {
1253
        $args   = func_get_args();
1254
        $having = $this->getQueryPart('having');
1255
 
1256
        if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_OR) {
1257
            $having = $having->with(...$args);
1258
        } else {
1259
            array_unshift($args, $having);
1260
            $having = CompositeExpression::or(...$args);
1261
        }
1262
 
1263
        return $this->add('having', $having);
1264
    }
1265
 
1266
    /**
1267
     * Specifies an ordering for the query results.
1268
     * Replaces any previously specified orderings, if any.
1269
     *
1270
     * @param string $sort  The ordering expression.
1271
     * @param string $order The ordering direction.
1272
     *
1273
     * @return $this This QueryBuilder instance.
1274
     */
1275
    public function orderBy($sort, $order = null)
1276
    {
1277
        return $this->add('orderBy', $sort . ' ' . ($order ?? 'ASC'), false);
1278
    }
1279
 
1280
    /**
1281
     * Adds an ordering to the query results.
1282
     *
1283
     * @param string $sort  The ordering expression.
1284
     * @param string $order The ordering direction.
1285
     *
1286
     * @return $this This QueryBuilder instance.
1287
     */
1288
    public function addOrderBy($sort, $order = null)
1289
    {
1290
        return $this->add('orderBy', $sort . ' ' . ($order ?? 'ASC'), true);
1291
    }
1292
 
1293
    /**
1294
     * Gets a query part by its name.
1295
     *
1296
     * @param string $queryPartName
1297
     *
1298
     * @return mixed
1299
     */
1300
    public function getQueryPart($queryPartName)
1301
    {
1302
        return $this->sqlParts[$queryPartName];
1303
    }
1304
 
1305
    /**
1306
     * Gets all query parts.
1307
     *
1308
     * @return mixed[]
1309
     */
1310
    public function getQueryParts()
1311
    {
1312
        return $this->sqlParts;
1313
    }
1314
 
1315
    /**
1316
     * Resets SQL parts.
1317
     *
1318
     * @param string[]|null $queryPartNames
1319
     *
1320
     * @return $this This QueryBuilder instance.
1321
     */
1322
    public function resetQueryParts($queryPartNames = null)
1323
    {
1324
        $queryPartNames ??= array_keys($this->sqlParts);
1325
 
1326
        foreach ($queryPartNames as $queryPartName) {
1327
            $this->resetQueryPart($queryPartName);
1328
        }
1329
 
1330
        return $this;
1331
    }
1332
 
1333
    /**
1334
     * Resets a single SQL part.
1335
     *
1336
     * @param string $queryPartName
1337
     *
1338
     * @return $this This QueryBuilder instance.
1339
     */
1340
    public function resetQueryPart($queryPartName)
1341
    {
1342
        $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName];
1343
 
1344
        $this->state = self::STATE_DIRTY;
1345
 
1346
        return $this;
1347
    }
1348
 
1349
    /** @throws QueryException */
1350
    private function getSQLForSelect(): string
1351
    {
1352
        $query = 'SELECT ' . ($this->sqlParts['distinct'] ? 'DISTINCT ' : '') .
1353
                  implode(', ', $this->sqlParts['select']);
1354
 
1355
        $query .= ($this->sqlParts['from'] ? ' FROM ' . implode(', ', $this->getFromClauses()) : '')
1356
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '')
1357
            . ($this->sqlParts['groupBy'] ? ' GROUP BY ' . implode(', ', $this->sqlParts['groupBy']) : '')
1358
            . ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '')
1359
            . ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : '');
1360
 
1361
        if ($this->isLimitQuery()) {
1362
            return $this->connection->getDatabasePlatform()->modifyLimitQuery(
1363
                $query,
1364
                $this->maxResults,
1365
                $this->firstResult,
1366
            );
1367
        }
1368
 
1369
        return $query;
1370
    }
1371
 
1372
    /**
1373
     * @return string[]
1374
     *
1375
     * @throws QueryException
1376
     */
1377
    private function getFromClauses(): array
1378
    {
1379
        $fromClauses  = [];
1380
        $knownAliases = [];
1381
 
1382
        // Loop through all FROM clauses
1383
        foreach ($this->sqlParts['from'] as $from) {
1384
            if ($from['alias'] === null) {
1385
                $tableSql       = $from['table'];
1386
                $tableReference = $from['table'];
1387
            } else {
1388
                $tableSql       = $from['table'] . ' ' . $from['alias'];
1389
                $tableReference = $from['alias'];
1390
            }
1391
 
1392
            $knownAliases[$tableReference] = true;
1393
 
1394
            $fromClauses[$tableReference] = $tableSql . $this->getSQLForJoins($tableReference, $knownAliases);
1395
        }
1396
 
1397
        $this->verifyAllAliasesAreKnown($knownAliases);
1398
 
1399
        return $fromClauses;
1400
    }
1401
 
1402
    /**
1403
     * @param array<string,true> $knownAliases
1404
     *
1405
     * @throws QueryException
1406
     */
1407
    private function verifyAllAliasesAreKnown(array $knownAliases): void
1408
    {
1409
        foreach ($this->sqlParts['join'] as $fromAlias => $joins) {
1410
            if (! isset($knownAliases[$fromAlias])) {
1411
                throw QueryException::unknownAlias($fromAlias, array_keys($knownAliases));
1412
            }
1413
        }
1414
    }
1415
 
1416
    private function isLimitQuery(): bool
1417
    {
1418
        return $this->maxResults !== null || $this->firstResult !== 0;
1419
    }
1420
 
1421
    /**
1422
     * Converts this instance into an INSERT string in SQL.
1423
     */
1424
    private function getSQLForInsert(): string
1425
    {
1426
        return 'INSERT INTO ' . $this->sqlParts['from']['table'] .
1427
        ' (' . implode(', ', array_keys($this->sqlParts['values'])) . ')' .
1428
        ' VALUES(' . implode(', ', $this->sqlParts['values']) . ')';
1429
    }
1430
 
1431
    /**
1432
     * Converts this instance into an UPDATE string in SQL.
1433
     */
1434
    private function getSQLForUpdate(): string
1435
    {
1436
        $table = $this->sqlParts['from']['table']
1437
            . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');
1438
 
1439
        return 'UPDATE ' . $table
1440
            . ' SET ' . implode(', ', $this->sqlParts['set'])
1441
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
1442
    }
1443
 
1444
    /**
1445
     * Converts this instance into a DELETE string in SQL.
1446
     */
1447
    private function getSQLForDelete(): string
1448
    {
1449
        $table = $this->sqlParts['from']['table']
1450
            . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');
1451
 
1452
        return 'DELETE FROM ' . $table
1453
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
1454
    }
1455
 
1456
    /**
1457
     * Gets a string representation of this QueryBuilder which corresponds to
1458
     * the final SQL query being constructed.
1459
     *
1460
     * @return string The string representation of this QueryBuilder.
1461
     */
1462
    public function __toString()
1463
    {
1464
        return $this->getSQL();
1465
    }
1466
 
1467
    /**
1468
     * Creates a new named parameter and bind the value $value to it.
1469
     *
1470
     * This method provides a shortcut for {@see Statement::bindValue()}
1471
     * when using prepared statements.
1472
     *
1473
     * The parameter $value specifies the value that you want to bind. If
1474
     * $placeholder is not provided createNamedParameter() will automatically
1475
     * create a placeholder for you. An automatic placeholder will be of the
1476
     * name ':dcValue1', ':dcValue2' etc.
1477
     *
1478
     * Example:
1479
     * <code>
1480
     * $value = 2;
1481
     * $q->eq( 'id', $q->createNamedParameter( $value ) );
1482
     * $stmt = $q->executeQuery(); // executed with 'id = 2'
1483
     * </code>
1484
     *
1485
     * @link http://www.zetacomponents.org
1486
     *
1487
     * @param mixed                $value
1488
     * @param int|string|Type|null $type
1489
     * @param string               $placeHolder The name to bind with. The string must start with a colon ':'.
1490
     *
1491
     * @return string the placeholder name used.
1492
     */
1493
    public function createNamedParameter($value, $type = ParameterType::STRING, $placeHolder = null)
1494
    {
1495
        if ($placeHolder === null) {
1496
            $this->boundCounter++;
1497
            $placeHolder = ':dcValue' . $this->boundCounter;
1498
        }
1499
 
1500
        $this->setParameter(substr($placeHolder, 1), $value, $type);
1501
 
1502
        return $placeHolder;
1503
    }
1504
 
1505
    /**
1506
     * Creates a new positional parameter and bind the given value to it.
1507
     *
1508
     * Attention: If you are using positional parameters with the query builder you have
1509
     * to be very careful to bind all parameters in the order they appear in the SQL
1510
     * statement , otherwise they get bound in the wrong order which can lead to serious
1511
     * bugs in your code.
1512
     *
1513
     * Example:
1514
     * <code>
1515
     *  $qb = $conn->createQueryBuilder();
1516
     *  $qb->select('u.*')
1517
     *     ->from('users', 'u')
1518
     *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', ParameterType::STRING))
1519
     *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', ParameterType::STRING))
1520
     * </code>
1521
     *
1522
     * @param mixed                $value
1523
     * @param int|string|Type|null $type
1524
     *
1525
     * @return string
1526
     */
1527
    public function createPositionalParameter($value, $type = ParameterType::STRING)
1528
    {
1529
        $this->setParameter($this->boundCounter, $value, $type);
1530
        $this->boundCounter++;
1531
 
1532
        return '?';
1533
    }
1534
 
1535
    /**
1536
     * @param string             $fromAlias
1537
     * @param array<string,true> $knownAliases
1538
     *
1539
     * @throws QueryException
1540
     */
1541
    private function getSQLForJoins($fromAlias, array &$knownAliases): string
1542
    {
1543
        $sql = '';
1544
 
1545
        if (isset($this->sqlParts['join'][$fromAlias])) {
1546
            foreach ($this->sqlParts['join'][$fromAlias] as $join) {
1547
                if (array_key_exists($join['joinAlias'], $knownAliases)) {
1548
                    throw QueryException::nonUniqueAlias($join['joinAlias'], array_keys($knownAliases));
1549
                }
1550
 
1551
                $sql .= ' ' . strtoupper($join['joinType'])
1552
                    . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias'];
1553
                if ($join['joinCondition'] !== null) {
1554
                    $sql .= ' ON ' . $join['joinCondition'];
1555
                }
1556
 
1557
                $knownAliases[$join['joinAlias']] = true;
1558
            }
1559
 
1560
            foreach ($this->sqlParts['join'][$fromAlias] as $join) {
1561
                $sql .= $this->getSQLForJoins($join['joinAlias'], $knownAliases);
1562
            }
1563
        }
1564
 
1565
        return $sql;
1566
    }
1567
 
1568
    /**
1569
     * Deep clone of all expression objects in the SQL parts.
1570
     *
1571
     * @return void
1572
     */
1573
    public function __clone()
1574
    {
1575
        foreach ($this->sqlParts as $part => $elements) {
1576
            if (is_array($this->sqlParts[$part])) {
1577
                foreach ($this->sqlParts[$part] as $idx => $element) {
1578
                    if (! is_object($element)) {
1579
                        continue;
1580
                    }
1581
 
1582
                    $this->sqlParts[$part][$idx] = clone $element;
1583
                }
1584
            } elseif (is_object($elements)) {
1585
                $this->sqlParts[$part] = clone $elements;
1586
            }
1587
        }
1588
 
1589
        foreach ($this->params as $name => $param) {
1590
            if (! is_object($param)) {
1591
                continue;
1592
            }
1593
 
1594
            $this->params[$name] = clone $param;
1595
        }
1596
    }
1597
 
1598
    /**
1599
     * Enables caching of the results of this query, for given amount of seconds
1600
     * and optionally specified witch key to use for the cache entry.
1601
     *
1602
     * @return $this
1603
     */
1604
    public function enableResultCache(QueryCacheProfile $cacheProfile): self
1605
    {
1606
        $this->resultCacheProfile = $cacheProfile;
1607
 
1608
        return $this;
1609
    }
1610
 
1611
    /**
1612
     * Disables caching of the results of this query.
1613
     *
1614
     * @return $this
1615
     */
1616
    public function disableResultCache(): self
1617
    {
1618
        $this->resultCacheProfile = null;
1619
 
1620
        return $this;
1621
    }
1622
}