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 = array();
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()
    {
jwage's avatar
jwage committed
267
        return isset($this->_params['host']) ? $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()
    {
jwage's avatar
jwage committed
277
        return isset($this->_params['port']) ? $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()
    {
jwage's avatar
jwage committed
287
        return isset($this->_params['user']) ? $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()
    {
jwage's avatar
jwage committed
297
        return isset($this->_params['password']) ? $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
romanb's avatar
romanb committed
334 335 336
     */
    public function getDatabasePlatform()
    {
337
        if (null === $this->platform) {
338
            $this->detectDatabasePlatform();
339 340 341
        }

        return $this->platform;
romanb's avatar
romanb committed
342
    }
343

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

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

        $driverOptions = isset($this->_params['driverOptions']) ?
367
            $this->_params['driverOptions'] : array();
romanb's avatar
romanb committed
368
        $user = isset($this->_params['user']) ? $this->_params['user'] : null;
romanb's avatar
romanb committed
369
        $password = isset($this->_params['password']) ?
370
            $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 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
        }

        $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
     */
    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) {
431 432 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
            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;
            }

464 465
        }

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

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

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

487 488 489 490 491 492 493
    /**
     * 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
     */
494
    public function isAutoCommit()
495
    {
496
        return true === $this->autoCommit;
497 498 499 500 501 502 503 504 505 506 507 508 509 510
    }

    /**
     * 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.
     *
511
     * @see   isAutoCommit
512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
     */
    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();
        }
    }

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

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

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

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

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

598 599
    /**
     * Checks whether a transaction is currently active.
600
     *
601 602 603 604 605 606 607
     * @return boolean TRUE if a transaction is currently active, FALSE otherwise.
     */
    public function isTransactionActive()
    {
        return $this->_transactionNestingLevel > 0;
    }

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

        foreach ($identifiers as $columnName => $value) {
            if (null === $value) {
626
                $conditions[] = $this->getDatabasePlatform()->getIsNullExpression($columnName);
627 628 629 630 631
                continue;
            }

            $columns[] = $columnName;
            $values[] = $value;
632
            $conditions[] = $columnName . ' = ?';
633 634
        }

635
        return [$columns, $values, $conditions];
636 637
    }

638 639
    /**
     * Executes an SQL DELETE statement on a table.
romanb's avatar
romanb committed
640
     *
641 642 643
     * 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
644 645 646
     * @param array  $identifier The deletion criteria. An associative array containing column-value pairs.
     * @param array  $types      The types of identifiers.
     *
647
     * @return integer The number of affected rows.
648
     *
649
     * @throws InvalidArgumentException
650
     */
651
    public function delete($tableExpression, array $identifier, array $types = array())
652 653
    {
        if (empty($identifier)) {
654
            throw InvalidArgumentException::fromEmptyCriteria();
655 656
        }

657
        list($columns, $values, $conditions) = $this->gatherConditions($identifier);
658

659
        return $this->executeUpdate(
660 661 662
            'DELETE FROM ' . $tableExpression . ' WHERE ' . implode(' AND ', $conditions),
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
663
        );
664 665
    }

romanb's avatar
romanb committed
666 667 668 669 670 671 672
    /**
     * Closes the connection.
     *
     * @return void
     */
    public function close()
    {
673
        $this->_conn = null;
674

romanb's avatar
romanb committed
675 676 677
        $this->_isConnected = false;
    }

678 679 680
    /**
     * Sets the transaction isolation level.
     *
681
     * @param integer $level The level to set.
Benjamin Morel's avatar
Benjamin Morel committed
682
     *
683
     * @return integer
684 685 686 687
     */
    public function setTransactionIsolation($level)
    {
        $this->_transactionIsolationLevel = $level;
688

689
        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
690 691 692 693 694
    }

    /**
     * Gets the currently active transaction isolation level.
     *
695
     * @return integer The current transaction isolation level.
696 697 698
     */
    public function getTransactionIsolation()
    {
699 700 701 702
        if (null === $this->_transactionIsolationLevel) {
            $this->_transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
        }

703 704 705
        return $this->_transactionIsolationLevel;
    }

