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

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

22
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
23
use Doctrine\DBAL\Exception\InvalidArgumentException;
Benjamin Morel's avatar
Benjamin Morel committed
24 25 26 27 28 29 30 31 32
use Closure;
use Exception;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Cache\ResultCacheStatement;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Cache\ArrayStatement;
use Doctrine\DBAL\Cache\CacheException;
33
use Doctrine\DBAL\Driver\PingableConnection;
34
use Throwable;
35 36 37 38 39 40 41
use function array_key_exists;
use function array_merge;
use function func_get_args;
use function implode;
use function is_int;
use function is_string;
use function key;
42 43

/**
44
 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
45 46
 * events, transaction isolation levels, configuration, emulated transaction nesting,
 * lazy connecting and more.
47
 *
Benjamin Morel's avatar
Benjamin Morel committed
48 49 50 51 52 53 54 55
 * @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>
56
 */
57
class Connection implements DriverConnection
58
{
59 60
    /**
     * Constant for transaction isolation level READ UNCOMMITTED.
61 62
     *
     * @deprecated Use TransactionIsolationLevel::READ_UNCOMMITTED.
63
     */
64
    public const TRANSACTION_READ_UNCOMMITTED = TransactionIsolationLevel::READ_UNCOMMITTED;
65

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

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

80 81
    /**
     * Constant for transaction isolation level SERIALIZABLE.
82 83
     *
     * @deprecated Use TransactionIsolationLevel::SERIALIZABLE.
84
     */
85
    public const TRANSACTION_SERIALIZABLE = TransactionIsolationLevel::SERIALIZABLE;
86

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

94 95
    /**
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
96
     *
97
     * @var int
98
     */
Sergei Morozov's avatar
Sergei Morozov committed
99
    public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET;
100

101 102
    /**
     * Offset by which PARAM_* constants are detected as arrays of the param type.
103
     *
104
     * @var int
105 106
     */
    const ARRAY_PARAM_OFFSET = 100;
107

romanb's avatar
romanb committed
108 109 110
    /**
     * The wrapped driver connection.
     *
111
     * @var \Doctrine\DBAL\Driver\Connection
romanb's avatar
romanb committed
112 113
     */
    protected $_conn;
114

romanb's avatar
romanb committed
115
    /**
116
     * @var \Doctrine\DBAL\Configuration
romanb's avatar
romanb committed
117 118
     */
    protected $_config;
119

romanb's avatar
romanb committed
120
    /**
121
     * @var \Doctrine\Common\EventManager
romanb's avatar
romanb committed
122 123
     */
    protected $_eventManager;
124

125
    /**
126
     * @var \Doctrine\DBAL\Query\Expression\ExpressionBuilder
127 128
     */
    protected $_expr;
129

romanb's avatar
romanb committed
130 131 132
    /**
     * Whether or not a connection has been established.
     *
133
     * @var bool
romanb's avatar
romanb committed
134
     */
135
    private $_isConnected = false;
136

137
    /**
138
     * The current auto-commit mode of this connection.
139
     *
140
     * @var bool
141 142 143
     */
    private $autoCommit = true;

144 145 146
    /**
     * The transaction nesting level.
     *
147
     * @var int
148 149 150 151 152 153
     */
    private $_transactionNestingLevel = 0;

    /**
     * The currently active transaction isolation level.
     *
154
     * @var int
155 156 157
     */
    private $_transactionIsolationLevel;

158
    /**
Benjamin Morel's avatar
Benjamin Morel committed
159
     * If nested transactions should use savepoints.
160
     *
161
     * @var bool
162
     */
163
    private $_nestTransactionsWithSavepoints = false;
Lukas Kahwe Smith's avatar
Lukas Kahwe Smith committed
164

romanb's avatar
romanb committed
165 166 167 168 169
    /**
     * The parameters used during creation of the Connection instance.
     *
     * @var array
     */
170
    private $_params = [];
171

romanb's avatar
romanb committed
172 173 174 175
    /**
     * The DatabasePlatform object that provides information about the
     * database platform used by the connection.
     *
176
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
romanb's avatar
romanb committed
177
     */
178
    private $platform;
179

romanb's avatar
romanb committed
180 181 182
    /**
     * The schema manager.
     *
183
     * @var \Doctrine\DBAL\Schema\AbstractSchemaManager
romanb's avatar
romanb committed
184 185
     */
    protected $_schemaManager;
186

romanb's avatar
romanb committed
187
    /**
romanb's avatar
romanb committed
188 189
     * The used DBAL driver.
     *
190
     * @var \Doctrine\DBAL\Driver
romanb's avatar
romanb committed
191 192
     */
    protected $_driver;
193

194
    /**
195
     * Flag that indicates whether the current transaction is marked for rollback only.
196
     *
197
     * @var bool
198
     */
199 200
    private $_isRollbackOnly = false;

Benjamin Morel's avatar
Benjamin Morel committed
201
    /**
202
     * @var int
Benjamin Morel's avatar
Benjamin Morel committed
203
     */
204
    protected $defaultFetchMode = FetchMode::ASSOCIATIVE;
205

romanb's avatar
romanb committed
206 207 208
    /**
     * Initializes a new instance of the Connection class.
     *
Benjamin Morel's avatar
Benjamin Morel committed
209 210 211 212 213 214
     * @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
215 216 217 218 219 220
     */
    public function __construct(array $params, Driver $driver, Configuration $config = null,
            EventManager $eventManager = null)
    {
        $this->_driver = $driver;
        $this->_params = $params;
221

romanb's avatar
romanb committed
222 223 224
        if (isset($params['pdo'])) {
            $this->_conn = $params['pdo'];
            $this->_isConnected = true;
225
            unset($this->_params['pdo']);
romanb's avatar
romanb committed
226
        }
227

228 229
        if (isset($params["platform"])) {
            if ( ! $params['platform'] instanceof Platforms\AbstractPlatform) {
230
                throw DBALException::invalidPlatformType($params['platform']);
231 232 233 234 235 236
            }

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

romanb's avatar
romanb committed
237 238 239
        // Create default config and event manager if none given
        if ( ! $config) {
            $config = new Configuration();
romanb's avatar
romanb committed
240
        }
241

romanb's avatar
romanb committed
242
        if ( ! $eventManager) {
romanb's avatar
romanb committed
243
            $eventManager = new EventManager();
romanb's avatar
romanb committed
244
        }
245

romanb's avatar
romanb committed
246
        $this->_config = $config;
romanb's avatar
romanb committed
247
        $this->_eventManager = $eventManager;
248

249
        $this->_expr = new Query\Expression\ExpressionBuilder($this);
250

251
        $this->autoCommit = $config->getAutoCommit();
romanb's avatar
romanb committed
252
    }
romanb's avatar
romanb committed
253

254
    /**
romanb's avatar
romanb committed
255
     * Gets the parameters used during instantiation.
256
     *
Benjamin Morel's avatar
Benjamin Morel committed
257
     * @return array
258 259 260 261 262 263
     */
    public function getParams()
    {
        return $this->_params;
    }

romanb's avatar
romanb committed
264
    /**
romanb's avatar
romanb committed
265
     * Gets the name of the database this Connection is connected to.
romanb's avatar
romanb committed
266
     *
Benjamin Morel's avatar
Benjamin Morel committed
267
     * @return string
romanb's avatar
romanb committed
268 269 270 271 272
     */
    public function getDatabase()
    {
        return $this->_driver->getDatabase($this);
    }
273

274 275
    /**
     * Gets the hostname of the currently connected database.
276
     *
Benjamin Morel's avatar
Benjamin Morel committed
277
     * @return string|null
278 279 280
     */
    public function getHost()
    {
281
        return $this->_params['host'] ?? null;
282
    }
283

284 285
    /**
     * Gets the port of the currently connected database.
286
     *
287 288 289 290
     * @return mixed
     */
    public function getPort()
    {
291
        return $this->_params['port'] ?? null;
292
    }
293

294 295
    /**
     * Gets the username used by this connection.
296
     *
Benjamin Morel's avatar
Benjamin Morel committed
297
     * @return string|null
298 299 300
     */
    public function getUsername()
    {
301
        return $this->_params['user'] ?? null;
302
    }
303

304 305
    /**
     * Gets the password used by this connection.
306
     *
Benjamin Morel's avatar
Benjamin Morel committed
307
     * @return string|null
308 309 310
     */
    public function getPassword()
    {
311
        return $this->_params['password'] ?? null;
312
    }
romanb's avatar
romanb committed
313 314 315 316

    /**
     * Gets the DBAL driver instance.
     *
317
     * @return \Doctrine\DBAL\Driver
romanb's avatar
romanb committed
318 319 320 321 322 323 324 325 326
     */
    public function getDriver()
    {
        return $this->_driver;
    }

    /**
     * Gets the Configuration used by the Connection.
     *
327
     * @return \Doctrine\DBAL\Configuration
romanb's avatar
romanb committed
328 329 330 331 332 333 334 335 336
     */
    public function getConfiguration()
    {
        return $this->_config;
    }

    /**
     * Gets the EventManager used by the Connection.
     *
337
     * @return \Doctrine\Common\EventManager
romanb's avatar
romanb committed
338 339 340 341 342 343 344 345 346
     */
    public function getEventManager()
    {
        return $this->_eventManager;
    }

    /**
     * Gets the DatabasePlatform for the connection.
     *
347
     * @return \Doctrine\DBAL\Platforms\AbstractPlatform
348 349
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
350 351 352
     */
    public function getDatabasePlatform()
    {
353
        if (null === $this->platform) {
354
            $this->detectDatabasePlatform();
355 356 357
        }

        return $this->platform;
romanb's avatar
romanb committed
358
    }
359

360 361 362
    /**
     * Gets the ExpressionBuilder for the connection.
     *
363
     * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
364 365 366 367 368
     */
    public function getExpressionBuilder()
    {
        return $this->_expr;
    }
369

romanb's avatar
romanb committed
370 371 372
    /**
     * Establishes the connection with the database.
     *
373 374
     * @return bool TRUE if the connection was successfully established, FALSE if
     *              the connection is already open.
romanb's avatar
romanb committed
375 376 377
     */
    public function connect()
    {
378 379 380
        if ($this->_isConnected) {
            return false;
        }
romanb's avatar
romanb committed
381

382 383 384
        $driverOptions = $this->_params['driverOptions'] ?? [];
        $user = $this->_params['user'] ?? null;
        $password = $this->_params['password'] ?? null;
romanb's avatar
romanb committed
385 386

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

389 390 391 392
        if (false === $this->autoCommit) {
            $this->beginTransaction();
        }

393
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
394
            $eventArgs = new Event\ConnectionEventArgs($this);
395 396 397
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
        }

romanb's avatar
romanb committed
398 399 400
        return true;
    }

401 402 403 404 405 406 407 408 409
    /**
     * 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()
    {
410
        $version = $this->getDatabasePlatformVersion();
411

412 413
        if (null !== $version) {
            $this->platform = $this->_driver->createDatabasePlatformForVersion($version);
414
        } else {
415
            $this->platform = $this->_driver->getDatabasePlatform();
416 417 418 419 420 421 422 423 424 425 426 427 428 429
        }

        $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
430
     *
431
     * @throws Exception
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
     */
    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) {
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
            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;
            }

