Connection.php 50.4 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 33
use PDO;
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;
34
use Doctrine\DBAL\Driver\PingableConnection;
35
use Throwable;
36 37

/**
38
 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
39 40
 * events, transaction isolation levels, configuration, emulated transaction nesting,
 * lazy connecting and more.
41
 *
Benjamin Morel's avatar
Benjamin Morel committed
42 43 44 45 46 47 48 49
 * @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>
50
 */
51
class Connection implements DriverConnection
52
{
53 54 55 56
    /**
     * Constant for transaction isolation level READ UNCOMMITTED.
     */
    const TRANSACTION_READ_UNCOMMITTED = 1;
57

58 59 60 61
    /**
     * Constant for transaction isolation level READ COMMITTED.
     */
    const TRANSACTION_READ_COMMITTED = 2;
62

63 64 65 66
    /**
     * Constant for transaction isolation level REPEATABLE READ.
     */
    const TRANSACTION_REPEATABLE_READ = 3;
67

68 69 70 71
    /**
     * Constant for transaction isolation level SERIALIZABLE.
     */
    const TRANSACTION_SERIALIZABLE = 4;
72

73 74
    /**
     * Represents an array of ints to be expanded by Doctrine SQL parsing.
75
     *
Benjamin Morel's avatar
Benjamin Morel committed
76
     * @var integer
77 78
     */
    const PARAM_INT_ARRAY = 101;
79

80 81
    /**
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
82
     *
Benjamin Morel's avatar
Benjamin Morel committed
83
     * @var integer
84 85
     */
    const PARAM_STR_ARRAY = 102;
86

87 88
    /**
     * Offset by which PARAM_* constants are detected as arrays of the param type.
89
     *
Benjamin Morel's avatar
Benjamin Morel committed
90
     * @var integer
91 92
     */
    const ARRAY_PARAM_OFFSET = 100;
93

romanb's avatar
romanb committed
94 95 96
    /**
     * The wrapped driver connection.
     *
97
     * @var \Doctrine\DBAL\Driver\Connection
romanb's avatar
romanb committed
98 99
     */
    protected $_conn;
100

romanb's avatar
romanb committed
101
    /**
102
     * @var \Doctrine\DBAL\Configuration
romanb's avatar
romanb committed
103 104
     */
    protected $_config;
105

romanb's avatar
romanb committed
106
    /**
107
     * @var \Doctrine\Common\EventManager
romanb's avatar
romanb committed
108 109
     */
    protected $_eventManager;
110

111
    /**
112
     * @var \Doctrine\DBAL\Query\Expression\ExpressionBuilder
113 114
     */
    protected $_expr;
115

romanb's avatar
romanb committed
116 117 118 119 120
    /**
     * Whether or not a connection has been established.
     *
     * @var boolean
     */
121
    private $_isConnected = false;
122

123
    /**
124
     * The current auto-commit mode of this connection.
125 126 127 128 129
     *
     * @var boolean
     */
    private $autoCommit = true;

130 131 132 133 134 135 136 137 138 139 140 141 142 143
    /**
     * The transaction nesting level.
     *
     * @var integer
     */
    private $_transactionNestingLevel = 0;

    /**
     * The currently active transaction isolation level.
     *
     * @var integer
     */
    private $_transactionIsolationLevel;

144
    /**
Benjamin Morel's avatar
Benjamin Morel committed
145
     * If nested transactions should use savepoints.
146
     *
147
     * @var boolean
148
     */
149
    private $_nestTransactionsWithSavepoints = false;
Lukas Kahwe Smith's avatar
Lukas Kahwe Smith committed
150

romanb's avatar
romanb committed
151 152 153 154 155
    /**
     * The parameters used during creation of the Connection instance.
     *
     * @var array
     */
156
    private $_params = [];
157

romanb's avatar
romanb committed
158 159 160 161
    /**
     * The DatabasePlatform object that provides information about the
     * database platform used by the connection.
     *
162
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
romanb's avatar
romanb committed
163
     */
164
    private $platform;
165

romanb's avatar
romanb committed
166 167 168
    /**
     * The schema manager.
     *
169
     * @var \Doctrine\DBAL\Schema\AbstractSchemaManager
romanb's avatar
romanb committed
170 171
     */
    protected $_schemaManager;
172

romanb's avatar
romanb committed
173
    /**
romanb's avatar
romanb committed
174 175
     * The used DBAL driver.
     *
176
     * @var \Doctrine\DBAL\Driver
romanb's avatar
romanb committed
177 178
     */
    protected $_driver;
179

180
    /**
181
     * Flag that indicates whether the current transaction is marked for rollback only.
182
     *
183
     * @var boolean
184
     */
185 186
    private $_isRollbackOnly = false;

Benjamin Morel's avatar
Benjamin Morel committed
187 188 189
    /**
     * @var integer
     */
190
    protected $defaultFetchMode = PDO::FETCH_ASSOC;
191

romanb's avatar
romanb committed
192 193 194
    /**
     * Initializes a new instance of the Connection class.
     *
Benjamin Morel's avatar
Benjamin Morel committed
195 196 197 198 199 200
     * @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
201 202 203 204 205 206
     */
    public function __construct(array $params, Driver $driver, Configuration $config = null,
            EventManager $eventManager = null)
    {
        $this->_driver = $driver;
        $this->_params = $params;
207

romanb's avatar
romanb committed
208 209 210
        if (isset($params['pdo'])) {
            $this->_conn = $params['pdo'];
            $this->_isConnected = true;
211
            unset($this->_params['pdo']);
romanb's avatar
romanb committed
212
        }
213

214 215
        if (isset($params["platform"])) {
            if ( ! $params['platform'] instanceof Platforms\AbstractPlatform) {
216
                throw DBALException::invalidPlatformType($params['platform']);
217 218 219 220 221 222
            }

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

romanb's avatar
romanb committed
223 224 225
        // Create default config and event manager if none given
        if ( ! $config) {
            $config = new Configuration();
romanb's avatar
romanb committed
226
        }
227

romanb's avatar
romanb committed
228
        if ( ! $eventManager) {
romanb's avatar
romanb committed
229
            $eventManager = new EventManager();
romanb's avatar
romanb committed
230
        }
231

romanb's avatar
romanb committed
232
        $this->_config = $config;
romanb's avatar
romanb committed
233
        $this->_eventManager = $eventManager;
234

235
        $this->_expr = new Query\Expression\ExpressionBuilder($this);
236

237
        $this->autoCommit = $config->getAutoCommit();
romanb's avatar
romanb committed
238
    }
romanb's avatar
romanb committed
239

240
    /**
romanb's avatar
romanb committed
241
     * Gets the parameters used during instantiation.
242
     *
Benjamin Morel's avatar
Benjamin Morel committed
243
     * @return array
244 245 246 247 248 249
     */
    public function getParams()
    {
        return $this->_params;
    }

romanb's avatar
romanb committed
250
    /**
romanb's avatar
romanb committed
251
     * Gets the name of the database this Connection is connected to.
romanb's avatar
romanb committed
252
     *
Benjamin Morel's avatar
Benjamin Morel committed
253
     * @return string
romanb's avatar
romanb committed
254 255 256 257 258
     */
    public function getDatabase()
    {
        return $this->_driver->getDatabase($this);
    }
259

260 261
    /**
     * Gets the hostname of the currently connected database.
262
     *
Benjamin Morel's avatar
Benjamin Morel committed
263
     * @return string|null
264 265 266
     */
    public function getHost()
    {
267
        return $this->_params['host'] ?? null;
268
    }
269

270 271
    /**
     * Gets the port of the currently connected database.
272
     *
273 274 275 276
     * @return mixed
     */
    public function getPort()
    {
277
        return $this->_params['port'] ?? null;
278
    }
279

280 281
    /**
     * Gets the username used by this connection.
282
     *
Benjamin Morel's avatar
Benjamin Morel committed
283
     * @return string|null
284 285 286
     */
    public function getUsername()
    {
287
        return $this->_params['user'] ?? null;
288
    }
289

290 291
    /**
     * Gets the password used by this connection.
292
     *
Benjamin Morel's avatar
Benjamin Morel committed
293
     * @return string|null
294 295 296
     */
    public function getPassword()
    {
297
        return $this->_params['password'] ?? null;
298
    }
romanb's avatar
romanb committed
299 300 301 302

    /**
     * Gets the DBAL driver instance.
     *
303
     * @return \Doctrine\DBAL\Driver
romanb's avatar
romanb committed
304 305 306 307 308 309 310 311 312
     */
    public function getDriver()
    {
        return $this->_driver;
    }

    /**
     * Gets the Configuration used by the Connection.
     *
313
     * @return \Doctrine\DBAL\Configuration
romanb's avatar
romanb committed
314 315 316 317 318 319 320 321 322
     */
    public function getConfiguration()
    {
        return $this->_config;
    }

    /**
     * Gets the EventManager used by the Connection.
     *
323
     * @return \Doctrine\Common\EventManager
romanb's avatar
romanb committed
324 325 326 327 328 329 330 331 332
     */
    public function getEventManager()
    {
        return $this->_eventManager;
    }

    /**
     * Gets the DatabasePlatform for the connection.
     *
333
     * @return \Doctrine\DBAL\Platforms\AbstractPlatform
334 335
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
336 337 338
     */
    public function getDatabasePlatform()
    {
339
        if (null === $this->platform) {
340
            $this->detectDatabasePlatform();
341 342 343
        }

        return $this->platform;
romanb's avatar
romanb committed
344
    }
345

346 347 348
    /**
     * Gets the ExpressionBuilder for the connection.
     *
349
     * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
350 351 352 353 354
     */
    public function getExpressionBuilder()
    {
        return $this->_expr;
    }
355

romanb's avatar
romanb committed
356 357 358
    /**
     * Establishes the connection with the database.
     *
359 360
     * @return boolean TRUE if the connection was successfully established, FALSE if
     *                 the connection is already open.
romanb's avatar
romanb committed
361 362 363
     */
    public function connect()
    {
364 365 366
        if ($this->_isConnected) {
            return false;
        }
romanb's avatar
romanb committed
367

368 369 370
        $driverOptions = $this->_params['driverOptions'] ?? [];
        $user = $this->_params['user'] ?? null;
        $password = $this->_params['password'] ?? null;
romanb's avatar
romanb committed
371 372

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

375 376 377 378
        if (false === $this->autoCommit) {
            $this->beginTransaction();
        }

379
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
380
            $eventArgs = new Event\ConnectionEventArgs($this);
381 382 383
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
        }

romanb's avatar
romanb committed
384 385 386
        return true;
    }

387 388 389 390 391 392 393 394 395
    /**
     * 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()
    {
396
        $version = $this->getDatabasePlatformVersion();
397

398 399
        if (null !== $version) {
            $this->platform = $this->_driver->createDatabasePlatformForVersion($version);
400
        } else {
401
            $this->platform = $this->_driver->getDatabasePlatform();
402 403 404 405 406 407 408 409 410 411 412 413 414 415
        }

        $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
416
     *
417
     * @throws Exception
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
     */
    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) {
433 434 435 436 437 438 439 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
            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;
            }