romanb's avatar
romanb committed
706
    /**
707
     * Executes an SQL UPDATE statement on a table.
romanb's avatar
romanb committed
708
     *
709 710 711
     * 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
712 713 714 715
     * @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.
     *
716
     * @return integer The number of affected rows.
romanb's avatar
romanb committed
717
     */
718
    public function update($tableExpression, array $data, array $identifier, array $types = array())
romanb's avatar
romanb committed
719
    {
720 721
        $setColumns = array();
        $setValues = array();
romanb's avatar
romanb committed
722
        $set = array();
723

romanb's avatar
romanb committed
724
        foreach ($data as $columnName => $value) {
725 726
            $setColumns[] = $columnName;
            $setValues[] = $value;
727
            $set[] = $columnName . ' = ?';
romanb's avatar
romanb committed
728 729
        }

730 731 732
        list($conditionColumns, $conditionValues, $conditions) = $this->gatherConditions($identifier);
        $columns = array_merge($setColumns, $conditionColumns);
        $values = array_merge($setValues, $conditionValues);
733

734
        if (is_string(key($types))) {
735
            $types = $this->extractTypeValues($columns, $types);
736
        }
romanb's avatar
romanb committed
737

738
        $sql  = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
739
                . ' WHERE ' . implode(' AND ', $conditions);
romanb's avatar
romanb committed
740

741
        return $this->executeUpdate($sql, $values, $types);
romanb's avatar
romanb committed
742 743 744 745 746
    }

    /**
     * Inserts a table row with specified data.
     *
747 748 749
     * 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
750 751 752
     * @param array  $data      An associative array containing column-value pairs.
     * @param array  $types     Types of the inserted data.
     *
753
     * @return integer The number of affected rows.
romanb's avatar
romanb committed
754
     */
755
    public function insert($tableExpression, array $data, array $types = array())
romanb's avatar
romanb committed
756
    {
757
        if (empty($data)) {
758
            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' ()' . ' VALUES ()');
759 760
        }

761 762 763
        $columns = array();
        $values = array();
        $set = array();
764 765

        foreach ($data as $columnName => $value) {
766 767 768
            $columns[] = $columnName;
            $values[] = $value;
            $set[] = '?';
769 770
        }

771
        return $this->executeUpdate(
772 773 774 775
            'INSERT INTO ' . $tableExpression . ' (' . implode(', ', $columns) . ')' .
            ' VALUES (' . implode(', ', $set) . ')',
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
776
        );
romanb's avatar
romanb committed
777 778
    }

779
    /**
780
     * Extract ordered type list from an ordered column list and type map.
781
     *
782
     * @param array $columnList
783 784 785 786
     * @param array $types
     *
     * @return array
     */
787
    private function extractTypeValues(array $columnList, array $types)
788 789 790
    {
        $typeValues = array();

791 792 793
        foreach ($columnList as $columnIndex => $columnName) {
            $typeValues[] = isset($types[$columnName])
                ? $types[$columnName]
794 795 796 797 798 799
                : \PDO::PARAM_STR;
        }

        return $typeValues;
    }

romanb's avatar
romanb committed
800
    /**
Benjamin Morel's avatar
Benjamin Morel committed
801
     * Quotes a string so it can be safely used as a table or column name, even if
romanb's avatar
romanb committed
802 803 804 805
     * it is a reserved name.
     *
     * Delimiting style depends on the underlying database platform that is being used.
     *
806 807
     * 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
808 809
     * problems than they solve.
     *
810
     * @param string $str The name to be quoted.
Benjamin Morel's avatar
Benjamin Morel committed
811
     *
812
     * @return string The quoted name.
romanb's avatar
romanb committed
813 814 815
     */
    public function quoteIdentifier($str)
    {
816
        return $this->getDatabasePlatform()->quoteIdentifier($str);
romanb's avatar
romanb committed
817 818 819 820 821
    }

    /**
     * Quotes a given input parameter.
     *
Benjamin Morel's avatar
Benjamin Morel committed
822
     * @param mixed       $input The parameter to be quoted.
helsner's avatar
helsner committed
823
     * @param int|null $type  The type of the parameter.
Benjamin Morel's avatar
Benjamin Morel committed
824
     *
825
     * @return string The quoted parameter.
romanb's avatar
romanb committed
826 827 828
     */
    public function quote($input, $type = null)
    {
829
        $this->connect();
830 831

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

833
        return $this->_conn->quote($value, $bindingType);
romanb's avatar
romanb committed
834 835 836
    }

    /**
837
     * Prepares and executes an SQL query and returns the result as an associative array.
romanb's avatar
romanb committed
838
     *
Benjamin Morel's avatar
Benjamin Morel committed
839 840 841 842
     * @param string $sql    The SQL query.
     * @param array  $params The query parameters.
     * @param array  $types  The query parameter types.
     *
romanb's avatar
romanb committed
843 844
     * @return array
     */
