Connection.php 53 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 12
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
13
use Doctrine\DBAL\Driver\PingableConnection;
14
use Doctrine\DBAL\Driver\Result as DriverResult;
15 16 17 18 19 20 21 22 23
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;
24
use Throwable;
25
use Traversable;
26

27
use function array_key_exists;
28
use function assert;
29
use function count;
30 31 32 33
use function implode;
use function is_int;
use function is_string;
use function key;
34 35

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

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

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

63 64
    /**
     * Constant for transaction isolation level SERIALIZABLE.
65 66
     *
     * @deprecated Use TransactionIsolationLevel::SERIALIZABLE.
67
     */
68
    public const TRANSACTION_SERIALIZABLE = TransactionIsolationLevel::SERIALIZABLE;
69

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

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

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

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

92
    /** @var Configuration */
romanb's avatar
romanb committed
93
    protected $_config;
94

95
    /** @var EventManager */
romanb's avatar
romanb committed
96
    protected $_eventManager;
97

98
    /** @var ExpressionBuilder */
99
    protected $_expr;
100

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

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

115 116 117
    /**
     * The transaction nesting level.
     *
118
     * @var int
119
     */
120
    private $transactionNestingLevel = 0;
121 122 123 124

    /**
     * The currently active transaction isolation level.
     *
125
     * @var int
126
     */
127
    private $transactionIsolationLevel;
128

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

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

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

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

romanb's avatar
romanb committed
158
    /**
romanb's avatar
romanb committed
159 160
     * The used DBAL driver.
     *
161
     * @var Driver
romanb's avatar
romanb committed
162 163
     */
    protected $_driver;
164

165
    /**
166
     * Flag that indicates whether the current transaction is marked for rollback only.
167
     *
168
     * @var bool
169
     */
170
    private $isRollbackOnly = false;
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 524 525 526 527 528 529 530 531 532 533 534 535 536 537
    /**
     * 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 {
538
            return $this->executeQuery($query, $params, $types)->fetchAssociative();
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558
        } catch (Throwable $e) {
            throw DBALException::driverExceptionDuringQuery($this->_driver, $e, $query);
        }
    }

    /**
     * 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 {
559
            return $this->executeQuery($query, $params, $types)->fetchNumeric();
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579
        } catch (Throwable $e) {
            throw DBALException::driverExceptionDuringQuery($this->_driver, $e, $query);
        }
    }

    /**
     * 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 {
580
            return $this->executeQuery($query, $params, $types)->fetchOne();
581 582 583 584 585
        } catch (Throwable $e) {
            throw DBALException::driverExceptionDuringQuery($this->_driver, $e, $query);
        }
    }

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

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

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

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

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

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

Sergei Morozov's avatar
Sergei Morozov committed
656 657 658
        $columns = $values = $conditions = [];

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

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

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

676
        $this->isConnected = false;
romanb's avatar
romanb committed
677 678
    }

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

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

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

704
        return $this->transactionIsolationLevel;
705 706
    }

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

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

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

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

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

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

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

762
        $columns = [];
763 764
        $values  = [];
        $set     = [];
765 766

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

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

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

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

        return $typeValues;
    }

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

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

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

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

    /**
831
     * Prepares and executes an SQL query and returns the result as an array of numeric arrays.
romanb's avatar
romanb committed
832
     *
833 834 835
     * @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.
836
     *
837
     * @return array<int,array<int,mixed>>
Benjamin Morel's avatar
Benjamin Morel committed
838
     *
839
     * @throws DBALException
romanb's avatar
romanb committed
840
     */
841
    public function fetchAllNumeric(string $query, array $params = [], array $types = []): array
romanb's avatar
romanb committed
842
    {
843 844 845 846 847
        try {
            return $this->executeQuery($query, $params, $types)->fetchAllNumeric();
        } catch (Throwable $e) {
            throw DBALException::driverExceptionDuringQuery($this->_driver, $e, $query);
        }
romanb's avatar
romanb committed
848 849
    }

850
    /**
851
     * Prepares and executes an SQL query and returns the result as an array of associative arrays.
852 853 854 855 856
     *
     * @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.
     *
857
     * @return array<int,array<string,mixed>>
858 859 860
     *
     * @throws DBALException
     */
861
    public function fetchAllAssociative(string $query, array $params = [], array $types = []): array
