Connection.php 50.8 KB
Newer Older
1
<?php
romanb's avatar
romanb committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
Benjamin Eberlei's avatar
Benjamin Eberlei committed
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
romanb's avatar
romanb committed
18 19
 */

20
namespace Doctrine\DBAL;
romanb's avatar
romanb committed
21

22
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
23
use Doctrine\DBAL\Exception\InvalidArgumentException;
Benjamin Morel's avatar
Benjamin Morel committed
24 25 26 27 28 29 30 31 32
use Closure;
use Exception;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Cache\ResultCacheStatement;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Cache\ArrayStatement;
use Doctrine\DBAL\Cache\CacheException;
33
use Doctrine\DBAL\Driver\PingableConnection;
34
use Throwable;
35 36

/**
37
 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
38 39
 * events, transaction isolation levels, configuration, emulated transaction nesting,
 * lazy connecting and more.
40
 *
Benjamin Morel's avatar
Benjamin Morel committed
41 42 43 44 45 46 47 48
 * @link   www.doctrine-project.org
 * @since  2.0
 * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @author Jonathan Wage <jonwage@gmail.com>
 * @author Roman Borschel <roman@code-factory.org>
 * @author Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @author Lukas Smith <smith@pooteeweet.org> (MDB2 library)
 * @author Benjamin Eberlei <kontakt@beberlei.de>
49
 */