480 481
        }

482 483 484 485 486 487 488 489 490 491
        return $this->getServerVersion();
    }

    /**
     * Returns the database server version if the underlying driver supports it.
     *
     * @return string|null
     */
    private function getServerVersion()
    {
492 493 494 495 496 497 498 499 500 501 502
        // Automatic platform version detection.
        if ($this->_conn instanceof ServerInfoAwareConnection &&
            ! $this->_conn->requiresQueryForServerVersion()
        ) {
            return $this->_conn->getServerVersion();
        }

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

503 504 505
    /**
     * Returns the current auto-commit mode for this connection.
     *
506
     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
507 508 509
     *
     * @see    setAutoCommit
     */
510
    public function isAutoCommit()
511
    {
512
        return true === $this->autoCommit;
513 514 515 516 517 518 519 520 521 522 523 524
    }

    /**
     * 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.
     *
525
     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
526
     *
527
     * @see   isAutoCommit
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
     */
    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();
        }
    }

546
    /**
Benjamin Morel's avatar
Benjamin Morel committed
547
     * Sets the fetch mode.
548
     *
549
     * @param int $fetchMode
Benjamin Morel's avatar
Benjamin Morel committed
550 551
     *
     * @return void
552
     */
553
    public function setFetchMode($fetchMode)