466 467
        }

468 469 470 471 472 473 474 475 476 477
        return $this->getServerVersion();
    }

    /**
     * Returns the database server version if the underlying driver supports it.
     *
     * @return string|null
     */
    private function getServerVersion()
    {
478 479 480 481 482 483 484 485 486 487 488
        // Automatic platform version detection.
        if ($this->_conn instanceof ServerInfoAwareConnection &&
            ! $this->_conn->requiresQueryForServerVersion()
        ) {
            return $this->_conn->getServerVersion();
        }

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

489 490 491 492 493 494 495
    /**
     * Returns the current auto-commit mode for this connection.
     *
     * @return boolean True if auto-commit mode is currently enabled for this connection, false otherwise.
     *
     * @see    setAutoCommit
     */
496
    public function isAutoCommit()
497
    {
498
        return true === $this->autoCommit;
499 500 501 502 503 504 505 506 507 508 509 510 511 512
    }

    /**
     * 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.
     *
     * @param boolean $autoCommit True to enable auto-commit mode; false to disable it.
     *
513
     * @see   isAutoCommit
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
     */
    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();
        }
    }

532
    /**
Benjamin Morel's avatar
Benjamin Morel committed
533
     * Sets the fetch mode.
534
     *
535
     * @param integer $fetchMode
Benjamin Morel's avatar
Benjamin Morel committed
536 537
     *
     * @return void
538
     */
