Connection.php 49.1 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 7 8
use Closure;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Cache\ArrayStatement;
use Doctrine\DBAL\Cache\CacheException;
9 10 11
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Cache\ResultCacheStatement;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
12
use Doctrine\DBAL\Driver\PingableConnection;
13 14 15 16 17 18 19 20 21 22
use Doctrine\DBAL\Driver\ResultStatement;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\Driver\Statement as DriverStatement;
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;
23
use Throwable;
24
use function array_key_exists;
25
use function assert;
26
use function count;
27 28 29 30
use function implode;
use function is_int;
use function is_string;
use function key;
31 32

/**
33
 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
34 35
 * events, transaction isolation levels, configuration, emulated transaction nesting,
 * lazy connecting and more.
36
 */
37
class Connection implements DriverConnection
38
{
39 40
    /**
     * Constant for transaction isolation level READ UNCOMMITTED.
41 42
     *
     * @deprecated Use TransactionIsolationLevel::READ_UNCOMMITTED.
43
     */
44
    public const TRANSACTION_READ_UNCOMMITTED = TransactionIsolationLevel::READ_UNCOMMITTED;
45

46 47
    /**
     * Constant for transaction isolation level READ COMMITTED.
48 49
     *
     * @deprecated Use TransactionIsolationLevel::READ_COMMITTED.
50
     */
51
    public const TRANSACTION_READ_COMMITTED = TransactionIsolationLevel::READ_COMMITTED;
52

53 54
    /**
     * Constant for transaction isolation level REPEATABLE READ.
55 56
     *
     * @deprecated Use TransactionIsolationLevel::REPEATABLE_READ.
57
     */
58
    public const TRANSACTION_REPEATABLE_READ = TransactionIsolationLevel::REPEATABLE_READ;
59

60 61
    /**
     * Constant for transaction isolation level SERIALIZABLE.
62 63
     *
     * @deprecated Use TransactionIsolationLevel::SERIALIZABLE.
64
     */
65
    public const TRANSACTION_SERIALIZABLE = TransactionIsolationLevel::SERIALIZABLE;
66

67 68 69
    /**
     * Represents an array of ints to be expanded by Doctrine SQL parsing.
     */
Sergei Morozov's avatar
Sergei Morozov committed
70
    public const PARAM_INT_ARRAY = ParameterType::INTEGER + self::ARRAY_PARAM_OFFSET;
71

72 73 74
    /**
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
     */
Sergei Morozov's avatar
Sergei Morozov committed
75
    public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET;
76

77 78 79
    /**
     * Offset by which PARAM_* constants are detected as arrays of the param type.
     */
80
    public const ARRAY_PARAM_OFFSET = 100;
81

romanb's avatar
romanb committed
82 83 84
    /**
     * The wrapped driver connection.
     *
85
     * @var \Doctrine\DBAL\Driver\Connection|null
romanb's avatar
romanb committed
86 87
     */
    protected $_conn;
88

89
    /** @var Configuration */
romanb's avatar
romanb committed
90
    protected $_config;
91

92
    /** @var EventManager */
romanb's avatar
romanb committed
93
    protected $_eventManager;
94

95
    /** @var ExpressionBuilder */
96
    protected $_expr;
97

romanb's avatar
romanb committed
98 99 100
    /**
     * Whether or not a connection has been established.
     *
101
     * @var bool
romanb's avatar
romanb committed
102
     */
103
    private $isConnected = false;
104

105
    /**
106
     * The current auto-commit mode of this connection.
107
     *
108
     * @var bool
109 110 111
     */
    private $autoCommit = true;

112 113 114
    /**
     * The transaction nesting level.
     *
115
     * @var int
116
     */
117
    private $transactionNestingLevel = 0;
118 119 120 121

    /**
     * The currently active transaction isolation level.
     *
122
     * @var int
123
     */
124
    private $transactionIsolationLevel;
125

126
    /**
Benjamin Morel's avatar
Benjamin Morel committed
127
     * If nested transactions should use savepoints.
128
     *
129
     * @var bool
130
     */
131
    private $nestTransactionsWithSavepoints = false;
Lukas Kahwe Smith's avatar
Lukas Kahwe Smith committed
132

romanb's avatar
romanb committed
133 134 135
    /**
     * The parameters used during creation of the Connection instance.
     *
136
     * @var mixed[]
romanb's avatar
romanb committed
137
     */
138
    private $params = [];
139

romanb's avatar
romanb committed
140 141 142 143
    /**
     * The DatabasePlatform object that provides information about the
     * database platform used by the connection.
     *
144
     * @var AbstractPlatform
romanb's avatar
romanb committed
145
     */
146
    private $platform;
147

romanb's avatar
romanb committed
148 149 150
    /**
     * The schema manager.
     *
Sergei Morozov's avatar
Sergei Morozov committed
151
     * @var AbstractSchemaManager|null
romanb's avatar
romanb committed
152 153
     */
    protected $_schemaManager;
154

romanb's avatar
romanb committed
155
    /**
romanb's avatar
romanb committed
156 157
     * The used DBAL driver.
     *
158
     * @var Driver
romanb's avatar
romanb committed
159 160
     */
    protected $_driver;
161

162
    /**
163
     * Flag that indicates whether the current transaction is marked for rollback only.
164
     *
165
     * @var bool
166
     */
167
    private $isRollbackOnly = false;
168

169
    /** @var int */
170
    protected $defaultFetchMode = FetchMode::ASSOCIATIVE;
171

romanb's avatar
romanb committed
172 173 174
    /**
     * Initializes a new instance of the Connection class.
     *
175
     * @param mixed[]            $params       The connection parameters.
176 177 178
     * @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
179
     *
180
     * @throws DBALException
romanb's avatar
romanb committed
181
     */
182 183 184 185 186 187
    public function __construct(
        array $params,
        Driver $driver,
        ?Configuration $config = null,
        ?EventManager $eventManager = null
    ) {
romanb's avatar
romanb committed
188
        $this->_driver = $driver;
189
        $this->params  = $params;
190

191 192
        if (isset($params['platform'])) {
            if (! $params['platform'] instanceof Platforms\AbstractPlatform) {
193
                throw DBALException::invalidPlatformType($params['platform']);
194 195
            }

196
            $this->platform = $params['platform'];
197 198
        }

romanb's avatar
romanb committed
199
        // Create default config and event manager if none given
200
        if ($config === null) {
romanb's avatar
romanb committed
201
            $config = new Configuration();
romanb's avatar
romanb committed
202
        }
203

204
        if ($eventManager === null) {
romanb's avatar
romanb committed
205
            $eventManager = new EventManager();
romanb's avatar
romanb committed
206
        }
207

208
        $this->_config       = $config;
romanb's avatar
romanb committed
209
        $this->_eventManager = $eventManager;
210

211
        $this->_expr = new Query\Expression\ExpressionBuilder($this);
212

213
        $this->autoCommit = $config->getAutoCommit();
romanb's avatar
romanb committed
214
    }
romanb's avatar
romanb committed
215

216
    /**
romanb's avatar
romanb committed
217
     * Gets the parameters used during instantiation.
218
     *
219
     * @return mixed[]
220 221 222
     */
    public function getParams()
    {
223
        return $this->params;
224 225
    }

romanb's avatar
romanb committed
226
    /**
romanb's avatar
romanb committed
227
     * Gets the name of the database this Connection is connected to.
romanb's avatar
romanb committed
228
     *
Benjamin Morel's avatar
Benjamin Morel committed
229
     * @return string
romanb's avatar
romanb committed
230 231 232 233 234
     */
    public function getDatabase()
    {
        return $this->_driver->getDatabase($this);
    }
235

236 237
    /**
     * Gets the hostname of the currently connected database.
238
     *
239 240
     * @deprecated
     *
Benjamin Morel's avatar
Benjamin Morel committed
241
     * @return string|null
242 243 244
     */
    public function getHost()
    {
245
        return $this->params['host'] ?? null;
246
    }
247

248 249
    /**
     * Gets the port of the currently connected database.
250
     *
251 252
     * @deprecated
     *
253 254 255 256
     * @return mixed
     */
    public function getPort()
    {
257
        return $this->params['port'] ?? null;
258
    }
259

260 261
    /**
     * Gets the username used by this connection.
262
     *
263 264
     * @deprecated
     *
Benjamin Morel's avatar
Benjamin Morel committed
265
     * @return string|null
266 267 268
     */
    public function getUsername()
    {
269
        return $this->params['user'] ?? null;
270
    }
271

272 273
    /**
     * Gets the password used by this connection.
274
     *
275 276
     * @deprecated
     *
Benjamin Morel's avatar
Benjamin Morel committed
277
     * @return string|null
278 279 280
     */
    public function getPassword()
    {
281
        return $this->params['password'] ?? null;
282
    }
romanb's avatar
romanb committed
283 284 285 286

    /**
     * Gets the DBAL driver instance.
     *
287
     * @return Driver
romanb's avatar
romanb committed
288 289 290 291 292 293 294 295 296
     */
    public function getDriver()
    {
        return $this->_driver;
    }

    /**
     * Gets the Configuration used by the Connection.
     *
297
     * @return Configuration
romanb's avatar
romanb committed
298 299 300 301 302 303 304 305 306
     */
    public function getConfiguration()
    {
        return $this->_config;
    }

    /**
     * Gets the EventManager used by the Connection.
     *
307
     * @return EventManager
romanb's avatar
romanb committed
308 309 310 311 312 313 314 315 316
     */
    public function getEventManager()
    {
        return $this->_eventManager;
    }

    /**
     * Gets the DatabasePlatform for the connection.
     *
317
     * @return AbstractPlatform
318
     *
319
     * @throws DBALException
romanb's avatar
romanb committed
320 321 322
     */
    public function getDatabasePlatform()
    {
323
        if ($this->platform === null) {
324
            $this->detectDatabasePlatform();
325 326 327
        }

        return $this->platform;
romanb's avatar
romanb committed
328
    }
329

330 331 332
    /**
     * Gets the ExpressionBuilder for the connection.
     *
333
     * @return ExpressionBuilder
334 335 336 337 338
     */
    public function getExpressionBuilder()
    {
        return $this->_expr;
    }
339

romanb's avatar
romanb committed
340 341 342
    /**
     * Establishes the connection with the database.
     *
343 344
     * @return bool TRUE if the connection was successfully established, FALSE if
     *              the connection is already open.
romanb's avatar
romanb committed
345 346 347
     */
    public function connect()
    {
348
        if ($this->isConnected) {
349 350
            return false;
        }
romanb's avatar
romanb committed
351

352 353 354
        $driverOptions = $this->params['driverOptions'] ?? [];
        $user          = $this->params['user'] ?? null;
        $password      = $this->params['password'] ?? null;
romanb's avatar
romanb committed
355

356 357
        $this->_conn       = $this->_driver->connect($this->params, $user, $password, $driverOptions);
        $this->isConnected = true;
romanb's avatar
romanb committed
358

359 360
        $this->transactionNestingLevel = 0;

361
        if ($this->autoCommit === false) {
362 363 364
            $this->beginTransaction();
        }

365
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
366
            $eventArgs = new Event\ConnectionEventArgs($this);
367 368 369
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
        }

romanb's avatar
romanb committed
370 371 372
        return true;
    }