554
    {
555
        $this->defaultFetchMode = $fetchMode;
556 557
    }

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

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

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

    /**
     * Whether an actual connection to the database is established.
     *
611
     * @return bool
romanb's avatar
romanb committed
612 613 614 615 616 617
     */
    public function isConnected()
    {
        return $this->_isConnected;
    }

618 619
    /**
     * Checks whether a transaction is currently active.
620
     *
621
     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
622 623 624 625 626 627
     */
    public function isTransactionActive()
    {
        return $this->_transactionNestingLevel > 0;
    }

628
    /**
629
     * Gathers conditions for an update or delete call.
630 631 632 633
     *
     * @param array $identifiers Input array of columns to values
     *
     * @return string[][] a triplet with:
634
     *                    - the first key being the columns
635
     *                    - the second key being the values
636
     *                    - the third key being the conditions
637
     */
638
    private function gatherConditions(array $identifiers)
639 640 641
    {
        $columns = [];
        $values = [];
642
        $conditions = [];
643 644 645

        foreach ($identifiers as $columnName => $value) {
            if (null === $value) {
646
                $conditions[] = $this->getDatabasePlatform()->getIsNullExpression($columnName);
647 648 649 650 651
                continue;
            }

            $columns[] = $columnName;
            $values[] = $value;
652
            $conditions[] = $columnName . ' = ?';
653 654
        }

655
        return [$columns, $values, $conditions];
656 657
    }

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