539
    public function setFetchMode($fetchMode)
540
    {
541
        $this->defaultFetchMode = $fetchMode;
542 543
    }

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

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

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

    /**
     * Whether an actual connection to the database is established.
     *
     * @return boolean
     */
    public function isConnected()
    {
        return $this->_isConnected;
    }

604 605
    /**
     * Checks whether a transaction is currently active.
606
     *
607 608 609 610 611 612 613
     * @return boolean TRUE if a transaction is currently active, FALSE otherwise.
     */
    public function isTransactionActive()
    {
        return $this->_transactionNestingLevel > 0;
    }

614
    /**
615
     * Gathers conditions for an update or delete call.
616 617 618 619
     *
     * @param array $identifiers Input array of columns to values
     *
     * @return string[][] a triplet with:
620
     *                    - the first key being the columns
621
     *                    - the second key being the values
622
     *                    - the third key being the conditions
623
     */
624
    private function gatherConditions(array $identifiers)
625 626 627
    {
        $columns = [];
        $values = [];
628
        $conditions = [];
629 630 631

        foreach ($identifiers as $columnName => $value) {
            if (null === $value) {
632
                $conditions[] = $this->getDatabasePlatform()->getIsNullExpression($columnName);
633 634 635 636 637
                continue;
            }

            $columns[] = $columnName;
            $values[] = $value;
638
            $conditions[] = $columnName . ' = ?';
639 640
        }

641
        return [$columns, $values, $conditions];
642 643
    }

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