373 374 375 376 377
    /**
     * Detects and sets the database platform.
     *
     * Evaluates custom platform class and version in order to set the correct platform.
     *
378
     * @throws DBALException If an invalid platform was specified for this connection.
379
     */
380
    private function detectDatabasePlatform() : void
381
    {
382
        $version = $this->getDatabasePlatformVersion();
383

384 385 386
        if ($version !== null) {
            assert($this->_driver instanceof VersionAwarePlatformDriver);

387
            $this->platform = $this->_driver->createDatabasePlatformForVersion($version);
388
        } else {
389
            $this->platform = $this->_driver->getDatabasePlatform();
390 391 392 393 394 395 396 397 398 399 400 401 402 403
        }

        $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
404
     *
405
     * @throws Exception
406 407 408 409
     */
    private function getDatabasePlatformVersion()
    {
        // Driver does not support version specific platforms.
410
        if (! $this->_driver instanceof VersionAwarePlatformDriver) {
411 412 413 414
            return null;
        }

        // Explicit platform version requested (supersedes auto-detection).
415 416
        if (isset($this->params['serverVersion'])) {
            return $this->params['serverVersion'];
417 418 419
        }

        // If not connected, we need to connect now to determine the platform version.
420
        if ($this->_conn === null) {
421 422
            try {
                $this->connect();
423
            } catch (Throwable $originalException) {
424
                if (! isset($this->params['dbname'])) {
425 426 427 428 429
                    throw $originalException;
                }

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

                try {
                    $this->connect();
435
                } catch (Throwable $fallbackException) {
436 437 438
                    // Either the platform does not support database-less connections
                    // or something else went wrong.
                    // Reset connection parameters and rethrow the original exception.
439
                    $this->params['dbname'] = $databaseName;
440 441 442 443 444

                    throw $originalException;
                }

                // Reset connection parameters.
445 446
                $this->params['dbname'] = $databaseName;
                $serverVersion          = $this->getServerVersion();
447 448 449 450 451 452

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

                return $serverVersion;
            }
453 454
        }

455 456 457 458 459 460 461 462 463 464
        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
465 466
        $connection = $this->getWrappedConnection();

467
        // Automatic platform version detection.
Sergei Morozov's avatar
Sergei Morozov committed
468 469
        if ($connection instanceof ServerInfoAwareConnection && ! $connection->requiresQueryForServerVersion()) {
            return $connection->getServerVersion();
470 471 472 473 474 475
        }

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

476 477 478 479
    /**
     * Returns the current auto-commit mode for this connection.
     *
     * @see    setAutoCommit
480 481
     *
     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
482
     */
483
    public function isAutoCommit()
484
    {
485
        return $this->autoCommit === true;
486 487 488 489 490 491 492 493 494 495 496 497
    }

    /**
     * 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.
     *
498
     * @see   isAutoCommit
499 500
     *
     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
501 502
     *
     * @return void
503 504 505
     */
    public function setAutoCommit($autoCommit)
    {
506
        $autoCommit = (bool) $autoCommit;
507 508 509 510 511 512 513 514 515

        // 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.
516
        if ($this->isConnected !== true || $this->transactionNestingLevel === 0) {
517
            return;
518
        }
519 520

        $this->commitAll();
521 522
    }

523
    /**
Benjamin Morel's avatar
Benjamin Morel committed
524
     * Sets the fetch mode.
525
     *
526
     * @param int $fetchMode
Benjamin Morel's avatar
Benjamin Morel committed
527 528
     *
     * @return void
529
     */
530
    public function setFetchMode($fetchMode)
531
    {
532
        $this->defaultFetchMode = $fetchMode;
533 534
    }

romanb's avatar
romanb committed
535
    /**
536 537
     * Prepares and executes an SQL query and returns the first row of the result
     * as an associative array.
538
     *
539 540 541
     * @param string         $statement The SQL query.
     * @param mixed[]        $params    The query parameters.
     * @param int[]|string[] $types     The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
542
     *
543
     * @return mixed[]|false False is returned if no rows are found.
544
     *
545
     * @throws DBALException
romanb's avatar
romanb committed
546
     */
547
    public function fetchAssoc($statement, array $params = [], array $types = [])
romanb's avatar
romanb committed
548
    {
549
        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::ASSOCIATIVE);
romanb's avatar
romanb committed
550 551 552
    }

    /**
553 554
     * Prepares and executes an SQL query and returns the first row of the result
     * as a numerically indexed array.
romanb's avatar
romanb committed
555
     *
556 557 558
     * @param string         $statement The SQL query to be executed.
     * @param mixed[]        $params    The prepared statement params.
     * @param int[]|string[] $types     The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
559
     *
560
     * @return mixed[]|false False is returned if no rows are found.
romanb's avatar
romanb committed
561
     */
562
    public function fetchArray($statement, array $params = [], array $types = [])
romanb's avatar
romanb committed
563
    {
564
        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::NUMERIC);
romanb's avatar
romanb committed
565 566 567
    }

    /**
568 569
     * Prepares and executes an SQL query and returns the value of a single column
     * of the first row of the result.
570
     *
571 572 573
     * @param string         $statement The SQL query to be executed.
     * @param mixed[]        $params    The prepared statement params.
     * @param int[]|string[] $types     The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
574
     *
575
     * @return mixed|false False is returned if no rows are found.
576
     *
577
     * @throws DBALException
romanb's avatar
romanb committed
578
     */