root's avatar
root committed
845
    public function fetchAll($sql, array $params = array(), $types = array())
romanb's avatar
romanb committed
846
    {
root's avatar
root committed
847
        return $this->executeQuery($sql, $params, $types)->fetchAll();
romanb's avatar
romanb committed
848 849 850 851 852
    }

    /**
     * Prepares an SQL statement.
     *
853
     * @param string $statement The SQL statement to prepare.
Benjamin Morel's avatar
Benjamin Morel committed
854
     *
855
     * @return \Doctrine\DBAL\Statement The prepared statement.
Benjamin Morel's avatar
Benjamin Morel committed
856 857
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
858 859 860
     */
    public function prepare($statement)
    {
861 862
        try {
            $stmt = new Statement($statement, $this);
Filip Procházka's avatar
Filip Procházka committed
863
        } catch (Exception $ex) {
864
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
865 866
        }

867
        $stmt->setFetchMode($this->defaultFetchMode);
868 869

        return $stmt;
romanb's avatar
romanb committed
870 871 872
    }

    /**
Pascal Borreli's avatar
Pascal Borreli committed
873
     * Executes an, optionally parametrized, SQL query.
romanb's avatar
romanb committed
874
     *
Pascal Borreli's avatar
Pascal Borreli committed
875
     * If the query is parametrized, a prepared statement is used.
876 877
     * If an SQLLogger is configured, the execution is logged.
     *
Benjamin Morel's avatar
Benjamin Morel committed
878 879 880 881 882
     * @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.
     *
883
     * @return \Doctrine\DBAL\Driver\Statement The executed statement.
Benjamin Morel's avatar
Benjamin Morel committed
884 885
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
886
     */
887
    public function executeQuery($query, array $params = array(), $types = array(), QueryCacheProfile $qcp = null)
romanb's avatar
romanb committed
888
    {
889
        if ($qcp !== null) {
890
            return $this->executeCacheQuery($query, $params, $types, $qcp);
891 892
        }

romanb's avatar
romanb committed
893 894
        $this->connect();

895 896 897
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
898
        }
899

900 901 902
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
903

904 905 906 907 908 909 910
                $stmt = $this->_conn->prepare($query);
                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
911
            } else {
912
                $stmt = $this->_conn->query($query);
913
            }
Filip Procházka's avatar
Filip Procházka committed
914
        } catch (Exception $ex) {
915
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
916
        }
917

918
        $stmt->setFetchMode($this->defaultFetchMode);
919

920 921
        if ($logger) {
            $logger->stopQuery();
922 923
        }

romanb's avatar
romanb committed
924
        return $stmt;
romanb's avatar
romanb committed
925
    }
926

927
    /**
Benjamin Morel's avatar
Benjamin Morel committed
928 929 930 931 932 933
     * 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.
934 935
     *
     * @return \Doctrine\DBAL\Driver\ResultStatement
Benjamin Morel's avatar
Benjamin Morel committed
936 937
     *
     * @throws \Doctrine\DBAL\Cache\CacheException
938 939 940 941
     */
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp)
    {
        $resultCache = $qcp->getResultCacheDriver() ?: $this->_config->getResultCacheImpl();
942
        if ( ! $resultCache) {
943 944 945
            throw CacheException::noResultDriverConfigured();
        }

946
        list($cacheKey, $realKey) = $qcp->generateCacheKeys($query, $params, $types, $this->getParams());
947 948 949 950 951

        // 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])) {
952
                $stmt = new ArrayStatement($data[$realKey]);
Steve Müller's avatar
Steve Müller committed
953
            } elseif (array_key_exists($realKey, $data)) {
954
                $stmt = new ArrayStatement(array());
955 956
            }
        }