664
        list($columns, $values, $conditions) = $this->gatherConditions($identifier);
665

666
        return $this->executeUpdate(
667 668 669
            'DELETE FROM ' . $tableExpression . ' WHERE ' . implode(' AND ', $conditions),
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
670
        );
671 672
    }

romanb's avatar
romanb committed
673 674 675 676 677 678 679
    /**
     * Closes the connection.
     *
     * @return void
     */
    public function close()
    {
680
        $this->_conn = null;
681

romanb's avatar
romanb committed
682 683 684
        $this->_isConnected = false;
    }

685 686 687
    /**
     * Sets the transaction isolation level.
     *
688
     * @param integer $level The level to set.
Benjamin Morel's avatar
Benjamin Morel committed
689
     *
690
     * @return integer
691 692 693 694
     */
    public function setTransactionIsolation($level)
    {
        $this->_transactionIsolationLevel = $level;
695

696
        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
697 698 699 700 701
    }

    /**
     * Gets the currently active transaction isolation level.
     *
702
     * @return integer The current transaction isolation level.
703 704 705
     */
    public function getTransactionIsolation()
    {
706 707 708 709
        if (null === $this->_transactionIsolationLevel) {
            $this->_transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
        }

710 711 712
        return $this->_transactionIsolationLevel;
    }

romanb's avatar
romanb committed
713
    /**
714
     * Executes an SQL UPDATE statement on a table.
romanb's avatar
romanb committed
715
     *
716 717 718
     * Table expression and columns are not escaped and are not safe for user-input.
     *
     * @param string $tableExpression  The expression of the table to update quoted or unquoted.
Benjamin Morel's avatar
Benjamin Morel committed
719 720 721 722
     * @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.
     *
723
     * @return integer The number of affected rows.
724 725
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
726
     */