678
        list($columns, $values, $conditions) = $this->gatherConditions($identifier);
679

680
        return $this->executeUpdate(
681 682 683
            'DELETE FROM ' . $tableExpression . ' WHERE ' . implode(' AND ', $conditions),
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
684
        );
685 686
    }

romanb's avatar
romanb committed
687 688 689 690 691 692 693
    /**
     * Closes the connection.
     *
     * @return void
     */
    public function close()
    {
694
        $this->_conn = null;
695

romanb's avatar
romanb committed
696 697 698
        $this->_isConnected = false;
    }

699 700 701
    /**
     * Sets the transaction isolation level.
     *
702
     * @param int $level The level to set.
Benjamin Morel's avatar
Benjamin Morel committed
703
     *
704
     * @return int
705 706 707 708
     */
    public function setTransactionIsolation($level)
    {
        $this->_transactionIsolationLevel = $level;
709

710
        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
711 712 713 714 715
    }

    /**
     * Gets the currently active transaction isolation level.
     *
716
     * @return int The current transaction isolation level.
717 718 719
     */
    public function getTransactionIsolation()
    {
720 721 722 723
        if (null === $this->_transactionIsolationLevel) {
            $this->_transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
        }

724 725 726
        return $this->_transactionIsolationLevel;
    }

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

romanb's avatar
romanb committed
747
        foreach ($data as $columnName => $value) {
748 749
            $setColumns[] = $columnName;
            $setValues[] = $value;
750
            $set[] = $columnName . ' = ?';
romanb's avatar
romanb committed
751 752
        }

753 754 755
        list($conditionColumns, $conditionValues, $conditions) = $this->gatherConditions($identifier);
        $columns = array_merge($setColumns, $conditionColumns);
        $values = array_merge($setValues, $conditionValues);
756

757
        if (is_string(key($types))) {
758
            $types = $this->extractTypeValues($columns, $types);
759
        }
romanb's avatar
romanb committed
760

761
        $sql  = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
762
                . ' WHERE ' . implode(' AND ', $conditions);
romanb's avatar
romanb committed
763

764
        return $this->executeUpdate($sql, $values, $types);
romanb's avatar
romanb committed
765 766 767 768 769
    }

    /**
     * Inserts a table row with specified data.
     *
770 771 772
     * 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.
773 774
     * @param array  $data            An associative array containing column-value pairs.
     * @param array  $types           Types of the inserted data.
Benjamin Morel's avatar
Benjamin Morel committed
775
     *
776
     * @return int The number of affected rows.
777 778
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
779
     */
780
    public function insert($tableExpression, array $data, array $types = [])
romanb's avatar
romanb committed
781
    {
782
        if (empty($data)) {
783
            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' ()' . ' VALUES ()');
784 785
        }

786 787 788
        $columns = [];
        $values = [];
        $set = [];
789 790

        foreach ($data as $columnName => $value) {
791 792 793
            $columns[] = $columnName;
            $values[] = $value;
            $set[] = '?';
794 795
        }

796
        return $this->executeUpdate(
797 798 799 800
            'INSERT INTO ' . $tableExpression . ' (' . implode(', ', $columns) . ')' .
            ' VALUES (' . implode(', ', $set) . ')',
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
801
        );
romanb's avatar
romanb committed
802 803
    }

804
    /**
805
     * Extract ordered type list from an ordered column list and type map.
806
     *
807
     * @param array $columnList
808 809 810 811
     * @param array $types
     *
     * @return array
     */
812
    private function extractTypeValues(array $columnList, array $types)
813
    {
814
        $typeValues = [];
815

816
        foreach ($columnList as $columnIndex => $columnName) {
817
            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
818 819 820 821 822
        }

        return $typeValues;
    }

