Connection.php 52.5 KB
Newer Older
1
<?php
romanb's avatar
romanb committed
2

3
namespace Doctrine\DBAL;
romanb's avatar
romanb committed
4

Benjamin Morel's avatar
Benjamin Morel committed
5 6
use Closure;
use Doctrine\Common\EventManager;
7 8
use Doctrine\DBAL\Abstraction\Result as AbstractionResult;
use Doctrine\DBAL\Cache\ArrayResult;
Benjamin Morel's avatar
Benjamin Morel committed
9
use Doctrine\DBAL\Cache\CacheException;
10
use Doctrine\DBAL\Cache\CachingResult;
11
use Doctrine\DBAL\Cache\QueryCacheProfile;
12
use Doctrine\DBAL\Driver\API\ExceptionConverter;
13
use Doctrine\DBAL\Driver\Connection as DriverConnection;
14
use Doctrine\DBAL\Driver\Exception as DriverException;
15
use Doctrine\DBAL\Driver\Result as DriverResult;
16 17
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\Driver\Statement as DriverStatement;
18
use Doctrine\DBAL\Exception\ConnectionLost;
19 20 21 22 23 24 25
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Types\Type;
use Exception;
26
use Throwable;
27
use Traversable;
28

29
use function array_key_exists;
30
use function array_map;
31
use function assert;
32
use function bin2hex;
33
use function count;
34 35
use function implode;
use function is_int;
36
use function is_resource;
37
use function is_string;
38
use function json_encode;
39
use function key;
40 41
use function preg_replace;
use function sprintf;
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
 */
48
class Connection implements DriverConnection
49
{
50 51 52
    /**
     * Represents an array of ints to be expanded by Doctrine SQL parsing.
     */
Sergei Morozov's avatar
Sergei Morozov committed
53
    public const PARAM_INT_ARRAY = ParameterType::INTEGER + self::ARRAY_PARAM_OFFSET;
54

55 56 57
    /**
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
     */
Sergei Morozov's avatar
Sergei Morozov committed
58
    public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET;
59

60 61 62
    /**
     * Offset by which PARAM_* constants are detected as arrays of the param type.
     */
63
    public const ARRAY_PARAM_OFFSET = 100;
64

romanb's avatar
romanb committed
65 66 67
    /**
     * The wrapped driver connection.
     *
68
     * @var \Doctrine\DBAL\Driver\Connection|null
romanb's avatar
romanb committed
69 70
     */
    protected $_conn;
71

72
    /** @var Configuration */
romanb's avatar
romanb committed
73
    protected $_config;
74

75
    /** @var EventManager */
romanb's avatar
romanb committed
76
    protected $_eventManager;
77

78
    /** @var ExpressionBuilder */
79
    protected $_expr;
80

81
    /**
82
     * The current auto-commit mode of this connection.
83
     *
84
     * @var bool
85 86 87
     */
    private $autoCommit = true;

88 89 90
    /**
     * The transaction nesting level.
     *
91
     * @var int
92
     */
93
    private $transactionNestingLevel = 0;
94 95 96 97

    /**
     * The currently active transaction isolation level.
     *
98
     * @var int
99
     */
100
    private $transactionIsolationLevel;
101

102
    /**
Benjamin Morel's avatar
Benjamin Morel committed
103
     * If nested transactions should use savepoints.
104
     *
105
     * @var bool
106
     */
107
    private $nestTransactionsWithSavepoints = false;
Lukas Kahwe Smith's avatar
Lukas Kahwe Smith committed
108

romanb's avatar
romanb committed
109 110 111
    /**
     * The parameters used during creation of the Connection instance.
     *
112
     * @var mixed[]
romanb's avatar
romanb committed
113
     */
114
    private $params = [];
115

romanb's avatar
romanb committed
116 117 118 119
    /**
     * The DatabasePlatform object that provides information about the
     * database platform used by the connection.
     *
120
     * @var AbstractPlatform
romanb's avatar
romanb committed
121
     */
122
    private $platform;
123

124 125 126
    /** @var ExceptionConverter|null */
    private $exceptionConverter;

romanb's avatar
romanb committed
127 128 129
    /**
     * The schema manager.
     *
Sergei Morozov's avatar
Sergei Morozov committed
130
     * @var AbstractSchemaManager|null
romanb's avatar
romanb committed
131 132
     */
    protected $_schemaManager;
133

romanb's avatar
romanb committed
134
    /**
romanb's avatar
romanb committed
135 136
     * The used DBAL driver.
     *
137
     * @var Driver
romanb's avatar
romanb committed
138 139
     */
    protected $_driver;
140

141
    /**
142
     * Flag that indicates whether the current transaction is marked for rollback only.
143
     *
144
     * @var bool
145
     */
146
    private $isRollbackOnly = false;
147

romanb's avatar
romanb committed
148 149 150
    /**
     * Initializes a new instance of the Connection class.
     *
151 152
     * @internal The connection can be only instantiated by the driver manager.
     *
153
     * @param mixed[]            $params       The connection parameters.
154 155 156
     * @param Driver             $driver       The driver to use.
     * @param Configuration|null $config       The configuration, optional.
     * @param EventManager|null  $eventManager The event manager, optional.
Benjamin Morel's avatar
Benjamin Morel committed
157
     *
158
     * @throws DBALException
romanb's avatar
romanb committed
159
     */
160 161 162 163 164 165
    public function __construct(
        array $params,
        Driver $driver,
        ?Configuration $config = null,
        ?EventManager $eventManager = null
    ) {
romanb's avatar
romanb committed
166
        $this->_driver = $driver;
167
        $this->params  = $params;
168

169 170
        if (isset($params['platform'])) {
            if (! $params['platform'] instanceof Platforms\AbstractPlatform) {
171
                throw DBALException::invalidPlatformType($params['platform']);
172 173
            }

174
            $this->platform = $params['platform'];
175 176
        }

romanb's avatar
romanb committed
177
        // Create default config and event manager if none given
178
        if ($config === null) {
romanb's avatar
romanb committed
179
            $config = new Configuration();
romanb's avatar
romanb committed
180
        }
181

182
        if ($eventManager === null) {
romanb's avatar
romanb committed
183
            $eventManager = new EventManager();
romanb's avatar
romanb committed
184
        }
185

186
        $this->_config       = $config;
romanb's avatar
romanb committed
187
        $this->_eventManager = $eventManager;
188

189
        $this->_expr = new Query\Expression\ExpressionBuilder($this);
190

191
        $this->autoCommit = $config->getAutoCommit();
romanb's avatar
romanb committed
192
    }
romanb's avatar
romanb committed
193

194
    /**
romanb's avatar
romanb committed
195
     * Gets the parameters used during instantiation.
196
     *
197 198
     * @internal
     *
199
     * @return mixed[]
200 201 202
     */
    public function getParams()
    {
203
        return $this->params;
204 205
    }

romanb's avatar
romanb committed
206
    /**
207
     * Gets the name of the currently selected database.
romanb's avatar
romanb committed
208
     *
209 210 211 212 213
     * @return string|null The name of the database or NULL if a database is not selected.
     *                     The platforms which don't support the concept of a database (e.g. embedded databases)
     *                     must always return a string as an indicator of an implicitly selected database.
     *
     * @throws DBALException
romanb's avatar
romanb committed
214 215 216
     */
    public function getDatabase()
    {
217 218 219 220 221 222 223
        $platform = $this->getDatabasePlatform();
        $query    = $platform->getDummySelectSQL($platform->getCurrentDatabaseExpression());
        $database = $this->query($query)->fetchOne();

        assert(is_string($database) || $database === null);

        return $database;
romanb's avatar
romanb committed
224
    }
225

romanb's avatar
romanb committed
226 227 228
    /**
     * Gets the DBAL driver instance.
     *
229
     * @return Driver
romanb's avatar
romanb committed
230 231 232 233 234 235 236 237 238
     */
    public function getDriver()
    {
        return $this->_driver;
    }

    /**
     * Gets the Configuration used by the Connection.
     *
239
     * @return Configuration
romanb's avatar
romanb committed
240 241 242 243 244 245 246 247 248
     */
    public function getConfiguration()
    {
        return $this->_config;
    }

    /**
     * Gets the EventManager used by the Connection.
     *
249
     * @return EventManager
romanb's avatar
romanb committed
250 251 252 253 254 255 256 257 258
     */
    public function getEventManager()
    {
        return $this->_eventManager;
    }

    /**
     * Gets the DatabasePlatform for the connection.
     *
259
     * @return AbstractPlatform
260
     *
261
     * @throws DBALException
romanb's avatar
romanb committed
262 263 264
     */
    public function getDatabasePlatform()
    {
265
        if ($this->platform === null) {
266
            $this->detectDatabasePlatform();
267 268 269
        }

        return $this->platform;
romanb's avatar
romanb committed
270
    }
271

272 273 274
    /**
     * Gets the ExpressionBuilder for the connection.
     *
275
     * @return ExpressionBuilder
276 277 278 279 280
     */
    public function getExpressionBuilder()
    {
        return $this->_expr;
    }
281

romanb's avatar
romanb committed
282 283 284
    /**
     * Establishes the connection with the database.
     *
285 286
     * @return bool TRUE if the connection was successfully established, FALSE if
     *              the connection is already open.
287 288
     *
     * @throws DBALException
romanb's avatar
romanb committed
289 290 291
     */
    public function connect()
    {
292
        if ($this->_conn !== null) {
293 294
            return false;
        }
romanb's avatar
romanb committed
295

296
        try {
297
            $this->_conn = $this->_driver->connect($this->params);
298
        } catch (DriverException $e) {
299
            throw $this->convertException($e);
300 301
        }

302 303
        $this->transactionNestingLevel = 0;

304
        if ($this->autoCommit === false) {
305 306 307
            $this->beginTransaction();
        }

308
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
309
            $eventArgs = new Event\ConnectionEventArgs($this);
310 311 312
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
        }

romanb's avatar
romanb committed
313 314 315
        return true;
    }