50
class Connection implements DriverConnection
51
{
52 53
    /**
     * Constant for transaction isolation level READ UNCOMMITTED.
54 55
     *
     * @deprecated Use TransactionIsolationLevel::READ_UNCOMMITTED.
56
     */
57
    public const TRANSACTION_READ_UNCOMMITTED = TransactionIsolationLevel::READ_UNCOMMITTED;
58

59 60
    /**
     * Constant for transaction isolation level READ COMMITTED.
61 62
     *
     * @deprecated Use TransactionIsolationLevel::READ_COMMITTED.
63
     */
64
    public const TRANSACTION_READ_COMMITTED = TransactionIsolationLevel::READ_COMMITTED;
65

66 67
    /**
     * Constant for transaction isolation level REPEATABLE READ.
68 69
     *
     * @deprecated Use TransactionIsolationLevel::REPEATABLE_READ.
70
     */
71
    public const TRANSACTION_REPEATABLE_READ = TransactionIsolationLevel::REPEATABLE_READ;
72

73 74
    /**
     * Constant for transaction isolation level SERIALIZABLE.
75 76
     *
     * @deprecated Use TransactionIsolationLevel::SERIALIZABLE.
77
     */
78
    public const TRANSACTION_SERIALIZABLE = TransactionIsolationLevel::SERIALIZABLE;
79

80 81
    /**
     * Represents an array of ints to be expanded by Doctrine SQL parsing.
82
     *
83
     * @var int
84
     */
Sergei Morozov's avatar
Sergei Morozov committed
85
    public const PARAM_INT_ARRAY = ParameterType::INTEGER + self::ARRAY_PARAM_OFFSET;
86

87 88
    /**
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
89
     *
90
     * @var int
91
     */
Sergei Morozov's avatar
Sergei Morozov committed
92
    public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET;
93

94 95
    /**
     * Offset by which PARAM_* constants are detected as arrays of the param type.
96
     *
97
     * @var int
98 99
     */
    const ARRAY_PARAM_OFFSET = 100;
100

romanb's avatar
romanb committed
101 102 103
    /**
     * The wrapped driver connection.
     *
104
     * @var \Doctrine\DBAL\Driver\Connection
romanb's avatar
romanb committed
105 106
     */
    protected $_conn;
107

romanb's avatar
romanb committed
108
    /**
109
     * @var \Doctrine\DBAL\Configuration
romanb's avatar
romanb committed
110 111
     */
    protected $_config;
112

romanb's avatar
romanb committed
113
    /**
114
     * @var \Doctrine\Common\EventManager
romanb's avatar
romanb committed
115 116
     */
    protected $_eventManager;
117

118
    /**
119
     * @var \Doctrine\DBAL\Query\Expression\ExpressionBuilder
120 121
     */
    protected $_expr;
122

romanb's avatar
romanb committed
123 124 125
    /**
     * Whether or not a connection has been established.
     *
126
     * @var bool
romanb's avatar
romanb committed
127
     */
128
    private $_isConnected = false;
129

130
    /**
131
     * The current auto-commit mode of this connection.
132
     *
133
     * @var bool
134 135 136
     */
    private $autoCommit = true;

137 138 139
    /**
     * The transaction nesting level.
     *
140
     * @var int
141 142 143 144 145 146
     */
    private $_transactionNestingLevel = 0;

    /**
     * The currently active transaction isolation level.
     *
147
     * @var int
148 149 150
     */
    private $_transactionIsolationLevel;

151
    /**
Benjamin Morel's avatar
Benjamin Morel committed
152
     * If nested transactions should use savepoints.
153
     *
154
     * @var bool
155
     */
156
    private $_nestTransactionsWithSavepoints = false;
Lukas Kahwe Smith's avatar
Lukas Kahwe Smith committed
157

romanb's avatar
romanb committed
158 159 160 161 162
    /**
     * The parameters used during creation of the Connection instance.
     *
     * @var array
     */
163
    private $_params = [];
164

romanb's avatar
romanb committed
165 166 167 168
    /**
     * The DatabasePlatform object that provides information about the
     * database platform used by the connection.
     *
169
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
romanb's avatar
romanb committed
170
     */
171
    private $platform;
172

romanb's avatar
romanb committed
173 174 175
    /**
     * The schema manager.
     *
176
     * @var \Doctrine\DBAL\Schema\AbstractSchemaManager
romanb's avatar
romanb committed
177 178
     */
    protected $_schemaManager;
179

romanb's avatar
romanb committed
180
    /**
romanb's avatar
romanb committed
181 182
     * The used DBAL driver.
     *
183
     * @var \Doctrine\DBAL\Driver
romanb's avatar
romanb committed
184 185
     */
    protected $_driver;
186

187
    /**
188
     * Flag that indicates whether the current transaction is marked for rollback only.
189
     *
190
     * @var bool
191
     */
192 193
    private $_isRollbackOnly = false;

Benjamin Morel's avatar
Benjamin Morel committed
194
    /**
195
     * @var int
Benjamin Morel's avatar
Benjamin Morel committed
196
     */
197
    protected $defaultFetchMode = FetchMode::ASSOCIATIVE;
198

romanb's avatar
romanb committed
199 200 201
    /**
     * Initializes a new instance of the Connection class.
     *
Benjamin Morel's avatar
Benjamin Morel committed
202 203 204 205 206 207
     * @param array                              $params       The connection parameters.
     * @param \Doctrine\DBAL\Driver              $driver       The driver to use.
     * @param \Doctrine\DBAL\Configuration|null  $config       The configuration, optional.
     * @param \Doctrine\Common\EventManager|null $eventManager The event manager, optional.
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
208 209 210 211 212 213
     */
    public function __construct(array $params, Driver $driver, Configuration $config = null,
            EventManager $eventManager = null)
    {
        $this->_driver = $driver;
        $this->_params = $params;
214

romanb's avatar
romanb committed
215 216 217
        if (isset($params['pdo'])) {
            $this->_conn = $params['pdo'];
            $this->_isConnected = true;
218
            unset($this->_params['pdo']);
romanb's avatar
romanb committed
219
        }
220

221 222
        if (isset($params["platform"])) {
            if ( ! $params['platform'] instanceof Platforms\AbstractPlatform) {
223
                throw DBALException::invalidPlatformType($params['platform']);
224 225 226 227 228 229
            }

            $this->platform = $params["platform"];
            unset($this->_params["platform"]);
        }

romanb's avatar
romanb committed
230 231 232
        // Create default config and event manager if none given
        if ( ! $config) {
            $config = new Configuration();
romanb's avatar
romanb committed
233
        }
234

romanb's avatar
romanb committed
235
        if ( ! $eventManager) {
romanb's avatar
romanb committed
236
            $eventManager = new EventManager();
romanb's avatar
romanb committed
237
        }
238

romanb's avatar
romanb committed
239
        $this->_config = $config;
romanb's avatar
romanb committed
240
        $this->_eventManager = $eventManager;
241

242
        $this->_expr = new Query\Expression\ExpressionBuilder($this);
243

244
        $this->autoCommit = $config->getAutoCommit();
romanb's avatar
romanb committed
245
    }
romanb's avatar
romanb committed
246

247
    /**
romanb's avatar
romanb committed
248
     * Gets the parameters used during instantiation.
249
     *
Benjamin Morel's avatar
Benjamin Morel committed
250
     * @return array
251 252 253 254 255 256
     */
    public function getParams()
    {
        return $this->_params;
    }

romanb's avatar
romanb committed
257
    /**
romanb's avatar
romanb committed
258
     * Gets the name of the database this Connection is connected to.
romanb's avatar
romanb committed
259
     *
Benjamin Morel's avatar
Benjamin Morel committed
260
     * @return string
romanb's avatar
romanb committed
261 262 263 264 265
     */
    public function getDatabase()
    {
        return $this->_driver->getDatabase($this);
    }
266

267 268
    /**
     * Gets the hostname of the currently connected database.
269
     *
Benjamin Morel's avatar
Benjamin Morel committed
270
     * @return string|null
271 272 273
     */
    public function getHost()
    {
274
        return $this->_params['host'] ?? null;
275
    }
276

277 278
    /**
     * Gets the port of the currently connected database.
279
     *
280 281 282 283
     * @return mixed
     */
    public function getPort()
    {
284
        return $this->_params['port'] ?? null;
285
    }
286

287 288
    /**
     * Gets the username used by this connection.
289
     *
Benjamin Morel's avatar
Benjamin Morel committed
290
     * @return string|null
291 292 293
     */
    public function getUsername()
    {
294
        return $this->_params['user'] ?? null;
295
    }
296

297 298
    /**
     * Gets the password used by this connection.
299
     *
Benjamin Morel's avatar
Benjamin Morel committed
300
     * @return string|null
301 302 303
     */
    public function getPassword()
    {
304
        return $this->_params['password'] ?? null;
305
    }
romanb's avatar
romanb committed
306 307 308 309

    /**
     * Gets the DBAL driver instance.
     *
310
     * @return \Doctrine\DBAL\Driver
romanb's avatar
romanb committed
311 312 313 314 315 316 317 318 319
     */
    public function getDriver()
    {
        return $this->_driver;
    }

    /**
     * Gets the Configuration used by the Connection.
     *
320
     * @return \Doctrine\DBAL\Configuration
romanb's avatar
romanb committed
321 322 323 324 325 326 327 328 329
     */
    public function getConfiguration()
    {
        return $this->_config;
    }

    /**
     * Gets the EventManager used by the Connection.
     *
330
     * @return \Doctrine\Common\EventManager
romanb's avatar
romanb committed
331 332 333 334 335 336 337 338 339
     */
    public function getEventManager()
    {
        return $this->_eventManager;
    }

    /**
     * Gets the DatabasePlatform for the connection.
     *
340
     * @return \Doctrine\DBAL\Platforms\AbstractPlatform
341 342
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
343 344 345
     */
    public function getDatabasePlatform()
    {
346
        if (null === $this->platform) {
347
            $this->detectDatabasePlatform();
348 349 350
        }

        return $this->platform;
romanb's avatar
romanb committed
351
    }
352

353 354 355
    /**
     * Gets the ExpressionBuilder for the connection.
     *
356
     * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
357 358 359 360 361
     */
    public function getExpressionBuilder()
    {
        return $this->_expr;
    }
362

romanb's avatar
romanb committed
363 364 365
    /**
     * Establishes the connection with the database.
     *
366 367
     * @return bool TRUE if the connection was successfully established, FALSE if
     *              the connection is already open.
romanb's avatar
romanb committed
368 369 370
     */
    public function connect()
    {
371 372 373
        if ($this->_isConnected) {
            return false;
        }
romanb's avatar
romanb committed
374

375 376 377
        $driverOptions = $this->_params['driverOptions'] ?? [];
        $user = $this->_params['user'] ?? null;
        $password = $this->_params['password'] ?? null;
romanb's avatar
romanb committed
378 379

        $this->_conn = $this->_driver->connect($this->_params, $user, $password, $driverOptions);
romanb's avatar
romanb committed
380 381
        $this->_isConnected = true;

382 383 384 385
        if (false === $this->autoCommit) {
            $this->beginTransaction();
        }

386
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
387
            $eventArgs = new Event\ConnectionEventArgs($this);
388 389 390
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
        }

romanb's avatar
romanb committed
391 392 393
        return true;
    }