romanb's avatar
romanb committed
823
    /**
Benjamin Morel's avatar
Benjamin Morel committed
824
     * Quotes a string so it can be safely used as a table or column name, even if
romanb's avatar
romanb committed
825 826 827 828
     * it is a reserved name.
     *
     * Delimiting style depends on the underlying database platform that is being used.
     *
829 830
     * 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
831 832
     * problems than they solve.
     *
833
     * @param string $str The name to be quoted.
Benjamin Morel's avatar
Benjamin Morel committed
834
     *
835
     * @return string The quoted name.
romanb's avatar
romanb committed
836 837 838
     */
    public function quoteIdentifier($str)
    {
839
        return $this->getDatabasePlatform()->quoteIdentifier($str);
romanb's avatar
romanb committed
840 841 842 843 844
    }

    /**
     * Quotes a given input parameter.
     *
845
     * @param mixed    $input The parameter to be quoted.
helsner's avatar
helsner committed
846
     * @param int|null $type  The type of the parameter.
Benjamin Morel's avatar
Benjamin Morel committed
847
     *
848
     * @return string The quoted parameter.
romanb's avatar
romanb committed
849 850 851
     */
    public function quote($input, $type = null)
    {
852
        $this->connect();
853 854

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

856
        return $this->_conn->quote($value, $bindingType);
romanb's avatar
romanb committed
857 858 859
    }

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

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

890
        $stmt->setFetchMode($this->defaultFetchMode);
891 892

        return $stmt;
romanb's avatar
romanb committed
893 894 895
    }

    /**
Pascal Borreli's avatar
Pascal Borreli committed
896
     * Executes an, optionally parametrized, SQL query.
romanb's avatar
romanb committed
897
     *
Pascal Borreli's avatar
Pascal Borreli committed
898
     * If the query is parametrized, a prepared statement is used.
899 900
     * If an SQLLogger is configured, the execution is logged.
     *
Benjamin Morel's avatar
Benjamin Morel committed
901 902 903 904 905
     * @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.
     *
906
     * @return \Doctrine\DBAL\Driver\Statement The executed statement.
Benjamin Morel's avatar
Benjamin Morel committed
907 908
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
909
     */
910
    public function executeQuery($query, array $params = [], $types = [], QueryCacheProfile $qcp = null)
romanb's avatar
romanb committed
911
    {
912
        if ($qcp !== null) {
913
            return $this->executeCacheQuery($query, $params, $types, $qcp);
914 915
        }

romanb's avatar
romanb committed
916 917
        $this->connect();

918 919 920
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
921
        }
922

923 924 925
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
926

927 928 929 930 931 932 933
                $stmt = $this->_conn->prepare($query);
                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
934
            } else {
935
                $stmt = $this->_conn->query($query);
936
            }
Filip Procházka's avatar
Filip Procházka committed
937
        } catch (Exception $ex) {
938
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
939
        }
940

941
        $stmt->setFetchMode($this->defaultFetchMode);
942

943 944
        if ($logger) {
            $logger->stopQuery();
945 946
        }

romanb's avatar
romanb committed
947
        return $stmt;
romanb's avatar
romanb committed
948
    }
949

950
    /**
Benjamin Morel's avatar
Benjamin Morel committed
951 952 953 954 955 956
     * 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.
957 958
     *
     * @return \Doctrine\DBAL\Driver\ResultStatement
Benjamin Morel's avatar
Benjamin Morel committed
959 960
     *
     * @throws \Doctrine\DBAL\Cache\CacheException
961 962 963 964
     */
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp)
    {
        $resultCache = $qcp->getResultCacheDriver() ?: $this->_config->getResultCacheImpl();
965
        if ( ! $resultCache) {
966 967 968
            throw CacheException::noResultDriverConfigured();
        }

969
        list($cacheKey, $realKey) = $qcp->generateCacheKeys($query, $params, $types, $this->getParams());
970 971 972 973 974

        // 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])) {
975
                $stmt = new ArrayStatement($data[$realKey]);
Steve Müller's avatar
Steve Müller committed
976
            } elseif (array_key_exists($realKey, $data)) {
977
                $stmt = new ArrayStatement([]);
978 979
            }
        }
980 981 982 983 984

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

985
        $stmt->setFetchMode($this->defaultFetchMode);
986 987

        return $stmt;
988 989
    }

990
    /**
Pascal Borreli's avatar
Pascal Borreli committed
991
     * Executes an, optionally parametrized, SQL query and returns the result,
992
     * applying a given projection/transformation function on each row of the result.
993
     *
Benjamin Morel's avatar
Benjamin Morel committed
994 995 996 997 998 999
     * @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.
     *
1000
     * @return array The projected result of the query.
1001
     */
1002
    public function project($query, array $params, Closure $function)