727
    public function update($tableExpression, array $data, array $identifier, array $types = [])
romanb's avatar
romanb committed
728
    {
729 730 731
        $setColumns = [];
        $setValues = [];
        $set = [];
732

romanb's avatar
romanb committed
733
        foreach ($data as $columnName => $value) {
734 735
            $setColumns[] = $columnName;
            $setValues[] = $value;
736
            $set[] = $columnName . ' = ?';
romanb's avatar
romanb committed
737 738
        }

739 740 741
        list($conditionColumns, $conditionValues, $conditions) = $this->gatherConditions($identifier);
        $columns = array_merge($setColumns, $conditionColumns);
        $values = array_merge($setValues, $conditionValues);
742

743
        if (is_string(key($types))) {
744
            $types = $this->extractTypeValues($columns, $types);
745
        }
romanb's avatar
romanb committed
746

747
        $sql  = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
748
                . ' WHERE ' . implode(' AND ', $conditions);
romanb's avatar
romanb committed
749

750
        return $this->executeUpdate($sql, $values, $types);
romanb's avatar
romanb committed
751 752 753 754 755
    }

    /**
     * Inserts a table row with specified data.
     *
756 757 758
     * 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.
Benjamin Morel's avatar
Benjamin Morel committed
759 760 761
     * @param array  $data      An associative array containing column-value pairs.
     * @param array  $types     Types of the inserted data.
     *
762
     * @return integer The number of affected rows.
763 764
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
765
     */
766
    public function insert($tableExpression, array $data, array $types = [])
romanb's avatar
romanb committed
767
    {
768
        if (empty($data)) {
769
            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' ()' . ' VALUES ()');
770 771
        }

772 773 774
        $columns = [];
        $values = [];
        $set = [];
775 776

        foreach ($data as $columnName => $value) {
777 778 779
            $columns[] = $columnName;
            $values[] = $value;
            $set[] = '?';
780 781
        }

782
        return $this->executeUpdate(
783 784 785 786
            'INSERT INTO ' . $tableExpression . ' (' . implode(', ', $columns) . ')' .
            ' VALUES (' . implode(', ', $set) . ')',
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
787
        );
romanb's avatar
romanb committed
788 789
    }

790
    /**
791
     * Extract ordered type list from an ordered column list and type map.
792
     *
793
     * @param array $columnList
794 795 796 797
     * @param array $types
     *
     * @return array
     */
798
    private function extractTypeValues(array $columnList, array $types)
799
    {
800
        $typeValues = [];
801

802 803 804
        foreach ($columnList as $columnIndex => $columnName) {
            $typeValues[] = isset($types[$columnName])
                ? $types[$columnName]
805 806 807 808 809 810
                : \PDO::PARAM_STR;
        }

        return $typeValues;
    }

romanb's avatar
romanb committed
811
    /**
Benjamin Morel's avatar
Benjamin Morel committed
812
     * Quotes a string so it can be safely used as a table or column name, even if
romanb's avatar
romanb committed
813 814 815 816
     * it is a reserved name.
     *
     * Delimiting style depends on the underlying database platform that is being used.
     *
817 818
     * 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
819 820
     * problems than they solve.
     *
821
     * @param string $str The name to be quoted.
Benjamin Morel's avatar
Benjamin Morel committed
822
     *
823
     * @return string The quoted name.
romanb's avatar
romanb committed
824 825 826
     */
    public function quoteIdentifier($str)
    {
827
        return $this->getDatabasePlatform()->quoteIdentifier($str);
romanb's avatar
romanb committed
828 829 830 831 832
    }

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

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

844
        return $this->_conn->quote($value, $bindingType);
romanb's avatar
romanb committed
845 846 847
    }

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

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

878
        $stmt->setFetchMode($this->defaultFetchMode);
879 880

        return $stmt;