579
    public function fetchColumn($statement, array $params = [], array $types = [])
romanb's avatar
romanb committed
580
    {
581
        return $this->executeQuery($statement, $params, $types)->fetchColumn();
romanb's avatar
romanb committed
582 583 584 585 586
    }

    /**
     * Whether an actual connection to the database is established.
     *
587
     * @return bool
romanb's avatar
romanb committed
588 589 590
     */
    public function isConnected()
    {
591
        return $this->isConnected;
romanb's avatar
romanb committed
592 593
    }

594 595
    /**
     * Checks whether a transaction is currently active.
596
     *
597
     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
598 599 600
     */
    public function isTransactionActive()
    {
601
        return $this->transactionNestingLevel > 0;
602 603
    }

604
    /**
Sergei Morozov's avatar
Sergei Morozov committed
605
     * Adds identifier condition to the query components
606
     *
Sergei Morozov's avatar
Sergei Morozov committed
607 608 609 610
     * @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
611
     *
Sergei Morozov's avatar
Sergei Morozov committed
612
     * @throws DBALException
613
     */
Sergei Morozov's avatar
Sergei Morozov committed
614 615 616 617 618 619 620
    private function addIdentifierCondition(
        array $identifier,
        array &$columns,
        array &$values,
        array &$conditions
    ) : void {
        $platform = $this->getDatabasePlatform();
621

Sergei Morozov's avatar
Sergei Morozov committed
622
        foreach ($identifier as $columnName => $value) {
623
            if ($value === null) {
Sergei Morozov's avatar
Sergei Morozov committed
624
                $conditions[] = $platform->getIsNullExpression($columnName);
625 626 627
                continue;
            }

628 629
            $columns[]    = $columnName;
            $values[]     = $value;
630
            $conditions[] = $columnName . ' = ?';
631 632 633
        }
    }