957 958 959 960 961

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

962
        $stmt->setFetchMode($this->defaultFetchMode);
963 964

        return $stmt;
965 966
    }

967
    /**
Pascal Borreli's avatar
Pascal Borreli committed
968
     * Executes an, optionally parametrized, SQL query and returns the result,
969
     * applying a given projection/transformation function on each row of the result.
970
     *
Benjamin Morel's avatar
Benjamin Morel committed
971 972 973 974 975 976
     * @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.
     *
977
     * @return array The projected result of the query.
978
     */
979
    public function project($query, array $params, Closure $function)
980 981
    {
        $result = array();
982
        $stmt = $this->executeQuery($query, $params);
983

984
        while ($row = $stmt->fetch()) {
985
            $result[] = $function($row);
986
        }
987

988
        $stmt->closeCursor();
989

990 991
        return $result;
    }
romanb's avatar
romanb committed
992 993

    /**
994
     * Executes an SQL statement, returning a result set as a Statement object.
995
     *
996
     * @return \Doctrine\DBAL\Driver\Statement
Benjamin Morel's avatar
Benjamin Morel committed
997 998
     *
     * @throws \Doctrine\DBAL\DBALException
999 1000 1001
     */
    public function query()
    {
1002 1003
        $this->connect();

1004 1005
        $args = func_get_args();

1006
        $logger = $this->_config->getSQLLogger();
1007 1008 1009 1010
        if ($logger) {
            $logger->startQuery($args[0]);
        }

1011
        try {
1012
            $statement = $this->_conn->query(...$args);
Filip Procházka's avatar
Filip Procházka committed
1013
        } catch (Exception $ex) {
1014
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $args[0]);
1015 1016
        }

1017
        $statement->setFetchMode($this->defaultFetchMode);
1018 1019 1020 1021 1022 1023

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

        return $statement;
1024 1025 1026 1027 1028
    }

    /**
     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
     * and returns the number of affected rows.
1029
     *
1030
     * This method supports PDO binding types as well as DBAL mapping types.
romanb's avatar
romanb committed
1031
     *
Benjamin Morel's avatar
Benjamin Morel committed
1032 1033 1034 1035
     * @param string $query  The SQL query.
     * @param array  $params The query parameters.
     * @param array  $types  The parameter types.
     *
1036
     * @return integer The number of affected rows.
Benjamin Morel's avatar
Benjamin Morel committed
1037 1038
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
1039
     */
1040
    public function executeUpdate($query, array $params = array(), array $types = array())
romanb's avatar
romanb committed
1041 1042 1043
    {
        $this->connect();

1044 1045 1046
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
1047 1048
        }

1049 1050 1051
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
1052

1053 1054 1055 1056 1057 1058 1059 1060
                $stmt = $this->_conn->prepare($query);
                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
                $result = $stmt->rowCount();
1061
            } else {
1062
                $result = $this->_conn->exec($query);
1063
            }
Filip Procházka's avatar
Filip Procházka committed
1064
        } catch (Exception $ex) {
1065
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
1066
        }
1067

1068 1069
        if ($logger) {
            $logger->stopQuery();
1070 1071
        }

romanb's avatar
romanb committed
1072
        return $result;
romanb's avatar
romanb committed
1073 1074
    }

1075
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1076
     * Executes an SQL statement and return the number of affected rows.
1077
     *
1078
     * @param string $statement
Benjamin Morel's avatar
Benjamin Morel committed
1079
     *
1080
     * @return integer The number of affected rows.
Benjamin Morel's avatar
Benjamin Morel committed
1081 1082
     *
     * @throws \Doctrine\DBAL\DBALException
1083 1084 1085 1086
     */
    public function exec($statement)
    {
        $this->connect();
1087 1088 1089 1090 1091 1092

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

1093 1094
        try {
            $result = $this->_conn->exec($statement);
Filip Procházka's avatar
Filip Procházka committed
1095
        } catch (Exception $ex) {
1096
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1097
        }
1098 1099 1100 1101 1102 1103

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

        return $result;
1104 1105
    }