1003
    {
1004
        $result = [];
1005
        $stmt = $this->executeQuery($query, $params);
1006

1007
        while ($row = $stmt->fetch()) {
1008
            $result[] = $function($row);
1009
        }
1010

1011
        $stmt->closeCursor();
1012

1013 1014
        return $result;
    }
romanb's avatar
romanb committed
1015 1016

    /**
1017
     * Executes an SQL statement, returning a result set as a Statement object.
1018
     *
1019
     * @return \Doctrine\DBAL\Driver\Statement
Benjamin Morel's avatar
Benjamin Morel committed
1020 1021
     *
     * @throws \Doctrine\DBAL\DBALException
1022 1023 1024
     */
    public function query()
    {
1025 1026
        $this->connect();

1027 1028
        $args = func_get_args();

1029
        $logger = $this->_config->getSQLLogger();
1030 1031 1032 1033
        if ($logger) {
            $logger->startQuery($args[0]);
        }

1034
        try {
1035
            $statement = $this->_conn->query(...$args);
Filip Procházka's avatar
Filip Procházka committed
1036
        } catch (Exception $ex) {
1037
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $args[0]);
1038 1039
        }

1040
        $statement->setFetchMode($this->defaultFetchMode);
1041 1042 1043 1044 1045 1046

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

        return $statement;
1047 1048 1049 1050 1051
    }

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

1067 1068 1069
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
1070 1071
        }

1072 1073 1074
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
1075

1076 1077 1078 1079 1080 1081 1082 1083
                $stmt = $this->_conn->prepare($query);
                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
                $result = $stmt->rowCount();
1084
            } else {
1085
                $result = $this->_conn->exec($query);
1086
            }
Filip Procházka's avatar
Filip Procházka committed
1087
        } catch (Exception $ex) {
1088
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
1089
        }
1090

1091 1092
        if ($logger) {
            $logger->stopQuery();
1093 1094
        }

romanb's avatar
romanb committed
1095
        return $result;
romanb's avatar
romanb committed
1096 1097
    }

1098
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1099
     * Executes an SQL statement and return the number of affected rows.
1100
     *
1101
     * @param string $statement
Benjamin Morel's avatar
Benjamin Morel committed
1102
     *
1103
     * @return int The number of affected rows.
Benjamin Morel's avatar
Benjamin Morel committed
1104 1105
     *
     * @throws \Doctrine\DBAL\DBALException
1106 1107 1108 1109
     */
    public function exec($statement)
    {
        $this->connect();
1110 1111 1112 1113 1114 1115

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

1116 1117
        try {
            $result = $this->_conn->exec($statement);
Filip Procházka's avatar
Filip Procházka committed
1118
        } catch (Exception $ex) {
1119
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1120
        }
1121 1122 1123 1124 1125 1126

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

        return $result;
1127 1128
    }

1129 1130 1131
    /**
     * Returns the current transaction nesting level.
     *
1132
     * @return int The nesting level. A value of 0 means there's no active transaction.
1133 1134 1135 1136 1137 1138
     */
    public function getTransactionNestingLevel()
    {
        return $this->_transactionNestingLevel;
    }

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

romanb's avatar
romanb committed
1148 1149 1150 1151
        return $this->_conn->errorCode();
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1152
     * Fetches extended error information associated with the last database operation.
romanb's avatar
romanb committed
1153
     *
1154
     * @return array The last error information.
romanb's avatar
romanb committed
1155 1156 1157 1158
     */
    public function errorInfo()
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1159

romanb's avatar
romanb committed
1160 1161 1162 1163 1164 1165 1166 1167
        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,
1168 1169
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
     * columns or sequences.
romanb's avatar
romanb committed
1170
     *
Benjamin Morel's avatar
Benjamin Morel committed
1171 1172
     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
     *
1173
     * @return string A string representation of the last inserted ID.
romanb's avatar
romanb committed
1174
     */
romanb's avatar
romanb committed
1175 1176 1177
    public function lastInsertId($seqName = null)
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1178

romanb's avatar
romanb committed
1179 1180
        return $this->_conn->lastInsertId($seqName);
    }
1181

1182 1183 1184 1185 1186 1187 1188 1189
    /**
     * 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
1190 1191
     * @param \Closure $func The function to execute transactionally.
     *
1192
     * @return mixed The value returned by $func
Benjamin Morel's avatar
Benjamin Morel committed
1193
     *
1194 1195
     * @throws Exception
     * @throws Throwable
1196 1197 1198 1199 1200
     */
    public function transactional(Closure $func)
    {
        $this->beginTransaction();
        try {
1201
            $res = $func($this);
1202
            $this->commit();
1203
            return $res;
1204
        } catch (Exception $e) {
1205
            $this->rollBack();
1206 1207 1208
            throw $e;
        } catch (Throwable $e) {
            $this->rollBack();
1209 1210 1211 1212
            throw $e;
        }
    }