394 395 396 397 398 399 400 401 402
    /**
     * Detects and sets the database platform.
     *
     * Evaluates custom platform class and version in order to set the correct platform.
     *
     * @throws DBALException if an invalid platform was specified for this connection.
     */
    private function detectDatabasePlatform()
    {
403
        $version = $this->getDatabasePlatformVersion();
404

405 406
        if (null !== $version) {
            $this->platform = $this->_driver->createDatabasePlatformForVersion($version);
407
        } else {
408
            $this->platform = $this->_driver->getDatabasePlatform();
409 410 411 412 413 414 415 416 417 418 419 420 421 422
        }

        $this->platform->setEventManager($this->_eventManager);
    }

    /**
     * Returns the version of the related platform if applicable.
     *
     * Returns null if either the driver is not capable to create version
     * specific platform instances, no explicit server version was specified
     * or the underlying driver connection cannot determine the platform
     * version without having to query it (performance reasons).
     *
     * @return string|null
423
     *
424
     * @throws Exception
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
     */
    private function getDatabasePlatformVersion()
    {
        // Driver does not support version specific platforms.
        if ( ! $this->_driver instanceof VersionAwarePlatformDriver) {
            return null;
        }

        // Explicit platform version requested (supersedes auto-detection).
        if (isset($this->_params['serverVersion'])) {
            return $this->_params['serverVersion'];
        }

        // If not connected, we need to connect now to determine the platform version.
        if (null === $this->_conn) {
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
            try {
                $this->connect();
            } catch (\Exception $originalException) {
                if (empty($this->_params['dbname'])) {
                    throw $originalException;
                }

                // The database to connect to might not yet exist.
                // Retry detection without database name connection parameter.
                $databaseName = $this->_params['dbname'];
                $this->_params['dbname'] = null;

                try {
                    $this->connect();
                } catch (\Exception $fallbackException) {
                    // Either the platform does not support database-less connections
                    // or something else went wrong.
                    // Reset connection parameters and rethrow the original exception.
                    $this->_params['dbname'] = $databaseName;

                    throw $originalException;
                }

                // Reset connection parameters.
                $this->_params['dbname'] = $databaseName;
                $serverVersion = $this->getServerVersion();

                // Close "temporary" connection to allow connecting to the real database again.
                $this->close();

                return $serverVersion;
            }

473 474
        }

475 476 477 478 479 480 481 482 483 484
        return $this->getServerVersion();
    }

    /**
     * Returns the database server version if the underlying driver supports it.
     *
     * @return string|null
     */
    private function getServerVersion()
    {
485 486 487 488 489 490 491 492 493 494 495
        // Automatic platform version detection.
        if ($this->_conn instanceof ServerInfoAwareConnection &&
            ! $this->_conn->requiresQueryForServerVersion()
        ) {
            return $this->_conn->getServerVersion();
        }

        // Unable to detect platform version.
        return null;
    }

496 497 498
    /**
     * Returns the current auto-commit mode for this connection.
     *
499
     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
500 501 502
     *
     * @see    setAutoCommit
     */
503
    public function isAutoCommit()
504
    {
505
        return true === $this->autoCommit;
506 507 508 509 510 511 512 513 514 515 516 517
    }

    /**
     * Sets auto-commit mode for this connection.
     *
     * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
     * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
     * the method commit or the method rollback. By default, new connections are in auto-commit mode.
     *
     * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is
     * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op.
     *
518
     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
519
     *
520
     * @see   isAutoCommit
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
     */
    public function setAutoCommit($autoCommit)
    {
        $autoCommit = (boolean) $autoCommit;

        // Mode not changed, no-op.
        if ($autoCommit === $this->autoCommit) {
            return;
        }

        $this->autoCommit = $autoCommit;

        // Commit all currently active transactions if any when switching auto-commit mode.
        if (true === $this->_isConnected && 0 !== $this->_transactionNestingLevel) {
            $this->commitAll();
        }
    }

539
    /**
Benjamin Morel's avatar
Benjamin Morel committed
540
     * Sets the fetch mode.
541
     *
542
     * @param int $fetchMode
Benjamin Morel's avatar
Benjamin Morel committed
543 544
     *
     * @return void
545
     */
546
    public function setFetchMode($fetchMode)
547
    {
548
        $this->defaultFetchMode = $fetchMode;
549 550
    }

romanb's avatar
romanb committed
551
    /**
552 553
     * Prepares and executes an SQL query and returns the first row of the result
     * as an associative array.
554
     *
romanb's avatar
romanb committed
555
     * @param string $statement The SQL query.
Benjamin Morel's avatar
Benjamin Morel committed
556
     * @param array  $params    The query parameters.
557
     * @param array  $types     The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
558
     *
559
     * @return array|bool False is returned if no rows are found.
560 561
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
562
     */
563
    public function fetchAssoc($statement, array $params = [], array $types = [])
romanb's avatar
romanb committed
564
    {
565
        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::ASSOCIATIVE);
romanb's avatar
romanb committed
566 567 568
    }

    /**
569 570
     * Prepares and executes an SQL query and returns the first row of the result
     * as a numerically indexed array.
romanb's avatar
romanb committed
571
     *
Benjamin Morel's avatar
Benjamin Morel committed
572 573
     * @param string $statement The SQL query to be executed.
     * @param array  $params    The prepared statement params.
574
     * @param array  $types     The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
575
     *
576
     * @return array|bool False is returned if no rows are found.
romanb's avatar
romanb committed
577
     */
578
    public function fetchArray($statement, array $params = [], array $types = [])
romanb's avatar
romanb committed
579
    {
580
        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::NUMERIC);
romanb's avatar
romanb committed
581 582 583
    }

    /**
584 585
     * Prepares and executes an SQL query and returns the value of a single column
     * of the first row of the result.
586
     *
587 588 589 590
     * @param string $statement The SQL query to be executed.
     * @param array  $params    The prepared statement params.
     * @param int    $column    The 0-indexed column number to retrieve.
     * @param array  $types     The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
591
     *
592
     * @return mixed|bool False is returned if no rows are found.
593 594
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
595
     */
596
    public function fetchColumn($statement, array $params = [], $column = 0, array $types = [])
romanb's avatar
romanb committed
597
    {
598
        return $this->executeQuery($statement, $params, $types)->fetchColumn($column);
romanb's avatar
romanb committed
599 600 601 602 603
    }

    /**
     * Whether an actual connection to the database is established.
     *
604
     * @return bool
romanb's avatar
romanb committed
605 606 607 608 609 610
     */
    public function isConnected()
    {
        return $this->_isConnected;
    }

611 612
    /**
     * Checks whether a transaction is currently active.
613
     *
614
     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
615 616 617 618 619 620
     */
    public function isTransactionActive()
    {
        return $this->_transactionNestingLevel > 0;
    }

621
    /**
622
     * Gathers conditions for an update or delete call.
623 624 625 626
     *
     * @param array $identifiers Input array of columns to values
     *
     * @return string[][] a triplet with:
627
     *                    - the first key being the columns
628
     *                    - the second key being the values
629
     *                    - the third key being the conditions
630
     */
631
    private function gatherConditions(array $identifiers)
632 633 634
    {
        $columns = [];
        $values = [];
635
        $conditions = [];
636 637 638

        foreach ($identifiers as $columnName => $value) {
            if (null === $value) {
639
                $conditions[] = $this->getDatabasePlatform()->getIsNullExpression($columnName);
640 641 642 643 644
                continue;
            }

            $columns[] = $columnName;
            $values[] = $value;
645
            $conditions[] = $columnName . ' = ?';
646 647
        }

648
        return [$columns, $values, $conditions];
649 650
    }