romanb's avatar
romanb committed
881 882 883
    }

    /**
Pascal Borreli's avatar
Pascal Borreli committed
884
     * Executes an, optionally parametrized, SQL query.
romanb's avatar
romanb committed
885
     *
Pascal Borreli's avatar
Pascal Borreli committed
886
     * If the query is parametrized, a prepared statement is used.
887 888
     * If an SQLLogger is configured, the execution is logged.
     *
Benjamin Morel's avatar
Benjamin Morel committed
889 890 891 892 893
     * @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.
     *
894
     * @return \Doctrine\DBAL\Driver\Statement The executed statement.
Benjamin Morel's avatar
Benjamin Morel committed
895 896
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
897
     */
898
    public function executeQuery($query, array $params = [], $types = [], QueryCacheProfile $qcp = null)
romanb's avatar
romanb committed
899
    {
900
        if ($qcp !== null) {
901
            return $this->executeCacheQuery($query, $params, $types, $qcp);
902 903
        }

romanb's avatar
romanb committed
904 905
        $this->connect();

906 907 908
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
909
        }
910

911 912 913
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
914

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

929
        $stmt->setFetchMode($this->defaultFetchMode);
930

931 932
        if ($logger) {
            $logger->stopQuery();
933 934
        }

romanb's avatar
romanb committed
935
        return $stmt;
romanb's avatar
romanb committed
936
    }
937

938
    /**
Benjamin Morel's avatar
Benjamin Morel committed
939 940 941 942 943 944
     * 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.
945 946
     *
     * @return \Doctrine\DBAL\Driver\ResultStatement
Benjamin Morel's avatar
Benjamin Morel committed
947 948
     *
     * @throws \Doctrine\DBAL\Cache\CacheException
949 950 951 952
     */
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp)
    {
        $resultCache = $qcp->getResultCacheDriver() ?: $this->_config->getResultCacheImpl();
953
        if ( ! $resultCache) {
954 955 956
            throw CacheException::noResultDriverConfigured();
        }

957
        list($cacheKey, $realKey) = $qcp->generateCacheKeys($query, $params, $types, $this->getParams());
958 959 960 961 962

        // 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])) {
963
                $stmt = new ArrayStatement($data[$realKey]);
Steve Müller's avatar
Steve Müller committed
964
            } elseif (array_key_exists($realKey, $data)) {
965
                $stmt = new ArrayStatement([]);
966 967
            }
        }
968 969 970 971 972

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

973
        $stmt->setFetchMode($this->defaultFetchMode);
974 975

        return $stmt;
976 977
    }

978
    /**
Pascal Borreli's avatar
Pascal Borreli committed
979
     * Executes an, optionally parametrized, SQL query and returns the result,
980
     * applying a given projection/transformation function on each row of the result.
981
     *
Benjamin Morel's avatar
Benjamin Morel committed
982 983 984 985 986 987
     * @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.
     *
988
     * @return array The projected result of the query.
989
     */
990
    public function project($query, array $params, Closure $function)
991
    {
992
        $result = [];
993
        $stmt = $this->executeQuery($query, $params);
994

995
        while ($row = $stmt->fetch()) {
996
            $result[] = $function($row);
997
        }
998

999
        $stmt->closeCursor();
1000

1001 1002
        return $result;
    }
romanb's avatar
romanb committed
1003 1004

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

1015 1016
        $args = func_get_args();

1017
        $logger = $this->_config->getSQLLogger();
1018 1019 1020 1021
        if ($logger) {
            $logger->startQuery($args[0]);
        }

1022
        try {
1023
            $statement = $this->_conn->query(...$args);
Filip Procházka's avatar
Filip Procházka committed
1024
        } catch (Exception $ex) {
1025
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $args[0]);
1026 1027
        }

1028
        $statement->setFetchMode($this->defaultFetchMode);
1029 1030 1031 1032 1033 1034

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

        return $statement;
1035 1036 1037 1038 1039
    }

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

1055 1056 1057
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
1058 1059
        }

1060 1061 1062
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
1063

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

1079 1080
        if ($logger) {
            $logger->stopQuery();
1081 1082
        }