634 635
    /**
     * Executes an SQL DELETE statement on a table.
romanb's avatar
romanb committed
636
     *
637 638
     * Table expression and columns are not escaped and are not safe for user-input.
     *
639 640 641
     * @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
642
     *
643
     * @return int The number of affected rows.
644
     *
645
     * @throws DBALException
646
     * @throws InvalidArgumentException
647
     */
648
    public function delete($tableExpression, array $identifier, array $types = [])
649
    {
650
        if (count($identifier) === 0) {
651
            throw InvalidArgumentException::fromEmptyCriteria();
652 653
        }

Sergei Morozov's avatar
Sergei Morozov committed
654 655 656
        $columns = $values = $conditions = [];

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

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

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

674
        $this->isConnected = false;
romanb's avatar
romanb committed
675 676
    }

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

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

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

702
        return $this->transactionIsolationLevel;
703 704
    }

romanb's avatar
romanb committed
705
    /**
706
     * Executes an SQL UPDATE statement on a table.
romanb's avatar
romanb committed
707
     *
708 709
     * Table expression and columns are not escaped and are not safe for user-input.
     *
710 711 712 713
     * @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
714
     *
715
     * @return int The number of affected rows.
716
     *
717
     * @throws DBALException
romanb's avatar
romanb committed
718
     */
719
    public function update($tableExpression, array $data, array $identifier, array $types = [])
romanb's avatar
romanb committed
720
    {
Sergei Morozov's avatar
Sergei Morozov committed
721
        $columns = $values = $conditions = $set = [];
722

romanb's avatar
romanb committed
723
        foreach ($data as $columnName => $value) {
Sergei Morozov's avatar
Sergei Morozov committed
724 725 726
            $columns[] = $columnName;
            $values[]  = $value;
            $set[]     = $columnName . ' = ?';
romanb's avatar
romanb committed
727 728
        }

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

731
        if (is_string(key($types))) {
732
            $types = $this->extractTypeValues($columns, $types);
733
        }
romanb's avatar
romanb committed
734

735
        $sql = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
736
                . ' WHERE ' . implode(' AND ', $conditions);
romanb's avatar
romanb committed
737

738
        return $this->executeUpdate($sql, $values, $types);
romanb's avatar
romanb committed
739 740 741 742 743
    }

    /**
     * Inserts a table row with specified data.
     *
744 745
     * Table expression and columns are not escaped and are not safe for user-input.
     *
746 747 748
     * @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
749
     *
750
     * @return int The number of affected rows.
751
     *
752
     * @throws DBALException
romanb's avatar
romanb committed
753
     */
754
    public function insert($tableExpression, array $data, array $types = [])
romanb's avatar
romanb committed
755
    {
756
        if (count($data) === 0) {
757
            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' () VALUES ()');
758 759
        }

760
        $columns = [];
761 762
        $values  = [];
        $set     = [];
763 764

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

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

778
    /**
779
     * Extract ordered type list from an ordered column list and type map.
780
     *
Sergei Morozov's avatar
Sergei Morozov committed
781
     * @param int[]|string[] $columnList
782
     * @param int[]|string[] $types
783
     *
784
     * @return int[]|string[]
785
     */
786
    private function extractTypeValues(array $columnList, array $types)
787
    {
788
        $typeValues = [];
789

790
        foreach ($columnList as $columnIndex => $columnName) {
791
            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
792 793 794 795 796
        }

        return $typeValues;
    }

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

    /**
Sergei Morozov's avatar
Sergei Morozov committed
817
     * {@inheritDoc}
romanb's avatar
romanb committed
818
     */
819
    public function quote($input, $type = ParameterType::STRING)
romanb's avatar
romanb committed
820
    {
Sergei Morozov's avatar
Sergei Morozov committed
821
        $connection = $this->getWrappedConnection();
822

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

Sergei Morozov's avatar
Sergei Morozov committed
825
        return $connection->quote($value, $bindingType);
romanb's avatar
romanb committed
826 827 828
    }

    /**
829
     * Prepares and executes an SQL query and returns the result as an associative array.
romanb's avatar
romanb committed
830
     *
831 832 833
     * @param string         $sql    The SQL query.
     * @param mixed[]        $params The query parameters.
     * @param int[]|string[] $types  The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
834
     *
835
     * @return mixed[]
romanb's avatar
romanb committed
836
     */
837
    public function fetchAll($sql, array $params = [], $types = [])
romanb's avatar
romanb committed
838
    {
root's avatar
root committed
839
        return $this->executeQuery($sql, $params, $types)->fetchAll();
romanb's avatar
romanb committed
840 841 842 843 844
    }

    /**
     * Prepares an SQL statement.
     *
845
     * @param string $sql The SQL statement to prepare.
Benjamin Morel's avatar
Benjamin Morel committed
846
     *
847
     * @throws DBALException
romanb's avatar
romanb committed
848
     */
