QueryBuilder.php 39.3 KB
Newer Older
1 2 3 4
<?php

namespace Doctrine\DBAL\Query;

5
use Doctrine\DBAL\Connection;
6
use Doctrine\DBAL\Driver\ResultStatement;
7
use Doctrine\DBAL\ParameterType;
Benjamin Morel's avatar
Benjamin Morel committed
8
use Doctrine\DBAL\Query\Expression\CompositeExpression;
9
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
10

11 12 13 14 15 16 17 18 19 20 21
use function array_key_exists;
use function array_keys;
use function array_unshift;
use function func_get_args;
use function func_num_args;
use function implode;
use function is_array;
use function is_object;
use function key;
use function strtoupper;
use function substr;
22 23 24

/**
 * QueryBuilder class is responsible to dynamically create SQL queries.
25
 *
26 27
 * Important: Verify that every feature you use will work with your database vendor.
 * SQL Query Builder does not attempt to validate the generated SQL at all.
28
 *
29 30 31
 * The query builder does no validation whatsoever if certain features even work with the
 * underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements
 * even if some vendors such as MySQL support it.
32 33 34
 */
class QueryBuilder
{
Benjamin Morel's avatar
Benjamin Morel committed
35 36 37
    /*
     * The query types.
     */
38 39 40 41
    public const SELECT = 0;
    public const DELETE = 1;
    public const UPDATE = 2;
    public const INSERT = 3;
42

Benjamin Morel's avatar
Benjamin Morel committed
43 44 45
    /*
     * The builder states.
     */
46 47
    public const STATE_DIRTY = 0;
    public const STATE_CLEAN = 1;
48 49

    /**
Benjamin Morel's avatar
Benjamin Morel committed
50 51
     * The DBAL Connection.
     *
52
     * @var Connection
53
     */
Benjamin Morel's avatar
Benjamin Morel committed
54
    private $connection;
55

56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
    /*
     * The default values of SQL parts collection
     */
    private const SQL_PARTS_DEFAULTS = [
        'select'   => [],
        'distinct' => false,
        'from'     => [],
        'join'     => [],
        'set'      => [],
        'where'    => null,
        'groupBy'  => [],
        'having'   => null,
        'orderBy'  => [],
        'values'   => [],
    ];

72 73 74 75 76
    /**
     * The array of SQL parts collected.
     *
     * @var mixed[]
     */
77
    private $sqlParts = self::SQL_PARTS_DEFAULTS;
78 79

    /**
Benjamin Morel's avatar
Benjamin Morel committed
80 81 82
     * The complete SQL string for this query.
     *
     * @var string
83 84 85 86
     */
    private $sql;

    /**
Benjamin Morel's avatar
Benjamin Morel committed
87 88
     * The query parameters.
     *
89
     * @var mixed[]
90
     */
91
    private $params = [];
92 93

    /**
Benjamin Morel's avatar
Benjamin Morel committed
94 95
     * The parameter type map of this query.
     *
96
     * @var int[]|string[]
97
     */
98
    private $paramTypes = [];
99 100

    /**
Benjamin Morel's avatar
Benjamin Morel committed
101 102
     * The type of query this is. Can be select, update or delete.
     *
103
     * @var int
104 105 106 107
     */
    private $type = self::SELECT;

    /**
Benjamin Morel's avatar
Benjamin Morel committed
108 109
     * The state of the query object. Can be dirty or clean.
     *
110
     * @var int
111 112 113 114
     */
    private $state = self::STATE_CLEAN;

    /**
Benjamin Morel's avatar
Benjamin Morel committed
115 116
     * The index of the first result to retrieve.
     *
117
     * @var int
118 119 120 121
     */
    private $firstResult = null;

    /**
122
     * The maximum number of results to retrieve or NULL to retrieve all results.
Benjamin Morel's avatar
Benjamin Morel committed
123
     *
124
     * @var int|null
125 126
     */
    private $maxResults = null;
127

128
    /**
Benjamin Morel's avatar
Benjamin Morel committed
129
     * The counter of bound parameters used with {@see bindValue).
130
     *
131
     * @var int
132 133
     */
    private $boundCounter = 0;
134 135 136 137