651 652
    /**
     * Executes an SQL DELETE statement on a table.
romanb's avatar
romanb committed
653
     *
654 655
     * Table expression and columns are not escaped and are not safe for user-input.
     *
656 657 658
     * @param string $tableExpression The expression of the table on which to delete.
     * @param array  $identifier      The deletion criteria. An associative array containing column-value pairs.
     * @param array  $types           The types of identifiers.
Benjamin Morel's avatar
Benjamin Morel committed
659
     *
660
     * @return int The number of affected rows.
661
     *
662
     * @throws \Doctrine\DBAL\DBALException
663
     * @throws InvalidArgumentException
664
     */
665
    public function delete($tableExpression, array $identifier, array $types = [])
666 667
    {
        if (empty($identifier)) {
668
            throw InvalidArgumentException::fromEmptyCriteria();
669 670
        }

671
        list($columns, $values, $conditions) = $this->gatherConditions($identifier);
672

673
        return $this->executeUpdate(
674 675 676
            'DELETE FROM ' . $tableExpression . ' WHERE ' . implode(' AND ', $conditions),
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
677
        );
678 679
    }

romanb's avatar
romanb committed
680 681 682 683 684 685 686
    /**
     * Closes the connection.
     *
     * @return void
     */
    public function close()
    {
687
        $this->_conn = null;
688

romanb's avatar
romanb committed
689 690 691
        $this->_isConnected = false;
    }

692 693 694
    /**
     * Sets the transaction isolation level.
     *
695
     * @param int $level The level to set.
Benjamin Morel's avatar
Benjamin Morel committed
696
     *
697
     * @return int
698 699 700 701
     */
    public function setTransactionIsolation($level)
    {
        $this->_transactionIsolationLevel = $level;
702

703
        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
704 705 706 707 708
    }

    /**
     * Gets the currently active transaction isolation level.
     *
709
     * @return int The current transaction isolation level.
710 711 712
     */
    public function getTransactionIsolation()
    {
713 714 715 716
        if (null === $this->_transactionIsolationLevel) {
            $this->_transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
        }

717 718 719
        return $this->_transactionIsolationLevel;
    }

romanb's avatar
romanb committed
720
    /**
721
     * Executes an SQL UPDATE statement on a table.
romanb's avatar
romanb committed
722
     *
723 724
     * Table expression and columns are not escaped and are not safe for user-input.
     *
725 726 727 728
     * @param string $tableExpression The expression of the table to update quoted or unquoted.
     * @param array  $data            An associative array containing column-value pairs.
     * @param array  $identifier      The update criteria. An associative array containing column-value pairs.
     * @param array  $types           Types of the merged $data and $identifier arrays in that order.
Benjamin Morel's avatar
Benjamin Morel committed
729
     *
730
     * @return int The number of affected rows.
731 732
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
733
     */
734
    public function update($tableExpression, array $data, array $identifier, array $types = [])
romanb's avatar
romanb committed
735
    {
736 737 738
        $setColumns = [];
        $setValues = [];
        $set = [];
739

romanb's avatar
romanb committed
740
        foreach ($data as $columnName => $value) {
741 742
            $setColumns[] = $columnName;
            $setValues[] = $value;
743
            $set[] = $columnName . ' = ?';
romanb's avatar
romanb committed
744 745
        }

746 747 748
        list($conditionColumns, $conditionValues, $conditions) = $this->gatherConditions($identifier);
        $columns = array_merge($setColumns, $conditionColumns);
        $values = array_merge($setValues, $conditionValues);
749

750
        if (is_string(key($types))) {
751
            $types = $this->extractTypeValues($columns, $types);
752
        }
romanb's avatar
romanb committed
753

754
        $sql  = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
755
                . ' WHERE ' . implode(' AND ', $conditions);
romanb's avatar
romanb committed
756

757
        return $this->executeUpdate($sql, $values, $types);
romanb's avatar
romanb committed
758 759 760 761 762
    }

    /**
     * Inserts a table row with specified data.
     *
763 764 765
     * Table expression and columns are not escaped and are not safe for user-input.
     *
     * @param string $tableExpression The expression of the table to insert data into, quoted or unquoted.
766 767
     * @param array  $data            An associative array containing column-value pairs.
     * @param array  $types           Types of the inserted data.
Benjamin Morel's avatar
Benjamin Morel committed
768
     *
769
     * @return int The number of affected rows.
770 771
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
772
     */
773
    public function insert($tableExpression, array $data, array $types = [])
romanb's avatar
romanb committed
774
    {
775
        if (empty($data)) {
776
            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' ()' . ' VALUES ()');
777 778
        }

779 780 781
        $columns = [];
        $values = [];
        $set = [];
782 783

        foreach ($data as $columnName => $value) {
784 785 786
            $columns[] = $columnName;
            $values[] = $value;
            $set[] = '?';
787 788
        }

789
        return $this->executeUpdate(
790 791 792 793
            'INSERT INTO ' . $tableExpression . ' (' . implode(', ', $columns) . ')' .
            ' VALUES (' . implode(', ', $set) . ')',
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
794
        );
romanb's avatar
romanb committed
795 796
    }

797
    /**
798
     * Extract ordered type list from an ordered column list and type map.
799
     *
800
     * @param array $columnList
801 802 803 804
     * @param array $types
     *
     * @return array
     */
805
    private function extractTypeValues(array $columnList, array $types)
806
    {
807
        $typeValues = [];
808

809
        foreach ($columnList as $columnIndex => $columnName) {
810
            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
811 812 813 814 815
        }

        return $typeValues;
    }

romanb's avatar
romanb committed
816
    /**
Benjamin Morel's avatar
Benjamin Morel committed
817
     * Quotes a string so it can be safely used as a table or column name, even if
romanb's avatar
romanb committed
818 819 820 821
     * it is a reserved name.
     *
     * Delimiting style depends on the underlying database platform that is being used.
     *
822 823
     * NOTE: Just because you CAN use quoted identifiers does not mean
     * you SHOULD use them. In general, they end up causing way more
romanb's avatar
romanb committed
824 825
     * problems than they solve.
     *
826
     * @param string $str The name to be quoted.
Benjamin Morel's avatar
Benjamin Morel committed
827
     *
828
     * @return string The quoted name.
romanb's avatar
romanb committed
829 830 831
     */
    public function quoteIdentifier($str)
    {
832
        return $this->getDatabasePlatform()->quoteIdentifier($str);
romanb's avatar
romanb committed
833 834 835 836 837
    }

    /**
     * Quotes a given input parameter.
     *
838
     * @param mixed    $input The parameter to be quoted.
helsner's avatar
helsner committed
839
     * @param int|null $type  The type of the parameter.
Benjamin Morel's avatar
Benjamin Morel committed
840
     *
841
     * @return string The quoted parameter.
romanb's avatar
romanb committed
842 843 844
     */
    public function quote($input, $type = null)
    {
845
        $this->connect();
846 847

        list($value, $bindingType) = $this->getBindingInfo($input, $type);
848

849
        return $this->_conn->quote($value, $bindingType);
romanb's avatar
romanb committed
850 851 852
    }

    /**
853
     * Prepares and executes an SQL query and returns the result as an associative array.
romanb's avatar
romanb committed
854
     *
Benjamin Morel's avatar
Benjamin Morel committed
855 856 857 858
     * @param string $sql    The SQL query.
     * @param array  $params The query parameters.
     * @param array  $types  The query parameter types.
     *
romanb's avatar
romanb committed
859 860
     * @return array
     */