1213
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1214
     * Sets if nested transactions should use savepoints.
1215
     *
1216
     * @param bool $nestTransactionsWithSavepoints
Benjamin Morel's avatar
Benjamin Morel committed
1217
     *
1218
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1219 1220
     *
     * @throws \Doctrine\DBAL\ConnectionException
1221 1222 1223
     */
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
    {
1224 1225 1226 1227
        if ($this->_transactionNestingLevel > 0) {
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
        }

1228
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1229
            throw ConnectionException::savepointsNotSupported();
1230 1231
        }

1232
        $this->_nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1233 1234 1235
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1236
     * Gets if nested transactions should use savepoints.
1237
     *
1238
     * @return bool
1239 1240 1241 1242 1243 1244
     */
    public function getNestTransactionsWithSavepoints()
    {
        return $this->_nestTransactionsWithSavepoints;
    }

1245 1246 1247 1248
    /**
     * 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
1249
     * @return mixed A string with the savepoint name or false.
1250
     */
1251 1252 1253
    protected function _getNestedTransactionSavePointName()
    {
        return 'DOCTRINE2_SAVEPOINT_'.$this->_transactionNestingLevel;
1254 1255
    }

1256 1257 1258 1259 1260 1261 1262 1263 1264
    /**
     * Starts a transaction by suspending auto-commit mode.
     *
     * @return void
     */
    public function beginTransaction()
    {
        $this->connect();

1265 1266
        ++$this->_transactionNestingLevel;

1267 1268
        $logger = $this->_config->getSQLLogger();

1269
        if ($this->_transactionNestingLevel == 1) {
1270 1271 1272
            if ($logger) {
                $logger->startQuery('"START TRANSACTION"');
            }
1273
            $this->_conn->beginTransaction();
1274 1275 1276
            if ($logger) {
                $logger->stopQuery();
            }
Steve Müller's avatar
Steve Müller committed
1277
        } elseif ($this->_nestTransactionsWithSavepoints) {
1278 1279 1280
            if ($logger) {
                $logger->startQuery('"SAVEPOINT"');
            }
1281
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1282 1283 1284
            if ($logger) {
                $logger->stopQuery();
            }
1285 1286 1287 1288 1289 1290 1291
        }
    }

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

        $this->connect();

1307 1308
        $logger = $this->_config->getSQLLogger();

1309
        if ($this->_transactionNestingLevel == 1) {
1310 1311 1312
            if ($logger) {
                $logger->startQuery('"COMMIT"');
            }
1313
            $this->_conn->commit();
1314 1315 1316
            if ($logger) {
                $logger->stopQuery();
            }
Steve Müller's avatar
Steve Müller committed
1317
        } elseif ($this->_nestTransactionsWithSavepoints) {
1318 1319 1320
            if ($logger) {
                $logger->startQuery('"RELEASE SAVEPOINT"');
            }
1321
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1322 1323 1324
            if ($logger) {
                $logger->stopQuery();
            }
1325 1326 1327
        }

        --$this->_transactionNestingLevel;
1328 1329 1330 1331 1332 1333 1334 1335 1336

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

    /**
     * Commits all current nesting transactions.
     */
1337
    private function commitAll()
1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349
    {
        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();
        }
1350 1351 1352
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1353
     * Cancels any database changes done during the current transaction.
1354
     *
Benjamin Morel's avatar
Benjamin Morel committed
1355
     * @throws \Doctrine\DBAL\ConnectionException If the rollback operation failed.
1356
     */
1357
    public function rollBack()
1358 1359
    {
        if ($this->_transactionNestingLevel == 0) {
1360
            throw ConnectionException::noActiveTransaction();
1361 1362 1363 1364
        }

        $this->connect();

1365 1366
        $logger = $this->_config->getSQLLogger();

1367
        if ($this->_transactionNestingLevel == 1) {
1368 1369 1370
            if ($logger) {
                $logger->startQuery('"ROLLBACK"');
            }
1371
            $this->_transactionNestingLevel = 0;
1372
            $this->_conn->rollBack();
1373
            $this->_isRollbackOnly = false;
1374 1375 1376
            if ($logger) {
                $logger->stopQuery();
            }
1377 1378 1379 1380

            if (false === $this->autoCommit) {
                $this->beginTransaction();
            }
Steve Müller's avatar
Steve Müller committed
1381
        } elseif ($this->_nestTransactionsWithSavepoints) {
1382 1383 1384
            if ($logger) {
                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
            }
1385 1386
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
            --$this->_transactionNestingLevel;
1387 1388 1389
            if ($logger) {
                $logger->stopQuery();
            }
1390
        } else {
1391
            $this->_isRollbackOnly = true;
1392 1393 1394 1395
            --$this->_transactionNestingLevel;
        }
    }

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