romanb's avatar
romanb committed
1083
        return $result;
romanb's avatar
romanb committed
1084 1085
    }

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

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

1104 1105
        try {
            $result = $this->_conn->exec($statement);
Filip Procházka's avatar
Filip Procházka committed
1106
        } catch (Exception $ex) {
1107
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1108
        }
1109 1110 1111 1112 1113 1114

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

        return $result;
1115 1116
    }

1117 1118 1119 1120 1121 1122 1123 1124 1125 1126
    /**
     * Returns the current transaction nesting level.
     *
     * @return integer The nesting level. A value of 0 means there's no active transaction.
     */
    public function getTransactionNestingLevel()
    {
        return $this->_transactionNestingLevel;
    }

romanb's avatar
romanb committed
1127
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1128
     * Fetches the SQLSTATE associated with the last database operation.
romanb's avatar
romanb committed
1129
     *
1130
     * @return integer The last error code.
romanb's avatar
romanb committed
1131 1132 1133 1134
     */
    public function errorCode()
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1135

romanb's avatar
romanb committed
1136 1137 1138 1139
        return $this->_conn->errorCode();
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1140
     * Fetches extended error information associated with the last database operation.
romanb's avatar
romanb committed
1141
     *
1142
     * @return array The last error information.
romanb's avatar
romanb committed
1143 1144 1145 1146
     */
    public function errorInfo()
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1147

romanb's avatar
romanb committed
1148 1149 1150 1151 1152 1153 1154 1155
        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,
1156 1157
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
     * columns or sequences.
romanb's avatar
romanb committed
1158
     *
Benjamin Morel's avatar
Benjamin Morel committed
1159 1160
     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
     *
1161
     * @return string A string representation of the last inserted ID.
romanb's avatar
romanb committed
1162
     */
romanb's avatar
romanb committed
1163 1164 1165
    public function lastInsertId($seqName = null)
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1166

romanb's avatar
romanb committed
1167 1168
        return $this->_conn->lastInsertId($seqName);
    }
1169

1170 1171 1172 1173 1174 1175 1176 1177
    /**
     * 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
1178 1179
     * @param \Closure $func The function to execute transactionally.
     *
1180
     * @return mixed The value returned by $func
Benjamin Morel's avatar
Benjamin Morel committed
1181
     *
1182 1183
     * @throws Exception
     * @throws Throwable
1184 1185 1186 1187 1188
     */
    public function transactional(Closure $func)
    {
        $this->beginTransaction();
        try {
1189
            $res = $func($this);
1190
            $this->commit();
1191
            return $res;
1192
        } catch (Exception $e) {
1193
            $this->rollBack();
1194 1195 1196
            throw $e;
        } catch (Throwable $e) {
            $this->rollBack();
1197 1198 1199 1200
            throw $e;
        }
    }

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

1216
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1217
            throw ConnectionException::savepointsNotSupported();
1218 1219
        }

1220
        $this->_nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1221 1222 1223
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1224
     * Gets if nested transactions should use savepoints.
1225 1226 1227 1228 1229 1230 1231 1232
     *
     * @return boolean
     */
    public function getNestTransactionsWithSavepoints()
    {
        return $this->_nestTransactionsWithSavepoints;
    }

1233 1234 1235 1236
    /**
     * 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
1237
     * @return mixed A string with the savepoint name or false.
1238
     */
1239 1240 1241
    protected function _getNestedTransactionSavePointName()
    {
        return 'DOCTRINE2_SAVEPOINT_'.$this->_transactionNestingLevel;
1242 1243
    }

1244 1245 1246 1247 1248 1249 1250 1251 1252
    /**
     * Starts a transaction by suspending auto-commit mode.
     *
     * @return void
     */
    public function beginTransaction()
    {
        $this->connect();

1253 1254
        ++$this->_transactionNestingLevel;

1255 1256
        $logger = $this->_config->getSQLLogger();

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

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

        $this->connect();

1295 1296
        $logger = $this->_config->getSQLLogger();

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

        --$this->_transactionNestingLevel;
1316 1317 1318 1319 1320 1321 1322 1323 1324

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

    /**
     * Commits all current nesting transactions.
     */
1325
    private function commitAll()
1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337
    {
        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();
        }
1338 1339 1340
    }

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

        $this->connect();