861
    public function fetchAll($sql, array $params = [], $types = [])
romanb's avatar
romanb committed
862
    {
root's avatar
root committed
863
        return $this->executeQuery($sql, $params, $types)->fetchAll();
romanb's avatar
romanb committed
864 865 866 867 868
    }

    /**
     * Prepares an SQL statement.
     *
869
     * @param string $statement The SQL statement to prepare.
Benjamin Morel's avatar
Benjamin Morel committed
870
     *
871
     * @return \Doctrine\DBAL\Statement The prepared statement.
Benjamin Morel's avatar
Benjamin Morel committed
872 873
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
874 875 876
     */
    public function prepare($statement)
    {
877 878
        try {
            $stmt = new Statement($statement, $this);
Filip Procházka's avatar
Filip Procházka committed
879
        } catch (Exception $ex) {
880
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
881 882
        }

883
        $stmt->setFetchMode($this->defaultFetchMode);
884 885

        return $stmt;
romanb's avatar
romanb committed
886 887 888
    }

    /**
Pascal Borreli's avatar
Pascal Borreli committed
889
     * Executes an, optionally parametrized, SQL query.
romanb's avatar
romanb committed
890
     *
Pascal Borreli's avatar
Pascal Borreli committed
891
     * If the query is parametrized, a prepared statement is used.
892 893
     * If an SQLLogger is configured, the execution is logged.
     *
Benjamin Morel's avatar
Benjamin Morel committed
894 895 896 897 898
     * @param string                                      $query  The SQL query to execute.
     * @param array                                       $params The parameters to bind to the query, if any.
     * @param array                                       $types  The types the previous parameters are in.
     * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp    The query cache profile, optional.
     *
899
     * @return \Doctrine\DBAL\Driver\Statement The executed statement.
Benjamin Morel's avatar
Benjamin Morel committed
900 901
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
902
     */
903
    public function executeQuery($query, array $params = [], $types = [], QueryCacheProfile $qcp = null)
romanb's avatar
romanb committed
904
    {
905
        if ($qcp !== null) {
906
            return $this->executeCacheQuery($query, $params, $types, $qcp);
907 908
        }

romanb's avatar
romanb committed
909 910
        $this->connect();

911 912 913
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
914
        }
915

916 917 918
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
919

920 921 922 923 924 925 926
                $stmt = $this->_conn->prepare($query);
                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
927
            } else {
928
                $stmt = $this->_conn->query($query);
929
            }
Filip Procházka's avatar
Filip Procházka committed
930
        } catch (Exception $ex) {
931
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
932
        }
933

934
        $stmt->setFetchMode($this->defaultFetchMode);
935

936 937
        if ($logger) {
            $logger->stopQuery();
938 939
        }

romanb's avatar
romanb committed
940
        return $stmt;
romanb's avatar
romanb committed
941
    }
942

943
    /**
Benjamin Morel's avatar
Benjamin Morel committed
944 945 946 947 948 949
     * Executes a caching query.
     *
     * @param string                                 $query  The SQL query to execute.
     * @param array                                  $params The parameters to bind to the query, if any.
     * @param array                                  $types  The types the previous parameters are in.
     * @param \Doctrine\DBAL\Cache\QueryCacheProfile $qcp    The query cache profile.
950 951
     *
     * @return \Doctrine\DBAL\Driver\ResultStatement
Benjamin Morel's avatar
Benjamin Morel committed
952 953
     *
     * @throws \Doctrine\DBAL\Cache\CacheException
954 955 956 957
     */
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp)
    {
        $resultCache = $qcp->getResultCacheDriver() ?: $this->_config->getResultCacheImpl();
958
        if ( ! $resultCache) {
959 960 961
            throw CacheException::noResultDriverConfigured();
        }

962
        list($cacheKey, $realKey) = $qcp->generateCacheKeys($query, $params, $types, $this->getParams());
963 964 965 966 967

        // fetch the row pointers entry
        if ($data = $resultCache->fetch($cacheKey)) {
            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
            if (isset($data[$realKey])) {
968
                $stmt = new ArrayStatement($data[$realKey]);
Steve Müller's avatar
Steve Müller committed
969
            } elseif (array_key_exists($realKey, $data)) {
970
                $stmt = new ArrayStatement([]);
971 972
            }
        }
973 974 975 976 977

        if (!isset($stmt)) {
            $stmt = new ResultCacheStatement($this->executeQuery($query, $params, $types), $resultCache, $cacheKey, $realKey, $qcp->getLifetime());
        }

978
        $stmt->setFetchMode($this->defaultFetchMode);
979 980

        return $stmt;
981 982
    }

983
    /**
Pascal Borreli's avatar
Pascal Borreli committed
984
     * Executes an, optionally parametrized, SQL query and returns the result,
985
     * applying a given projection/transformation function on each row of the result.
986
     *
Benjamin Morel's avatar
Benjamin Morel committed
987 988 989 990 991 992
     * @param string   $query    The SQL query to execute.
     * @param array    $params   The parameters, if any.
     * @param \Closure $function The transformation function that is applied on each row.
     *                           The function receives a single parameter, an array, that
     *                           represents a row of the result set.
     *
993
     * @return array The projected result of the query.
994
     */
995
    public function project($query, array $params, Closure $function)
996
    {
997
        $result = [];
998
        $stmt = $this->executeQuery($query, $params);
999

1000
        while ($row = $stmt->fetch()) {
1001
            $result[] = $function($row);
1002
        }
1003

1004
        $stmt->closeCursor();
1005

1006 1007
        return $result;
    }
romanb's avatar
romanb committed
1008 1009

    /**
1010
     * Executes an SQL statement, returning a result set as a Statement object.
1011
     *
1012
     * @return \Doctrine\DBAL\Driver\Statement
Benjamin Morel's avatar
Benjamin Morel committed
1013 1014
     *
     * @throws \Doctrine\DBAL\DBALException
1015 1016 1017
     */
    public function query()
    {
1018 1019
        $this->connect();

1020 1021
        $args = func_get_args();

1022
        $logger = $this->_config->getSQLLogger();
1023 1024 1025 1026
        if ($logger) {
            $logger->startQuery($args[0]);
        }

1027
        try {
1028
            $statement = $this->_conn->query(...$args);
Filip Procházka's avatar
Filip Procházka committed
1029
        } catch (Exception $ex) {
1030
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $args[0]);
1031 1032
        }