1411
        $this->_conn->exec($this->platform->createSavePoint($savepoint));
1412 1413 1414
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1415 1416 1417
     * Releases the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to release.
1418 1419
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1420 1421
     *
     * @throws \Doctrine\DBAL\ConnectionException
1422
     */
1423
    public function releaseSavepoint($savepoint)
1424
    {
1425
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1426
            throw ConnectionException::savepointsNotSupported();
1427 1428
        }

1429 1430
        if ($this->platform->supportsReleaseSavepoints()) {
            $this->_conn->exec($this->platform->releaseSavePoint($savepoint));
1431
        }
1432 1433 1434
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1435 1436 1437
     * Rolls back to the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to rollback to.
1438 1439
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1440 1441
     *
     * @throws \Doctrine\DBAL\ConnectionException
1442
     */
1443
    public function rollbackSavepoint($savepoint)
1444
    {
1445
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1446
            throw ConnectionException::savepointsNotSupported();
1447 1448
        }

1449
        $this->_conn->exec($this->platform->rollbackSavePoint($savepoint));
1450 1451
    }

romanb's avatar
romanb committed
1452 1453 1454
    /**
     * Gets the wrapped driver connection.
     *
1455
     * @return \Doctrine\DBAL\Driver\Connection
romanb's avatar
romanb committed
1456 1457 1458 1459
     */
    public function getWrappedConnection()
    {
        $this->connect();
1460

romanb's avatar
romanb committed
1461 1462
        return $this->_conn;
    }
1463

romanb's avatar
romanb committed
1464 1465 1466 1467
    /**
     * Gets the SchemaManager that can be used to inspect or change the
     * database schema through the connection.
     *
1468
     * @return \Doctrine\DBAL\Schema\AbstractSchemaManager
romanb's avatar
romanb committed
1469 1470 1471 1472 1473 1474
     */
    public function getSchemaManager()
    {
        if ( ! $this->_schemaManager) {
            $this->_schemaManager = $this->_driver->getSchemaManager($this);
        }
1475

romanb's avatar
romanb committed
1476 1477
        return $this->_schemaManager;
    }
1478

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

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1496
     * Checks whether the current transaction is marked for rollback only.
1497
     *
1498
     * @return bool
Benjamin Morel's avatar
Benjamin Morel committed
1499 1500
     *
     * @throws \Doctrine\DBAL\ConnectionException If no transaction is active.
1501
     */
1502
    public function isRollbackOnly()
1503 1504 1505 1506
    {
        if ($this->_transactionNestingLevel == 0) {
            throw ConnectionException::noActiveTransaction();
        }
Benjamin Morel's avatar
Benjamin Morel committed
1507

1508 1509 1510
        return $this->_isRollbackOnly;
    }

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

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

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

    /**
     * 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
1587 1588 1589 1590
     * @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.
1591 1592 1593 1594 1595 1596 1597
     */
    private function getBindingInfo($value, $type)
    {
        if (is_string($type)) {
            $type = Type::getType($type);
        }
        if ($type instanceof Type) {
1598
            $value = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1599 1600
            $bindingType = $type->getBindingType();
        } else {
1601
            $bindingType = $type;
1602
        }
Benjamin Morel's avatar
Benjamin Morel committed
1603

1604
        return [$value, $bindingType];
1605 1606
    }

1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619
    /**
     * 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)
    {
1620
        $resolvedParams = [];
1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653

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

1654
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1655
     * Creates a new instance of a SQL query builder.
1656
     *
1657
     * @return \Doctrine\DBAL\Query\QueryBuilder
1658 1659 1660 1661 1662
     */
    public function createQueryBuilder()
    {
        return new Query\QueryBuilder($this);
    }
1663 1664

    /**
1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681
     * 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.
1682 1683 1684 1685 1686
     *
     * @return bool
     */
    public function ping()
    {
1687
        $this->connect();
1688

1689 1690 1691 1692 1693
        if ($this->_conn instanceof PingableConnection) {
            return $this->_conn->ping();
        }

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

1696 1697
            return true;
        } catch (DBALException $e) {
1698
            return false;
1699
        }
1700
    }
1701
}