862 863
    {
        try {
864
            return $this->executeQuery($query, $params, $types)->fetchAllAssociative();
865 866 867 868 869 870
        } catch (Throwable $e) {
            throw DBALException::driverExceptionDuringQuery($this->_driver, $e, $query);
        }
    }

    /**
871
     * Prepares and executes an SQL query and returns the result as an array of the first column values.
872 873 874 875 876
     *
     * @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.
     *
877
     * @return array<int,mixed>
878 879 880
     *
     * @throws DBALException
     */
881
    public function fetchFirstColumn(string $query, array $params = [], array $types = []): array
882 883
    {
        try {
884
            return $this->executeQuery($query, $params, $types)->fetchFirstColumn();
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
        } catch (Throwable $e) {
            throw DBALException::driverExceptionDuringQuery($this->_driver, $e, $query);
        }
    }

    /**
     * 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
     */
901
    public function iterateNumeric(string $query, array $params = [], array $types = []): Traversable
902 903
    {
        try {
904
            $result = $this->executeQuery($query, $params, $types);
905

906
            while (($row = $result->fetchNumeric()) !== false) {
907
                yield $row;
908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
            }
        } catch (Throwable $e) {
            throw DBALException::driverExceptionDuringQuery($this->_driver, $e, $query);
        }
    }

    /**
     * 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
     */
925
    public function iterateAssociative(string $query, array $params = [], array $types = []): Traversable
926 927
    {
        try {
928
            $result = $this->executeQuery($query, $params, $types);
929

930
            while (($row = $result->fetchAssociative()) !== false) {
931
                yield $row;
932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948
            }
        } catch (Throwable $e) {
            throw DBALException::driverExceptionDuringQuery($this->_driver, $e, $query);
        }
    }

    /**
     * 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
     */
949
    public function iterateColumn(string $query, array $params = [], array $types = []): Traversable
950 951
    {
        try {
952
            $result = $this->executeQuery($query, $params, $types);
953

954
            while (($value = $result->fetchOne()) !== false) {
955
                yield $value;
956 957 958 959 960 961
            }
        } catch (Throwable $e) {
            throw DBALException::driverExceptionDuringQuery($this->_driver, $e, $query);
        }
    }

romanb's avatar
romanb committed
962 963 964
    /**
     * Prepares an SQL statement.
     *
965
     * @param string $sql The SQL statement to prepare.
Benjamin Morel's avatar
Benjamin Morel committed
966
     *
967
     * @throws DBALException
romanb's avatar
romanb committed
968
     */
969
    public function prepare(string $sql): DriverStatement
romanb's avatar
romanb committed
970
    {
971
        try {
972
            return new Statement($sql, $this);
973
        } catch (Throwable $ex) {
974
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
975
        }
romanb's avatar
romanb committed
976 977 978
    }

    /**
Pascal Borreli's avatar
Pascal Borreli committed
979
     * Executes an, optionally parametrized, SQL query.
romanb's avatar
romanb committed
980
     *
Pascal Borreli's avatar
Pascal Borreli committed
981
     * If the query is parametrized, a prepared statement is used.
982 983
     * If an SQLLogger is configured, the execution is logged.
     *
984
     * @param string                 $query  The SQL query to execute.
985 986
     * @param mixed[]                $params The parameters to bind to the query, if any.
     * @param int[]|string[]         $types  The types the previous parameters are in.
987
     * @param QueryCacheProfile|null $qcp    The query cache profile, optional.
Benjamin Morel's avatar
Benjamin Morel committed
988
     *
989
     * @throws DBALException
romanb's avatar
romanb committed
990
     */
991 992 993 994 995 996
    public function executeQuery(
        string $query,
        array $params = [],
        $types = [],
        ?QueryCacheProfile $qcp = null
    ): AbstractionResult {
997
        if ($qcp !== null) {
998
            return $this->executeCacheQuery($query, $params, $types, $qcp);
999 1000
        }

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

1003
        $logger = $this->_config->getSQLLogger();
1004
        if ($logger !== null) {
1005
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
1006
        }
1007

1008
        try {
1009
            if (count($params) > 0) {
1010
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
1011

Sergei Morozov's avatar
Sergei Morozov committed
1012
                $stmt = $connection->prepare($query);
1013
                if (count($types) > 0) {
1014
                    $this->_bindTypedValues($stmt, $params, $types);
1015
                    $result = $stmt->execute();
1016
                } else {
1017
                    $result = $stmt->execute($params);
1018
                }
1019
            } else {
1020
                $result = $connection->query($query);
1021
            }
1022 1023

            return new Result($result, $this);
1024
        } catch (Throwable $ex) {
1025
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
1026 1027 1028 1029
        } finally {
            if ($logger !== null) {
                $logger->stopQuery();
            }
romanb's avatar
romanb committed
1030 1031
        }
    }