316 317 318 319 320
    /**
     * Detects and sets the database platform.
     *
     * Evaluates custom platform class and version in order to set the correct platform.
     *
321
     * @throws DBALException If an invalid platform was specified for this connection.
322
     */
323
    private function detectDatabasePlatform(): void
324
    {
325
        $version = $this->getDatabasePlatformVersion();
326

327 328 329
        if ($version !== null) {
            assert($this->_driver instanceof VersionAwarePlatformDriver);

330
            $this->platform = $this->_driver->createDatabasePlatformForVersion($version);
331
        } else {
332
            $this->platform = $this->_driver->getDatabasePlatform();
333 334 335 336 337 338 339 340 341 342 343 344 345 346
        }

        $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
347
     *
348
     * @throws Exception
349 350 351 352
     */
    private function getDatabasePlatformVersion()
    {
        // Driver does not support version specific platforms.
353
        if (! $this->_driver instanceof VersionAwarePlatformDriver) {
354 355 356 357
            return null;
        }

        // Explicit platform version requested (supersedes auto-detection).
358 359
        if (isset($this->params['serverVersion'])) {
            return $this->params['serverVersion'];
360 361 362
        }

        // If not connected, we need to connect now to determine the platform version.
363
        if ($this->_conn === null) {
364 365
            try {
                $this->connect();
366
            } catch (DBALException $originalException) {
367
                if (! isset($this->params['dbname'])) {
368 369 370 371 372
                    throw $originalException;
                }

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

                try {
                    $this->connect();
378
                } catch (DBALException $fallbackException) {
379 380 381
                    // Either the platform does not support database-less connections
                    // or something else went wrong.
                    // Reset connection parameters and rethrow the original exception.
382
                    $this->params['dbname'] = $databaseName;
383 384 385 386 387

                    throw $originalException;
                }

                // Reset connection parameters.
388 389
                $this->params['dbname'] = $databaseName;
                $serverVersion          = $this->getServerVersion();
390 391 392 393 394 395

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

                return $serverVersion;
            }
396 397
        }

398 399 400 401 402 403 404 405 406 407
        return $this->getServerVersion();
    }

    /**
     * Returns the database server version if the underlying driver supports it.
     *
     * @return string|null
     */
    private function getServerVersion()
    {
Sergei Morozov's avatar
Sergei Morozov committed
408 409
        $connection = $this->getWrappedConnection();

410
        // Automatic platform version detection.
411
        if ($connection instanceof ServerInfoAwareConnection) {
Sergei Morozov's avatar
Sergei Morozov committed
412
            return $connection->getServerVersion();
413 414 415 416 417 418
        }

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

419 420 421 422
    /**
     * Returns the current auto-commit mode for this connection.
     *
     * @see    setAutoCommit
423 424
     *
     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
425
     */
426
    public function isAutoCommit()
427
    {
428
        return $this->autoCommit === true;
429 430 431 432 433 434 435 436 437 438 439 440
    }

    /**
     * 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.
     *
441
     * @see   isAutoCommit
442 443
     *
     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
444 445
     *
     * @return void
446 447 448
     */
    public function setAutoCommit($autoCommit)
    {
449
        $autoCommit = (bool) $autoCommit;
450 451 452 453 454 455 456 457 458

        // 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.
459
        if ($this->_conn === null || $this->transactionNestingLevel === 0) {
460
            return;
461
        }
462 463

        $this->commitAll();
464 465
    }

466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
    /**
     * Prepares and executes an SQL query and returns the first row of the result
     * as an associative array.
     *
     * @param string                                           $query  The SQL query.
     * @param array<int, mixed>|array<string, mixed>           $params The prepared statement params.
     * @param array<int, int|string>|array<string, int|string> $types  The query parameter types.
     *
     * @return array<string, mixed>|false False is returned if no rows are found.
     *
     * @throws DBALException
     */
    public function fetchAssociative(string $query, array $params = [], array $types = [])
    {
        try {
481
            return $this->executeQuery($query, $params, $types)->fetchAssociative();
482
        } catch (DriverException $e) {
483
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
        }
    }

    /**
     * Prepares and executes an SQL query and returns the first row of the result
     * as a numerically indexed array.
     *
     * @param string                                           $query  The SQL query to be executed.
     * @param array<int, mixed>|array<string, mixed>           $params The prepared statement params.
     * @param array<int, int|string>|array<string, int|string> $types  The query parameter types.
     *
     * @return array<int, mixed>|false False is returned if no rows are found.
     *
     * @throws DBALException
     */
    public function fetchNumeric(string $query, array $params = [], array $types = [])
    {
        try {
502
            return $this->executeQuery($query, $params, $types)->fetchNumeric();
503
        } catch (DriverException $e) {
504
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
        }
    }

    /**
     * Prepares and executes an SQL query and returns the value of a single column
     * of the first row of the result.
     *
     * @param string                                           $query  The SQL query to be executed.
     * @param array<int, mixed>|array<string, mixed>           $params The prepared statement params.
     * @param array<int, int|string>|array<string, int|string> $types  The query parameter types.
     *
     * @return mixed|false False is returned if no rows are found.
     *
     * @throws DBALException
     */
    public function fetchOne(string $query, array $params = [], array $types = [])
    {
        try {
523
            return $this->executeQuery($query, $params, $types)->fetchOne();
524
        } catch (DriverException $e) {
525
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
526 527 528
        }
    }

romanb's avatar
romanb committed
529 530 531
    /**
     * Whether an actual connection to the database is established.
     *
532
     * @return bool
romanb's avatar
romanb committed
533 534 535
     */
    public function isConnected()
    {
536
        return $this->_conn !== null;
romanb's avatar
romanb committed
537 538
    }

539 540
    /**
     * Checks whether a transaction is currently active.
541
     *
542
     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
543 544 545
     */
    public function isTransactionActive()
    {
546
        return $this->transactionNestingLevel > 0;
547 548
    }

549
    /**
Sergei Morozov's avatar
Sergei Morozov committed
550
     * Adds identifier condition to the query components
551
     *
Sergei Morozov's avatar
Sergei Morozov committed
552 553 554 555
     * @param mixed[]  $identifier Map of key columns to their values
     * @param string[] $columns    Column names
     * @param mixed[]  $values     Column values
     * @param string[] $conditions Key conditions
556
     *
Sergei Morozov's avatar
Sergei Morozov committed
557
     * @throws DBALException
558
     */
Sergei Morozov's avatar
Sergei Morozov committed
559 560 561 562 563
    private function addIdentifierCondition(
        array $identifier,
        array &$columns,
        array &$values,
        array &$conditions
564
    ): void {
Sergei Morozov's avatar
Sergei Morozov committed
565
        $platform = $this->getDatabasePlatform();
566

Sergei Morozov's avatar
Sergei Morozov committed
567
        foreach ($identifier as $columnName => $value) {
568
            if ($value === null) {
Sergei Morozov's avatar
Sergei Morozov committed
569
                $conditions[] = $platform->getIsNullExpression($columnName);
570 571 572
                continue;
            }

573 574
            $columns[]    = $columnName;
            $values[]     = $value;
575
            $conditions[] = $columnName . ' = ?';
576 577 578
        }
    }

579 580
    /**
     * Executes an SQL DELETE statement on a table.
romanb's avatar
romanb committed
581
     *
582 583
     * Table expression and columns are not escaped and are not safe for user-input.
     *
584 585 586
     * @param string         $tableExpression The expression of the table on which to delete.
     * @param mixed[]        $identifier      The deletion criteria. An associative array containing column-value pairs.
     * @param int[]|string[] $types           The types of identifiers.
Benjamin Morel's avatar
Benjamin Morel committed
587
     *
588
     * @return int The number of affected rows.
589
     *
590
     * @throws DBALException
591
     * @throws InvalidArgumentException
592
     */
593
    public function delete($tableExpression, array $identifier, array $types = [])
594
    {
595
        if (count($identifier) === 0) {
596
            throw InvalidArgumentException::fromEmptyCriteria();
597 598
        }

Sergei Morozov's avatar
Sergei Morozov committed
599 600 601
        $columns = $values = $conditions = [];

        $this->addIdentifierCondition($identifier, $columns, $values, $conditions);
602

603
        return $this->executeUpdate(
604 605 606
            'DELETE FROM ' . $tableExpression . ' WHERE ' . implode(' AND ', $conditions),
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
607
        );
608 609
    }

romanb's avatar
romanb committed
610 611 612 613 614 615 616
    /**
     * Closes the connection.
     *
     * @return void
     */
    public function close()
    {
617
        $this->_conn = null;
romanb's avatar
romanb committed
618 619
    }

620 621 622
    /**
     * Sets the transaction isolation level.
     *
623
     * @param int $level The level to set.
Benjamin Morel's avatar
Benjamin Morel committed
624
     *
625
     * @return int
626 627 628
     */
    public function setTransactionIsolation($level)
    {
629
        $this->transactionIsolationLevel = $level;
630

631
        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
632 633 634 635 636
    }

    /**
     * Gets the currently active transaction isolation level.
     *
637
     * @return int The current transaction isolation level.
638 639 640
     */
    public function getTransactionIsolation()
    {
641 642
        if ($this->transactionIsolationLevel === null) {
            $this->transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
643 644
        }

645
        return $this->transactionIsolationLevel;
646 647
    }

romanb's avatar
romanb committed
648
    /**
649
     * Executes an SQL UPDATE statement on a table.
romanb's avatar
romanb committed
650
     *
651 652
     * Table expression and columns are not escaped and are not safe for user-input.
     *
653 654 655 656
     * @param string         $tableExpression The expression of the table to update quoted or unquoted.
     * @param mixed[]        $data            An associative array containing column-value pairs.
     * @param mixed[]        $identifier      The update criteria. An associative array containing column-value pairs.
     * @param int[]|string[] $types           Types of the merged $data and $identifier arrays in that order.
Benjamin Morel's avatar
Benjamin Morel committed
657
     *
658
     * @return int The number of affected rows.
659
     *
660
     * @throws DBALException
romanb's avatar
romanb committed
661
     */
662
    public function update($tableExpression, array $data, array $identifier, array $types = [])
romanb's avatar
romanb committed
663
    {
Sergei Morozov's avatar
Sergei Morozov committed
664
        $columns = $values = $conditions = $set = [];
665

romanb's avatar
romanb committed
666
        foreach ($data as $columnName => $value) {
Sergei Morozov's avatar
Sergei Morozov committed
667 668 669
            $columns[] = $columnName;
            $values[]  = $value;
            $set[]     = $columnName . ' = ?';
romanb's avatar
romanb committed
670 671
        }

Sergei Morozov's avatar
Sergei Morozov committed
672
        $this->addIdentifierCondition($identifier, $columns, $values, $conditions);
673

674
        if (is_string(key($types))) {
675
            $types = $this->extractTypeValues($columns, $types);
676
        }
romanb's avatar
romanb committed
677

678
        $sql = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
679
                . ' WHERE ' . implode(' AND ', $conditions);
romanb's avatar
romanb committed
680

681
        return $this->executeUpdate($sql, $values, $types);
romanb's avatar
romanb committed
682 683 684 685 686
    }

    /**
     * Inserts a table row with specified data.
     *
687 688
     * Table expression and columns are not escaped and are not safe for user-input.
     *
689 690 691
     * @param string         $tableExpression The expression of the table to insert data into, quoted or unquoted.
     * @param mixed[]        $data            An associative array containing column-value pairs.
     * @param int[]|string[] $types           Types of the inserted data.
Benjamin Morel's avatar
Benjamin Morel committed
692
     *
693
     * @return int The number of affected rows.
694
     *
695
     * @throws DBALException
romanb's avatar
romanb committed
696
     */
697
    public function insert($tableExpression, array $data, array $types = [])
romanb's avatar
romanb committed
698
    {
699
        if (count($data) === 0) {
700
            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' () VALUES ()');
701 702
        }

703
        $columns = [];
704 705
        $values  = [];
        $set     = [];
706 707

        foreach ($data as $columnName => $value) {
708
            $columns[] = $columnName;
709 710
            $values[]  = $value;
            $set[]     = '?';
711 712
        }

713
        return $this->executeUpdate(
714 715 716 717
            'INSERT INTO ' . $tableExpression . ' (' . implode(', ', $columns) . ')' .
            ' VALUES (' . implode(', ', $set) . ')',
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
718
        );
romanb's avatar
romanb committed
719 720
    }

721
    /**
722
     * Extract ordered type list from an ordered column list and type map.
723
     *
Sergei Morozov's avatar
Sergei Morozov committed
724
     * @param int[]|string[] $columnList
725
     * @param int[]|string[] $types
726
     *
727
     * @return int[]|string[]
728
     */
729
    private function extractTypeValues(array $columnList, array $types)
730
    {
731
        $typeValues = [];
732

733
        foreach ($columnList as $columnIndex => $columnName) {
734
            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
735 736 737 738 739
        }

        return $typeValues;
    }

romanb's avatar
romanb committed
740
    /**
Benjamin Morel's avatar
Benjamin Morel committed
741
     * Quotes a string so it can be safely used as a table or column name, even if
romanb's avatar
romanb committed
742 743 744 745
     * it is a reserved name.
     *
     * Delimiting style depends on the underlying database platform that is being used.
     *
746 747
     * 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
748 749
     * problems than they solve.
     *
750
     * @param string $str The name to be quoted.
Benjamin Morel's avatar
Benjamin Morel committed
751
     *
752
     * @return string The quoted name.
romanb's avatar
romanb committed
753 754 755
     */
    public function quoteIdentifier($str)
    {
756
        return $this->getDatabasePlatform()->quoteIdentifier($str);
romanb's avatar
romanb committed
757 758 759
    }

    /**
Sergei Morozov's avatar
Sergei Morozov committed
760
     * {@inheritDoc}
romanb's avatar
romanb committed
761
     */
762
    public function quote($input, $type = ParameterType::STRING)
romanb's avatar
romanb committed
763
    {
Sergei Morozov's avatar
Sergei Morozov committed
764
        $connection = $this->getWrappedConnection();
765

766
        [$value, $bindingType] = $this->getBindingInfo($input, $type);
767

Sergei Morozov's avatar
Sergei Morozov committed
768
        return $connection->quote($value, $bindingType);
romanb's avatar
romanb committed
769 770 771
    }

    /**
772
     * Prepares and executes an SQL query and returns the result as an array of numeric arrays.
romanb's avatar
romanb committed
773
     *
774 775 776
     * @param string                                           $query  The SQL query.
     * @param array<int, mixed>|array<string, mixed>           $params The query parameters.
     * @param array<int, int|string>|array<string, int|string> $types  The query parameter types.
777
     *
778
     * @return array<int,array<int,mixed>>
Benjamin Morel's avatar
Benjamin Morel committed
779
     *
780
     * @throws DBALException
romanb's avatar
romanb committed
781
     */
782
    public function fetchAllNumeric(string $query, array $params = [], array $types = []): array
romanb's avatar
romanb committed
783
    {
784 785
        try {
            return $this->executeQuery($query, $params, $types)->fetchAllNumeric();
786
        } catch (DriverException $e) {
787
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
788
        }
romanb's avatar
romanb committed
789 790
    }

791
    /**
792
     * Prepares and executes an SQL query and returns the result as an array of associative arrays.
793 794 795 796 797
     *
     * @param string                                           $query  The SQL query.
     * @param array<int, mixed>|array<string, mixed>           $params The query parameters.
     * @param array<int, int|string>|array<string, int|string> $types  The query parameter types.
     *
798
     * @return array<int,array<string,mixed>>
799 800 801
     *
     * @throws DBALException
     */
802
    public function fetchAllAssociative(string $query, array $params = [], array $types = []): array
803 804
    {
        try {
805
            return $this->executeQuery($query, $params, $types)->fetchAllAssociative();
806
        } catch (DriverException $e) {
807
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
808 809 810 811
        }
    }

    /**
812
     * Prepares and executes an SQL query and returns the result as an array of the first column values.
813 814 815 816 817
     *
     * @param string                                           $query  The SQL query.
     * @param array<int, mixed>|array<string, mixed>           $params The query parameters.
     * @param array<int, int|string>|array<string, int|string> $types  The query parameter types.
     *
818
     * @return array<int,mixed>
819 820 821
     *
     * @throws DBALException
     */
822
    public function fetchFirstColumn(string $query, array $params = [], array $types = []): array
823 824
    {
        try {
825
            return $this->executeQuery($query, $params, $types)->fetchFirstColumn();
826
        } catch (DriverException $e) {
827
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
828 829 830 831 832 833 834 835 836 837 838 839 840 841
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an iterator over rows represented as numeric arrays.
     *
     * @param string                                           $query  The SQL query.
     * @param array<int, mixed>|array<string, mixed>           $params The query parameters.
     * @param array<int, int|string>|array<string, int|string> $types  The query parameter types.
     *
     * @return Traversable<int,array<int,mixed>>
     *
     * @throws DBALException
     */
842
    public function iterateNumeric(string $query, array $params = [], array $types = []): Traversable
843 844
    {
        try {
845
            $result = $this->executeQuery($query, $params, $types);
846

847
            while (($row = $result->fetchNumeric()) !== false) {
848
                yield $row;
849
            }
850
        } catch (DriverException $e) {
851
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
852 853 854 855 856 857 858 859 860 861 862 863 864 865
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an iterator over rows represented as associative arrays.
     *
     * @param string                                           $query  The SQL query.
     * @param array<int, mixed>|array<string, mixed>           $params The query parameters.
     * @param array<int, int|string>|array<string, int|string> $types  The query parameter types.
     *
     * @return Traversable<int,array<string,mixed>>
     *
     * @throws DBALException
     */
866
    public function iterateAssociative(string $query, array $params = [], array $types = []): Traversable
867 868
    {
        try {
869
            $result = $this->executeQuery($query, $params, $types);
870

871
            while (($row = $result->fetchAssociative()) !== false) {
872
                yield $row;
873
            }
874
        } catch (DriverException $e) {
875
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
876 877 878 879 880 881 882 883 884 885 886 887 888 889
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an iterator over the first column values.
     *
     * @param string                                           $query  The SQL query.
     * @param array<int, mixed>|array<string, mixed>           $params The query parameters.
     * @param array<int, int|string>|array<string, int|string> $types  The query parameter types.
     *
     * @return Traversable<int,mixed>
     *
     * @throws DBALException
     */
890
    public function iterateColumn(string $query, array $params = [], array $types = []): Traversable
891 892
    {
        try {
893
            $result = $this->executeQuery($query, $params, $types);
894

895
            while (($value = $result->fetchOne()) !== false) {
896
                yield $value;
897
            }
898
        } catch (DriverException $e) {
899
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
900 901 902
        }
    }

romanb's avatar
romanb committed
903 904 905
    /**
     * Prepares an SQL statement.
     *
906
     * @param string $sql The SQL statement to prepare.
Benjamin Morel's avatar
Benjamin Morel committed
907
     *
908
     * @return Statement
Benjamin Morel's avatar
Benjamin Morel committed
909
     *
910
     * @throws DBALException
romanb's avatar
romanb committed
911
     */
912
    public function prepare(string $sql): DriverStatement
romanb's avatar
romanb committed
913
    {
914
        return new Statement($sql, $this);
romanb's avatar
romanb committed
915 916 917
    }

    /**
Pascal Borreli's avatar
Pascal Borreli committed
918
     * Executes an, optionally parametrized, SQL query.
romanb's avatar
romanb committed
919
     *
Pascal Borreli's avatar
Pascal Borreli committed
920
     * If the query is parametrized, a prepared statement is used.
921 922
     * If an SQLLogger is configured, the execution is logged.
     *
923
     * @param string                 $query  The SQL query to execute.
924 925
     * @param mixed[]                $params The parameters to bind to the query, if any.
     * @param int[]|string[]         $types  The types the previous parameters are in.
926
     * @param QueryCacheProfile|null $qcp    The query cache profile, optional.
Benjamin Morel's avatar
Benjamin Morel committed
927
     *
928
     * @throws DBALException
romanb's avatar
romanb committed
929
     */
930 931 932 933 934 935
    public function executeQuery(
        string $query,
        array $params = [],
        $types = [],
        ?QueryCacheProfile $qcp = null
    ): AbstractionResult {
936
        if ($qcp !== null) {
937
            return $this->executeCacheQuery($query, $params, $types, $qcp);
938 939
        }

Sergei Morozov's avatar
Sergei Morozov committed
940
        $connection = $this->getWrappedConnection();
romanb's avatar
romanb committed
941

942
        $logger = $this->_config->getSQLLogger();
943
        if ($logger !== null) {
944
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
945
        }
946

947
        try {
948
            if (count($params) > 0) {
949
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
950

Sergei Morozov's avatar
Sergei Morozov committed
951
                $stmt = $connection->prepare($query);
952
                if (count($types) > 0) {
953
                    $this->_bindTypedValues($stmt, $params, $types);
954
                    $result = $stmt->execute();
955
                } else {
956
                    $result = $stmt->execute($params);
957
                }
958
            } else {
959
                $result = $connection->query($query);
960
            }
961 962

            return new Result($result, $this);
963
        } catch (DriverException $e) {
964
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
965 966 967 968
        } finally {
            if ($logger !== null) {
                $logger->stopQuery();
            }
romanb's avatar
romanb committed
969 970
        }
    }
971

972
    /**
Benjamin Morel's avatar
Benjamin Morel committed
973 974
     * Executes a caching query.
     *
975
     * @param string            $query  The SQL query to execute.
976 977
     * @param mixed[]           $params The parameters to bind to the query, if any.
     * @param int[]|string[]    $types  The types the previous parameters are in.
978
     * @param QueryCacheProfile $qcp    The query cache profile.
979
     *
980
     * @throws CacheException
981
     * @throws DBALException
982
     */
983
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp): Result
984
    {
Sergei Morozov's avatar
Sergei Morozov committed
985 986 987
        $resultCache = $qcp->getResultCacheDriver() ?? $this->_config->getResultCacheImpl();

        if ($resultCache === null) {
988 989 990
            throw CacheException::noResultDriverConfigured();
        }

991
        $connectionParams = $this->params;
992 993 994
        unset($connectionParams['platform']);

        [$cacheKey, $realKey] = $qcp->generateCacheKeys($query, $params, $types, $connectionParams);
995 996

        // fetch the row pointers entry
997 998 999
        $data = $resultCache->fetch($cacheKey);

        if ($data !== false) {
1000 1001
            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
            if (isset($data[$realKey])) {
1002
                $result = new ArrayResult($data[$realKey]);
Steve Müller's avatar
Steve Müller committed
1003
            } elseif (array_key_exists($realKey, $data)) {
1004
                $result = new ArrayResult([]);
1005 1006
            }
        }
1007

1008 1009 1010 1011 1012 1013 1014 1015
        if (! isset($result)) {
            $result = new CachingResult(
                $this->executeQuery($query, $params, $types),
                $resultCache,
                $cacheKey,
                $realKey,
                $qcp->getLifetime()
            );
1016 1017
        }

1018
        return new Result($result, $this);
1019 1020
    }

1021 1022 1023
    /**
     * @throws DBALException
     */
1024
    public function query(string $sql): DriverResult
1025
    {
Sergei Morozov's avatar
Sergei Morozov committed
1026
        $connection = $this->getWrappedConnection();
1027

1028
        $logger = $this->_config->getSQLLogger();
1029
        if ($logger !== null) {
1030
            $logger->startQuery($sql);
1031 1032
        }

1033
        try {
1034
            return $connection->query($sql);
1035
        } catch (DriverException $e) {
1036
            throw $this->convertExceptionDuringQuery($e, $sql);
1037 1038 1039 1040
        } finally {
            if ($logger !== null) {
                $logger->stopQuery();
            }
1041
        }
1042 1043 1044 1045 1046
    }

    /**
     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
     * and returns the number of affected rows.
1047
     *
1048 1049 1050
     * @param string                 $query  The SQL query.
     * @param array<mixed>           $params The query parameters.
     * @param array<int|string|null> $types  The parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
1051
     *
1052
     * @throws DBALException
romanb's avatar
romanb committed
1053
     */
1054
    public function executeUpdate(string $query, array $params = [], array $types = []): int
romanb's avatar
romanb committed
1055
    {
Sergei Morozov's avatar
Sergei Morozov committed
1056
        $connection = $this->getWrappedConnection();
romanb's avatar
romanb committed
1057

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

1063
        try {
1064
            if (count($params) > 0) {
1065
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
1066

Sergei Morozov's avatar
Sergei Morozov committed
1067 1068
                $stmt = $connection->prepare($query);

1069
                if (count($types) > 0) {
1070
                    $this->_bindTypedValues($stmt, $params, $types);
1071 1072

                    $result = $stmt->execute();
1073
                } else {
1074
                    $result = $stmt->execute($params);
1075
                }
Grégoire Paris's avatar
Grégoire Paris committed
1076

1077
                return $result->rowCount();
1078
            }
1079 1080

            return $connection->exec($query);
1081
        } catch (DriverException $e) {
1082
            throw $this->convertExceptionDuringQuery($e, $query, $params, $types);
1083 1084 1085 1086
        } finally {
            if ($logger !== null) {
                $logger->stopQuery();
            }
romanb's avatar
romanb committed
1087 1088 1089
        }
    }

1090 1091 1092
    /**
     * @throws DBALException
     */
1093
    public function exec(string $statement): int
1094
    {
Sergei Morozov's avatar
Sergei Morozov committed
1095
        $connection = $this->getWrappedConnection();
1096 1097

        $logger = $this->_config->getSQLLogger();
1098
        if ($logger !== null) {
1099 1100 1101
            $logger->startQuery($statement);
        }

1102
        try {
1103
            return $connection->exec($statement);
1104
        } catch (DriverException $e) {
1105
            throw $this->convertExceptionDuringQuery($e, $statement);
1106 1107 1108 1109
        } finally {
            if ($logger !== null) {
                $logger->stopQuery();
            }
1110
        }
1111 1112
    }

1113 1114 1115
    /**
     * Returns the current transaction nesting level.
     *
1116
     * @return int The nesting level. A value of 0 means there's no active transaction.
1117 1118 1119
     */
    public function getTransactionNestingLevel()
    {
1120
        return $this->transactionNestingLevel;
1121 1122
    }

romanb's avatar
romanb committed
1123 1124 1125 1126 1127
    /**
     * 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,
1128 1129
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
     * columns or sequences.
romanb's avatar
romanb committed
1130
     *
Benjamin Morel's avatar
Benjamin Morel committed
1131 1132
     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
     *
1133
     * @return string A string representation of the last inserted ID.
romanb's avatar
romanb committed
1134
     */
romanb's avatar
romanb committed
1135 1136
    public function lastInsertId($seqName = null)
    {
Sergei Morozov's avatar
Sergei Morozov committed
1137
        return $this->getWrappedConnection()->lastInsertId($seqName);
romanb's avatar
romanb committed
1138
    }
1139

1140 1141 1142 1143 1144 1145 1146 1147
    /**
     * 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.
     *
1148
     * @param Closure $func The function to execute transactionally.
Benjamin Morel's avatar
Benjamin Morel committed
1149
     *
1150
     * @return mixed The value returned by $func
Benjamin Morel's avatar
Benjamin Morel committed
1151
     *
1152 1153
     * @throws Exception
     * @throws Throwable
1154 1155 1156 1157 1158
     */
    public function transactional(Closure $func)
    {
        $this->beginTransaction();
        try {
1159
            $res = $func($this);
1160
            $this->commit();
1161

1162
            return $res;
1163
        } catch (Exception $e) {
1164
            $this->rollBack();
Grégoire Paris's avatar
Grégoire Paris committed
1165

1166 1167 1168
            throw $e;
        } catch (Throwable $e) {
            $this->rollBack();
Grégoire Paris's avatar
Grégoire Paris committed
1169

1170 1171 1172 1173
            throw $e;
        }
    }

1174
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1175
     * Sets if nested transactions should use savepoints.
1176
     *
1177
     * @param bool $nestTransactionsWithSavepoints
Benjamin Morel's avatar
Benjamin Morel committed
1178
     *
1179
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1180
     *
1181
     * @throws ConnectionException
1182 1183 1184
     */
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
    {
1185
        if ($this->transactionNestingLevel > 0) {
1186 1187 1188
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
        }

1189
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1190
            throw ConnectionException::savepointsNotSupported();
1191 1192
        }

1193
        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1194 1195 1196
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1197
     * Gets if nested transactions should use savepoints.
1198
     *
1199
     * @return bool
1200 1201 1202
     */
    public function getNestTransactionsWithSavepoints()
    {
1203
        return $this->nestTransactionsWithSavepoints;
1204 1205
    }

1206 1207 1208 1209
    /**
     * 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
1210
     * @return mixed A string with the savepoint name or false.
1211
     */
1212 1213
    protected function _getNestedTransactionSavePointName()
    {
1214
        return 'DOCTRINE2_SAVEPOINT_' . $this->transactionNestingLevel;
1215 1216
    }

1217
    /**
1218
     * {@inheritDoc}
1219 1220 1221
     */
    public function beginTransaction()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1222
        $connection = $this->getWrappedConnection();
1223

1224
        ++$this->transactionNestingLevel;
1225

1226 1227
        $logger = $this->_config->getSQLLogger();

1228
        if ($this->transactionNestingLevel === 1) {
1229
            if ($logger !== null) {
1230 1231
                $logger->startQuery('"START TRANSACTION"');
            }
Sergei Morozov's avatar
Sergei Morozov committed
1232 1233 1234

            $connection->beginTransaction();

1235
            if ($logger !== null) {
1236 1237
                $logger->stopQuery();
            }
1238
        } elseif ($this->nestTransactionsWithSavepoints) {
1239
            if ($logger !== null) {
1240 1241
                $logger->startQuery('"SAVEPOINT"');
            }
Grégoire Paris's avatar
Grégoire Paris committed
1242

1243
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1244
            if ($logger !== null) {
1245 1246
                $logger->stopQuery();
            }
1247
        }
1248 1249

        return true;
1250 1251 1252
    }

    /**
1253
     * {@inheritDoc}
Benjamin Morel's avatar
Benjamin Morel committed
1254
     *
1255
     * @throws ConnectionException If the commit failed due to no active transaction or
Benjamin Morel's avatar
Benjamin Morel committed
1256
     *                                            because the transaction was marked for rollback only.
1257 1258 1259
     */
    public function commit()
    {
1260
        if ($this->transactionNestingLevel === 0) {
1261
            throw ConnectionException::noActiveTransaction();
1262
        }
Grégoire Paris's avatar
Grégoire Paris committed
1263

1264
        if ($this->isRollbackOnly) {
1265 1266 1267
            throw ConnectionException::commitFailedRollbackOnly();
        }

1268 1269
        $result = true;

Sergei Morozov's avatar
Sergei Morozov committed
1270
        $connection = $this->getWrappedConnection();
1271

1272 1273
        $logger = $this->_config->getSQLLogger();

1274
        if ($this->transactionNestingLevel === 1) {
1275
            if ($logger !== null) {
1276 1277
                $logger->startQuery('"COMMIT"');
            }
Sergei Morozov's avatar
Sergei Morozov committed
1278

1279
            $result = $connection->commit();
Sergei Morozov's avatar
Sergei Morozov committed
1280

1281
            if ($logger !== null) {
1282 1283
                $logger->stopQuery();
            }
1284
        } elseif ($this->nestTransactionsWithSavepoints) {
1285
            if ($logger !== null) {
1286 1287
                $logger->startQuery('"RELEASE SAVEPOINT"');
            }
Grégoire Paris's avatar
Grégoire Paris committed
1288

1289
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1290
            if ($logger !== null) {
1291 1292
                $logger->stopQuery();
            }
1293 1294
        }

1295
        --$this->transactionNestingLevel;
1296

1297
        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1298
            return $result;
1299
        }
1300 1301

        $this->beginTransaction();
1302

1303
        return $result;
1304 1305 1306 1307 1308
    }

    /**
     * Commits all current nesting transactions.
     */
1309
    private function commitAll(): void
1310
    {
1311 1312
        while ($this->transactionNestingLevel !== 0) {
            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
1313 1314 1315 1316 1317 1318 1319 1320 1321
                // 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();
        }
1322 1323 1324
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1325
     * Cancels any database changes done during the current transaction.
1326
     *
1327 1328
     * @return bool
     *
1329
     * @throws ConnectionException If the rollback operation failed.
1330
     */
1331
    public function rollBack()
1332
    {
1333
        if ($this->transactionNestingLevel === 0) {
1334
            throw ConnectionException::noActiveTransaction();
1335 1336
        }

Sergei Morozov's avatar
Sergei Morozov committed
1337
        $connection = $this->getWrappedConnection();
1338

1339 1340
        $logger = $this->_config->getSQLLogger();

1341
        if ($this->transactionNestingLevel === 1) {
1342
            if ($logger !== null) {
1343 1344
                $logger->startQuery('"ROLLBACK"');
            }
Grégoire Paris's avatar
Grégoire Paris committed
1345

1346
            $this->transactionNestingLevel = 0;
Sergei Morozov's avatar
Sergei Morozov committed
1347
            $connection->rollBack();
1348
            $this->isRollbackOnly = false;
1349
            if ($logger !== null) {
1350 1351
                $logger->stopQuery();
            }
1352

1353
            if ($this->autoCommit === false) {
1354 1355
                $this->beginTransaction();
            }
1356
        } elseif ($this->nestTransactionsWithSavepoints) {
1357
            if ($logger !== null) {
1358 1359
                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
            }
Grégoire Paris's avatar
Grégoire Paris committed
1360

1361
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
1362
            --$this->transactionNestingLevel;
1363
            if ($logger !== null) {
1364 1365
                $logger->stopQuery();
            }
1366
        } else {
1367 1368
            $this->isRollbackOnly = true;
            --$this->transactionNestingLevel;
1369
        }
1370 1371

        return true;
1372 1373
    }

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

Sergei Morozov's avatar
Sergei Morozov committed
1389
        $this->getWrappedConnection()->exec($this->platform->createSavePoint($savepoint));
1390 1391 1392
    }

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

1407 1408
        if (! $this->platform->supportsReleaseSavepoints()) {
            return;
1409
        }
1410

Sergei Morozov's avatar
Sergei Morozov committed
1411
        $this->getWrappedConnection()->exec($this->platform->releaseSavePoint($savepoint));
1412 1413 1414
    }

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

Sergei Morozov's avatar
Sergei Morozov committed
1429
        $this->getWrappedConnection()->exec($this->platform->rollbackSavePoint($savepoint));
1430 1431
    }

romanb's avatar
romanb committed
1432 1433 1434
    /**
     * Gets the wrapped driver connection.
     *
Sergei Morozov's avatar
Sergei Morozov committed
1435
     * @return DriverConnection
romanb's avatar
romanb committed
1436 1437 1438 1439
     */
    public function getWrappedConnection()
    {
        $this->connect();
1440

1441 1442
        assert($this->_conn !== null);

romanb's avatar
romanb committed
1443 1444
        return $this->_conn;
    }
1445

romanb's avatar
romanb committed
1446 1447 1448 1449
    /**
     * Gets the SchemaManager that can be used to inspect or change the
     * database schema through the connection.
     *
1450
     * @return AbstractSchemaManager
1451 1452
     *
     * @throws DBALException
romanb's avatar
romanb committed
1453 1454 1455
     */
    public function getSchemaManager()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1456
        if ($this->_schemaManager === null) {
1457 1458 1459 1460
            $this->_schemaManager = $this->_driver->getSchemaManager(
                $this,
                $this->getDatabasePlatform()
            );
romanb's avatar
romanb committed
1461
        }
1462

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

1466 1467 1468
    /**
     * Marks the current transaction so that the only possible
     * outcome for the transaction to be rolled back.
1469
     *
Benjamin Morel's avatar
Benjamin Morel committed
1470 1471
     * @return void
     *
1472
     * @throws ConnectionException If no transaction is active.
1473 1474 1475
     */
    public function setRollbackOnly()
    {
1476
        if ($this->transactionNestingLevel === 0) {
1477 1478
            throw ConnectionException::noActiveTransaction();
        }
Grégoire Paris's avatar
Grégoire Paris committed
1479

1480
        $this->isRollbackOnly = true;
1481 1482 1483
    }

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

1496
        return $this->isRollbackOnly;
1497 1498
    }

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

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

    /**
     * Binds a set of parameters, some or all of which are typed with a PDO binding type
     * or DBAL mapping type, to a given statement.
1530
     *
1531 1532 1533
     * @param DriverStatement $stmt   The statement to bind the values to.
     * @param mixed[]         $params The map/list of named/positional parameters.
     * @param int[]|string[]  $types  The parameter types (PDO binding types or DBAL mapping types).
1534
     */
1535
    private function _bindTypedValues(DriverStatement $stmt, array $params, array $types): void
1536
    {
1537
        // Check whether parameters are positional or named. Mixing is not allowed.
1538 1539
        if (is_int(key($params))) {
            // Positional parameters
1540
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1541
            $bindIndex  = 1;
1542
            foreach ($params as $value) {
1543 1544
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
1545 1546
                    $type                  = $types[$typeIndex];
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1547 1548 1549 1550
                    $stmt->bindValue($bindIndex, $value, $bindingType);
                } else {
                    $stmt->bindValue($bindIndex, $value);
                }
Grégoire Paris's avatar
Grégoire Paris committed
1551

1552 1553 1554 1555 1556 1557
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
1558 1559
                    $type                  = $types[$name];
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1560 1561 1562 1563 1564 1565 1566
                    $stmt->bindValue($name, $value, $bindingType);
                } else {
                    $stmt->bindValue($name, $value);
                }
            }
        }
    }