    /**
     * Initializes a new <tt>QueryBuilder</tt>.
     *
138
     * @param Connection $connection The DBAL Connection.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
     */
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    /**
     * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
     * This producer method is intended for convenient inline usage. Example:
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u')
     *         ->from('users', 'u')
     *         ->where($qb->expr()->eq('u.id', 1));
     * </code>
     *
     * For more complex expression construction, consider storing the expression
     * builder object in a local variable.
     *
159
     * @return ExpressionBuilder
160 161 162 163 164 165 166
     */
    public function expr()
    {
        return $this->connection->getExpressionBuilder();
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
167
     * Gets the type of the currently built query.
168
     *
169
     * @return int
170 171 172 173 174 175 176
     */
    public function getType()
    {
        return $this->type;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
177
     * Gets the associated DBAL Connection for this query builder.
178
     *
179
     * @return Connection
180 181 182 183 184 185 186
     */
    public function getConnection()
    {
        return $this->connection;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
187
     * Gets the state of this query builder instance.
188
     *
189
     * @return int Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
190 191 192 193 194
     */
    public function getState()
    {
        return $this->state;
    }
195

196
    /**
Benjamin Morel's avatar
Benjamin Morel committed
197
     * Executes this query using the bound parameters and their types.
198
     *
199
     * @return ResultStatement|int
200 201 202
     */
    public function execute()
    {
203
        if ($this->type === self::SELECT) {
204 205
            return $this->connection->executeQuery($this->getSQL(), $this->params, $this->paramTypes);
        }
Gabriel Caruso's avatar
Gabriel Caruso committed
206

207
        return $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes);
208
    }
209 210

    /**
Benjamin Morel's avatar
Benjamin Morel committed
211
     * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
212 213 214 215 216
     *
     * <code>
     *     $qb = $em->createQueryBuilder()
     *         ->select('u')
     *         ->from('User', 'u')
217
     *     echo $qb->getSQL(); // SELECT u FROM User u
218 219
     * </code>
     *
Benjamin Morel's avatar
Benjamin Morel committed
220
     * @return string The SQL query string.
221 222 223 224 225 226 227 228
     */
    public function getSQL()
    {
        if ($this->sql !== null && $this->state === self::STATE_CLEAN) {
            return $this->sql;
        }

        switch ($this->type) {
Steve Müller's avatar
Steve Müller committed
229 230 231
            case self::INSERT:
                $sql = $this->getSQLForInsert();
                break;
Sergei Morozov's avatar
Sergei Morozov committed
232

233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
            case self::DELETE:
                $sql = $this->getSQLForDelete();
                break;

            case self::UPDATE:
                $sql = $this->getSQLForUpdate();
                break;

            case self::SELECT:
            default:
                $sql = $this->getSQLForSelect();
                break;
        }

        $this->state = self::STATE_CLEAN;
248
        $this->sql   = $sql;
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263

        return $sql;
    }

    /**
     * Sets a query parameter for the query being constructed.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u')
     *         ->from('users', 'u')
     *         ->where('u.id = :user_id')
     *         ->setParameter(':user_id', 1);
     * </code>
     *
264 265
     * @param string|int      $key   The parameter position or name.
     * @param mixed           $value The parameter value.
266
     * @param string|int|null $type  One of the {@link ParameterType} constants.
Benjamin Morel's avatar
Benjamin Morel committed
267
     *
268
     * @return $this This QueryBuilder instance.
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
     */
    public function setParameter($key, $value, $type = null)
    {
        if ($type !== null) {
            $this->paramTypes[$key] = $type;
        }

        $this->params[$key] = $value;

        return $this;
    }

    /**
     * Sets a collection of query parameters for the query being constructed.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u')
     *         ->from('users', 'u')
     *         ->where('u.id = :user_id1 OR u.id = :user_id2')
     *         ->setParameters(array(
     *             ':user_id1' => 1,
     *             ':user_id2' => 2
     *         ));
     * </code>
     *
295 296
     * @param mixed[]        $params The query parameters to set.
     * @param int[]|string[] $types  The query parameters types to set.
Benjamin Morel's avatar
Benjamin Morel committed
297
     *
298
     * @return $this This QueryBuilder instance.
299
     */
300
    public function setParameters(array $params, array $types = [])
301 302
    {
        $this->paramTypes = $types;
303
        $this->params     = $params;
304 305 306 307 308

        return $this;
    }

    /**
309
     * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
310
     *
311
     * @return mixed[] The currently defined query parameters indexed by parameter index or name.
312 313 314 315 316 317 318 319 320 321
     */
    public function getParameters()
    {
        return $this->params;
    }

    /**
     * Gets a (previously set) query parameter of the query being constructed.
     *
     * @param mixed $key The key (index or name) of the bound parameter.
Benjamin Morel's avatar
Benjamin Morel committed
322
     *
323 324 325 326
     * @return mixed The value of the bound parameter.
     */
    public function getParameter($key)
    {
327
        return $this->params[$key] ?? null;
328 329
    }

330 331 332
    /**
     * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
     *
333
     * @return int[]|string[] The currently defined query parameter types indexed by parameter index or name.
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
     */
    public function getParameterTypes()
    {
        return $this->paramTypes;
    }

    /**
     * Gets a (previously set) query parameter type of the query being constructed.
     *
     * @param mixed $key The key (index or name) of the bound parameter type.
     *
     * @return mixed The value of the bound parameter type.
     */
    public function getParameterType($key)
    {
349
        return $this->paramTypes[$key] ?? null;
350 351
    }

352 353 354
    /**
     * Sets the position of the first result to retrieve (the "offset").
     *
355
     * @param int $firstResult The first result to return.
Benjamin Morel's avatar
Benjamin Morel committed
356
     *
357
     * @return $this This QueryBuilder instance.
358 359 360
     */
    public function setFirstResult($firstResult)
    {
361
        $this->state       = self::STATE_DIRTY;
362
        $this->firstResult = $firstResult;
Benjamin Morel's avatar
Benjamin Morel committed
363

364 365 366 367 368 369
        return $this;
    }

    /**
     * Gets the position of the first result the query object was set to retrieve (the "offset").
     *
370
     * @return int The position of the first result.
371 372 373 374 375 376 377 378 379
     */
    public function getFirstResult()
    {
        return $this->firstResult;
    }

    /**
     * Sets the maximum number of results to retrieve (the "limit").
     *
380
     * @param int|null $maxResults The maximum number of results to retrieve or NULL to retrieve all results.
Benjamin Morel's avatar
Benjamin Morel committed
381
     *
382
     * @return $this This QueryBuilder instance.
383 384 385
     */
    public function setMaxResults($maxResults)
    {
386
        $this->state      = self::STATE_DIRTY;
387
        $this->maxResults = $maxResults;
Benjamin Morel's avatar
Benjamin Morel committed
388

389 390 391 392 393
        return $this;
    }

    /**
     * Gets the maximum number of results the query object was set to retrieve (the "limit").
394
     * Returns NULL if all results will be returned.
395
     *
396
     * @return int|null The maximum number of results.
397 398 399 400 401 402 403 404 405 406 407 408
     */
    public function getMaxResults()
    {
        return $this->maxResults;
    }

    /**
     * Either appends to or replaces a single, generic query part.
     *
     * The available parts are: 'select', 'from', 'set', 'where',
     * 'groupBy', 'having' and 'orderBy'.
     *
409
     * @param string $sqlPartName
Sergei Morozov's avatar
Sergei Morozov committed
410
     * @param mixed  $sqlPart
411
     * @param bool   $append
Benjamin Morel's avatar
Benjamin Morel committed
412
     *
413
     * @return $this This QueryBuilder instance.
414 415 416
     */
    public function add($sqlPartName, $sqlPart, $append = false)
    {
417
        $isArray    = is_array($sqlPart);
418 419
        $isMultiple = is_array($this->sqlParts[$sqlPartName]);

420
        if ($isMultiple && ! $isArray) {
421
            $sqlPart = [$sqlPart];
422 423 424 425 426
        }

        $this->state = self::STATE_DIRTY;

        if ($append) {
Sergei Morozov's avatar
Sergei Morozov committed
427 428 429 430 431 432
            if (
                $sqlPartName === 'orderBy'
                || $sqlPartName === 'groupBy'
                || $sqlPartName === 'select'
                || $sqlPartName === 'set'
            ) {
433
                foreach ($sqlPart as $part) {
434 435
                    $this->sqlParts[$sqlPartName][] = $part;
                }
Steve Müller's avatar
Steve Müller committed
436
            } elseif ($isArray && is_array($sqlPart[key($sqlPart)])) {
437
                $key                                  = key($sqlPart);
438
                $this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key];
Steve Müller's avatar
Steve Müller committed
439
            } elseif ($isMultiple) {
440 441 442 443 444 445 446 447 448
                $this->sqlParts[$sqlPartName][] = $sqlPart;
            } else {
                $this->sqlParts[$sqlPartName] = $sqlPart;
            }

            return $this;
        }

        $this->sqlParts[$sqlPartName] = $sqlPart;
449

450 451 452 453 454 455 456
        return $this;
    }

    /**
     * Specifies an item that is to be returned in the query result.
     * Replaces any previously specified selections, if any.
     *
457 458
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
     *
459 460 461 462 463 464 465
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.id', 'p.id')
     *         ->from('users', 'u')
     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
     * </code>
     *
466 467
     * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED.
     *                                     Pass each value as an individual argument.
Benjamin Morel's avatar
Benjamin Morel committed
468
     *
469
     * @return $this This QueryBuilder instance.
470
     */
471
    public function select($select = null/*, string ...$selects*/)
472 473 474 475 476 477 478 479 480
    {
        $this->type = self::SELECT;

        if (empty($select)) {
            return $this;
        }

        $selects = is_array($select) ? $select : func_get_args();

481
        return $this->add('select', $selects);
482 483
    }

484 485 486 487 488 489 490 491 492 493 494 495
    /**
     * Adds DISTINCT to the query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.id')
     *         ->distinct()
     *         ->from('users', 'u')
     * </code>
     *
     * @return $this This QueryBuilder instance.
     */
496
    public function distinct(): self
497 498 499 500 501 502
    {
        $this->sqlParts['distinct'] = true;

        return $this;
    }

503 504 505
    /**
     * Adds an item that is to be returned in the query result.
     *
506 507
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
     *
508 509 510 511 512 513 514 515
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.id')
     *         ->addSelect('p.id')
     *         ->from('users', 'u')
     *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
     * </code>
     *
516 517
     * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED.
     *                                     Pass each value as an individual argument.
Benjamin Morel's avatar
Benjamin Morel committed
518
     *
519
     * @return $this This QueryBuilder instance.
520
     */
521
    public function addSelect($select = null/*, string ...$selects*/)
522 523 524 525 526 527 528 529 530 531 532 533 534 535
    {
        $this->type = self::SELECT;

        if (empty($select)) {
            return $this;
        }

        $selects = is_array($select) ? $select : func_get_args();

        return $this->add('select', $selects, true);
    }

    /**
     * Turns the query being built into a bulk delete query that ranges over
536
     * a certain table.
537 538 539 540
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->delete('users', 'u')
541
     *         ->where('u.id = :user_id')
542 543 544
     *         ->setParameter(':user_id', 1);
     * </code>
     *
545
     * @param string $delete The table whose rows are subject to the deletion.
Benjamin Morel's avatar
Benjamin Morel committed
546 547
     * @param string $alias  The table alias used in the constructed query.
     *
548
     * @return $this This QueryBuilder instance.
549 550 551 552 553
     */
    public function delete($delete = null, $alias = null)
    {
        $this->type = self::DELETE;

554
        if (! $delete) {
555 556 557
            return $this;
        }

558
        return $this->add('from', [
559
            'table' => $delete,
560
            'alias' => $alias,
561
        ]);
562 563 564 565
    }

    /**
     * Turns the query being built into a bulk update query that ranges over
566
     * a certain table
567 568 569
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
570 571 572
     *         ->update('counters', 'c')
     *         ->set('c.value', 'c.value + 1')
     *         ->where('c.id = ?');
573 574
     * </code>
     *
575
     * @param string $update The table whose rows are subject to the update.
Benjamin Morel's avatar
Benjamin Morel committed
576 577
     * @param string $alias  The table alias used in the constructed query.
     *
578
     * @return $this This QueryBuilder instance.
579 580 581 582 583
     */
    public function update($update = null, $alias = null)
    {
        $this->type = self::UPDATE;

584
        if (! $update) {
585 586 587
            return $this;
        }

588
        return $this->add('from', [
589
            'table' => $update,
590
            'alias' => $alias,
591
        ]);
592 593
    }

Steve Müller's avatar
Steve Müller committed
594 595 596 597 598 599 600 601 602
    /**
     * Turns the query being built into an insert query that inserts into
     * a certain table
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->insert('users')
     *         ->values(
     *             array(
Steve Müller's avatar
Steve Müller committed
603 604
     *                 'name' => '?',
     *                 'password' => '?'
Steve Müller's avatar
Steve Müller committed
605 606 607 608 609 610
     *             )
     *         );
     * </code>
     *
     * @param string $insert The table into which the rows should be inserted.
     *
611
     * @return $this This QueryBuilder instance.
Steve Müller's avatar
Steve Müller committed
612 613 614 615 616
     */
    public function insert($insert = null)
    {
        $this->type = self::INSERT;

617
        if (! $insert) {
Steve Müller's avatar
Steve Müller committed
618 619 620
            return $this;
        }

621
        return $this->add('from', ['table' => $insert]);
Steve Müller's avatar
Steve Müller committed
622 623
    }

624
    /**
Benjamin Morel's avatar
Benjamin Morel committed
625
     * Creates and adds a query root corresponding to the table identified by the
626 627 628 629 630 631 632 633
     * given alias, forming a cartesian product with any existing query roots.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.id')
     *         ->from('users', 'u')
     * </code>
     *
634 635
     * @param string      $from  The table.
     * @param string|null $alias The alias of the table.
Benjamin Morel's avatar
Benjamin Morel committed
636
     *
637
     * @return $this This QueryBuilder instance.
638
     */
639
    public function from($from, $alias = null)
640
    {
641
        return $this->add('from', [
642
            'table' => $from,
643
            'alias' => $alias,
644
        ], true);
645 646 647
    }

    /**
648
     * Creates and adds a join to the query.
649 650 651 652 653 654 655 656
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
     * </code>
     *
Benjamin Morel's avatar
Benjamin Morel committed
657 658 659 660 661
     * @param string $fromAlias The alias that points to a from clause.
     * @param string $join      The table name to join.
     * @param string $alias     The alias of the join table.
     * @param string $condition The condition for the join.
     *
662
     * @return $this This QueryBuilder instance.
663 664 665 666 667 668 669
     */
    public function join($fromAlias, $join, $alias, $condition = null)
    {
        return $this->innerJoin($fromAlias, $join, $alias, $condition);
    }

    /**
670
     * Creates and adds a join to the query.
671 672 673 674 675 676 677 678
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
     * </code>
     *
Benjamin Morel's avatar
Benjamin Morel committed
679 680 681 682 683
     * @param string $fromAlias The alias that points to a from clause.
     * @param string $join      The table name to join.
     * @param string $alias     The alias of the join table.
     * @param string $condition The condition for the join.
     *
684
     * @return $this This QueryBuilder instance.
685 686 687
     */
    public function innerJoin($fromAlias, $join, $alias, $condition = null)
    {
688 689
        return $this->add('join', [
            $fromAlias => [
690 691 692
                'joinType'      => 'inner',
                'joinTable'     => $join,
                'joinAlias'     => $alias,
693 694
                'joinCondition' => $condition,
            ],
695
        ], true);
696 697 698
    }

    /**
699
     * Creates and adds a left join to the query.
700 701 702 703 704 705 706 707
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
     * </code>
     *
Benjamin Morel's avatar
Benjamin Morel committed
708 709 710 711 712
     * @param string $fromAlias The alias that points to a from clause.
     * @param string $join      The table name to join.
     * @param string $alias     The alias of the join table.
     * @param string $condition The condition for the join.
     *
713
     * @return $this This QueryBuilder instance.
714 715 716
     */
    public function leftJoin($fromAlias, $join, $alias, $condition = null)
    {
717 718
        return $this->add('join', [
            $fromAlias => [
719 720 721
                'joinType'      => 'left',
                'joinTable'     => $join,
                'joinAlias'     => $alias,
722 723
                'joinCondition' => $condition,
            ],
724
        ], true);
725
    }
726

727 728 729 730 731 732 733 734 735 736
    /**
     * Creates and adds a right join to the query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
     * </code>
     *
Benjamin Morel's avatar
Benjamin Morel committed
737 738 739 740 741
     * @param string $fromAlias The alias that points to a from clause.
     * @param string $join      The table name to join.
     * @param string $alias     The alias of the join table.
     * @param string $condition The condition for the join.
     *
742
     * @return $this This QueryBuilder instance.
743 744 745
     */
    public function rightJoin($fromAlias, $join, $alias, $condition = null)
    {
746 747
        return $this->add('join', [
            $fromAlias => [
748 749 750
                'joinType'      => 'right',
                'joinTable'     => $join,
                'joinAlias'     => $alias,
751 752
                'joinCondition' => $condition,
            ],
753
        ], true);
754
    }
755 756

    /**
757
     * Sets a new value for a column in a bulk update query.
758 759 760
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
761 762 763
     *         ->update('counters', 'c')
     *         ->set('c.value', 'c.value + 1')
     *         ->where('c.id = ?');
764 765
     * </code>
     *
Benjamin Morel's avatar
Benjamin Morel committed
766
     * @param string $key   The column to set.
767
     * @param string $value The value, expression, placeholder, etc.
Benjamin Morel's avatar
Benjamin Morel committed
768
     *
769
     * @return $this This QueryBuilder instance.
770 771 772
     */
    public function set($key, $value)
    {
773
        return $this->add('set', $key . ' = ' . $value, true);
774 775 776 777 778 779 780 781
    }

    /**
     * Specifies one or more restrictions to the query result.
     * Replaces any previously specified restrictions, if any.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
782 783 784
     *         ->select('c.value')
     *         ->from('counters', 'c')
     *         ->where('c.id = ?');
785 786 787 788 789
     *
     *     // You can optionally programatically build and/or expressions
     *     $qb = $conn->createQueryBuilder();
     *
     *     $or = $qb->expr()->orx();
790 791
     *     $or->add($qb->expr()->eq('c.id', 1));
     *     $or->add($qb->expr()->eq('c.id', 2));
792
     *
793 794
     *     $qb->update('counters', 'c')
     *         ->set('c.value', 'c.value + 1')
795 796 797 798
     *         ->where($or);
     * </code>
     *
     * @param mixed $predicates The restriction predicates.
Benjamin Morel's avatar
Benjamin Morel committed
799
     *
800
     * @return $this This QueryBuilder instance.
801 802 803
     */
    public function where($predicates)
    {
804
        if (! (func_num_args() === 1 && $predicates instanceof CompositeExpression)) {
805
            $predicates = CompositeExpression::and(...func_get_args());
806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822
        }

        return $this->add('where', $predicates);
    }

    /**
     * Adds one or more restrictions to the query results, forming a logical
     * conjunction with any previously specified restrictions.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u')
     *         ->from('users', 'u')
     *         ->where('u.username LIKE ?')
     *         ->andWhere('u.is_active = 1');
     * </code>
     *
823 824
     * @see where()
     *
825
     * @param mixed $where The query restrictions.
Benjamin Morel's avatar
Benjamin Morel committed
826
     *
827
     * @return $this This QueryBuilder instance.
828 829 830
     */
    public function andWhere($where)
    {
831
        $args  = func_get_args();
832
        $where = $this->getQueryPart('where');
833 834

        if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_AND) {
835
            $where = $where->with(...$args);
836 837
        } else {
            array_unshift($args, $where);
838
            $where = CompositeExpression::and(...$args);
839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855
        }

        return $this->add('where', $where, true);
    }