849
    public function prepare(string $sql) : DriverStatement
romanb's avatar
romanb committed
850
    {
851
        try {
852
            $stmt = new Statement($sql, $this);
853
        } catch (Throwable $ex) {
854
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
855 856
        }

857
        $stmt->setFetchMode($this->defaultFetchMode);
858 859

        return $stmt;
romanb's avatar
romanb committed
860 861 862
    }

    /**
Pascal Borreli's avatar
Pascal Borreli committed
863
     * Executes an, optionally parametrized, SQL query.
romanb's avatar
romanb committed
864
     *
Pascal Borreli's avatar
Pascal Borreli committed
865
     * If the query is parametrized, a prepared statement is used.
866 867
     * If an SQLLogger is configured, the execution is logged.
     *
868
     * @param string                 $query  The SQL query to execute.
869 870
     * @param mixed[]                $params The parameters to bind to the query, if any.
     * @param int[]|string[]         $types  The types the previous parameters are in.
871
     * @param QueryCacheProfile|null $qcp    The query cache profile, optional.
Benjamin Morel's avatar
Benjamin Morel committed
872
     *
873
     * @return ResultStatement The executed statement.
Benjamin Morel's avatar
Benjamin Morel committed
874
     *
875
     * @throws DBALException
romanb's avatar
romanb committed
876
     */
877
    public function executeQuery(string $query, array $params = [], $types = [], ?QueryCacheProfile $qcp = null) : ResultStatement
romanb's avatar
romanb committed
878
    {
879
        if ($qcp !== null) {
880
            return $this->executeCacheQuery($query, $params, $types, $qcp);
881 882
        }

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

885
        $logger = $this->_config->getSQLLogger();
886
        if ($logger !== null) {
887
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
888
        }
889

890
        try {
891
            if (count($params) > 0) {
892
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
893

Sergei Morozov's avatar
Sergei Morozov committed
894
                $stmt = $connection->prepare($query);
895
                if (count($types) > 0) {
896 897 898 899 900
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
901
            } else {
Sergei Morozov's avatar
Sergei Morozov committed
902
                $stmt = $connection->query($query);
903
            }
904
        } catch (Throwable $ex) {
905
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
906
        }
907

908
        $stmt->setFetchMode($this->defaultFetchMode);
909

910
        if ($logger !== null) {
911
            $logger->stopQuery();
912 913
        }

romanb's avatar
romanb committed
914
        return $stmt;
romanb's avatar
romanb committed
915
    }
916

917
    /**
Benjamin Morel's avatar
Benjamin Morel committed
918 919
     * Executes a caching query.
     *
920
     * @param string            $query  The SQL query to execute.
921 922
     * @param mixed[]           $params The parameters to bind to the query, if any.
     * @param int[]|string[]    $types  The types the previous parameters are in.
923
     * @param QueryCacheProfile $qcp    The query cache profile.
924
     *
925
     * @throws CacheException
926
     */
927
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp) : ResultStatement
928
    {
Sergei Morozov's avatar
Sergei Morozov committed
929 930 931
        $resultCache = $qcp->getResultCacheDriver() ?? $this->_config->getResultCacheImpl();

        if ($resultCache === null) {
932 933 934
            throw CacheException::noResultDriverConfigured();
        }

935 936 937 938
        $connectionParams = $this->getParams();
        unset($connectionParams['platform']);

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

        // fetch the row pointers entry
941 942 943
        $data = $resultCache->fetch($cacheKey);

        if ($data !== false) {
944 945
            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
            if (isset($data[$realKey])) {
946
                $stmt = new ArrayStatement($data[$realKey]);
Steve Müller's avatar
Steve Müller committed
947
            } elseif (array_key_exists($realKey, $data)) {
948
                $stmt = new ArrayStatement([]);
949 950
            }
        }
951

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

956
        $stmt->setFetchMode($this->defaultFetchMode);
957 958

        return $stmt;
959 960
    }

961
    /**
Pascal Borreli's avatar
Pascal Borreli committed
962
     * Executes an, optionally parametrized, SQL query and returns the result,
963
     * applying a given projection/transformation function on each row of the result.
964
     *
965
     * @param string  $query    The SQL query to execute.
966
     * @param mixed[] $params   The parameters, if any.
967
     * @param Closure $function The transformation function that is applied on each row.
Benjamin Morel's avatar
Benjamin Morel committed
968 969 970
     *                           The function receives a single parameter, an array, that
     *                           represents a row of the result set.
     *
971
     * @return mixed[] The projected result of the query.
972
     */
973
    public function project($query, array $params, Closure $function)
974
    {
975
        $result = [];
976
        $stmt   = $this->executeQuery($query, $params);
977

978
        while ($row = $stmt->fetch()) {
979
            $result[] = $function($row);
980
        }
981

982
        $stmt->closeCursor();
983

984 985
        return $result;
    }
romanb's avatar
romanb committed
986 987

    /**
988
     * {@inheritDoc}
989
     */
990
    public function query(string $sql) : ResultStatement
991
    {
Sergei Morozov's avatar
Sergei Morozov committed
992
        $connection = $this->getWrappedConnection();
993

994
        $logger = $this->_config->getSQLLogger();
995
        if ($logger !== null) {
996
            $logger->startQuery($sql);
997 998
        }

999
        try {
1000
            $statement = $connection->query($sql);
1001
        } catch (Throwable $ex) {
1002
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
1003 1004
        }

1005
        $statement->setFetchMode($this->defaultFetchMode);
1006

1007
        if ($logger !== null) {
1008 1009 1010 1011
            $logger->stopQuery();
        }

        return $statement;
1012 1013 1014 1015 1016
    }

    /**
     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
     * and returns the number of affected rows.
1017
     *
1018
     * This method supports PDO binding types as well as DBAL mapping types.
romanb's avatar
romanb committed
1019
     *
1020 1021 1022
     * @param string         $query  The SQL query.
     * @param mixed[]        $params The query parameters.
     * @param int[]|string[] $types  The parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
1023
     *
1024
     * @throws DBALException
romanb's avatar
romanb committed
1025
     */