1032

1033
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1034 1035
     * Executes a caching query.
     *
1036
     * @param string            $query  The SQL query to execute.
1037 1038
     * @param mixed[]           $params The parameters to bind to the query, if any.
     * @param int[]|string[]    $types  The types the previous parameters are in.
1039
     * @param QueryCacheProfile $qcp    The query cache profile.
1040
     *
1041
     * @throws CacheException
1042
     * @throws DBALException
1043
     */
1044
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp): Result
1045
    {
Sergei Morozov's avatar
Sergei Morozov committed
1046 1047 1048
        $resultCache = $qcp->getResultCacheDriver() ?? $this->_config->getResultCacheImpl();

        if ($resultCache === null) {
1049 1050 1051
            throw CacheException::noResultDriverConfigured();
        }

1052 1053 1054 1055
        $connectionParams = $this->getParams();
        unset($connectionParams['platform']);

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

        // fetch the row pointers entry
1058 1059 1060
        $data = $resultCache->fetch($cacheKey);

        if ($data !== false) {
1061 1062
            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
            if (isset($data[$realKey])) {
1063
                $result = new ArrayResult($data[$realKey]);
Steve Müller's avatar
Steve Müller committed
1064
            } elseif (array_key_exists($realKey, $data)) {
1065
                $result = new ArrayResult([]);
1066 1067
            }
        }
1068

1069 1070 1071 1072 1073 1074 1075 1076
        if (! isset($result)) {
            $result = new CachingResult(
                $this->executeQuery($query, $params, $types),
                $resultCache,
                $cacheKey,
                $realKey,
                $qcp->getLifetime()
            );
1077 1078
        }

1079
        return new Result($result, $this);
1080 1081
    }

1082
    public function query(string $sql): DriverResult
1083
    {
Sergei Morozov's avatar
Sergei Morozov committed
1084
        $connection = $this->getWrappedConnection();
1085

1086
        $logger = $this->_config->getSQLLogger();
1087
        if ($logger !== null) {
1088
            $logger->startQuery($sql);
1089 1090
        }

1091
        try {
1092
            return $connection->query($sql);
1093
        } catch (Throwable $ex) {
1094
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
1095 1096 1097 1098
        } finally {
            if ($logger !== null) {
                $logger->stopQuery();
            }
1099
        }
1100 1101 1102 1103 1104
    }

    /**
     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
     * and returns the number of affected rows.
1105
     *
1106
     * This method supports PDO binding types as well as DBAL mapping types.
romanb's avatar
romanb committed
1107
     *
1108 1109 1110
     * @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
1111
     *
1112
     * @throws DBALException
romanb's avatar
romanb committed
1113
     */
1114
    public function executeUpdate(string $query, array $params = [], array $types = []): int
romanb's avatar
romanb committed
1115
    {
Sergei Morozov's avatar
Sergei Morozov committed
1116
        $connection = $this->getWrappedConnection();
romanb's avatar
romanb committed
1117

1118
        $logger = $this->_config->getSQLLogger();
1119
        if ($logger !== null) {
1120
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
1121 1122
        }

1123
        try {
1124
            if (count($params) > 0) {
1125
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
1126

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

1129
                if (count($types) > 0) {
1130
                    $this->_bindTypedValues($stmt, $params, $types);
1131 1132

                    $result = $stmt->execute();
1133
                } else {
1134
                    $result = $stmt->execute($params);
1135
                }
Grégoire Paris's avatar
Grégoire Paris committed
1136

1137
                return $result->rowCount();
1138
            }
1139 1140

            return $connection->exec($query);
1141
        } catch (Throwable $ex) {
1142
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
1143 1144 1145 1146
        } finally {
            if ($logger !== null) {
                $logger->stopQuery();
            }
romanb's avatar
romanb committed
1147 1148 1149
        }
    }

1150
    public function exec(string $statement): int