    /**
     * Adds one or more restrictions to the query results, forming a logical
     * disjunction with any previously specified restrictions.
     *
     * <code>
     *     $qb = $em->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->where('u.id = 1')
     *         ->orWhere('u.id = 2');
     * </code>
     *
856 857
     * @see where()
     *
Benjamin Morel's avatar
Benjamin Morel committed
858 859
     * @param mixed $where The WHERE statement.
     *
860
     * @return $this This QueryBuilder instance.
861 862 863
     */
    public function orWhere($where)
    {
864
        $args  = func_get_args();
865
        $where = $this->getQueryPart('where');
866 867

        if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_OR) {
868
            $where = $where->with(...$args);
869 870
        } else {
            array_unshift($args, $where);
871
            $where = CompositeExpression::or(...$args);
872 873 874 875 876 877 878 879 880
        }

        return $this->add('where', $where, true);
    }

    /**
     * Specifies a grouping over the results of the query.
     * Replaces any previously specified groupings, if any.
     *
881 882
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
     *
883 884 885 886 887 888 889
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->groupBy('u.id');
     * </code>
     *
890 891
     * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED.
     *                                 Pass each value as an individual argument.
Benjamin Morel's avatar
Benjamin Morel committed
892
     *
893
     * @return $this This QueryBuilder instance.
894
     */