1026
    public function executeUpdate(string $query, array $params = [], array $types = []) : int
romanb's avatar
romanb committed
1027
    {
Sergei Morozov's avatar
Sergei Morozov committed
1028
        $connection = $this->getWrappedConnection();
romanb's avatar
romanb committed
1029

1030
        $logger = $this->_config->getSQLLogger();
1031
        if ($logger !== null) {
1032
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
1033 1034
        }

1035
        try {
1036
            if (count($params) > 0) {
1037
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
1038

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

1041
                if (count($types) > 0) {
1042 1043 1044 1045 1046 1047
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
                $result = $stmt->rowCount();
1048
            } else {
Sergei Morozov's avatar
Sergei Morozov committed
1049
                $result = $connection->exec($query);
1050
            }
1051
        } catch (Throwable $ex) {
1052
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
1053
        }
1054

1055
        if ($logger !== null) {
1056
            $logger->stopQuery();
1057 1058
        }

romanb's avatar
romanb committed
1059
        return $result;
romanb's avatar
romanb committed
1060 1061
    }

1062
    /**
1063
     * {@inheritDoc}
1064
     */
1065
    public function exec(string $statement) : int
1066
    {
Sergei Morozov's avatar
Sergei Morozov committed
1067
        $connection = $this->getWrappedConnection();
1068 1069

        $logger = $this->_config->getSQLLogger();
1070
        if ($logger !== null) {
1071 1072 1073
            $logger->startQuery($statement);
        }

1074
        try {
Sergei Morozov's avatar
Sergei Morozov committed
1075
            $result = $connection->exec($statement);
1076
        } catch (Throwable $ex) {
1077
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1078
        }
1079

1080
        if ($logger !== null) {
1081 1082 1083 1084
            $logger->stopQuery();
        }

        return $result;
1085 1086
    }

1087 1088 1089
    /**
     * Returns the current transaction nesting level.
     *
1090
     * @return int The nesting level. A value of 0 means there's no active transaction.
1091 1092 1093
     */
    public function getTransactionNestingLevel()
    {
1094
        return $this->transactionNestingLevel;
1095 1096
    }

romanb's avatar
romanb committed
1097
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1098
     * Fetches the SQLSTATE associated with the last database operation.
romanb's avatar
romanb committed
1099
     *
1100
     * @return string|null The last error code.
romanb's avatar
romanb committed
1101 1102 1103
     */
    public function errorCode()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1104
        return $this->getWrappedConnection()->errorCode();
romanb's avatar
romanb committed
1105 1106 1107
    }

    /**
1108
     * {@inheritDoc}
romanb's avatar
romanb committed
1109 1110 1111
     */
    public function errorInfo()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1112
        return $this->getWrappedConnection()->errorInfo();
romanb's avatar
romanb committed
1113 1114 1115 1116 1117 1118 1119
    }

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

1132 1133 1134 1135 1136 1137 1138 1139
    /**
     * 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.
     *
1140
     * @param Closure $func The function to execute transactionally.
Benjamin Morel's avatar
Benjamin Morel committed
1141
     *
1142
     * @return mixed The value returned by $func
Benjamin Morel's avatar
Benjamin Morel committed
1143
     *
1144 1145
     * @throws Exception
     * @throws Throwable
1146 1147 1148 1149 1150
     */
    public function transactional(Closure $func)
    {
        $this->beginTransaction();
        try {
1151
            $res = $func($this);
1152
            $this->commit();
1153

1154
            return $res;
1155
        } catch (Exception $e) {
1156
            $this->rollBack();
1157 1158 1159
            throw $e;
        } catch (Throwable $e) {
            $this->rollBack();
1160 1161 1162 1163
            throw $e;
        }
    }

1164
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1165
     * Sets if nested transactions should use savepoints.
1166
     *
1167
     * @param bool $nestTransactionsWithSavepoints
Benjamin Morel's avatar
Benjamin Morel committed
1168
     *
1169
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1170
     *
1171
     * @throws ConnectionException
1172 1173 1174
     */
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
    {
1175
        if ($this->transactionNestingLevel > 0) {
1176 1177 1178
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
        }

1179
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1180
            throw ConnectionException::savepointsNotSupported();
1181 1182
        }

1183
        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1184 1185 1186
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1187
     * Gets if nested transactions should use savepoints.
1188
     *
1189
     * @return bool
1190 1191 1192
     */
    public function getNestTransactionsWithSavepoints()
    {
1193
        return $this->nestTransactionsWithSavepoints;
1194 1195
    }

1196 1197 1198 1199
    /**
     * 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
1200
     * @return mixed A string with the savepoint name or false.
1201
     */
1202 1203
    protected function _getNestedTransactionSavePointName()
    {
1204
        return 'DOCTRINE2_SAVEPOINT_' . $this->transactionNestingLevel;
1205 1206
    }

1207
    /**
1208
     * {@inheritDoc}
1209 1210 1211
     */
    public function beginTransaction()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1212
        $connection = $this->getWrappedConnection();
1213

1214
        ++$this->transactionNestingLevel;
1215

1216 1217
        $logger = $this->_config->getSQLLogger();

1218
        if ($this->transactionNestingLevel === 1) {
1219
            if ($logger !== null) {
1220 1221
                $logger->startQuery('"START TRANSACTION"');
            }
Sergei Morozov's avatar
Sergei Morozov committed
1222 1223 1224

            $connection->beginTransaction();

1225
            if ($logger !== null) {
1226 1227
                $logger->stopQuery();
            }
1228
        } elseif ($this->nestTransactionsWithSavepoints) {
1229
            if ($logger !== null) {
1230 1231
                $logger->startQuery('"SAVEPOINT"');
            }
1232
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1233
            if ($logger !== null) {
1234 1235
                $logger->stopQuery();
            }
1236
        }