1033
        $statement->setFetchMode($this->defaultFetchMode);
1034 1035 1036 1037 1038 1039

        if ($logger) {
            $logger->stopQuery();
        }

        return $statement;
1040 1041 1042 1043 1044
    }

    /**
     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
     * and returns the number of affected rows.
1045
     *
1046
     * This method supports PDO binding types as well as DBAL mapping types.
romanb's avatar
romanb committed
1047
     *
Benjamin Morel's avatar
Benjamin Morel committed
1048 1049 1050 1051
     * @param string $query  The SQL query.
     * @param array  $params The query parameters.
     * @param array  $types  The parameter types.
     *
1052
     * @return int The number of affected rows.
Benjamin Morel's avatar
Benjamin Morel committed
1053 1054
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
1055
     */
1056
    public function executeUpdate($query, array $params = [], array $types = [])
romanb's avatar
romanb committed
1057 1058 1059
    {
        $this->connect();

1060 1061 1062
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
1063 1064
        }

1065 1066 1067
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
1068

1069 1070 1071 1072 1073 1074 1075 1076
                $stmt = $this->_conn->prepare($query);
                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
                $result = $stmt->rowCount();
1077
            } else {
1078
                $result = $this->_conn->exec($query);
1079
            }
Filip Procházka's avatar
Filip Procházka committed
1080
        } catch (Exception $ex) {
1081
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
1082
        }
1083

1084 1085
        if ($logger) {
            $logger->stopQuery();
1086 1087
        }

romanb's avatar
romanb committed
1088
        return $result;
romanb's avatar
romanb committed
1089 1090
    }

1091
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1092
     * Executes an SQL statement and return the number of affected rows.
1093
     *
1094
     * @param string $statement
Benjamin Morel's avatar
Benjamin Morel committed
1095
     *
1096
     * @return int The number of affected rows.
Benjamin Morel's avatar
Benjamin Morel committed
1097 1098
     *
     * @throws \Doctrine\DBAL\DBALException
1099 1100 1101 1102
     */
    public function exec($statement)
    {
        $this->connect();
1103 1104 1105 1106 1107 1108

        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($statement);
        }

1109 1110
        try {
            $result = $this->_conn->exec($statement);
Filip Procházka's avatar
Filip Procházka committed
1111
        } catch (Exception $ex) {
1112
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1113
        }
1114 1115 1116 1117 1118 1119

        if ($logger) {
            $logger->stopQuery();
        }

        return $result;
1120 1121
    }

1122 1123 1124
    /**
     * Returns the current transaction nesting level.
     *
1125
     * @return int The nesting level. A value of 0 means there's no active transaction.
1126 1127 1128 1129 1130 1131
     */
    public function getTransactionNestingLevel()
    {
        return $this->_transactionNestingLevel;
    }

romanb's avatar
romanb committed
1132
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1133
     * Fetches the SQLSTATE associated with the last database operation.
romanb's avatar
romanb committed
1134
     *
1135
     * @return int The last error code.
romanb's avatar
romanb committed
1136 1137 1138 1139
     */
    public function errorCode()
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1140

romanb's avatar
romanb committed
1141 1142 1143 1144
        return $this->_conn->errorCode();
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1145
     * Fetches extended error information associated with the last database operation.
romanb's avatar
romanb committed
1146
     *
1147
     * @return array The last error information.
romanb's avatar
romanb committed
1148 1149 1150 1151
     */
    public function errorInfo()
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1152

romanb's avatar
romanb committed
1153 1154 1155 1156 1157 1158 1159 1160
        return $this->_conn->errorInfo();
    }

    /**
     * Returns the ID of the last inserted row, or the last value from a sequence object,
     * depending on the underlying driver.
     *
     * Note: This method may not return a meaningful or consistent result across different drivers,
1161 1162
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
     * columns or sequences.
romanb's avatar
romanb committed
1163
     *
Benjamin Morel's avatar
Benjamin Morel committed
1164 1165
     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
     *
1166
     * @return string A string representation of the last inserted ID.
romanb's avatar
romanb committed
1167
     */
romanb's avatar
romanb committed
1168 1169 1170
    public function lastInsertId($seqName = null)
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1171

romanb's avatar
romanb committed
1172 1173
        return $this->_conn->lastInsertId($seqName);
    }
1174

1175 1176 1177 1178 1179 1180 1181 1182
    /**
     * Executes a function in a transaction.
     *
     * The function gets passed this Connection instance as an (optional) parameter.
     *
     * If an exception occurs during execution of the function or transaction commit,
     * the transaction is rolled back and the exception re-thrown.
     *
Benjamin Morel's avatar
Benjamin Morel committed
1183 1184
     * @param \Closure $func The function to execute transactionally.
     *
1185
     * @return mixed The value returned by $func
Benjamin Morel's avatar
Benjamin Morel committed
1186
     *
1187 1188
     * @throws Exception
     * @throws Throwable
1189 1190 1191 1192 1193
     */
    public function transactional(Closure $func)
    {
        $this->beginTransaction();
        try {
1194
            $res = $func($this);
1195
            $this->commit();
1196
            return $res;
1197
        } catch (Exception $e) {
1198
            $this->rollBack();
1199 1200 1201
            throw $e;
        } catch (Throwable $e) {
            $this->rollBack();
1202 1203 1204 1205
            throw $e;
        }
    }

1206
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1207
     * Sets if nested transactions should use savepoints.
1208
     *
1209
     * @param bool $nestTransactionsWithSavepoints
Benjamin Morel's avatar
Benjamin Morel committed
1210
     *
1211
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1212 1213
     *
     * @throws \Doctrine\DBAL\ConnectionException
1214 1215 1216
     */
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
    {
1217 1218 1219 1220
        if ($this->_transactionNestingLevel > 0) {
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
        }

1221
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1222
            throw ConnectionException::savepointsNotSupported();
1223 1224
        }

1225
        $this->_nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1226 1227 1228
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1229
     * Gets if nested transactions should use savepoints.
1230
     *
1231
     * @return bool
1232 1233 1234 1235 1236 1237
     */
    public function getNestTransactionsWithSavepoints()
    {
        return $this->_nestTransactionsWithSavepoints;
    }

1238 1239 1240 1241
    /**
     * Returns the savepoint name to use for nested transactions are false if they are not supported
     * "savepointFormat" parameter is not set
     *
Benjamin Morel's avatar
Benjamin Morel committed
1242
     * @return mixed A string with the savepoint name or false.
1243
     */
1244 1245 1246
    protected function _getNestedTransactionSavePointName()
    {
        return 'DOCTRINE2_SAVEPOINT_'.$this->_transactionNestingLevel;
1247 1248
    }