895
    public function groupBy($groupBy/*, string ...$groupBys*/)
896 897 898 899 900 901 902 903 904 905 906 907 908
    {
        if (empty($groupBy)) {
            return $this;
        }

        $groupBy = is_array($groupBy) ? $groupBy : func_get_args();

        return $this->add('groupBy', $groupBy, false);
    }

    /**
     * Adds a grouping expression to the query.
     *
909 910
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
     *
911 912 913 914
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
915 916
     *         ->groupBy('u.lastLogin')
     *         ->addGroupBy('u.createdAt');
917 918
     * </code>
     *
919 920
     * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED.
     *                                 Pass each value as an individual argument.
Benjamin Morel's avatar
Benjamin Morel committed
921
     *
922
     * @return $this This QueryBuilder instance.
923
     */
924
    public function addGroupBy($groupBy/*, string ...$groupBys*/)
925 926 927 928 929 930 931 932 933 934
    {
        if (empty($groupBy)) {
            return $this;
        }

        $groupBy = is_array($groupBy) ? $groupBy : func_get_args();

        return $this->add('groupBy', $groupBy, true);
    }

Steve Müller's avatar
Steve Müller committed
935 936 937 938 939 940 941 942
    /**
     * Sets a value for a column in an insert query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->insert('users')
     *         ->values(
     *             array(
Steve Müller's avatar
Steve Müller committed
943
     *                 'name' => '?'
Steve Müller's avatar
Steve Müller committed
944 945
     *             )
     *         )
Steve Müller's avatar
Steve Müller committed
946
     *         ->setValue('password', '?');
Steve Müller's avatar
Steve Müller committed
947 948 949 950 951
     * </code>
     *
     * @param string $column The column into which the value should be inserted.
     * @param string $value  The value that should be inserted into the column.
     *
952
     * @return $this This QueryBuilder instance.
Steve Müller's avatar
Steve Müller committed
953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969
     */
    public function setValue($column, $value)
    {
        $this->sqlParts['values'][$column] = $value;

        return $this;
    }

    /**
     * Specifies values for an insert query indexed by column names.
     * Replaces any previous values, if any.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->insert('users')
     *         ->values(
     *             array(
Steve Müller's avatar
Steve Müller committed
970 971
     *                 'name' => '?',
     *                 'password' => '?'
Steve Müller's avatar
Steve Müller committed
972 973 974 975
     *             )
     *         );
     * </code>
     *
976
     * @param mixed[] $values The values to specify for the insert query indexed by column names.
Steve Müller's avatar
Steve Müller committed
977
     *
978
     * @return $this This QueryBuilder instance.
Steve Müller's avatar
Steve Müller committed
979 980 981 982 983 984
     */
    public function values(array $values)
    {
        return $this->add('values', $values);
    }