1237 1238

        return true;
1239 1240 1241
    }

    /**
1242
     * {@inheritDoc}
Benjamin Morel's avatar
Benjamin Morel committed
1243
     *
1244
     * @throws ConnectionException If the commit failed due to no active transaction or
Benjamin Morel's avatar
Benjamin Morel committed
1245
     *                                            because the transaction was marked for rollback only.
1246 1247 1248
     */
    public function commit()
    {
1249
        if ($this->transactionNestingLevel === 0) {
1250
            throw ConnectionException::noActiveTransaction();
1251
        }
1252
        if ($this->isRollbackOnly) {
1253 1254 1255
            throw ConnectionException::commitFailedRollbackOnly();
        }

1256 1257
        $result = true;

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

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

1262
        if ($this->transactionNestingLevel === 1) {
1263
            if ($logger !== null) {
1264 1265
                $logger->startQuery('"COMMIT"');
            }
Sergei Morozov's avatar
Sergei Morozov committed
1266

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

1269
            if ($logger !== null) {
1270 1271
                $logger->stopQuery();
            }
1272
        } elseif ($this->nestTransactionsWithSavepoints) {
1273
            if ($logger !== null) {
1274 1275
                $logger->startQuery('"RELEASE SAVEPOINT"');
            }
1276
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1277
            if ($logger !== null) {
1278 1279
                $logger->stopQuery();
            }
1280 1281
        }

1282
        --$this->transactionNestingLevel;
1283

1284
        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1285
            return $result;
1286
        }
1287 1288

        $this->beginTransaction();
1289

1290
        return $result;
1291 1292 1293 1294 1295
    }

    /**
     * Commits all current nesting transactions.
     */
1296
    private function commitAll() : void
1297
    {
1298 1299
        while ($this->transactionNestingLevel !== 0) {
            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
1300 1301 1302 1303 1304 1305 1306 1307 1308
                // 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();
        }
1309 1310 1311
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1312
     * Cancels any database changes done during the current transaction.
1313
     *
1314 1315
     * @return bool
     *
1316
     * @throws ConnectionException If the rollback operation failed.
1317
     */
1318
    public function rollBack()
1319
    {
1320
        if ($this->transactionNestingLevel === 0) {
1321
            throw ConnectionException::noActiveTransaction();
1322 1323
        }

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

1326 1327
        $logger = $this->_config->getSQLLogger();

1328
        if ($this->transactionNestingLevel === 1) {
1329
            if ($logger !== null) {
1330 1331
                $logger->startQuery('"ROLLBACK"');
            }
1332
            $this->transactionNestingLevel = 0;
Sergei Morozov's avatar
Sergei Morozov committed
1333
            $connection->rollBack();
1334
            $this->isRollbackOnly = false;
1335
            if ($logger !== null) {
1336 1337
                $logger->stopQuery();
            }
1338

1339
            if ($this->autoCommit === false) {
1340 1341
                $this->beginTransaction();
            }
1342
        } elseif ($this->nestTransactionsWithSavepoints) {
1343
            if ($logger !== null) {
1344 1345
                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
            }
1346
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
1347
            --$this->transactionNestingLevel;
1348
            if ($logger !== null) {
1349 1350
                $logger->stopQuery();
            }
1351
        } else {
1352 1353
            $this->isRollbackOnly = true;
            --$this->transactionNestingLevel;
1354
        }
1355 1356

        return true;
1357 1358
    }

1359
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1360 1361 1362
     * Creates a new savepoint.
     *
     * @param string $savepoint The name of the savepoint to create.
1363 1364
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1365
     *
1366
     * @throws ConnectionException
1367
     */
1368
    public function createSavepoint($savepoint)
1369
    {
1370
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1371
            throw ConnectionException::savepointsNotSupported();
1372 1373
        }

Sergei Morozov's avatar
Sergei Morozov committed
1374
        $this->getWrappedConnection()->exec($this->platform->createSavePoint($savepoint));
1375 1376 1377
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1378 1379 1380
     * Releases the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to release.
1381 1382
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1383
     *
1384
     * @throws ConnectionException
1385
     */
1386
    public function releaseSavepoint($savepoint)
1387
    {
1388
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1389
            throw ConnectionException::savepointsNotSupported();
1390 1391
        }

1392 1393
        if (! $this->platform->supportsReleaseSavepoints()) {
            return;
1394
        }
1395

Sergei Morozov's avatar
Sergei Morozov committed
1396
        $this->getWrappedConnection()->exec($this->platform->releaseSavePoint($savepoint));
1397 1398 1399
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1400 1401 1402
     * Rolls back to the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to rollback to.
1403 1404
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1405
     *
1406
     * @throws ConnectionException
1407
     */
1408
    public function rollbackSavepoint($savepoint)
1409
    {
1410
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1411
            throw ConnectionException::savepointsNotSupported();
1412 1413
        }

Sergei Morozov's avatar
Sergei Morozov committed
1414
        $this->getWrappedConnection()->exec($this->platform->rollbackSavePoint($savepoint));
1415 1416
    }

romanb's avatar
romanb committed
1417 1418 1419
    /**
     * Gets the wrapped driver connection.
     *
Sergei Morozov's avatar
Sergei Morozov committed
1420
     * @return DriverConnection
romanb's avatar
romanb committed
1421 1422 1423 1424
     */
    public function getWrappedConnection()
    {
        $this->connect();
1425

romanb's avatar
romanb committed
1426 1427
        return $this->_conn;
    }
1428