1353 1354
        $logger = $this->_config->getSQLLogger();

1355
        if ($this->_transactionNestingLevel == 1) {
1356 1357 1358
            if ($logger) {
                $logger->startQuery('"ROLLBACK"');
            }
1359
            $this->_transactionNestingLevel = 0;
1360
            $this->_conn->rollBack();
1361
            $this->_isRollbackOnly = false;
1362 1363 1364
            if ($logger) {
                $logger->stopQuery();
            }
1365 1366 1367 1368

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

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

1399
        $this->_conn->exec($this->platform->createSavePoint($savepoint));
1400 1401 1402
    }

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

1417 1418
        if ($this->platform->supportsReleaseSavepoints()) {
            $this->_conn->exec($this->platform->releaseSavePoint($savepoint));
1419
        }
1420 1421 1422
    }

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

1437
        $this->_conn->exec($this->platform->rollbackSavePoint($savepoint));
1438 1439
    }

romanb's avatar
romanb committed
1440 1441 1442
    /**
     * Gets the wrapped driver connection.
     *
1443
     * @return \Doctrine\DBAL\Driver\Connection
romanb's avatar
romanb committed
1444 1445 1446 1447
     */
    public function getWrappedConnection()
    {
        $this->connect();
1448

romanb's avatar
romanb committed
1449 1450
        return $this->_conn;
    }
1451

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

romanb's avatar
romanb committed
1464 1465
        return $this->_schemaManager;
    }
1466

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

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

1496 1497 1498
        return $this->_isRollbackOnly;
    }

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

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

    /**
     * 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.
1530
     *
Benjamin Morel's avatar
Benjamin Morel committed
1531 1532 1533 1534 1535 1536
     * @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
     *
1537 1538
     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
     *           raw PDOStatement instances.
1539
     */
1540
    private function _bindTypedValues($stmt, array $params, array $types)
1541 1542 1543 1544
    {
        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
        if (is_int(key($params))) {
            // Positional parameters
1545
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1546
            $bindIndex = 1;
1547
            foreach ($params as $value) {
1548 1549 1550
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
                    $type = $types[$typeIndex];
1551
                    list($value, $bindingType) = $this->getBindingInfo($value, $type);
1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562
                    $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];
1563
                    list($value, $bindingType) = $this->getBindingInfo($value, $type);
1564 1565 1566 1567 1568 1569 1570
                    $stmt->bindValue($name, $value, $bindingType);
                } else {
                    $stmt->bindValue($name, $value);
                }
            }
        }
    }
1571 1572 1573 1574

    /**
     * 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
1575 1576 1577 1578
     * @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.
1579 1580 1581 1582 1583 1584 1585
     */
    private function getBindingInfo($value, $type)
    {
        if (is_string($type)) {
            $type = Type::getType($type);
        }
        if ($type instanceof Type) {
1586
            $value = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1587 1588 1589 1590
            $bindingType = $type->getBindingType();
        } else {
            $bindingType = $type; // PDO::PARAM_* constants
        }
Benjamin Morel's avatar
Benjamin Morel committed
1591

1592
        return [$value, $bindingType];
1593 1594
    }

1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607
    /**
     * 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)
    {
1608
        $resolvedParams = [];
1609 1610 1611 1612 1613 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

        // 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;
    }

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

    /**
1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669
     * 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.
1670 1671 1672 1673 1674
     *
     * @return bool
     */
    public function ping()
    {
1675
        $this->connect();
1676

1677 1678 1679 1680 1681
        if ($this->_conn instanceof PingableConnection) {
            return $this->_conn->ping();
        }

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

1684 1685
            return true;
        } catch (DBALException $e) {
1686
            return false;
1687
        }
1688
    }
1689
}