1151
    {
Sergei Morozov's avatar
Sergei Morozov committed
1152
        $connection = $this->getWrappedConnection();
1153 1154

        $logger = $this->_config->getSQLLogger();
1155
        if ($logger !== null) {
1156 1157 1158
            $logger->startQuery($statement);
        }

1159
        try {
1160
            return $connection->exec($statement);
1161
        } catch (Throwable $ex) {
1162
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1163 1164 1165 1166
        } finally {
            if ($logger !== null) {
                $logger->stopQuery();
            }
1167
        }
1168 1169
    }

1170 1171 1172
    /**
     * Returns the current transaction nesting level.
     *
1173
     * @return int The nesting level. A value of 0 means there's no active transaction.
1174 1175 1176
     */
    public function getTransactionNestingLevel()
    {
1177
        return $this->transactionNestingLevel;
1178 1179
    }

romanb's avatar
romanb committed
1180 1181 1182 1183 1184
    /**
     * 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,
1185 1186
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
     * columns or sequences.
romanb's avatar
romanb committed
1187
     *
Benjamin Morel's avatar
Benjamin Morel committed
1188 1189
     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
     *
1190
     * @return string A string representation of the last inserted ID.
romanb's avatar
romanb committed
1191
     */
romanb's avatar
romanb committed
1192 1193
    public function lastInsertId($seqName = null)
    {
Sergei Morozov's avatar
Sergei Morozov committed
1194
        return $this->getWrappedConnection()->lastInsertId($seqName);
romanb's avatar
romanb committed
1195
    }
1196

1197 1198 1199 1200 1201 1202 1203 1204
    /**
     * 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.
     *
1205
     * @param Closure $func The function to execute transactionally.
Benjamin Morel's avatar
Benjamin Morel committed
1206
     *
1207
     * @return mixed The value returned by $func
Benjamin Morel's avatar
Benjamin Morel committed
1208
     *
1209 1210
     * @throws Exception
     * @throws Throwable
1211 1212 1213 1214 1215
     */
    public function transactional(Closure $func)
    {
        $this->beginTransaction();
        try {
1216
            $res = $func($this);
1217
            $this->commit();
1218

1219
            return $res;
1220
        } catch (Exception $e) {
1221
            $this->rollBack();
Grégoire Paris's avatar
Grégoire Paris committed
1222

1223 1224 1225
            throw $e;
        } catch (Throwable $e) {
            $this->rollBack();
Grégoire Paris's avatar
Grégoire Paris committed
1226

1227 1228 1229 1230
            throw $e;
        }
    }

1231
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1232
     * Sets if nested transactions should use savepoints.
1233
     *
1234
     * @param bool $nestTransactionsWithSavepoints
Benjamin Morel's avatar
Benjamin Morel committed
1235
     *
1236
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1237
     *
1238
     * @throws ConnectionException
1239 1240 1241
     */
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
    {
1242
        if ($this->transactionNestingLevel > 0) {
1243 1244 1245
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
        }

1246
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1247
            throw ConnectionException::savepointsNotSupported();
1248 1249
        }

1250
        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1251 1252 1253
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1254
     * Gets if nested transactions should use savepoints.
1255
     *
1256
     * @return bool
1257 1258 1259
     */
    public function getNestTransactionsWithSavepoints()
    {
1260
        return $this->nestTransactionsWithSavepoints;
1261 1262
    }

1263 1264 1265 1266
    /**
     * 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
1267
     * @return mixed A string with the savepoint name or false.
1268
     */
1269 1270
    protected function _getNestedTransactionSavePointName()
    {
1271
        return 'DOCTRINE2_SAVEPOINT_' . $this->transactionNestingLevel;
1272 1273
    }

1274
    /**
1275
     * {@inheritDoc}
1276 1277 1278
     */
    public function beginTransaction()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1279
        $connection = $this->getWrappedConnection();
1280

1281
        ++$this->transactionNestingLevel;
1282

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

1285
        if ($this->transactionNestingLevel === 1) {
1286
            if ($logger !== null) {
1287 1288
                $logger->startQuery('"START TRANSACTION"');
            }
Sergei Morozov's avatar
Sergei Morozov committed
1289 1290 1291

            $connection->beginTransaction();

1292
            if ($logger !== null) {
1293 1294
                $logger->stopQuery();
            }
1295
        } elseif ($this->nestTransactionsWithSavepoints) {
1296
            if ($logger !== null) {
1297 1298
                $logger->startQuery('"SAVEPOINT"');
            }
Grégoire Paris's avatar
Grégoire Paris committed
1299

1300
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1301
            if ($logger !== null) {
1302 1303
                $logger->stopQuery();
            }
1304
        }