985 986 987 988 989
    /**
     * Specifies a restriction over the groups of the query.
     * Replaces any previous having restrictions, if any.
     *
     * @param mixed $having The restriction over the groups.
Benjamin Morel's avatar
Benjamin Morel committed
990
     *
991
     * @return $this This QueryBuilder instance.
992 993 994
     */
    public function having($having)
    {
995
        if (! (func_num_args() === 1 && $having instanceof CompositeExpression)) {
996
            $having = CompositeExpression::and(...func_get_args());
997 998 999 1000 1001 1002 1003 1004 1005 1006
        }

        return $this->add('having', $having);
    }

    /**
     * Adds a restriction over the groups of the query, forming a logical
     * conjunction with any existing having restrictions.
     *
     * @param mixed $having The restriction to append.
Benjamin Morel's avatar
Benjamin Morel committed
1007
     *
1008
     * @return $this This QueryBuilder instance.
1009 1010 1011
     */
    public function andHaving($having)
    {
1012
        $args   = func_get_args();
1013
        $having = $this->getQueryPart('having');
1014

1015
        if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_AND) {
1016
            $having = $having->with(...$args);
1017 1018
        } else {
            array_unshift($args, $having);
1019
            $having = CompositeExpression::and(...$args);
1020 1021 1022 1023 1024 1025 1026 1027 1028 1029
        }

        return $this->add('having', $having);
    }

    /**
     * Adds a restriction over the groups of the query, forming a logical
     * disjunction with any existing having restrictions.
     *
     * @param mixed $having The restriction to add.
Benjamin Morel's avatar
Benjamin Morel committed
1030
     *
1031
     * @return $this This QueryBuilder instance.
1032 1033 1034
     */
    public function orHaving($having)
    {
1035
        $args   = func_get_args();
1036
        $having = $this->getQueryPart('having');
1037

1038
        if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_OR) {
1039
            $having = $having->with(...$args);
1040 1041
        } else {
            array_unshift($args, $having);
1042
            $having = CompositeExpression::or(...$args);
1043 1044 1045 1046 1047 1048 1049 1050 1051
        }

        return $this->add('having', $having);
    }

    /**
     * Specifies an ordering for the query results.
     * Replaces any previously specified orderings, if any.
     *
Benjamin Morel's avatar
Benjamin Morel committed
1052
     * @param string $sort  The ordering expression.
1053
     * @param string $order The ordering direction.
Benjamin Morel's avatar
Benjamin Morel committed
1054
     *
1055
     * @return $this This QueryBuilder instance.
1056 1057 1058
     */
    public function orderBy($sort, $order = null)
    {
1059
        return $this->add('orderBy', $sort . ' ' . (! $order ? 'ASC' : $order), false);
1060 1061 1062 1063 1064
    }

    /**
     * Adds an ordering to the query results.
     *
Benjamin Morel's avatar
Benjamin Morel committed
1065
     * @param string $sort  The ordering expression.
1066
     * @param string $order The ordering direction.
Benjamin Morel's avatar
Benjamin Morel committed
1067
     *
1068
     * @return $this This QueryBuilder instance.
1069 1070 1071
     */
    public function addOrderBy($sort, $order = null)
    {
1072
        return $this->add('orderBy', $sort . ' ' . (! $order ? 'ASC' : $order), true);
1073 1074 1075
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1076
     * Gets a query part by its name.
1077 1078
     *
     * @param string $queryPartName
Benjamin Morel's avatar
Benjamin Morel committed
1079 1080
     *
     * @return mixed
1081 1082 1083 1084 1085 1086 1087
     */
    public function getQueryPart($queryPartName)
    {
        return $this->sqlParts[$queryPartName];
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1088
     * Gets all query parts.
1089
     *
1090
     * @return mixed[]
1091 1092 1093 1094 1095 1096 1097
     */
    public function getQueryParts()
    {
        return $this->sqlParts;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1098
     * Resets SQL parts.
1099
     *
1100
     * @param string[]|null $queryPartNames
Benjamin Morel's avatar
Benjamin Morel committed
1101
     *
1102
     * @return $this This QueryBuilder instance.
1103 1104 1105
     */
    public function resetQueryParts($queryPartNames = null)
    {
1106
        if ($queryPartNames === null) {
1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117
            $queryPartNames = array_keys($this->sqlParts);
        }

        foreach ($queryPartNames as $queryPartName) {
            $this->resetQueryPart($queryPartName);
        }

        return $this;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1118
     * Resets a single SQL part.
1119 1120
     *
     * @param string $queryPartName
Benjamin Morel's avatar
Benjamin Morel committed
1121
     *
1122
     * @return $this This QueryBuilder instance.
1123 1124 1125
     */
    public function resetQueryPart($queryPartName)
    {
1126
        $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName];
1127 1128 1129 1130 1131

        $this->state = self::STATE_DIRTY;

        return $this;
    }
1132

Benjamin Morel's avatar
Benjamin Morel committed
1133 1134 1135
    /**
     * @return string
     *
1136
     * @throws QueryException
Benjamin Morel's avatar
Benjamin Morel committed
1137
     */
1138 1139
    private function getSQLForSelect()
    {
1140 1141
        $query = 'SELECT ' . ($this->sqlParts['distinct'] ? 'DISTINCT ' : '') .
                  implode(', ', $this->sqlParts['select']);
1142

1143
        $query .= ($this->sqlParts['from'] ? ' FROM ' . implode(', ', $this->getFromClauses()) : '')
jeroendedauw's avatar
jeroendedauw committed
1144 1145 1146 1147 1148
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '')
            . ($this->sqlParts['groupBy'] ? ' GROUP BY ' . implode(', ', $this->sqlParts['groupBy']) : '')
            . ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '')
            . ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : '');

1149 1150 1151 1152 1153 1154 1155 1156 1157
        if ($this->isLimitQuery()) {
            return $this->connection->getDatabasePlatform()->modifyLimitQuery(
                $query,
                $this->maxResults,
                $this->firstResult
            );
        }

        return $query;
jeroendedauw's avatar
jeroendedauw committed
1158 1159
    }