1106 1107 1108 1109 1110 1111 1112 1113 1114 1115
    /**
     * 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
1116
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1117
     * Fetches the SQLSTATE associated with the last database operation.
romanb's avatar
romanb committed
1118
     *
1119
     * @return integer The last error code.
romanb's avatar
romanb committed
1120 1121 1122 1123
     */
    public function errorCode()
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1124

romanb's avatar
romanb committed
1125 1126 1127 1128
        return $this->_conn->errorCode();
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1129
     * Fetches extended error information associated with the last database operation.
romanb's avatar
romanb committed
1130
     *
1131
     * @return array The last error information.
romanb's avatar
romanb committed
1132 1133 1134 1135
     */
    public function errorInfo()
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1136

romanb's avatar
romanb committed
1137 1138 1139 1140 1141 1142 1143 1144
        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,
1145 1146
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
     * columns or sequences.
romanb's avatar
romanb committed
1147
     *
Benjamin Morel's avatar
Benjamin Morel committed
1148 1149
     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
     *
1150
     * @return string A string representation of the last inserted ID.
romanb's avatar
romanb committed
1151
     */
romanb's avatar
romanb committed
1152 1153 1154
    public function lastInsertId($seqName = null)
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1155

romanb's avatar
romanb committed
1156 1157
        return $this->_conn->lastInsertId($seqName);
    }
1158

1159 1160 1161 1162 1163 1164 1165 1166
    /**
     * 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
1167 1168
     * @param \Closure $func The function to execute transactionally.
     *
1169
     * @return mixed The value returned by $func
Benjamin Morel's avatar
Benjamin Morel committed
1170 1171
     *
     * @throws \Exception
1172 1173 1174 1175 1176
     */
    public function transactional(Closure $func)
    {
        $this->beginTransaction();
        try {
1177
            $res = $func($this);
1178
            $this->commit();
1179
            return $res;
1180
        } catch (Exception $e) {
1181
            $this->rollBack();
1182 1183 1184
            throw $e;
        } catch (Throwable $e) {
            $this->rollBack();
1185 1186 1187 1188
            throw $e;
        }
    }

1189
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1190
     * Sets if nested transactions should use savepoints.
1191
     *
1192
     * @param boolean $nestTransactionsWithSavepoints
Benjamin Morel's avatar
Benjamin Morel committed
1193
     *
1194
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1195 1196
     *
     * @throws \Doctrine\DBAL\ConnectionException
1197 1198 1199
     */
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
    {
1200 1201 1202 1203
        if ($this->_transactionNestingLevel > 0) {
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
        }

1204
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1205
            throw ConnectionException::savepointsNotSupported();
1206 1207
        }

1208
        $this->_nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1209 1210 1211
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1212
     * Gets if nested transactions should use savepoints.
1213 1214 1215 1216 1217 1218 1219 1220
     *
     * @return boolean
     */
    public function getNestTransactionsWithSavepoints()
    {
        return $this->_nestTransactionsWithSavepoints;
    }

1221 1222 1223 1224
    /**
     * 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
1225
     * @return mixed A string with the savepoint name or false.
1226
     */
1227 1228 1229
    protected function _getNestedTransactionSavePointName()
    {
        return 'DOCTRINE2_SAVEPOINT_'.$this->_transactionNestingLevel;
1230 1231
    }