1305 1306

        return true;
1307 1308 1309
    }

    /**
1310
     * {@inheritDoc}
Benjamin Morel's avatar
Benjamin Morel committed
1311
     *
1312
     * @throws ConnectionException If the commit failed due to no active transaction or
Benjamin Morel's avatar
Benjamin Morel committed
1313
     *                                            because the transaction was marked for rollback only.
1314 1315 1316
     */
    public function commit()
    {
1317
        if ($this->transactionNestingLevel === 0) {
1318
            throw ConnectionException::noActiveTransaction();
1319
        }
Grégoire Paris's avatar
Grégoire Paris committed
1320

1321
        if ($this->isRollbackOnly) {
1322 1323 1324
            throw ConnectionException::commitFailedRollbackOnly();
        }

1325 1326
        $result = true;

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

1329 1330
        $logger = $this->_config->getSQLLogger();

1331
        if ($this->transactionNestingLevel === 1) {
1332
            if ($logger !== null) {
1333 1334
                $logger->startQuery('"COMMIT"');
            }
Sergei Morozov's avatar
Sergei Morozov committed
1335

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

1338
            if ($logger !== null) {
1339 1340
                $logger->stopQuery();
            }
1341
        } elseif ($this->nestTransactionsWithSavepoints) {
1342
            if ($logger !== null) {
1343 1344
                $logger->startQuery('"RELEASE SAVEPOINT"');
            }
Grégoire Paris's avatar
Grégoire Paris committed
1345

1346
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1347
            if ($logger !== null) {
1348 1349
                $logger->stopQuery();
            }
1350 1351
        }

1352
        --$this->transactionNestingLevel;
1353

1354
        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1355
            return $result;
1356
        }
1357 1358

        $this->beginTransaction();
1359

1360
        return $result;
1361 1362 1363 1364 1365
    }

    /**
     * Commits all current nesting transactions.
     */
1366
    private function commitAll(): void
1367
    {
1368 1369
        while ($this->transactionNestingLevel !== 0) {
            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
1370 1371 1372 1373 1374 1375 1376 1377 1378
                // 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();
        }
1379 1380 1381
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1382
     * Cancels any database changes done during the current transaction.
1383
     *
1384 1385
     * @return bool
     *
1386
     * @throws ConnectionException If the rollback operation failed.
1387
     */
1388
    public function rollBack()
1389
    {
1390
        if ($this->transactionNestingLevel === 0) {
1391
            throw ConnectionException::noActiveTransaction();
1392 1393
        }

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

1396 1397
        $logger = $this->_config->getSQLLogger();

1398
        if ($this->transactionNestingLevel === 1) {
1399
            if ($logger !== null) {
1400 1401
                $logger->startQuery('"ROLLBACK"');
            }
Grégoire Paris's avatar
Grégoire Paris committed
1402

1403
            $this->transactionNestingLevel = 0;
Sergei Morozov's avatar
Sergei Morozov committed
1404
            $connection->rollBack();
1405
            $this->isRollbackOnly = false;
1406
            if ($logger !== null) {
1407 1408
                $logger->stopQuery();
            }
1409

1410
            if ($this->autoCommit === false) {
1411 1412
                $this->beginTransaction();
            }
1413
        } elseif ($this->nestTransactionsWithSavepoints) {
1414
            if ($logger !== null) {
1415 1416
                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
            }
Grégoire Paris's avatar
Grégoire Paris committed
1417

1418
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
1419
            --$this->transactionNestingLevel;
1420
            if ($logger !== null) {
1421 1422
                $logger->stopQuery();
            }
1423
        } else {
1424 1425
            $this->isRollbackOnly = true;
            --$this->transactionNestingLevel;
1426
        }
1427 1428

        return true;
1429 1430
    }

1431
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1432 1433 1434
     * Creates a new savepoint.
     *
     * @param string $savepoint The name of the savepoint to create.
1435 1436
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1437
     *
1438
     * @throws ConnectionException
1439
     */
1440
    public function createSavepoint($savepoint)
1441
    {
1442
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1443
            throw ConnectionException::savepointsNotSupported();
1444 1445
        }

Sergei Morozov's avatar
Sergei Morozov committed
1446
        $this->getWrappedConnection()->exec($this->platform->createSavePoint($savepoint));
1447 1448 1449
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1450 1451 1452
     * Releases the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to release.
1453 1454
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1455
     *
1456
     * @throws ConnectionException
1457
     */
1458
    public function releaseSavepoint($savepoint)
1459
    {
1460
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1461
            throw ConnectionException::savepointsNotSupported();
1462 1463
        }

1464 1465
        if (! $this->platform->supportsReleaseSavepoints()) {
            return;
1466
        }
1467

Sergei Morozov's avatar
Sergei Morozov committed
1468
        $this->getWrappedConnection()->exec($this->platform->releaseSavePoint($savepoint));
1469 1470 1471
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1472 1473 1474
     * Rolls back to the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to rollback to.
1475 1476
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1477
     *
1478
     * @throws ConnectionException
1479
     */
1480
    public function rollbackSavepoint($savepoint)
1481
    {
1482
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1483
            throw ConnectionException::savepointsNotSupported();
1484 1485
        }

Sergei Morozov's avatar
Sergei Morozov committed
1486
        $this->getWrappedConnection()->exec($this->platform->rollbackSavePoint($savepoint));
1487 1488
    }