1160 1161 1162
    /**
     * @return string[]
     */
jeroendedauw's avatar
jeroendedauw committed
1163 1164
    private function getFromClauses()
    {
1165
        $fromClauses  = [];
1166
        $knownAliases = [];
Deni's avatar
Deni committed
1167

1168 1169
        // Loop through all FROM clauses
        foreach ($this->sqlParts['from'] as $from) {
1170
            if ($from['alias'] === null) {
1171
                $tableSql       = $from['table'];
1172
                $tableReference = $from['table'];
1173
            } else {
1174
                $tableSql       = $from['table'] . ' ' . $from['alias'];
1175 1176
                $tableReference = $from['alias'];
            }
jeroendedauw's avatar
jeroendedauw committed
1177

1178
            $knownAliases[$tableReference] = true;
Deni's avatar
Deni committed
1179

1180
            $fromClauses[$tableReference] = $tableSql . $this->getSQLForJoins($tableReference, $knownAliases);
1181
        }
1182

jeroendedauw's avatar
jeroendedauw committed
1183
        $this->verifyAllAliasesAreKnown($knownAliases);
jeroendedauw's avatar
jeroendedauw committed
1184 1185 1186 1187

        return $fromClauses;
    }

1188
    /**
1189
     * @param string[] $knownAliases
1190 1191 1192
     *
     * @throws QueryException
     */