romanb's avatar
romanb committed
1429 1430 1431 1432
    /**
     * Gets the SchemaManager that can be used to inspect or change the
     * database schema through the connection.
     *
1433
     * @return AbstractSchemaManager
romanb's avatar
romanb committed
1434 1435 1436
     */
    public function getSchemaManager()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1437
        if ($this->_schemaManager === null) {
romanb's avatar
romanb committed
1438 1439
            $this->_schemaManager = $this->_driver->getSchemaManager($this);
        }
1440

romanb's avatar
romanb committed
1441 1442
        return $this->_schemaManager;
    }
1443

1444 1445 1446
    /**
     * Marks the current transaction so that the only possible
     * outcome for the transaction to be rolled back.
1447
     *
Benjamin Morel's avatar
Benjamin Morel committed
1448 1449
     * @return void
     *
1450
     * @throws ConnectionException If no transaction is active.
1451 1452 1453
     */
    public function setRollbackOnly()
    {
1454
        if ($this->transactionNestingLevel === 0) {
1455 1456
            throw ConnectionException::noActiveTransaction();
        }
1457
        $this->isRollbackOnly = true;
1458 1459 1460
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1461
     * Checks whether the current transaction is marked for rollback only.
1462
     *
1463
     * @return bool
Benjamin Morel's avatar
Benjamin Morel committed
1464
     *
1465
     * @throws ConnectionException If no transaction is active.
1466
     */
1467
    public function isRollbackOnly()
1468
    {
1469
        if ($this->transactionNestingLevel === 0) {
1470 1471
            throw ConnectionException::noActiveTransaction();
        }
Benjamin Morel's avatar
Benjamin Morel committed
1472

1473
        return $this->isRollbackOnly;
1474 1475
    }

1476 1477 1478
    /**
     * Converts a given value to its database representation according to the conversion
     * rules of a specific DBAL mapping type.
1479
     *
Benjamin Morel's avatar
Benjamin Morel committed
1480 1481 1482
     * @param mixed  $value The value to convert.
     * @param string $type  The name of the DBAL mapping type.
     *
1483 1484 1485 1486
     * @return mixed The converted value.
     */
    public function convertToDatabaseValue($value, $type)
    {
1487
        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1488 1489 1490 1491 1492
    }

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

    /**
     * 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.
1507
     *
1508 1509 1510
     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
     *           raw PDOStatement instances.
     *
1511 1512 1513
     * @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).
1514
     */
1515
    private function _bindTypedValues(DriverStatement $stmt, array $params, array $types) : void
1516 1517 1518 1519
    {
        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
        if (is_int(key($params))) {
            // Positional parameters
1520
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1521
            $bindIndex  = 1;
1522
            foreach ($params as $value) {
1523 1524
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
1525 1526
                    $type                  = $types[$typeIndex];
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1527 1528 1529 1530 1531 1532 1533 1534 1535 1536
                    $stmt->bindValue($bindIndex, $value, $bindingType);
                } else {
                    $stmt->bindValue($bindIndex, $value);
                }
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
1537 1538
                    $type                  = $types[$name];
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1539 1540 1541 1542 1543 1544 1545
                    $stmt->bindValue($name, $value, $bindingType);
                } else {
                    $stmt->bindValue($name, $value);
                }
            }
        }
    }
1546 1547 1548 1549

    /**
     * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type.
     *
Sergei Morozov's avatar
Sergei Morozov committed
1550 1551
     * @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
1552
     *
1553
     * @return mixed[] [0] => the (escaped) value, [1] => the binding type.
1554 1555 1556 1557 1558 1559 1560
     */
    private function getBindingInfo($value, $type)
    {
        if (is_string($type)) {
            $type = Type::getType($type);
        }
        if ($type instanceof Type) {
1561
            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1562 1563
            $bindingType = $type->getBindingType();
        } else {
1564
            $bindingType = $type;
1565
        }
Benjamin Morel's avatar
Benjamin Morel committed
1566

1567
        return [$value, $bindingType];
1568 1569
    }

1570 1571 1572 1573 1574 1575
    /**
     * 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.
     *
1576 1577
     * @param mixed[]        $params
     * @param int[]|string[] $types
1578
     *
1579
     * @return mixed[]
1580 1581 1582
     */
    public function resolveParams(array $params, array $types)
    {
1583
        $resolvedParams = [];
1584 1585 1586 1587 1588

        // 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;
1589
            $bindIndex  = 1;
1590 1591 1592
            foreach ($params as $value) {
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
1593 1594
                    $type                       = $types[$typeIndex];
                    [$value]                    = $this->getBindingInfo($value, $type);
1595 1596 1597 1598 1599 1600 1601 1602 1603 1604
                    $resolvedParams[$bindIndex] = $value;
                } else {
                    $resolvedParams[$bindIndex] = $value;
                }
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
1605 1606
                    $type                  = $types[$name];
                    [$value]               = $this->getBindingInfo($value, $type);
1607 1608 1609 1610 1611 1612 1613 1614 1615 1616
                    $resolvedParams[$name] = $value;
                } else {
                    $resolvedParams[$name] = $value;
                }
            }
        }

        return $resolvedParams;
    }

1617
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1618
     * Creates a new instance of a SQL query builder.
1619
     *
1620
     * @return QueryBuilder
1621 1622 1623 1624 1625
     */
    public function createQueryBuilder()
    {
        return new Query\QueryBuilder($this);
    }
1626 1627

    /**
1628 1629 1630 1631 1632 1633
     * 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:
     *
1634 1635
     * @return bool
     *
1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646
     * @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.
1647 1648 1649
     */
    public function ping()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1650
        $connection = $this->getWrappedConnection();
1651

Sergei Morozov's avatar
Sergei Morozov committed
1652 1653
        if ($connection instanceof PingableConnection) {
            return $connection->ping();
1654 1655 1656
        }

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

1659 1660
            return true;
        } catch (DBALException $e) {
1661
            return false;
1662
        }
1663
    }
1664
}