1249 1250 1251 1252 1253 1254 1255 1256 1257
    /**
     * Starts a transaction by suspending auto-commit mode.
     *
     * @return void
     */
    public function beginTransaction()
    {
        $this->connect();

1258 1259
        ++$this->_transactionNestingLevel;

1260 1261
        $logger = $this->_config->getSQLLogger();

1262
        if ($this->_transactionNestingLevel == 1) {
1263 1264 1265
            if ($logger) {
                $logger->startQuery('"START TRANSACTION"');
            }
1266
            $this->_conn->beginTransaction();
1267 1268 1269
            if ($logger) {
                $logger->stopQuery();
            }
Steve Müller's avatar
Steve Müller committed
1270
        } elseif ($this->_nestTransactionsWithSavepoints) {
1271 1272 1273
            if ($logger) {
                $logger->startQuery('"SAVEPOINT"');
            }
1274
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1275 1276 1277
            if ($logger) {
                $logger->stopQuery();
            }
1278 1279 1280 1281 1282 1283 1284
        }
    }

    /**
     * Commits the current transaction.
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1285 1286 1287
     *
     * @throws \Doctrine\DBAL\ConnectionException If the commit failed due to no active transaction or
     *                                            because the transaction was marked for rollback only.
1288 1289 1290 1291
     */
    public function commit()
    {
        if ($this->_transactionNestingLevel == 0) {
1292
            throw ConnectionException::noActiveTransaction();
1293 1294 1295 1296 1297 1298 1299
        }
        if ($this->_isRollbackOnly) {
            throw ConnectionException::commitFailedRollbackOnly();
        }

        $this->connect();

1300 1301
        $logger = $this->_config->getSQLLogger();

1302
        if ($this->_transactionNestingLevel == 1) {
1303 1304 1305
            if ($logger) {
                $logger->startQuery('"COMMIT"');
            }
1306
            $this->_conn->commit();
1307 1308 1309
            if ($logger) {
                $logger->stopQuery();
            }
Steve Müller's avatar
Steve Müller committed
1310
        } elseif ($this->_nestTransactionsWithSavepoints) {
1311 1312 1313
            if ($logger) {
                $logger->startQuery('"RELEASE SAVEPOINT"');
            }
1314
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1315 1316 1317
            if ($logger) {
                $logger->stopQuery();
            }
1318 1319 1320
        }

        --$this->_transactionNestingLevel;
1321 1322 1323 1324 1325 1326 1327 1328 1329

        if (false === $this->autoCommit && 0 === $this->_transactionNestingLevel) {
            $this->beginTransaction();
        }
    }

    /**
     * Commits all current nesting transactions.
     */
1330
    private function commitAll()
1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342
    {
        while (0 !== $this->_transactionNestingLevel) {
            if (false === $this->autoCommit && 1 === $this->_transactionNestingLevel) {
                // When in no auto-commit mode, the last nesting commit immediately starts a new transaction.
                // Therefore we need to do the final commit here and then leave to avoid an infinite loop.
                $this->commit();

                return;
            }

            $this->commit();
        }
1343 1344 1345
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1346
     * Cancels any database changes done during the current transaction.
1347
     *
Benjamin Morel's avatar
Benjamin Morel committed
1348
     * @throws \Doctrine\DBAL\ConnectionException If the rollback operation failed.
1349
     */
1350
    public function rollBack()
1351 1352
    {
        if ($this->_transactionNestingLevel == 0) {
1353
            throw ConnectionException::noActiveTransaction();
1354 1355 1356 1357
        }

        $this->connect();

1358 1359
        $logger = $this->_config->getSQLLogger();

1360
        if ($this->_transactionNestingLevel == 1) {
1361 1362 1363
            if ($logger) {
                $logger->startQuery('"ROLLBACK"');
            }
1364
            $this->_transactionNestingLevel = 0;
1365
            $this->_conn->rollBack();
1366
            $this->_isRollbackOnly = false;
1367 1368 1369
            if ($logger) {
                $logger->stopQuery();
            }
1370 1371 1372 1373

            if (false === $this->autoCommit) {
                $this->beginTransaction();
            }
Steve Müller's avatar
Steve Müller committed
1374
        } elseif ($this->_nestTransactionsWithSavepoints) {
1375 1376 1377
            if ($logger) {
                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
            }
1378 1379
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
            --$this->_transactionNestingLevel;
1380 1381 1382
            if ($logger) {
                $logger->stopQuery();
            }
1383
        } else {
1384
            $this->_isRollbackOnly = true;
1385 1386 1387 1388
            --$this->_transactionNestingLevel;
        }
    }

1389
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1390 1391 1392
     * Creates a new savepoint.
     *
     * @param string $savepoint The name of the savepoint to create.
1393 1394
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1395 1396
     *
     * @throws \Doctrine\DBAL\ConnectionException
1397
     */
1398
    public function createSavepoint($savepoint)
1399
    {
1400
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1401
            throw ConnectionException::savepointsNotSupported();
1402 1403
        }

1404
        $this->_conn->exec($this->platform->createSavePoint($savepoint));
1405 1406 1407
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1408 1409 1410
     * Releases the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to release.
1411 1412
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1413 1414
     *
     * @throws \Doctrine\DBAL\ConnectionException
1415
     */
1416
    public function releaseSavepoint($savepoint)
1417
    {
1418
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1419
            throw ConnectionException::savepointsNotSupported();
1420 1421
        }

1422 1423
        if ($this->platform->supportsReleaseSavepoints()) {
            $this->_conn->exec($this->platform->releaseSavePoint($savepoint));
1424
        }
1425 1426 1427
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1428 1429 1430
     * Rolls back to the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to rollback to.
1431 1432
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1433 1434
     *
     * @throws \Doctrine\DBAL\ConnectionException
1435
     */
1436
    public function rollbackSavepoint($savepoint)
1437
    {
1438
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1439
            throw ConnectionException::savepointsNotSupported();
1440 1441
        }

1442
        $this->_conn->exec($this->platform->rollbackSavePoint($savepoint));
1443 1444
    }

romanb's avatar
romanb committed
1445 1446 1447
    /**
     * Gets the wrapped driver connection.
     *
1448
     * @return \Doctrine\DBAL\Driver\Connection
romanb's avatar
romanb committed
1449 1450 1451 1452
     */
    public function getWrappedConnection()
    {
        $this->connect();
1453

romanb's avatar
romanb committed
1454 1455
        return $this->_conn;
    }
1456

romanb's avatar
romanb committed
1457 1458 1459 1460
    /**
     * Gets the SchemaManager that can be used to inspect or change the
     * database schema through the connection.
     *
1461
     * @return \Doctrine\DBAL\Schema\AbstractSchemaManager
romanb's avatar
romanb committed
1462 1463 1464 1465 1466 1467
     */
    public function getSchemaManager()
    {
        if ( ! $this->_schemaManager) {
            $this->_schemaManager = $this->_driver->getSchemaManager($this);
        }
1468

romanb's avatar
romanb committed
1469 1470
        return $this->_schemaManager;
    }