1193
    private function verifyAllAliasesAreKnown(array $knownAliases): void
jeroendedauw's avatar
jeroendedauw committed
1194
    {
Deni's avatar
Deni committed
1195
        foreach ($this->sqlParts['join'] as $fromAlias => $joins) {
1196
            if (! isset($knownAliases[$fromAlias])) {
Deni's avatar
Deni committed
1197 1198 1199
                throw QueryException::unknownAlias($fromAlias, array_keys($knownAliases));
            }
        }
1200
    }
1201

1202 1203 1204
    /**
     * @return bool
     */
1205 1206
    private function isLimitQuery()
    {
1207
        return $this->maxResults !== null || $this->firstResult !== null;
1208 1209
    }

Steve Müller's avatar
Steve Müller committed
1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221
    /**
     * Converts this instance into an INSERT string in SQL.
     *
     * @return string
     */
    private function getSQLForInsert()
    {
        return 'INSERT INTO ' . $this->sqlParts['from']['table'] .
        ' (' . implode(', ', array_keys($this->sqlParts['values'])) . ')' .
        ' VALUES(' . implode(', ', $this->sqlParts['values']) . ')';
    }

1222 1223
    /**
     * Converts this instance into an UPDATE string in SQL.
1224
     *
1225 1226 1227 1228
     * @return string
     */
    private function getSQLForUpdate()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1229 1230
        $table = $this->sqlParts['from']['table']
            . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');
1231

1232 1233
        return 'UPDATE ' . $table
            . ' SET ' . implode(', ', $this->sqlParts['set'])
Steve Müller's avatar
Steve Müller committed
1234
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
1235
    }
1236

1237 1238
    /**
     * Converts this instance into a DELETE string in SQL.
1239
     *
1240 1241 1242 1243
     * @return string
     */
    private function getSQLForDelete()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1244 1245
        $table = $this->sqlParts['from']['table']
            . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');
1246

Sergei Morozov's avatar
Sergei Morozov committed
1247 1248
        return 'DELETE FROM ' . $table
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260
    }

    /**
     * Gets a string representation of this QueryBuilder which corresponds to
     * the final SQL query being constructed.
     *
     * @return string The string representation of this QueryBuilder.
     */
    public function __toString()
    {
        return $this->getSQL();
    }
1261

1262
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1263
     * Creates a new named parameter and bind the value $value to it.