1232 1233 1234 1235 1236 1237 1238 1239 1240
    /**
     * Starts a transaction by suspending auto-commit mode.
     *
     * @return void
     */
    public function beginTransaction()
    {
        $this->connect();

1241 1242
        ++$this->_transactionNestingLevel;

1243 1244
        $logger = $this->_config->getSQLLogger();

1245
        if ($this->_transactionNestingLevel == 1) {
1246 1247 1248
            if ($logger) {
                $logger->startQuery('"START TRANSACTION"');
            }
1249
            $this->_conn->beginTransaction();
1250 1251 1252
            if ($logger) {
                $logger->stopQuery();
            }
Steve Müller's avatar
Steve Müller committed
1253
        } elseif ($this->_nestTransactionsWithSavepoints) {
1254 1255 1256
            if ($logger) {
                $logger->startQuery('"SAVEPOINT"');
            }
1257
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1258 1259 1260
            if ($logger) {
                $logger->stopQuery();
            }
1261 1262 1263 1264 1265 1266 1267
        }
    }

    /**
     * Commits the current transaction.
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1268 1269 1270
     *
     * @throws \Doctrine\DBAL\ConnectionException If the commit failed due to no active transaction or
     *                                            because the transaction was marked for rollback only.
1271 1272 1273 1274
     */
    public function commit()
    {
        if ($this->_transactionNestingLevel == 0) {
1275
            throw ConnectionException::noActiveTransaction();
1276 1277 1278 1279 1280 1281 1282
        }
        if ($this->_isRollbackOnly) {
            throw ConnectionException::commitFailedRollbackOnly();
        }

        $this->connect();

1283 1284
        $logger = $this->_config->getSQLLogger();

1285
        if ($this->_transactionNestingLevel == 1) {
1286 1287 1288
            if ($logger) {
                $logger->startQuery('"COMMIT"');
            }
1289
            $this->_conn->commit();
1290 1291 1292
            if ($logger) {
                $logger->stopQuery();
            }
Steve Müller's avatar
Steve Müller committed
1293
        } elseif ($this->_nestTransactionsWithSavepoints) {
1294 1295 1296
            if ($logger) {
                $logger->startQuery('"RELEASE SAVEPOINT"');
            }
1297
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1298 1299 1300
            if ($logger) {
                $logger->stopQuery();
            }
1301 1302 1303
        }

        --$this->_transactionNestingLevel;
1304 1305 1306 1307 1308 1309 1310 1311 1312

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

    /**
     * Commits all current nesting transactions.
     */
1313
    private function commitAll()
1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325
    {
        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();
        }
1326 1327 1328
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1329
     * Cancels any database changes done during the current transaction.
1330
     *
Benjamin Morel's avatar
Benjamin Morel committed
1331
     * @throws \Doctrine\DBAL\ConnectionException If the rollback operation failed.
1332
     */
1333
    public function rollBack()
1334 1335
    {
        if ($this->_transactionNestingLevel == 0) {
1336
            throw ConnectionException::noActiveTransaction();
1337 1338 1339 1340
        }

        $this->connect();

1341 1342
        $logger = $this->_config->getSQLLogger();

1343
        if ($this->_transactionNestingLevel == 1) {
1344 1345 1346
            if ($logger) {
                $logger->startQuery('"ROLLBACK"');
            }
1347
            $this->_transactionNestingLevel = 0;
1348
            $this->_conn->rollBack();
1349
            $this->_isRollbackOnly = false;
1350 1351 1352
            if ($logger) {
                $logger->stopQuery();
            }
1353 1354 1355 1356

            if (false === $this->autoCommit) {
                $this->beginTransaction();
            }
Steve Müller's avatar
Steve Müller committed
1357
        } elseif ($this->_nestTransactionsWithSavepoints) {
1358 1359 1360
            if ($logger) {
                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
            }
1361 1362
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
            --$this->_transactionNestingLevel;
1363 1364 1365
            if ($logger) {
                $logger->stopQuery();
            }
1366
        } else {
1367
            $this->_isRollbackOnly = true;
1368 1369 1370 1371
            --$this->_transactionNestingLevel;
        }
    }

1372
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1373 1374 1375
     * Creates a new savepoint.
     *
     * @param string $savepoint The name of the savepoint to create.
1376 1377
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1378 1379
     *
     * @throws \Doctrine\DBAL\ConnectionException
1380
     */
1381
    public function createSavepoint($savepoint)
1382
    {
1383
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1384
            throw ConnectionException::savepointsNotSupported();
1385 1386
        }

1387
        $this->_conn->exec($this->platform->createSavePoint($savepoint));
1388 1389 1390
    }

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

1405 1406
        if ($this->platform->supportsReleaseSavepoints()) {
            $this->_conn->exec($this->platform->releaseSavePoint($savepoint));
1407
        }
1408 1409 1410
    }

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

1425
        $this->_conn->exec($this->platform->rollbackSavePoint($savepoint));
1426 1427
    }