romanb's avatar
romanb committed
1489 1490 1491
    /**
     * Gets the wrapped driver connection.
     *
Sergei Morozov's avatar
Sergei Morozov committed
1492
     * @return DriverConnection
romanb's avatar
romanb committed
1493 1494 1495 1496
     */
    public function getWrappedConnection()
    {
        $this->connect();
1497

romanb's avatar
romanb committed
1498 1499
        return $this->_conn;
    }
1500

romanb's avatar
romanb committed
1501 1502 1503 1504
    /**
     * Gets the SchemaManager that can be used to inspect or change the
     * database schema through the connection.
     *
1505
     * @return AbstractSchemaManager
romanb's avatar
romanb committed
1506 1507 1508
     */
    public function getSchemaManager()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1509
        if ($this->_schemaManager === null) {
romanb's avatar
romanb committed
1510 1511
            $this->_schemaManager = $this->_driver->getSchemaManager($this);
        }
1512

romanb's avatar
romanb committed
1513 1514
        return $this->_schemaManager;
    }
1515

1516 1517 1518
    /**
     * Marks the current transaction so that the only possible
     * outcome for the transaction to be rolled back.
1519
     *
Benjamin Morel's avatar
Benjamin Morel committed
1520 1521
     * @return void
     *
1522
     * @throws ConnectionException If no transaction is active.
1523 1524 1525
     */
    public function setRollbackOnly()
    {
1526
        if ($this->transactionNestingLevel === 0) {
1527 1528
            throw ConnectionException::noActiveTransaction();
        }
Grégoire Paris's avatar
Grégoire Paris committed
1529

1530
        $this->isRollbackOnly = true;
1531 1532 1533
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1534
     * Checks whether the current transaction is marked for rollback only.
1535
     *
1536
     * @return bool
Benjamin Morel's avatar
Benjamin Morel committed
1537
     *
1538
     * @throws ConnectionException If no transaction is active.
1539
     */
1540
    public function isRollbackOnly()
1541
    {
1542
        if ($this->transactionNestingLevel === 0) {
1543 1544
            throw ConnectionException::noActiveTransaction();
        }
Benjamin Morel's avatar
Benjamin Morel committed
1545

1546
        return $this->isRollbackOnly;
1547 1548
    }

1549 1550 1551
    /**
     * Converts a given value to its database representation according to the conversion
     * rules of a specific DBAL mapping type.
1552
     *
Benjamin Morel's avatar
Benjamin Morel committed
1553 1554 1555
     * @param mixed  $value The value to convert.
     * @param string $type  The name of the DBAL mapping type.
     *
1556 1557 1558 1559
     * @return mixed The converted value.
     */
    public function convertToDatabaseValue($value, $type)
    {
1560
        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1561 1562 1563 1564 1565
    }

    /**
     * Converts a given value to its PHP representation according to the conversion
     * rules of a specific DBAL mapping type.
1566
     *
Benjamin Morel's avatar
Benjamin Morel committed
1567 1568 1569
     * @param mixed  $value The value to convert.
     * @param string $type  The name of the DBAL mapping type.
     *
1570 1571 1572 1573
     * @return mixed The converted type.
     */
    public function convertToPHPValue($value, $type)
    {
1574
        return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform());
1575 1576 1577 1578 1579
    }

    /**
     * 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.
1580
     *
1581 1582 1583
     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
     *           raw PDOStatement instances.
     *
1584 1585 1586
     * @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).
1587
     */