1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282
     *
     * This method provides a shortcut for PDOStatement::bindValue
     * when using prepared statements.
     *
     * The parameter $value specifies the value that you want to bind. If
     * $placeholder is not provided bindValue() will automatically create a
     * placeholder for you. An automatic placeholder will be of the name
     * ':dcValue1', ':dcValue2' etc.
     *
     * For more information see {@link http://php.net/pdostatement-bindparam}
     *
     * Example:
     * <code>
     * $value = 2;
     * $q->eq( 'id', $q->bindValue( $value ) );
     * $stmt = $q->executeQuery(); // executed with 'id = 2'
     * </code>
     *
     * @link http://www.zetacomponents.org
Benjamin Morel's avatar
Benjamin Morel committed
1283 1284 1285 1286 1287
     *
     * @param mixed  $value
     * @param mixed  $type
     * @param string $placeHolder The name to bind with. The string must start with a colon ':'.
     *
1288 1289
     * @return string the placeholder name used.
     */
1290
    public function createNamedParameter($value, $type = ParameterType::STRING, $placeHolder = null)
1291
    {
Steve Müller's avatar
Steve Müller committed
1292
        if ($placeHolder === null) {
1293
            $this->boundCounter++;
1294
            $placeHolder = ':dcValue' . $this->boundCounter;
1295
        }
Grégoire Paris's avatar
Grégoire Paris committed
1296

1297 1298 1299 1300
        $this->setParameter(substr($placeHolder, 1), $value, $type);

        return $placeHolder;
    }
1301

1302
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1303
     * Creates a new positional parameter and bind the given value to it.
1304
     *
1305 1306 1307 1308
     * Attention: If you are using positional parameters with the query builder you have
     * to be very careful to bind all parameters in the order they appear in the SQL
     * statement , otherwise they get bound in the wrong order which can lead to serious
     * bugs in your code.
1309
     *
1310 1311 1312 1313 1314
     * Example:
     * <code>
     *  $qb = $conn->createQueryBuilder();
     *  $qb->select('u.*')
     *     ->from('users', 'u')
1315 1316
     *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', ParameterType::STRING))
     *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', ParameterType::STRING))
1317
     * </code>
1318
     *
1319 1320
     * @param mixed $value
     * @param int   $type
Benjamin Morel's avatar
Benjamin Morel committed
1321
     *
1322 1323
     * @return string
     */
1324
    public function createPositionalParameter($value, $type = ParameterType::STRING)
1325 1326 1327
    {
        $this->boundCounter++;
        $this->setParameter($this->boundCounter, $value, $type);
1328

1329
        return '?';
1330
    }
Deni's avatar
Deni committed
1331

Benjamin Morel's avatar
Benjamin Morel committed
1332
    /**
1333 1334
     * @param string   $fromAlias
     * @param string[] $knownAliases
Benjamin Morel's avatar
Benjamin Morel committed
1335 1336
     *
     * @return string
1337 1338
     *
     * @throws QueryException
Benjamin Morel's avatar
Benjamin Morel committed
1339
     */
Deni's avatar
Deni committed
1340 1341 1342 1343 1344 1345
    private function getSQLForJoins($fromAlias, array &$knownAliases)
    {
        $sql = '';

        if (isset($this->sqlParts['join'][$fromAlias])) {
            foreach ($this->sqlParts['join'][$fromAlias] as $join) {
jarekj's avatar
jarekj committed
1346
                if (array_key_exists($join['joinAlias'], $knownAliases)) {
1347
                    throw QueryException::nonUniqueAlias($join['joinAlias'], array_keys($knownAliases));
jarekj's avatar
jarekj committed
1348
                }
Grégoire Paris's avatar
Grégoire Paris committed
1349

1350 1351 1352 1353 1354
                $sql .= ' ' . strtoupper($join['joinType'])
                    . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias'];
                if ($join['joinCondition'] !== null) {
                    $sql .= ' ON ' . $join['joinCondition'];
                }
Grégoire Paris's avatar
Grégoire Paris committed
1355

Deni's avatar
Deni committed
1356
                $knownAliases[$join['joinAlias']] = true;
1357
            }
Deni's avatar
Deni committed
1358

1359
            foreach ($this->sqlParts['join'][$fromAlias] as $join) {
Deni's avatar
Deni committed
1360 1361 1362 1363 1364 1365
                $sql .= $this->getSQLForJoins($join['joinAlias'], $knownAliases);
            }
        }

        return $sql;
    }
Paul's avatar
Paul committed
1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376

    /**
     * Deep clone of all expression objects in the SQL parts.
     *
     * @return void
     */
    public function __clone()
    {
        foreach ($this->sqlParts as $part => $elements) {
            if (is_array($this->sqlParts[$part])) {
                foreach ($this->sqlParts[$part] as $idx => $element) {
1377 1378
                    if (! is_object($element)) {
                        continue;
Paul's avatar
Paul committed
1379
                    }
1380 1381

                    $this->sqlParts[$part][$idx] = clone $element;
Paul's avatar
Paul committed
1382
                }
Steve Müller's avatar
Steve Müller committed
1383
            } elseif (is_object($elements)) {
Paul's avatar
Paul committed
1384 1385 1386 1387
                $this->sqlParts[$part] = clone $elements;
            }
        }

1388
        foreach ($this->params as $name => $param) {
1389 1390
            if (! is_object($param)) {
                continue;
Paul's avatar
Paul committed
1391
            }
1392 1393

            $this->params[$name] = clone $param;
Paul's avatar
Paul committed
1394 1395
        }
    }
1396
}