1<?php
2
3namespace Doctrine\DBAL;
4
5use Closure;
6use Doctrine\Common\EventManager;
7use Doctrine\DBAL\Cache\ArrayStatement;
8use Doctrine\DBAL\Cache\CacheException;
9use Doctrine\DBAL\Cache\QueryCacheProfile;
10use Doctrine\DBAL\Cache\ResultCacheStatement;
11use Doctrine\DBAL\Driver\Connection as DriverConnection;
12use Doctrine\DBAL\Driver\PDO\Statement as PDODriverStatement;
13use Doctrine\DBAL\Driver\PingableConnection;
14use Doctrine\DBAL\Driver\ResultStatement;
15use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
16use Doctrine\DBAL\Exception\ConnectionLost;
17use Doctrine\DBAL\Exception\InvalidArgumentException;
18use Doctrine\DBAL\Exception\NoKeyValue;
19use Doctrine\DBAL\Platforms\AbstractPlatform;
20use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
21use Doctrine\DBAL\Query\QueryBuilder;
22use Doctrine\DBAL\Schema\AbstractSchemaManager;
23use Doctrine\DBAL\Types\Type;
24use Doctrine\Deprecations\Deprecation;
25use PDO;
26use Throwable;
27use Traversable;
28
29use function array_key_exists;
30use function array_shift;
31use function assert;
32use function func_get_args;
33use function implode;
34use function is_int;
35use function is_string;
36use function key;
37
38/**
39 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
40 * events, transaction isolation levels, configuration, emulated transaction nesting,
41 * lazy connecting and more.
42 *
43 * @psalm-import-type Params from DriverManager
44 */
45class Connection implements DriverConnection
46{
47    /**
48     * Constant for transaction isolation level READ UNCOMMITTED.
49     *
50     * @deprecated Use TransactionIsolationLevel::READ_UNCOMMITTED.
51     */
52    public const TRANSACTION_READ_UNCOMMITTED = TransactionIsolationLevel::READ_UNCOMMITTED;
53
54    /**
55     * Constant for transaction isolation level READ COMMITTED.
56     *
57     * @deprecated Use TransactionIsolationLevel::READ_COMMITTED.
58     */
59    public const TRANSACTION_READ_COMMITTED = TransactionIsolationLevel::READ_COMMITTED;
60
61    /**
62     * Constant for transaction isolation level REPEATABLE READ.
63     *
64     * @deprecated Use TransactionIsolationLevel::REPEATABLE_READ.
65     */
66    public const TRANSACTION_REPEATABLE_READ = TransactionIsolationLevel::REPEATABLE_READ;
67
68    /**
69     * Constant for transaction isolation level SERIALIZABLE.
70     *
71     * @deprecated Use TransactionIsolationLevel::SERIALIZABLE.
72     */
73    public const TRANSACTION_SERIALIZABLE = TransactionIsolationLevel::SERIALIZABLE;
74
75    /**
76     * Represents an array of ints to be expanded by Doctrine SQL parsing.
77     */
78    public const PARAM_INT_ARRAY = ParameterType::INTEGER + self::ARRAY_PARAM_OFFSET;
79
80    /**
81     * Represents an array of strings to be expanded by Doctrine SQL parsing.
82     */
83    public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET;
84
85    /**
86     * Offset by which PARAM_* constants are detected as arrays of the param type.
87     */
88    public const ARRAY_PARAM_OFFSET = 100;
89
90    /**
91     * The wrapped driver connection.
92     *
93     * @var \Doctrine\DBAL\Driver\Connection|null
94     */
95    protected $_conn;
96
97    /** @var Configuration */
98    protected $_config;
99
100    /** @var EventManager */
101    protected $_eventManager;
102
103    /** @var ExpressionBuilder */
104    protected $_expr;
105
106    /**
107     * The current auto-commit mode of this connection.
108     *
109     * @var bool
110     */
111    private $autoCommit = true;
112
113    /**
114     * The transaction nesting level.
115     *
116     * @var int
117     */
118    private $transactionNestingLevel = 0;
119
120    /**
121     * The currently active transaction isolation level or NULL before it has been determined.
122     *
123     * @var int|null
124     */
125    private $transactionIsolationLevel;
126
127    /**
128     * If nested transactions should use savepoints.
129     *
130     * @var bool
131     */
132    private $nestTransactionsWithSavepoints = false;
133
134    /**
135     * The parameters used during creation of the Connection instance.
136     *
137     * @var array<string,mixed>
138     * @phpstan-var array<string,mixed>
139     * @psalm-var Params
140     */
141    private $params;
142
143    /**
144     * The database platform object used by the connection or NULL before it's initialized.
145     *
146     * @var AbstractPlatform|null
147     */
148    private $platform;
149
150    /**
151     * The schema manager.
152     *
153     * @var AbstractSchemaManager|null
154     */
155    protected $_schemaManager;
156
157    /**
158     * The used DBAL driver.
159     *
160     * @var Driver
161     */
162    protected $_driver;
163
164    /**
165     * Flag that indicates whether the current transaction is marked for rollback only.
166     *
167     * @var bool
168     */
169    private $isRollbackOnly = false;
170
171    /** @var int */
172    protected $defaultFetchMode = FetchMode::ASSOCIATIVE;
173
174    /**
175     * Initializes a new instance of the Connection class.
176     *
177     * @internal The connection can be only instantiated by the driver manager.
178     *
179     * @param array<string,mixed> $params       The connection parameters.
180     * @param Driver              $driver       The driver to use.
181     * @param Configuration|null  $config       The configuration, optional.
182     * @param EventManager|null   $eventManager The event manager, optional.
183     * @psalm-param Params $params
184     * @phpstan-param array<string,mixed> $params
185     *
186     * @throws Exception
187     */
188    public function __construct(
189        array $params,
190        Driver $driver,
191        ?Configuration $config = null,
192        ?EventManager $eventManager = null
193    ) {
194        $this->_driver = $driver;
195        $this->params  = $params;
196
197        if (isset($params['pdo'])) {
198            Deprecation::trigger(
199                'doctrine/dbal',
200                'https://github.com/doctrine/dbal/pull/3554',
201                'Passing a user provided PDO instance directly to Doctrine is deprecated.'
202            );
203
204            if (! $params['pdo'] instanceof PDO) {
205                throw Exception::invalidPdoInstance();
206            }
207
208            $this->_conn = $params['pdo'];
209            $this->_conn->setAttribute(PDO::ATTR_STATEMENT_CLASS, [PDODriverStatement::class, []]);
210            unset($this->params['pdo']);
211        }
212
213        if (isset($params['platform'])) {
214            if (! $params['platform'] instanceof Platforms\AbstractPlatform) {
215                throw Exception::invalidPlatformType($params['platform']);
216            }
217
218            $this->platform = $params['platform'];
219        }
220
221        // Create default config and event manager if none given
222        if (! $config) {
223            $config = new Configuration();
224        }
225
226        if (! $eventManager) {
227            $eventManager = new EventManager();
228        }
229
230        $this->_config       = $config;
231        $this->_eventManager = $eventManager;
232
233        $this->_expr = new Query\Expression\ExpressionBuilder($this);
234
235        $this->autoCommit = $config->getAutoCommit();
236    }
237
238    /**
239     * Gets the parameters used during instantiation.
240     *
241     * @internal
242     *
243     * @return array<string,mixed>
244     * @psalm-return Params
245     * @phpstan-return array<string,mixed>
246     */
247    public function getParams()
248    {
249        return $this->params;
250    }
251
252    /**
253     * Gets the name of the database this Connection is connected to.
254     *
255     * @return string
256     */
257    public function getDatabase()
258    {
259        return $this->_driver->getDatabase($this);
260    }
261
262    /**
263     * Gets the hostname of the currently connected database.
264     *
265     * @deprecated
266     *
267     * @return string|null
268     */
269    public function getHost()
270    {
271        Deprecation::trigger(
272            'doctrine/dbal',
273            'https://github.com/doctrine/dbal/issues/3580',
274            'Connection::getHost() is deprecated, get the database server host from application config ' .
275            'or as a last resort from internal Connection::getParams() API.'
276        );
277
278        return $this->params['host'] ?? null;
279    }
280
281    /**
282     * Gets the port of the currently connected database.
283     *
284     * @deprecated
285     *
286     * @return mixed
287     */
288    public function getPort()
289    {
290        Deprecation::trigger(
291            'doctrine/dbal',
292            'https://github.com/doctrine/dbal/issues/3580',
293            'Connection::getPort() is deprecated, get the database server port from application config ' .
294            'or as a last resort from internal Connection::getParams() API.'
295        );
296
297        return $this->params['port'] ?? null;
298    }
299
300    /**
301     * Gets the username used by this connection.
302     *
303     * @deprecated
304     *
305     * @return string|null
306     */
307    public function getUsername()
308    {
309        Deprecation::trigger(
310            'doctrine/dbal',
311            'https://github.com/doctrine/dbal/issues/3580',
312            'Connection::getUsername() is deprecated, get the username from application config ' .
313            'or as a last resort from internal Connection::getParams() API.'
314        );
315
316        return $this->params['user'] ?? null;
317    }
318
319    /**
320     * Gets the password used by this connection.
321     *
322     * @deprecated
323     *
324     * @return string|null
325     */
326    public function getPassword()
327    {
328        Deprecation::trigger(
329            'doctrine/dbal',
330            'https://github.com/doctrine/dbal/issues/3580',
331            'Connection::getPassword() is deprecated, get the password from application config ' .
332            'or as a last resort from internal Connection::getParams() API.'
333        );
334
335        return $this->params['password'] ?? null;
336    }
337
338    /**
339     * Gets the DBAL driver instance.
340     *
341     * @return Driver
342     */
343    public function getDriver()
344    {
345        return $this->_driver;
346    }
347
348    /**
349     * Gets the Configuration used by the Connection.
350     *
351     * @return Configuration
352     */
353    public function getConfiguration()
354    {
355        return $this->_config;
356    }
357
358    /**
359     * Gets the EventManager used by the Connection.
360     *
361     * @return EventManager
362     */
363    public function getEventManager()
364    {
365        return $this->_eventManager;
366    }
367
368    /**
369     * Gets the DatabasePlatform for the connection.
370     *
371     * @return AbstractPlatform
372     *
373     * @throws Exception
374     */
375    public function getDatabasePlatform()
376    {
377        if ($this->platform === null) {
378            $this->platform = $this->detectDatabasePlatform();
379            $this->platform->setEventManager($this->_eventManager);
380        }
381
382        return $this->platform;
383    }
384
385    /**
386     * Gets the ExpressionBuilder for the connection.
387     *
388     * @return ExpressionBuilder
389     */
390    public function getExpressionBuilder()
391    {
392        return $this->_expr;
393    }
394
395    /**
396     * Establishes the connection with the database.
397     *
398     * @return bool TRUE if the connection was successfully established, FALSE if
399     *              the connection is already open.
400     */
401    public function connect()
402    {
403        if ($this->_conn !== null) {
404            return false;
405        }
406
407        $driverOptions = $this->params['driverOptions'] ?? [];
408        $user          = $this->params['user'] ?? null;
409        $password      = $this->params['password'] ?? null;
410
411        $this->_conn = $this->_driver->connect($this->params, $user, $password, $driverOptions);
412
413        $this->transactionNestingLevel = 0;
414
415        if ($this->autoCommit === false) {
416            $this->beginTransaction();
417        }
418
419        if ($this->_eventManager->hasListeners(Events::postConnect)) {
420            $eventArgs = new Event\ConnectionEventArgs($this);
421            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
422        }
423
424        return true;
425    }
426
427    /**
428     * Detects and sets the database platform.
429     *
430     * Evaluates custom platform class and version in order to set the correct platform.
431     *
432     * @throws Exception If an invalid platform was specified for this connection.
433     */
434    private function detectDatabasePlatform(): AbstractPlatform
435    {
436        $version = $this->getDatabasePlatformVersion();
437
438        if ($version !== null) {
439            assert($this->_driver instanceof VersionAwarePlatformDriver);
440
441            return $this->_driver->createDatabasePlatformForVersion($version);
442        }
443
444        return $this->_driver->getDatabasePlatform();
445    }
446
447    /**
448     * Returns the version of the related platform if applicable.
449     *
450     * Returns null if either the driver is not capable to create version
451     * specific platform instances, no explicit server version was specified
452     * or the underlying driver connection cannot determine the platform
453     * version without having to query it (performance reasons).
454     *
455     * @return string|null
456     *
457     * @throws Throwable
458     */
459    private function getDatabasePlatformVersion()
460    {
461        // Driver does not support version specific platforms.
462        if (! $this->_driver instanceof VersionAwarePlatformDriver) {
463            return null;
464        }
465
466        // Explicit platform version requested (supersedes auto-detection).
467        if (isset($this->params['serverVersion'])) {
468            return $this->params['serverVersion'];
469        }
470
471        // If not connected, we need to connect now to determine the platform version.
472        if ($this->_conn === null) {
473            try {
474                $this->connect();
475            } catch (Throwable $originalException) {
476                if (empty($this->params['dbname'])) {
477                    throw $originalException;
478                }
479
480                // The database to connect to might not yet exist.
481                // Retry detection without database name connection parameter.
482                $params = $this->params;
483
484                unset($this->params['dbname']);
485
486                try {
487                    $this->connect();
488                } catch (Throwable $fallbackException) {
489                    // Either the platform does not support database-less connections
490                    // or something else went wrong.
491                    throw $originalException;
492                } finally {
493                    $this->params = $params;
494                }
495
496                $serverVersion = $this->getServerVersion();
497
498                // Close "temporary" connection to allow connecting to the real database again.
499                $this->close();
500
501                return $serverVersion;
502            }
503        }
504
505        return $this->getServerVersion();
506    }
507
508    /**
509     * Returns the database server version if the underlying driver supports it.
510     *
511     * @return string|null
512     */
513    private function getServerVersion()
514    {
515        $connection = $this->getWrappedConnection();
516
517        // Automatic platform version detection.
518        if ($connection instanceof ServerInfoAwareConnection && ! $connection->requiresQueryForServerVersion()) {
519            return $connection->getServerVersion();
520        }
521
522        // Unable to detect platform version.
523        return null;
524    }
525
526    /**
527     * Returns the current auto-commit mode for this connection.
528     *
529     * @see    setAutoCommit
530     *
531     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
532     */
533    public function isAutoCommit()
534    {
535        return $this->autoCommit === true;
536    }
537
538    /**
539     * Sets auto-commit mode for this connection.
540     *
541     * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
542     * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
543     * the method commit or the method rollback. By default, new connections are in auto-commit mode.
544     *
545     * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is
546     * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op.
547     *
548     * @see   isAutoCommit
549     *
550     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
551     *
552     * @return void
553     */
554    public function setAutoCommit($autoCommit)
555    {
556        $autoCommit = (bool) $autoCommit;
557
558        // Mode not changed, no-op.
559        if ($autoCommit === $this->autoCommit) {
560            return;
561        }
562
563        $this->autoCommit = $autoCommit;
564
565        // Commit all currently active transactions if any when switching auto-commit mode.
566        if ($this->_conn === null || $this->transactionNestingLevel === 0) {
567            return;
568        }
569
570        $this->commitAll();
571    }
572
573    /**
574     * Sets the fetch mode.
575     *
576     * @deprecated Use one of the fetch- or iterate-related methods.
577     *
578     * @param int $fetchMode
579     *
580     * @return void
581     */
582    public function setFetchMode($fetchMode)
583    {
584        Deprecation::trigger(
585            'doctrine/dbal',
586            'https://github.com/doctrine/dbal/pull/4019',
587            'Default Fetch Mode configuration is deprecated, use explicit Connection::fetch*() APIs instead.'
588        );
589
590        $this->defaultFetchMode = $fetchMode;
591    }
592
593    /**
594     * Prepares and executes an SQL query and returns the first row of the result
595     * as an associative array.
596     *
597     * @deprecated Use fetchAssociative()
598     *
599     * @param string                                                               $sql    SQL query
600     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
601     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
602     *
603     * @return array<string, mixed>|false False is returned if no rows are found.
604     *
605     * @throws Exception
606     */
607    public function fetchAssoc($sql, array $params = [], array $types = [])
608    {
609        Deprecation::trigger(
610            'doctrine/dbal',
611            'https://github.com/doctrine/dbal/pull/4019',
612            'Connection::fetchAssoc() is deprecated, use Connection::fetchAssociative() API instead.'
613        );
614
615        return $this->executeQuery($sql, $params, $types)->fetch(FetchMode::ASSOCIATIVE);
616    }
617
618    /**
619     * Prepares and executes an SQL query and returns the first row of the result
620     * as a numerically indexed array.
621     *
622     * @deprecated Use fetchNumeric()
623     *
624     * @param string                                                               $sql    SQL query
625     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
626     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
627     *
628     * @return array<int, mixed>|false False is returned if no rows are found.
629     */
630    public function fetchArray($sql, array $params = [], array $types = [])
631    {
632        Deprecation::trigger(
633            'doctrine/dbal',
634            'https://github.com/doctrine/dbal/pull/4019',
635            'Connection::fetchArray() is deprecated, use Connection::fetchNumeric() API instead.'
636        );
637
638        return $this->executeQuery($sql, $params, $types)->fetch(FetchMode::NUMERIC);
639    }
640
641    /**
642     * Prepares and executes an SQL query and returns the value of a single column
643     * of the first row of the result.
644     *
645     * @deprecated Use fetchOne() instead.
646     *
647     * @param string                                                               $sql    SQL query
648     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
649     * @param int                                                                  $column 0-indexed column number
650     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
651     *
652     * @return mixed|false False is returned if no rows are found.
653     *
654     * @throws Exception
655     */
656    public function fetchColumn($sql, array $params = [], $column = 0, array $types = [])
657    {
658        Deprecation::trigger(
659            'doctrine/dbal',
660            'https://github.com/doctrine/dbal/pull/4019',
661            'Connection::fetchColumn() is deprecated, use Connection::fetchOne() API instead.'
662        );
663
664        return $this->executeQuery($sql, $params, $types)->fetchColumn($column);
665    }
666
667    /**
668     * Prepares and executes an SQL query and returns the first row of the result
669     * as an associative array.
670     *
671     * @param string                                                               $query  SQL query
672     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
673     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
674     *
675     * @return array<string, mixed>|false False is returned if no rows are found.
676     *
677     * @throws Exception
678     */
679    public function fetchAssociative(string $query, array $params = [], array $types = [])
680    {
681        try {
682            $stmt = $this->ensureForwardCompatibilityStatement(
683                $this->executeQuery($query, $params, $types)
684            );
685
686            return $stmt->fetchAssociative();
687        } catch (Throwable $e) {
688            $this->handleExceptionDuringQuery($e, $query, $params, $types);
689        }
690    }
691
692    /**
693     * Prepares and executes an SQL query and returns the first row of the result
694     * as a numerically indexed array.
695     *
696     * @param string                                                               $query  SQL query
697     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
698     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
699     *
700     * @return array<int, mixed>|false False is returned if no rows are found.
701     *
702     * @throws Exception
703     */
704    public function fetchNumeric(string $query, array $params = [], array $types = [])
705    {
706        try {
707            $stmt = $this->ensureForwardCompatibilityStatement(
708                $this->executeQuery($query, $params, $types)
709            );
710
711            return $stmt->fetchNumeric();
712        } catch (Throwable $e) {
713            $this->handleExceptionDuringQuery($e, $query, $params, $types);
714        }
715    }
716
717    /**
718     * Prepares and executes an SQL query and returns the value of a single column
719     * of the first row of the result.
720     *
721     * @param string                                                               $query  SQL query
722     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
723     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
724     *
725     * @return mixed|false False is returned if no rows are found.
726     *
727     * @throws Exception
728     */
729    public function fetchOne(string $query, array $params = [], array $types = [])
730    {
731        try {
732            $stmt = $this->ensureForwardCompatibilityStatement(
733                $this->executeQuery($query, $params, $types)
734            );
735
736            return $stmt->fetchOne();
737        } catch (Throwable $e) {
738            $this->handleExceptionDuringQuery($e, $query, $params, $types);
739        }
740    }
741
742    /**
743     * Whether an actual connection to the database is established.
744     *
745     * @return bool
746     */
747    public function isConnected()
748    {
749        return $this->_conn !== null;
750    }
751
752    /**
753     * Checks whether a transaction is currently active.
754     *
755     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
756     */
757    public function isTransactionActive()
758    {
759        return $this->transactionNestingLevel > 0;
760    }
761
762    /**
763     * Adds condition based on the criteria to the query components
764     *
765     * @param array<string,mixed> $criteria   Map of key columns to their values
766     * @param string[]            $columns    Column names
767     * @param mixed[]             $values     Column values
768     * @param string[]            $conditions Key conditions
769     *
770     * @throws Exception
771     */
772    private function addCriteriaCondition(
773        array $criteria,
774        array &$columns,
775        array &$values,
776        array &$conditions
777    ): void {
778        $platform = $this->getDatabasePlatform();
779
780        foreach ($criteria as $columnName => $value) {
781            if ($value === null) {
782                $conditions[] = $platform->getIsNullExpression($columnName);
783                continue;
784            }
785
786            $columns[]    = $columnName;
787            $values[]     = $value;
788            $conditions[] = $columnName . ' = ?';
789        }
790    }
791
792    /**
793     * Executes an SQL DELETE statement on a table.
794     *
795     * Table expression and columns are not escaped and are not safe for user-input.
796     *
797     * @param string                                                               $table    Table name
798     * @param array<string, mixed>                                                 $criteria Deletion criteria
799     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types    Parameter types
800     *
801     * @return int The number of affected rows.
802     *
803     * @throws Exception
804     */
805    public function delete($table, array $criteria, array $types = [])
806    {
807        if (empty($criteria)) {
808            throw InvalidArgumentException::fromEmptyCriteria();
809        }
810
811        $columns = $values = $conditions = [];
812
813        $this->addCriteriaCondition($criteria, $columns, $values, $conditions);
814
815        return $this->executeStatement(
816            'DELETE FROM ' . $table . ' WHERE ' . implode(' AND ', $conditions),
817            $values,
818            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
819        );
820    }
821
822    /**
823     * Closes the connection.
824     *
825     * @return void
826     */
827    public function close()
828    {
829        $this->_conn = null;
830    }
831
832    /**
833     * Sets the transaction isolation level.
834     *
835     * @param int $level The level to set.
836     *
837     * @return int
838     */
839    public function setTransactionIsolation($level)
840    {
841        $this->transactionIsolationLevel = $level;
842
843        return $this->executeStatement($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
844    }
845
846    /**
847     * Gets the currently active transaction isolation level.
848     *
849     * @return int The current transaction isolation level.
850     */
851    public function getTransactionIsolation()
852    {
853        if ($this->transactionIsolationLevel === null) {
854            $this->transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
855        }
856
857        return $this->transactionIsolationLevel;
858    }
859
860    /**
861     * Executes an SQL UPDATE statement on a table.
862     *
863     * Table expression and columns are not escaped and are not safe for user-input.
864     *
865     * @param string                                                               $table    Table name
866     * @param array<string, mixed>                                                 $data     Column-value pairs
867     * @param array<string, mixed>                                                 $criteria Update criteria
868     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types    Parameter types
869     *
870     * @return int The number of affected rows.
871     *
872     * @throws Exception
873     */
874    public function update($table, array $data, array $criteria, array $types = [])
875    {
876        $columns = $values = $conditions = $set = [];
877
878        foreach ($data as $columnName => $value) {
879            $columns[] = $columnName;
880            $values[]  = $value;
881            $set[]     = $columnName . ' = ?';
882        }
883
884        $this->addCriteriaCondition($criteria, $columns, $values, $conditions);
885
886        if (is_string(key($types))) {
887            $types = $this->extractTypeValues($columns, $types);
888        }
889
890        $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $set)
891                . ' WHERE ' . implode(' AND ', $conditions);
892
893        return $this->executeStatement($sql, $values, $types);
894    }
895
896    /**
897     * Inserts a table row with specified data.
898     *
899     * Table expression and columns are not escaped and are not safe for user-input.
900     *
901     * @param string                                                               $table Table name
902     * @param array<string, mixed>                                                 $data  Column-value pairs
903     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types Parameter types
904     *
905     * @return int The number of affected rows.
906     *
907     * @throws Exception
908     */
909    public function insert($table, array $data, array $types = [])
910    {
911        if (empty($data)) {
912            return $this->executeStatement('INSERT INTO ' . $table . ' () VALUES ()');
913        }
914
915        $columns = [];
916        $values  = [];
917        $set     = [];
918
919        foreach ($data as $columnName => $value) {
920            $columns[] = $columnName;
921            $values[]  = $value;
922            $set[]     = '?';
923        }
924
925        return $this->executeStatement(
926            'INSERT INTO ' . $table . ' (' . implode(', ', $columns) . ')' .
927            ' VALUES (' . implode(', ', $set) . ')',
928            $values,
929            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
930        );
931    }
932
933    /**
934     * Extract ordered type list from an ordered column list and type map.
935     *
936     * @param array<int, string>                                                   $columnList
937     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
938     *
939     * @return array<int, int|string|Type|null>|array<string, int|string|Type|null>
940     */
941    private function extractTypeValues(array $columnList, array $types)
942    {
943        $typeValues = [];
944
945        foreach ($columnList as $columnIndex => $columnName) {
946            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
947        }
948
949        return $typeValues;
950    }
951
952    /**
953     * Quotes a string so it can be safely used as a table or column name, even if
954     * it is a reserved name.
955     *
956     * Delimiting style depends on the underlying database platform that is being used.
957     *
958     * NOTE: Just because you CAN use quoted identifiers does not mean
959     * you SHOULD use them. In general, they end up causing way more
960     * problems than they solve.
961     *
962     * @param string $str The name to be quoted.
963     *
964     * @return string The quoted name.
965     */
966    public function quoteIdentifier($str)
967    {
968        return $this->getDatabasePlatform()->quoteIdentifier($str);
969    }
970
971    /**
972     * {@inheritDoc}
973     *
974     * @param mixed                $value
975     * @param int|string|Type|null $type
976     */
977    public function quote($value, $type = ParameterType::STRING)
978    {
979        $connection = $this->getWrappedConnection();
980
981        [$value, $bindingType] = $this->getBindingInfo($value, $type);
982
983        return $connection->quote($value, $bindingType);
984    }
985
986    /**
987     * Prepares and executes an SQL query and returns the result as an associative array.
988     *
989     * @deprecated Use fetchAllAssociative()
990     *
991     * @param string         $sql    The SQL query.
992     * @param mixed[]        $params The query parameters.
993     * @param int[]|string[] $types  The query parameter types.
994     *
995     * @return mixed[]
996     */
997    public function fetchAll($sql, array $params = [], $types = [])
998    {
999        return $this->executeQuery($sql, $params, $types)->fetchAll();
1000    }
1001
1002    /**
1003     * Prepares and executes an SQL query and returns the result as an array of numeric arrays.
1004     *
1005     * @param string                                                               $query  SQL query
1006     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
1007     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1008     *
1009     * @return array<int,array<int,mixed>>
1010     *
1011     * @throws Exception
1012     */
1013    public function fetchAllNumeric(string $query, array $params = [], array $types = []): array
1014    {
1015        try {
1016            $stmt = $this->ensureForwardCompatibilityStatement(
1017                $this->executeQuery($query, $params, $types)
1018            );
1019
1020            return $stmt->fetchAllNumeric();
1021        } catch (Throwable $e) {
1022            $this->handleExceptionDuringQuery($e, $query, $params, $types);
1023        }
1024    }
1025
1026    /**
1027     * Prepares and executes an SQL query and returns the result as an array of associative arrays.
1028     *
1029     * @param string                                                               $query  SQL query
1030     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
1031     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1032     *
1033     * @return array<int,array<string,mixed>>
1034     *
1035     * @throws Exception
1036     */
1037    public function fetchAllAssociative(string $query, array $params = [], array $types = []): array
1038    {
1039        try {
1040            $stmt = $this->ensureForwardCompatibilityStatement(
1041                $this->executeQuery($query, $params, $types)
1042            );
1043
1044            return $stmt->fetchAllAssociative();
1045        } catch (Throwable $e) {
1046            $this->handleExceptionDuringQuery($e, $query, $params, $types);
1047        }
1048    }
1049
1050    /**
1051     * Prepares and executes an SQL query and returns the result as an associative array with the keys
1052     * mapped to the first column and the values mapped to the second column.
1053     *
1054     * @param string                                           $query  SQL query
1055     * @param array<int, mixed>|array<string, mixed>           $params Query parameters
1056     * @param array<int, int|string>|array<string, int|string> $types  Parameter types
1057     *
1058     * @return array<mixed,mixed>
1059     *
1060     * @throws Exception
1061     */
1062    public function fetchAllKeyValue(string $query, array $params = [], array $types = []): array
1063    {
1064        $stmt = $this->executeQuery($query, $params, $types);
1065
1066        $this->ensureHasKeyValue($stmt);
1067
1068        $data = [];
1069
1070        foreach ($stmt->fetchAll(FetchMode::NUMERIC) as [$key, $value]) {
1071            $data[$key] = $value;
1072        }
1073
1074        return $data;
1075    }
1076
1077    /**
1078     * Prepares and executes an SQL query and returns the result as an associative array with the keys mapped
1079     * to the first column and the values being an associative array representing the rest of the columns
1080     * and their values.
1081     *
1082     * @param string                                           $query  SQL query
1083     * @param array<int, mixed>|array<string, mixed>           $params Query parameters
1084     * @param array<int, int|string>|array<string, int|string> $types  Parameter types
1085     *
1086     * @return array<mixed,array<string,mixed>>
1087     *
1088     * @throws Exception
1089     */
1090    public function fetchAllAssociativeIndexed(string $query, array $params = [], array $types = []): array
1091    {
1092        $stmt = $this->executeQuery($query, $params, $types);
1093
1094        $data = [];
1095
1096        foreach ($stmt->fetchAll(FetchMode::ASSOCIATIVE) as $row) {
1097            $data[array_shift($row)] = $row;
1098        }
1099
1100        return $data;
1101    }
1102
1103    /**
1104     * Prepares and executes an SQL query and returns the result as an array of the first column values.
1105     *
1106     * @param string                                                               $query  SQL query
1107     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
1108     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1109     *
1110     * @return array<int,mixed>
1111     *
1112     * @throws Exception
1113     */
1114    public function fetchFirstColumn(string $query, array $params = [], array $types = []): array
1115    {
1116        try {
1117            $stmt = $this->ensureForwardCompatibilityStatement(
1118                $this->executeQuery($query, $params, $types)
1119            );
1120
1121            return $stmt->fetchFirstColumn();
1122        } catch (Throwable $e) {
1123            $this->handleExceptionDuringQuery($e, $query, $params, $types);
1124        }
1125    }
1126
1127    /**
1128     * Prepares and executes an SQL query and returns the result as an iterator over rows represented as numeric arrays.
1129     *
1130     * @param string                                                               $query  SQL query
1131     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
1132     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1133     *
1134     * @return Traversable<int,array<int,mixed>>
1135     *
1136     * @throws Exception
1137     */
1138    public function iterateNumeric(string $query, array $params = [], array $types = []): Traversable
1139    {
1140        try {
1141            $stmt = $this->ensureForwardCompatibilityStatement(
1142                $this->executeQuery($query, $params, $types)
1143            );
1144
1145            yield from $stmt->iterateNumeric();
1146        } catch (Throwable $e) {
1147            $this->handleExceptionDuringQuery($e, $query, $params, $types);
1148        }
1149    }
1150
1151    /**
1152     * Prepares and executes an SQL query and returns the result as an iterator over rows represented
1153     * as associative arrays.
1154     *
1155     * @param string                                                               $query  SQL query
1156     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
1157     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1158     *
1159     * @return Traversable<int,array<string,mixed>>
1160     *
1161     * @throws Exception
1162     */
1163    public function iterateAssociative(string $query, array $params = [], array $types = []): Traversable
1164    {
1165        try {
1166            $stmt = $this->ensureForwardCompatibilityStatement(
1167                $this->executeQuery($query, $params, $types)
1168            );
1169
1170            yield from $stmt->iterateAssociative();
1171        } catch (Throwable $e) {
1172            $this->handleExceptionDuringQuery($e, $query, $params, $types);
1173        }
1174    }
1175
1176    /**
1177     * Prepares and executes an SQL query and returns the result as an iterator with the keys
1178     * mapped to the first column and the values mapped to the second column.
1179     *
1180     * @param string                                           $query  SQL query
1181     * @param array<int, mixed>|array<string, mixed>           $params Query parameters
1182     * @param array<int, int|string>|array<string, int|string> $types  Parameter types
1183     *
1184     * @return Traversable<mixed,mixed>
1185     *
1186     * @throws Exception
1187     */
1188    public function iterateKeyValue(string $query, array $params = [], array $types = []): Traversable
1189    {
1190        $stmt = $this->executeQuery($query, $params, $types);
1191
1192        $this->ensureHasKeyValue($stmt);
1193
1194        while (($row = $stmt->fetch(FetchMode::NUMERIC)) !== false) {
1195            yield $row[0] => $row[1];
1196        }
1197    }
1198
1199    /**
1200     * Prepares and executes an SQL query and returns the result as an iterator with the keys mapped
1201     * to the first column and the values being an associative array representing the rest of the columns
1202     * and their values.
1203     *
1204     * @param string                                           $query  SQL query
1205     * @param array<int, mixed>|array<string, mixed>           $params Query parameters
1206     * @param array<int, int|string>|array<string, int|string> $types  Parameter types
1207     *
1208     * @return Traversable<mixed,array<string,mixed>>
1209     *
1210     * @throws Exception
1211     */
1212    public function iterateAssociativeIndexed(string $query, array $params = [], array $types = []): Traversable
1213    {
1214        $stmt = $this->executeQuery($query, $params, $types);
1215
1216        while (($row = $stmt->fetch(FetchMode::ASSOCIATIVE)) !== false) {
1217            yield array_shift($row) => $row;
1218        }
1219    }
1220
1221    /**
1222     * Prepares and executes an SQL query and returns the result as an iterator over the first column values.
1223     *
1224     * @param string                                                               $query  SQL query
1225     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
1226     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1227     *
1228     * @return Traversable<int,mixed>
1229     *
1230     * @throws Exception
1231     */
1232    public function iterateColumn(string $query, array $params = [], array $types = []): Traversable
1233    {
1234        try {
1235            $stmt = $this->ensureForwardCompatibilityStatement(
1236                $this->executeQuery($query, $params, $types)
1237            );
1238
1239            yield from $stmt->iterateColumn();
1240        } catch (Throwable $e) {
1241            $this->handleExceptionDuringQuery($e, $query, $params, $types);
1242        }
1243    }
1244
1245    /**
1246     * Prepares an SQL statement.
1247     *
1248     * @param string $sql The SQL statement to prepare.
1249     *
1250     * @return Statement The prepared statement.
1251     *
1252     * @throws Exception
1253     */
1254    public function prepare($sql)
1255    {
1256        try {
1257            $stmt = new Statement($sql, $this);
1258        } catch (Throwable $e) {
1259            $this->handleExceptionDuringQuery($e, $sql);
1260        }
1261
1262        $stmt->setFetchMode($this->defaultFetchMode);
1263
1264        return $stmt;
1265    }
1266
1267    /**
1268     * Executes an, optionally parametrized, SQL query.
1269     *
1270     * If the query is parametrized, a prepared statement is used.
1271     * If an SQLLogger is configured, the execution is logged.
1272     *
1273     * @param string                                                               $sql    SQL query
1274     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
1275     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1276     *
1277     * @return ForwardCompatibility\DriverStatement|ForwardCompatibility\DriverResultStatement
1278     *
1279     * The executed statement or the cached result statement if a query cache profile is used
1280     *
1281     * @throws Exception
1282     */
1283    public function executeQuery($sql, array $params = [], $types = [], ?QueryCacheProfile $qcp = null)
1284    {
1285        if ($qcp !== null) {
1286            return $this->executeCacheQuery($sql, $params, $types, $qcp);
1287        }
1288
1289        $connection = $this->getWrappedConnection();
1290
1291        $logger = $this->_config->getSQLLogger();
1292        if ($logger) {
1293            $logger->startQuery($sql, $params, $types);
1294        }
1295
1296        try {
1297            if ($params) {
1298                [$sql, $params, $types] = SQLParserUtils::expandListParameters($sql, $params, $types);
1299
1300                $stmt = $connection->prepare($sql);
1301                if ($types) {
1302                    $this->_bindTypedValues($stmt, $params, $types);
1303                    $stmt->execute();
1304                } else {
1305                    $stmt->execute($params);
1306                }
1307            } else {
1308                $stmt = $connection->query($sql);
1309            }
1310        } catch (Throwable $e) {
1311            $this->handleExceptionDuringQuery(
1312                $e,
1313                $sql,
1314                $params,
1315                $types
1316            );
1317        }
1318
1319        $stmt->setFetchMode($this->defaultFetchMode);
1320
1321        if ($logger) {
1322            $logger->stopQuery();
1323        }
1324
1325        return $this->ensureForwardCompatibilityStatement($stmt);
1326    }
1327
1328    /**
1329     * Executes a caching query.
1330     *
1331     * @param string                                                               $sql    SQL query
1332     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
1333     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1334     *
1335     * @return ForwardCompatibility\DriverResultStatement
1336     *
1337     * @throws CacheException
1338     */
1339    public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp)
1340    {
1341        $resultCache = $qcp->getResultCacheDriver() ?? $this->_config->getResultCacheImpl();
1342
1343        if ($resultCache === null) {
1344            throw CacheException::noResultDriverConfigured();
1345        }
1346
1347        $connectionParams = $this->params;
1348        unset($connectionParams['platform']);
1349
1350        [$cacheKey, $realKey] = $qcp->generateCacheKeys($sql, $params, $types, $connectionParams);
1351
1352        // fetch the row pointers entry
1353        $data = $resultCache->fetch($cacheKey);
1354
1355        if ($data !== false) {
1356            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
1357            if (isset($data[$realKey])) {
1358                $stmt = new ArrayStatement($data[$realKey]);
1359            } elseif (array_key_exists($realKey, $data)) {
1360                $stmt = new ArrayStatement([]);
1361            }
1362        }
1363
1364        if (! isset($stmt)) {
1365            $stmt = new ResultCacheStatement(
1366                $this->executeQuery($sql, $params, $types),
1367                $resultCache,
1368                $cacheKey,
1369                $realKey,
1370                $qcp->getLifetime()
1371            );
1372        }
1373
1374        $stmt->setFetchMode($this->defaultFetchMode);
1375
1376        return $this->ensureForwardCompatibilityStatement($stmt);
1377    }
1378
1379    /**
1380     * @return ForwardCompatibility\Result
1381     */
1382    private function ensureForwardCompatibilityStatement(ResultStatement $stmt)
1383    {
1384        return ForwardCompatibility\Result::ensure($stmt);
1385    }
1386
1387    /**
1388     * Executes an, optionally parametrized, SQL query and returns the result,
1389     * applying a given projection/transformation function on each row of the result.
1390     *
1391     * @deprecated
1392     *
1393     * @param string  $sql      The SQL query to execute.
1394     * @param mixed[] $params   The parameters, if any.
1395     * @param Closure $function The transformation function that is applied on each row.
1396     *                           The function receives a single parameter, an array, that
1397     *                           represents a row of the result set.
1398     *
1399     * @return mixed[] The projected result of the query.
1400     */
1401    public function project($sql, array $params, Closure $function)
1402    {
1403        Deprecation::trigger(
1404            'doctrine/dbal',
1405            'https://github.com/doctrine/dbal/pull/3823',
1406            'Connection::project() is deprecated without replacement, implement data projections in your own code.'
1407        );
1408
1409        $result = [];
1410        $stmt   = $this->executeQuery($sql, $params);
1411
1412        while ($row = $stmt->fetch()) {
1413            $result[] = $function($row);
1414        }
1415
1416        $stmt->closeCursor();
1417
1418        return $result;
1419    }
1420
1421    /**
1422     * Executes an SQL statement, returning a result set as a Statement object.
1423     *
1424     * @deprecated Use {@link executeQuery()} instead.
1425     *
1426     * @return \Doctrine\DBAL\Driver\Statement
1427     *
1428     * @throws Exception
1429     */
1430    public function query()
1431    {
1432        Deprecation::trigger(
1433            'doctrine/dbal',
1434            'https://github.com/doctrine/dbal/pull/4163',
1435            'Connection::query() is deprecated, use Connection::executeQuery() instead.'
1436        );
1437
1438        $connection = $this->getWrappedConnection();
1439
1440        $args = func_get_args();
1441
1442        $logger = $this->_config->getSQLLogger();
1443        if ($logger) {
1444            $logger->startQuery($args[0]);
1445        }
1446
1447        try {
1448            $statement = $connection->query(...$args);
1449        } catch (Throwable $e) {
1450            $this->handleExceptionDuringQuery($e, $args[0]);
1451        }
1452
1453        $statement->setFetchMode($this->defaultFetchMode);
1454
1455        if ($logger) {
1456            $logger->stopQuery();
1457        }
1458
1459        return $statement;
1460    }
1461
1462    /**
1463     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
1464     * and returns the number of affected rows.
1465     *
1466     * This method supports PDO binding types as well as DBAL mapping types.
1467     *
1468     * @deprecated Use {@link executeStatement()} instead.
1469     *
1470     * @param string                                                               $sql    SQL statement
1471     * @param array<int, mixed>|array<string, mixed>                               $params Statement parameters
1472     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1473     *
1474     * @return int The number of affected rows.
1475     *
1476     * @throws Exception
1477     */
1478    public function executeUpdate($sql, array $params = [], array $types = [])
1479    {
1480        Deprecation::trigger(
1481            'doctrine/dbal',
1482            'https://github.com/doctrine/dbal/pull/4163',
1483            'Connection::executeUpdate() is deprecated, use Connection::executeStatement() instead.'
1484        );
1485
1486        return $this->executeStatement($sql, $params, $types);
1487    }
1488
1489    /**
1490     * Executes an SQL statement with the given parameters and returns the number of affected rows.
1491     *
1492     * Could be used for:
1493     *  - DML statements: INSERT, UPDATE, DELETE, etc.
1494     *  - DDL statements: CREATE, DROP, ALTER, etc.
1495     *  - DCL statements: GRANT, REVOKE, etc.
1496     *  - Session control statements: ALTER SESSION, SET, DECLARE, etc.
1497     *  - Other statements that don't yield a row set.
1498     *
1499     * This method supports PDO binding types as well as DBAL mapping types.
1500     *
1501     * @param string                                                               $sql    SQL statement
1502     * @param array<int, mixed>|array<string, mixed>                               $params Statement parameters
1503     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1504     *
1505     * @return int The number of affected rows.
1506     *
1507     * @throws Exception
1508     */
1509    public function executeStatement($sql, array $params = [], array $types = [])
1510    {
1511        $connection = $this->getWrappedConnection();
1512
1513        $logger = $this->_config->getSQLLogger();
1514        if ($logger) {
1515            $logger->startQuery($sql, $params, $types);
1516        }
1517
1518        try {
1519            if ($params) {
1520                [$sql, $params, $types] = SQLParserUtils::expandListParameters($sql, $params, $types);
1521
1522                $stmt = $connection->prepare($sql);
1523
1524                if ($types) {
1525                    $this->_bindTypedValues($stmt, $params, $types);
1526                    $stmt->execute();
1527                } else {
1528                    $stmt->execute($params);
1529                }
1530
1531                $result = $stmt->rowCount();
1532            } else {
1533                $result = $connection->exec($sql);
1534            }
1535        } catch (Throwable $e) {
1536            $this->handleExceptionDuringQuery(
1537                $e,
1538                $sql,
1539                $params,
1540                $types
1541            );
1542        }
1543
1544        if ($logger) {
1545            $logger->stopQuery();
1546        }
1547
1548        return $result;
1549    }
1550
1551    /**
1552     * Executes an SQL statement and return the number of affected rows.
1553     *
1554     * @deprecated Use {@link executeStatement()} instead.
1555     *
1556     * @param string $sql
1557     *
1558     * @return int The number of affected rows.
1559     *
1560     * @throws Exception
1561     */
1562    public function exec($sql)
1563    {
1564        Deprecation::trigger(
1565            'doctrine/dbal',
1566            'https://github.com/doctrine/dbal/pull/4163',
1567            'Connection::exec() is deprecated, use Connection::executeStatement() instead.'
1568        );
1569
1570        $connection = $this->getWrappedConnection();
1571
1572        $logger = $this->_config->getSQLLogger();
1573        if ($logger) {
1574            $logger->startQuery($sql);
1575        }
1576
1577        try {
1578            $result = $connection->exec($sql);
1579        } catch (Throwable $e) {
1580            $this->handleExceptionDuringQuery($e, $sql);
1581        }
1582
1583        if ($logger) {
1584            $logger->stopQuery();
1585        }
1586
1587        return $result;
1588    }
1589
1590    /**
1591     * Returns the current transaction nesting level.
1592     *
1593     * @return int The nesting level. A value of 0 means there's no active transaction.
1594     */
1595    public function getTransactionNestingLevel()
1596    {
1597        return $this->transactionNestingLevel;
1598    }
1599
1600    /**
1601     * Fetches the SQLSTATE associated with the last database operation.
1602     *
1603     * @deprecated The error information is available via exceptions.
1604     *
1605     * @return string|null The last error code.
1606     */
1607    public function errorCode()
1608    {
1609        Deprecation::trigger(
1610            'doctrine/dbal',
1611            'https://github.com/doctrine/dbal/pull/3507',
1612            'Connection::errorCode() is deprecated, use getCode() or getSQLState() on Exception instead.'
1613        );
1614
1615        return $this->getWrappedConnection()->errorCode();
1616    }
1617
1618    /**
1619     * {@inheritDoc}
1620     *
1621     * @deprecated The error information is available via exceptions.
1622     */
1623    public function errorInfo()
1624    {
1625        Deprecation::trigger(
1626            'doctrine/dbal',
1627            'https://github.com/doctrine/dbal/pull/3507',
1628            'Connection::errorInfo() is deprecated, use getCode() or getSQLState() on Exception instead.'
1629        );
1630
1631        return $this->getWrappedConnection()->errorInfo();
1632    }
1633
1634    /**
1635     * Returns the ID of the last inserted row, or the last value from a sequence object,
1636     * depending on the underlying driver.
1637     *
1638     * Note: This method may not return a meaningful or consistent result across different drivers,
1639     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
1640     * columns or sequences.
1641     *
1642     * @param string|null $name Name of the sequence object from which the ID should be returned.
1643     *
1644     * @return string|int|false A string representation of the last inserted ID.
1645     */
1646    public function lastInsertId($name = null)
1647    {
1648        return $this->getWrappedConnection()->lastInsertId($name);
1649    }
1650
1651    /**
1652     * Executes a function in a transaction.
1653     *
1654     * The function gets passed this Connection instance as an (optional) parameter.
1655     *
1656     * If an exception occurs during execution of the function or transaction commit,
1657     * the transaction is rolled back and the exception re-thrown.
1658     *
1659     * @param Closure $func The function to execute transactionally.
1660     *
1661     * @return mixed The value returned by $func
1662     *
1663     * @throws Throwable
1664     */
1665    public function transactional(Closure $func)
1666    {
1667        $this->beginTransaction();
1668        try {
1669            $res = $func($this);
1670            $this->commit();
1671
1672            return $res;
1673        } catch (Throwable $e) {
1674            $this->rollBack();
1675
1676            throw $e;
1677        }
1678    }
1679
1680    /**
1681     * Sets if nested transactions should use savepoints.
1682     *
1683     * @param bool $nestTransactionsWithSavepoints
1684     *
1685     * @return void
1686     *
1687     * @throws ConnectionException
1688     */
1689    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
1690    {
1691        if ($this->transactionNestingLevel > 0) {
1692            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
1693        }
1694
1695        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1696            throw ConnectionException::savepointsNotSupported();
1697        }
1698
1699        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1700    }
1701
1702    /**
1703     * Gets if nested transactions should use savepoints.
1704     *
1705     * @return bool
1706     */
1707    public function getNestTransactionsWithSavepoints()
1708    {
1709        return $this->nestTransactionsWithSavepoints;
1710    }
1711
1712    /**
1713     * Returns the savepoint name to use for nested transactions are false if they are not supported
1714     * "savepointFormat" parameter is not set
1715     *
1716     * @return mixed A string with the savepoint name or false.
1717     */
1718    protected function _getNestedTransactionSavePointName()
1719    {
1720        return 'DOCTRINE2_SAVEPOINT_' . $this->transactionNestingLevel;
1721    }
1722
1723    /**
1724     * {@inheritDoc}
1725     */
1726    public function beginTransaction()
1727    {
1728        $connection = $this->getWrappedConnection();
1729
1730        ++$this->transactionNestingLevel;
1731
1732        $logger = $this->_config->getSQLLogger();
1733
1734        if ($this->transactionNestingLevel === 1) {
1735            if ($logger) {
1736                $logger->startQuery('"START TRANSACTION"');
1737            }
1738
1739            $connection->beginTransaction();
1740
1741            if ($logger) {
1742                $logger->stopQuery();
1743            }
1744        } elseif ($this->nestTransactionsWithSavepoints) {
1745            if ($logger) {
1746                $logger->startQuery('"SAVEPOINT"');
1747            }
1748
1749            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1750            if ($logger) {
1751                $logger->stopQuery();
1752            }
1753        }
1754
1755        return true;
1756    }
1757
1758    /**
1759     * {@inheritDoc}
1760     *
1761     * @throws ConnectionException If the commit failed due to no active transaction or
1762     *                                            because the transaction was marked for rollback only.
1763     */
1764    public function commit()
1765    {
1766        if ($this->transactionNestingLevel === 0) {
1767            throw ConnectionException::noActiveTransaction();
1768        }
1769
1770        if ($this->isRollbackOnly) {
1771            throw ConnectionException::commitFailedRollbackOnly();
1772        }
1773
1774        $result = true;
1775
1776        $connection = $this->getWrappedConnection();
1777
1778        $logger = $this->_config->getSQLLogger();
1779
1780        if ($this->transactionNestingLevel === 1) {
1781            if ($logger) {
1782                $logger->startQuery('"COMMIT"');
1783            }
1784
1785            $result = $connection->commit();
1786
1787            if ($logger) {
1788                $logger->stopQuery();
1789            }
1790        } elseif ($this->nestTransactionsWithSavepoints) {
1791            if ($logger) {
1792                $logger->startQuery('"RELEASE SAVEPOINT"');
1793            }
1794
1795            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1796            if ($logger) {
1797                $logger->stopQuery();
1798            }
1799        }
1800
1801        --$this->transactionNestingLevel;
1802
1803        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1804            return $result;
1805        }
1806
1807        $this->beginTransaction();
1808
1809        return $result;
1810    }
1811
1812    /**
1813     * Commits all current nesting transactions.
1814     */
1815    private function commitAll(): void
1816    {
1817        while ($this->transactionNestingLevel !== 0) {
1818            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
1819                // When in no auto-commit mode, the last nesting commit immediately starts a new transaction.
1820                // Therefore we need to do the final commit here and then leave to avoid an infinite loop.
1821                $this->commit();
1822
1823                return;
1824            }
1825
1826            $this->commit();
1827        }
1828    }
1829
1830    /**
1831     * Cancels any database changes done during the current transaction.
1832     *
1833     * @return bool
1834     *
1835     * @throws ConnectionException If the rollback operation failed.
1836     */
1837    public function rollBack()
1838    {
1839        if ($this->transactionNestingLevel === 0) {
1840            throw ConnectionException::noActiveTransaction();
1841        }
1842
1843        $connection = $this->getWrappedConnection();
1844
1845        $logger = $this->_config->getSQLLogger();
1846
1847        if ($this->transactionNestingLevel === 1) {
1848            if ($logger) {
1849                $logger->startQuery('"ROLLBACK"');
1850            }
1851
1852            $this->transactionNestingLevel = 0;
1853            $connection->rollBack();
1854            $this->isRollbackOnly = false;
1855            if ($logger) {
1856                $logger->stopQuery();
1857            }
1858
1859            if ($this->autoCommit === false) {
1860                $this->beginTransaction();
1861            }
1862        } elseif ($this->nestTransactionsWithSavepoints) {
1863            if ($logger) {
1864                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
1865            }
1866
1867            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
1868            --$this->transactionNestingLevel;
1869            if ($logger) {
1870                $logger->stopQuery();
1871            }
1872        } else {
1873            $this->isRollbackOnly = true;
1874            --$this->transactionNestingLevel;
1875        }
1876
1877        return true;
1878    }
1879
1880    /**
1881     * Creates a new savepoint.
1882     *
1883     * @param string $savepoint The name of the savepoint to create.
1884     *
1885     * @return void
1886     *
1887     * @throws ConnectionException
1888     */
1889    public function createSavepoint($savepoint)
1890    {
1891        $platform = $this->getDatabasePlatform();
1892
1893        if (! $platform->supportsSavepoints()) {
1894            throw ConnectionException::savepointsNotSupported();
1895        }
1896
1897        $this->getWrappedConnection()->exec($platform->createSavePoint($savepoint));
1898    }
1899
1900    /**
1901     * Releases the given savepoint.
1902     *
1903     * @param string $savepoint The name of the savepoint to release.
1904     *
1905     * @return void
1906     *
1907     * @throws ConnectionException
1908     */
1909    public function releaseSavepoint($savepoint)
1910    {
1911        $platform = $this->getDatabasePlatform();
1912
1913        if (! $platform->supportsSavepoints()) {
1914            throw ConnectionException::savepointsNotSupported();
1915        }
1916
1917        if (! $platform->supportsReleaseSavepoints()) {
1918            return;
1919        }
1920
1921        $this->getWrappedConnection()->exec($platform->releaseSavePoint($savepoint));
1922    }
1923
1924    /**
1925     * Rolls back to the given savepoint.
1926     *
1927     * @param string $savepoint The name of the savepoint to rollback to.
1928     *
1929     * @return void
1930     *
1931     * @throws ConnectionException
1932     */
1933    public function rollbackSavepoint($savepoint)
1934    {
1935        $platform = $this->getDatabasePlatform();
1936
1937        if (! $platform->supportsSavepoints()) {
1938            throw ConnectionException::savepointsNotSupported();
1939        }
1940
1941        $this->getWrappedConnection()->exec($platform->rollbackSavePoint($savepoint));
1942    }
1943
1944    /**
1945     * Gets the wrapped driver connection.
1946     *
1947     * @return DriverConnection
1948     */
1949    public function getWrappedConnection()
1950    {
1951        $this->connect();
1952
1953        assert($this->_conn !== null);
1954
1955        return $this->_conn;
1956    }
1957
1958    /**
1959     * Gets the SchemaManager that can be used to inspect or change the
1960     * database schema through the connection.
1961     *
1962     * @return AbstractSchemaManager
1963     */
1964    public function getSchemaManager()
1965    {
1966        if ($this->_schemaManager === null) {
1967            $this->_schemaManager = $this->_driver->getSchemaManager($this);
1968        }
1969
1970        return $this->_schemaManager;
1971    }
1972
1973    /**
1974     * Marks the current transaction so that the only possible
1975     * outcome for the transaction to be rolled back.
1976     *
1977     * @return void
1978     *
1979     * @throws ConnectionException If no transaction is active.
1980     */
1981    public function setRollbackOnly()
1982    {
1983        if ($this->transactionNestingLevel === 0) {
1984            throw ConnectionException::noActiveTransaction();
1985        }
1986
1987        $this->isRollbackOnly = true;
1988    }
1989
1990    /**
1991     * Checks whether the current transaction is marked for rollback only.
1992     *
1993     * @return bool
1994     *
1995     * @throws ConnectionException If no transaction is active.
1996     */
1997    public function isRollbackOnly()
1998    {
1999        if ($this->transactionNestingLevel === 0) {
2000            throw ConnectionException::noActiveTransaction();
2001        }
2002
2003        return $this->isRollbackOnly;
2004    }
2005
2006    /**
2007     * Converts a given value to its database representation according to the conversion
2008     * rules of a specific DBAL mapping type.
2009     *
2010     * @param mixed  $value The value to convert.
2011     * @param string $type  The name of the DBAL mapping type.
2012     *
2013     * @return mixed The converted value.
2014     */
2015    public function convertToDatabaseValue($value, $type)
2016    {
2017        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
2018    }
2019
2020    /**
2021     * Converts a given value to its PHP representation according to the conversion
2022     * rules of a specific DBAL mapping type.
2023     *
2024     * @param mixed  $value The value to convert.
2025     * @param string $type  The name of the DBAL mapping type.
2026     *
2027     * @return mixed The converted type.
2028     */
2029    public function convertToPHPValue($value, $type)
2030    {
2031        return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform());
2032    }
2033
2034    /**
2035     * Binds a set of parameters, some or all of which are typed with a PDO binding type
2036     * or DBAL mapping type, to a given statement.
2037     *
2038     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
2039     *           raw PDOStatement instances.
2040     *
2041     * @param \Doctrine\DBAL\Driver\Statement                                      $stmt   Prepared statement
2042     * @param array<int, mixed>|array<string, mixed>                               $params Statement parameters
2043     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
2044     *
2045     * @return void
2046     */
2047    private function _bindTypedValues($stmt, array $params, array $types)
2048    {
2049        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
2050        if (is_int(key($params))) {
2051            // Positional parameters
2052            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
2053            $bindIndex  = 1;
2054            foreach ($params as $value) {
2055                $typeIndex = $bindIndex + $typeOffset;
2056                if (isset($types[$typeIndex])) {
2057                    $type                  = $types[$typeIndex];
2058                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
2059                    $stmt->bindValue($bindIndex, $value, $bindingType);
2060                } else {
2061                    $stmt->bindValue($bindIndex, $value);
2062                }
2063
2064                ++$bindIndex;
2065            }
2066        } else {
2067            // Named parameters
2068            foreach ($params as $name => $value) {
2069                if (isset($types[$name])) {
2070                    $type                  = $types[$name];
2071                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
2072                    $stmt->bindValue($name, $value, $bindingType);
2073                } else {
2074                    $stmt->bindValue($name, $value);
2075                }
2076            }
2077        }
2078    }
2079
2080    /**
2081     * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type.
2082     *
2083     * @param mixed                $value The value to bind.
2084     * @param int|string|Type|null $type  The type to bind (PDO or DBAL).
2085     *
2086     * @return array{mixed, int} [0] => the (escaped) value, [1] => the binding type.
2087     */
2088    private function getBindingInfo($value, $type): array
2089    {
2090        if (is_string($type)) {
2091            $type = Type::getType($type);
2092        }
2093
2094        if ($type instanceof Type) {
2095            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
2096            $bindingType = $type->getBindingType();
2097        } else {
2098            $bindingType = $type ?? ParameterType::STRING;
2099        }
2100
2101        return [$value, $bindingType];
2102    }
2103
2104    /**
2105     * Resolves the parameters to a format which can be displayed.
2106     *
2107     * @internal This is a purely internal method. If you rely on this method, you are advised to
2108     *           copy/paste the code as this method may change, or be removed without prior notice.
2109     *
2110     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
2111     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
2112     *
2113     * @return array<int, int|string|Type|null>|array<string, int|string|Type|null>
2114     */
2115    public function resolveParams(array $params, array $types)
2116    {
2117        $resolvedParams = [];
2118
2119        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
2120        if (is_int(key($params))) {
2121            // Positional parameters
2122            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
2123            $bindIndex  = 1;
2124            foreach ($params as $value) {
2125                $typeIndex = $bindIndex + $typeOffset;
2126                if (isset($types[$typeIndex])) {
2127                    $type                       = $types[$typeIndex];
2128                    [$value]                    = $this->getBindingInfo($value, $type);
2129                    $resolvedParams[$bindIndex] = $value;
2130                } else {
2131                    $resolvedParams[$bindIndex] = $value;
2132                }
2133
2134                ++$bindIndex;
2135            }
2136        } else {
2137            // Named parameters
2138            foreach ($params as $name => $value) {
2139                if (isset($types[$name])) {
2140                    $type                  = $types[$name];
2141                    [$value]               = $this->getBindingInfo($value, $type);
2142                    $resolvedParams[$name] = $value;
2143                } else {
2144                    $resolvedParams[$name] = $value;
2145                }
2146            }
2147        }
2148
2149        return $resolvedParams;
2150    }
2151
2152    /**
2153     * Creates a new instance of a SQL query builder.
2154     *
2155     * @return QueryBuilder
2156     */
2157    public function createQueryBuilder()
2158    {
2159        return new Query\QueryBuilder($this);
2160    }
2161
2162    /**
2163     * Ping the server
2164     *
2165     * When the server is not available the method returns FALSE.
2166     * It is responsibility of the developer to handle this case
2167     * and abort the request or reconnect manually:
2168     *
2169     * @deprecated
2170     *
2171     * @return bool
2172     *
2173     * @example
2174     *
2175     *   if ($conn->ping() === false) {
2176     *      $conn->close();
2177     *      $conn->connect();
2178     *   }
2179     *
2180     * It is undefined if the underlying driver attempts to reconnect
2181     * or disconnect when the connection is not available anymore
2182     * as long it returns TRUE when a reconnect succeeded and
2183     * FALSE when the connection was dropped.
2184     */
2185    public function ping()
2186    {
2187        Deprecation::trigger(
2188            'doctrine/dbal',
2189            'https://github.com/doctrine/dbal/pull/4119',
2190            'Retry and reconnecting lost connections now happens automatically, ping() will be removed in DBAL 3.'
2191        );
2192
2193        $connection = $this->getWrappedConnection();
2194
2195        if ($connection instanceof PingableConnection) {
2196            return $connection->ping();
2197        }
2198
2199        try {
2200            $this->query($this->getDatabasePlatform()->getDummySelectSQL());
2201
2202            return true;
2203        } catch (DBALException $e) {
2204            return false;
2205        }
2206    }
2207
2208    /**
2209     * @internal
2210     *
2211     * @param array<int, mixed>|array<string, mixed>                               $params
2212     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
2213     *
2214     * @psalm-return never-return
2215     *
2216     * @throws Exception
2217     */
2218    public function handleExceptionDuringQuery(Throwable $e, string $sql, array $params = [], array $types = []): void
2219    {
2220        $this->throw(
2221            Exception::driverExceptionDuringQuery(
2222                $this->_driver,
2223                $e,
2224                $sql,
2225                $this->resolveParams($params, $types)
2226            )
2227        );
2228    }
2229
2230    /**
2231     * @internal
2232     *
2233     * @psalm-return never-return
2234     *
2235     * @throws Exception
2236     */
2237    public function handleDriverException(Throwable $e): void
2238    {
2239        $this->throw(
2240            Exception::driverException(
2241                $this->_driver,
2242                $e
2243            )
2244        );
2245    }
2246
2247    /**
2248     * @internal
2249     *
2250     * @psalm-return never-return
2251     *
2252     * @throws Exception
2253     */
2254    private function throw(Exception $e): void
2255    {
2256        if ($e instanceof ConnectionLost) {
2257            $this->close();
2258        }
2259
2260        throw $e;
2261    }
2262
2263    private function ensureHasKeyValue(ResultStatement $stmt): void
2264    {
2265        $columnCount = $stmt->columnCount();
2266
2267        if ($columnCount < 2) {
2268            throw NoKeyValue::fromColumnCount($columnCount);
2269        }
2270    }
2271}
2272