1588
    private function _bindTypedValues(DriverStatement $stmt, array $params, array $types): void
1589 1590 1591 1592
    {
        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
        if (is_int(key($params))) {
            // Positional parameters
1593
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1594
            $bindIndex  = 1;
1595
            foreach ($params as $value) {
1596 1597
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
1598 1599
                    $type                  = $types[$typeIndex];
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1600 1601 1602 1603
                    $stmt->bindValue($bindIndex, $value, $bindingType);
                } else {
                    $stmt->bindValue($bindIndex, $value);
                }
Grégoire Paris's avatar
Grégoire Paris committed
1604

1605 1606 1607 1608 1609 1610
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
1611 1612
                    $type                  = $types[$name];
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1613 1614 1615 1616 1617 1618 1619
                    $stmt->bindValue($name, $value, $bindingType);
                } else {
                    $stmt->bindValue($name, $value);
                }
            }
        }
    }
1620 1621 1622 1623

    /**
     * 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
1624 1625
     * @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
1626
     *
1627
     * @return mixed[] [0] => the (escaped) value, [1] => the binding type.
1628 1629 1630 1631 1632 1633
     */
    private function getBindingInfo($value, $type)
    {
        if (is_string($type)) {
            $type = Type::getType($type);
        }
Grégoire Paris's avatar
Grégoire Paris committed
1634

1635
        if ($type instanceof Type) {
1636
            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1637 1638
            $bindingType = $type->getBindingType();
        } else {
1639
            $bindingType = $type;
1640
        }
Benjamin Morel's avatar
Benjamin Morel committed
1641

1642
        return [$value, $bindingType];
1643 1644
    }

1645 1646 1647 1648 1649 1650
    /**
     * 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.
     *
1651 1652
     * @param mixed[]                $params
     * @param array<int|string|null> $types
1653
     *
1654
     * @return mixed[]
1655 1656 1657
     */
    public function resolveParams(array $params, array $types)
    {
1658
        $resolvedParams = [];
1659 1660 1661 1662 1663

        // 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;
1664
            $bindIndex  = 1;
1665 1666 1667
            foreach ($params as $value) {
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
1668 1669
                    $type                       = $types[$typeIndex];
                    [$value]                    = $this->getBindingInfo($value, $type);
1670 1671 1672 1673
                    $resolvedParams[$bindIndex] = $value;
                } else {
                    $resolvedParams[$bindIndex] = $value;
                }
Grégoire Paris's avatar
Grégoire Paris committed
1674

1675 1676 1677 1678 1679 1680
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
1681 1682
                    $type                  = $types[$name];
                    [$value]               = $this->getBindingInfo($value, $type);
1683 1684 1685 1686 1687 1688 1689 1690 1691 1692
                    $resolvedParams[$name] = $value;
                } else {
                    $resolvedParams[$name] = $value;
                }
            }
        }

        return $resolvedParams;
    }

1693
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1694
     * Creates a new instance of a SQL query builder.
1695
     *
1696
     * @return QueryBuilder
1697 1698 1699 1700 1701
     */
    public function createQueryBuilder()
    {
        return new Query\QueryBuilder($this);
    }
1702 1703

    /**
1704 1705 1706 1707 1708 1709
     * 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:
     *
1710 1711
     * @return bool
     *
1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722
     * @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.
1723 1724 1725
     */
    public function ping()
    {
Sergei Morozov's avatar
Sergei Morozov committed
1726
        $connection = $this->getWrappedConnection();
1727

Sergei Morozov's avatar
Sergei Morozov committed
1728 1729
        if ($connection instanceof PingableConnection) {
            return $connection->ping();
1730 1731 1732
        }

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

1735 1736
            return true;
        } catch (DBALException $e) {
1737
            return false;
1738
        }
1739
    }
1740
}