romanb's avatar
romanb committed
1428 1429 1430
    /**
     * Gets the wrapped driver connection.
     *
1431
     * @return \Doctrine\DBAL\Driver\Connection
romanb's avatar
romanb committed
1432 1433 1434 1435
     */
    public function getWrappedConnection()
    {
        $this->connect();
1436

romanb's avatar
romanb committed
1437 1438
        return $this->_conn;
    }
1439

romanb's avatar
romanb committed
1440 1441 1442 1443
    /**
     * Gets the SchemaManager that can be used to inspect or change the
     * database schema through the connection.
     *
1444
     * @return \Doctrine\DBAL\Schema\AbstractSchemaManager
romanb's avatar
romanb committed
1445 1446 1447 1448 1449 1450
     */
    public function getSchemaManager()
    {
        if ( ! $this->_schemaManager) {
            $this->_schemaManager = $this->_driver->getSchemaManager($this);
        }
1451

romanb's avatar
romanb committed
1452 1453
        return $this->_schemaManager;
    }
1454

1455 1456 1457
    /**
     * Marks the current transaction so that the only possible
     * outcome for the transaction to be rolled back.
1458
     *
Benjamin Morel's avatar
Benjamin Morel committed
1459 1460 1461
     * @return void
     *
     * @throws \Doctrine\DBAL\ConnectionException If no transaction is active.
1462 1463 1464 1465 1466 1467 1468 1469 1470 1471
     */
    public function setRollbackOnly()
    {
        if ($this->_transactionNestingLevel == 0) {
            throw ConnectionException::noActiveTransaction();
        }
        $this->_isRollbackOnly = true;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1472
     * Checks whether the current transaction is marked for rollback only.
1473
     *
1474
     * @return boolean
Benjamin Morel's avatar
Benjamin Morel committed
1475 1476
     *
     * @throws \Doctrine\DBAL\ConnectionException If no transaction is active.
1477
     */
1478
    public function isRollbackOnly()
1479 1480 1481 1482
    {
        if ($this->_transactionNestingLevel == 0) {
            throw ConnectionException::noActiveTransaction();
        }
Benjamin Morel's avatar
Benjamin Morel committed
1483

1484 1485 1486
        return $this->_isRollbackOnly;
    }

1487 1488 1489
    /**
     * Converts a given value to its database representation according to the conversion
     * rules of a specific DBAL mapping type.
1490
     *
Benjamin Morel's avatar
Benjamin Morel committed
1491 1492 1493
     * @param mixed  $value The value to convert.
     * @param string $type  The name of the DBAL mapping type.
     *
1494 1495 1496 1497
     * @return mixed The converted value.
     */
    public function convertToDatabaseValue($value, $type)
    {
1498
        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1499 1500 1501 1502 1503
    }

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

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

    /**
     * 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
1563 1564 1565 1566
     * @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.
1567 1568 1569 1570 1571 1572 1573
     */
    private function getBindingInfo($value, $type)
    {
        if (is_string($type)) {
            $type = Type::getType($type);
        }
        if ($type instanceof Type) {
1574
            $value = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1575 1576 1577 1578
            $bindingType = $type->getBindingType();
        } else {
            $bindingType = $type; // PDO::PARAM_* constants
        }
Benjamin Morel's avatar
Benjamin Morel committed
1579

1580 1581 1582
        return array($value, $bindingType);
    }

1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629
    /**
     * 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)
    {
        $resolvedParams = array();

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

1630
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1631
     * Creates a new instance of a SQL query builder.
1632
     *
1633
     * @return \Doctrine\DBAL\Query\QueryBuilder
1634 1635 1636 1637 1638
     */
    public function createQueryBuilder()
    {
        return new Query\QueryBuilder($this);
    }
1639 1640

    /**
1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657
     * 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.
1658 1659 1660 1661 1662
     *
     * @return bool
     */
    public function ping()
    {
1663
        $this->connect();
1664

1665 1666 1667 1668 1669
        if ($this->_conn instanceof PingableConnection) {
            return $this->_conn->ping();
        }

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

1672 1673
            return true;
        } catch (DBALException $e) {
1674
            return false;
1675
        }
1676
    }
1677
}