1567 1568

    /**
1569
     * Gets the binding type of a given type.
1570
     *
Sergei Morozov's avatar
Sergei Morozov committed
1571 1572
     * @param mixed           $value The value to bind.
     * @param int|string|null $type  The type to bind (PDO or DBAL).
Benjamin Morel's avatar
Benjamin Morel committed
1573
     *
1574
     * @return mixed[] [0] => the (escaped) value, [1] => the binding type.
1575 1576 1577 1578 1579 1580
     */
    private function getBindingInfo($value, $type)
    {
        if (is_string($type)) {
            $type = Type::getType($type);
        }
Grégoire Paris's avatar
Grégoire Paris committed
1581

1582
        if ($type instanceof Type) {
1583
            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1584 1585
            $bindingType = $type->getBindingType();
        } else {
1586
            $bindingType = $type;
1587
        }
Benjamin Morel's avatar
Benjamin Morel committed
1588

1589
        return [$value, $bindingType];
1590 1591
    }

1592 1593 1594
    /**
     * Resolves the parameters to a format which can be displayed.
     *
1595 1596
     * @param mixed[]                $params
     * @param array<int|string|null> $types
1597
     *
1598
     * @return mixed[]
1599
     */
1600
    private function resolveParams(array $params, array $types): array
1601
    {
1602
        $resolvedParams = [];
1603

1604
        // Check whether parameters are positional or named. Mixing is not allowed.
1605 1606 1607
        if (is_int(key($params))) {
            // Positional parameters
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1608
            $bindIndex  = 1;
1609 1610 1611
            foreach ($params as $value) {
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
1612 1613
                    $type                       = $types[$typeIndex];
                    [$value]                    = $this->getBindingInfo($value, $type);
1614 1615 1616 1617
                    $resolvedParams[$bindIndex] = $value;
                } else {
                    $resolvedParams[$bindIndex] = $value;
                }
Grégoire Paris's avatar
Grégoire Paris committed
1618

1619 1620 1621 1622 1623 1624
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
1625 1626
                    $type                  = $types[$name];
                    [$value]               = $this->getBindingInfo($value, $type);
1627 1628 1629 1630 1631 1632 1633 1634 1635 1636
                    $resolvedParams[$name] = $value;
                } else {
                    $resolvedParams[$name] = $value;
                }
            }
        }

        return $resolvedParams;
    }