1471

1472 1473 1474
    /**
     * Marks the current transaction so that the only possible
     * outcome for the transaction to be rolled back.
1475
     *
Benjamin Morel's avatar
Benjamin Morel committed
1476 1477 1478
     * @return void
     *
     * @throws \Doctrine\DBAL\ConnectionException If no transaction is active.
1479 1480 1481 1482 1483 1484 1485 1486 1487 1488
     */
    public function setRollbackOnly()
    {
        if ($this->_transactionNestingLevel == 0) {
            throw ConnectionException::noActiveTransaction();
        }
        $this->_isRollbackOnly = true;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1489
     * Checks whether the current transaction is marked for rollback only.
1490
     *
1491
     * @return bool
Benjamin Morel's avatar
Benjamin Morel committed
1492 1493
     *
     * @throws \Doctrine\DBAL\ConnectionException If no transaction is active.
1494
     */
1495
    public function isRollbackOnly()
1496 1497 1498 1499
    {
        if ($this->_transactionNestingLevel == 0) {
            throw ConnectionException::noActiveTransaction();
        }
Benjamin Morel's avatar
Benjamin Morel committed
1500

1501 1502 1503
        return $this->_isRollbackOnly;
    }

1504 1505 1506
    /**
     * Converts a given value to its database representation according to the conversion
     * rules of a specific DBAL mapping type.
1507
     *
Benjamin Morel's avatar
Benjamin Morel committed
1508 1509 1510
     * @param mixed  $value The value to convert.
     * @param string $type  The name of the DBAL mapping type.
     *
1511 1512 1513 1514
     * @return mixed The converted value.
     */
    public function convertToDatabaseValue($value, $type)
    {
1515
        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1516 1517 1518 1519 1520
    }

    /**
     * Converts a given value to its PHP representation according to the conversion
     * rules of a specific DBAL mapping type.
1521
     *
Benjamin Morel's avatar
Benjamin Morel committed
1522 1523 1524
     * @param mixed  $value The value to convert.
     * @param string $type  The name of the DBAL mapping type.
     *
1525 1526 1527 1528
     * @return mixed The converted type.
     */
    public function convertToPHPValue($value, $type)
    {
1529
        return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform());
1530 1531 1532 1533 1534
    }

    /**
     * Binds a set of parameters, some or all of which are typed with a PDO binding type
     * or DBAL mapping type, to a given statement.
1535
     *
Benjamin Morel's avatar
Benjamin Morel committed
1536 1537 1538 1539 1540 1541
     * @param \Doctrine\DBAL\Driver\Statement $stmt   The statement to bind the values to.
     * @param array                           $params The map/list of named/positional parameters.
     * @param array                           $types  The parameter types (PDO binding types or DBAL mapping types).
     *
     * @return void
     *
1542 1543
     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
     *           raw PDOStatement instances.
1544
     */
1545
    private function _bindTypedValues($stmt, array $params, array $types)
1546 1547 1548 1549
    {
        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
        if (is_int(key($params))) {
            // Positional parameters
1550
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1551
            $bindIndex = 1;
1552
            foreach ($params as $value) {
1553 1554 1555
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
                    $type = $types[$typeIndex];
1556
                    list($value, $bindingType) = $this->getBindingInfo($value, $type);
1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567
                    $stmt->bindValue($bindIndex, $value, $bindingType);
                } else {
                    $stmt->bindValue($bindIndex, $value);
                }
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
                    $type = $types[$name];
1568
                    list($value, $bindingType) = $this->getBindingInfo($value, $type);
1569 1570 1571 1572 1573 1574 1575
                    $stmt->bindValue($name, $value, $bindingType);
                } else {
                    $stmt->bindValue($name, $value);
                }
            }
        }
    }
1576 1577 1578 1579

    /**
     * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type.
     *
Benjamin Morel's avatar
Benjamin Morel committed
1580 1581 1582 1583
     * @param mixed $value The value to bind.
     * @param mixed $type  The type to bind (PDO or DBAL).
     *
     * @return array [0] => the (escaped) value, [1] => the binding type.
1584 1585 1586 1587 1588 1589 1590
     */
    private function getBindingInfo($value, $type)
    {
        if (is_string($type)) {
            $type = Type::getType($type);
        }
        if ($type instanceof Type) {
1591
            $value = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1592 1593
            $bindingType = $type->getBindingType();
        } else {
1594
            $bindingType = $type;
1595
        }
Benjamin Morel's avatar
Benjamin Morel committed
1596

1597
        return [$value, $bindingType];
1598 1599
    }

1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612
    /**
     * Resolves the parameters to a format which can be displayed.
     *
     * @internal This is a purely internal method. If you rely on this method, you are advised to
     *           copy/paste the code as this method may change, or be removed without prior notice.
     *
     * @param array $params
     * @param array $types
     *
     * @return array
     */
    public function resolveParams(array $params, array $types)
    {
1613
        $resolvedParams = [];
1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646

        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
        if (is_int(key($params))) {
            // Positional parameters
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
            $bindIndex = 1;
            foreach ($params as $value) {
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
                    $type = $types[$typeIndex];
                    list($value,) = $this->getBindingInfo($value, $type);
                    $resolvedParams[$bindIndex] = $value;
                } else {
                    $resolvedParams[$bindIndex] = $value;
                }
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
                    $type = $types[$name];
                    list($value,) = $this->getBindingInfo($value, $type);
                    $resolvedParams[$name] = $value;
                } else {
                    $resolvedParams[$name] = $value;
                }
            }
        }

        return $resolvedParams;
    }

1647
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1648
     * Creates a new instance of a SQL query builder.
1649
     *
1650
     * @return \Doctrine\DBAL\Query\QueryBuilder
1651 1652 1653 1654 1655
     */
    public function createQueryBuilder()
    {
        return new Query\QueryBuilder($this);
    }
1656 1657

    /**
1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674
     * Ping the server
     *
     * When the server is not available the method returns FALSE.
     * It is responsibility of the developer to handle this case
     * and abort the request or reconnect manually:
     *
     * @example
     *
     *   if ($conn->ping() === false) {
     *      $conn->close();
     *      $conn->connect();
     *   }
     *
     * It is undefined if the underlying driver attempts to reconnect
     * or disconnect when the connection is not available anymore
     * as long it returns TRUE when a reconnect succeeded and
     * FALSE when the connection was dropped.
1675 1676 1677 1678 1679
     *
     * @return bool
     */
    public function ping()
    {
1680
        $this->connect();
1681

1682 1683 1684 1685 1686
        if ($this->_conn instanceof PingableConnection) {
            return $this->_conn->ping();
        }

        try {
1687
            $this->query($this->getDatabasePlatform()->getDummySelectSQL());
till's avatar
till committed
1688

1689 1690
            return true;
        } catch (DBALException $e) {
1691
            return false;
1692
        }
1693
    }
1694
}