1637
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1638
     * Creates a new instance of a SQL query builder.
1639
     *
1640
     * @return QueryBuilder
1641 1642 1643 1644 1645
     */
    public function createQueryBuilder()
    {
        return new Query\QueryBuilder($this);
    }
1646

1647 1648 1649
    /**
     * @internal
     *
1650 1651
     * @param array<mixed>           $params
     * @param array<int|string|null> $types
1652
     */
1653 1654 1655 1656 1657 1658 1659 1660 1661 1662
    final public function convertExceptionDuringQuery(
        DriverException $e,
        string $sql,
        array $params = [],
        array $types = []
    ): DBALException {
        $message = "An exception occurred while executing '" . $sql . "'";

        if (count($params) > 0) {
            $message .= ' with params ' . $this->formatParameters(
1663
                $this->resolveParams($params, $types)
1664 1665 1666 1667 1668 1669
            );
        }

        $message .= ":\n\n" . $e->getMessage();

        return $this->handleDriverException($e, $message);
1670 1671 1672 1673 1674
    }

    /**
     * @internal
     */
1675
    final public function convertException(DriverException $e): DBALException
1676
    {
1677 1678 1679
        return $this->handleDriverException(
            $e,
            'An exception occurred in driver: ' . $e->getMessage()
1680 1681 1682 1683
        );
    }

    /**
1684 1685
     * Returns a human-readable representation of an array of parameters.
     * This properly handles binary data by returning a hex representation.
1686
     *
1687
     * @param mixed[] $params
1688
     */
1689
    private function formatParameters(array $params): string
1690
    {
1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707
        return '[' . implode(', ', array_map(static function ($param): string {
            if (is_resource($param)) {
                return (string) $param;
            }

            $json = @json_encode($param);

            if (! is_string($json) || $json === 'null' && is_string($param)) {
                // JSON encoding failed, this is not a UTF-8 string.
                return sprintf('"%s"', preg_replace('/.{2}/', '\\x$0', bin2hex($param)));
            }

            return $json;
        }, $params)) . ']';
    }

    private function handleDriverException(DriverException $driverException, string $message): DBALException
1708
    {
1709 1710 1711 1712 1713 1714 1715
        if ($this->exceptionConverter === null) {
            $this->exceptionConverter = $this->_driver->getExceptionConverter();
        }

        $exception = $this->exceptionConverter->convert($message, $driverException);

        if ($exception instanceof ConnectionLost) {
1716 1717 1718
            $this->close();
        }

1719
        return $exception;
1720
    }
1721
}