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\PingableConnection;
13use Doctrine\DBAL\Driver\ResultStatement;
14use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
15use Doctrine\DBAL\Driver\Statement as DriverStatement;
16use Doctrine\DBAL\Exception\InvalidArgumentException;
17use Doctrine\DBAL\Platforms\AbstractPlatform;
18use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
19use Doctrine\DBAL\Query\QueryBuilder;
20use Doctrine\DBAL\Schema\AbstractSchemaManager;
21use Doctrine\DBAL\Types\Type;
22use Exception;
23use Throwable;
24use function array_key_exists;
25use function assert;
26use function func_get_args;
27use function implode;
28use function is_int;
29use function is_string;
30use function key;
31
32/**
33 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
34 * events, transaction isolation levels, configuration, emulated transaction nesting,
35 * lazy connecting and more.
36 */
37class Connection implements DriverConnection
38{
39    /**
40     * Constant for transaction isolation level READ UNCOMMITTED.
41     *
42     * @deprecated Use TransactionIsolationLevel::READ_UNCOMMITTED.
43     */
44    public const TRANSACTION_READ_UNCOMMITTED = TransactionIsolationLevel::READ_UNCOMMITTED;
45
46    /**
47     * Constant for transaction isolation level READ COMMITTED.
48     *
49     * @deprecated Use TransactionIsolationLevel::READ_COMMITTED.
50     */
51    public const TRANSACTION_READ_COMMITTED = TransactionIsolationLevel::READ_COMMITTED;
52
53    /**
54     * Constant for transaction isolation level REPEATABLE READ.
55     *
56     * @deprecated Use TransactionIsolationLevel::REPEATABLE_READ.
57     */
58    public const TRANSACTION_REPEATABLE_READ = TransactionIsolationLevel::REPEATABLE_READ;
59
60    /**
61     * Constant for transaction isolation level SERIALIZABLE.
62     *
63     * @deprecated Use TransactionIsolationLevel::SERIALIZABLE.
64     */
65    public const TRANSACTION_SERIALIZABLE = TransactionIsolationLevel::SERIALIZABLE;
66
67    /**
68     * Represents an array of ints to be expanded by Doctrine SQL parsing.
69     */
70    public const PARAM_INT_ARRAY = ParameterType::INTEGER + self::ARRAY_PARAM_OFFSET;
71
72    /**
73     * Represents an array of strings to be expanded by Doctrine SQL parsing.
74     */
75    public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET;
76
77    /**
78     * Offset by which PARAM_* constants are detected as arrays of the param type.
79     */
80    public const ARRAY_PARAM_OFFSET = 100;
81
82    /**
83     * The wrapped driver connection.
84     *
85     * @var \Doctrine\DBAL\Driver\Connection|null
86     */
87    protected $_conn;
88
89    /** @var Configuration */
90    protected $_config;
91
92    /** @var EventManager */
93    protected $_eventManager;
94
95    /** @var ExpressionBuilder */
96    protected $_expr;
97
98    /**
99     * Whether or not a connection has been established.
100     *
101     * @var bool
102     */
103    private $isConnected = false;
104
105    /**
106     * The current auto-commit mode of this connection.
107     *
108     * @var bool
109     */
110    private $autoCommit = true;
111
112    /**
113     * The transaction nesting level.
114     *
115     * @var int
116     */
117    private $transactionNestingLevel = 0;
118
119    /**
120     * The currently active transaction isolation level.
121     *
122     * @var int
123     */
124    private $transactionIsolationLevel;
125
126    /**
127     * If nested transactions should use savepoints.
128     *
129     * @var bool
130     */
131    private $nestTransactionsWithSavepoints = false;
132
133    /**
134     * The parameters used during creation of the Connection instance.
135     *
136     * @var mixed[]
137     */
138    private $params = [];
139
140    /**
141     * The DatabasePlatform object that provides information about the
142     * database platform used by the connection.
143     *
144     * @var AbstractPlatform
145     */
146    private $platform;
147
148    /**
149     * The schema manager.
150     *
151     * @var AbstractSchemaManager|null
152     */
153    protected $_schemaManager;
154
155    /**
156     * The used DBAL driver.
157     *
158     * @var Driver
159     */
160    protected $_driver;
161
162    /**
163     * Flag that indicates whether the current transaction is marked for rollback only.
164     *
165     * @var bool
166     */
167    private $isRollbackOnly = false;
168
169    /** @var int */
170    protected $defaultFetchMode = FetchMode::ASSOCIATIVE;
171
172    /**
173     * Initializes a new instance of the Connection class.
174     *
175     * @param mixed[]            $params       The connection parameters.
176     * @param Driver             $driver       The driver to use.
177     * @param Configuration|null $config       The configuration, optional.
178     * @param EventManager|null  $eventManager The event manager, optional.
179     *
180     * @throws DBALException
181     */
182    public function __construct(
183        array $params,
184        Driver $driver,
185        ?Configuration $config = null,
186        ?EventManager $eventManager = null
187    ) {
188        $this->_driver = $driver;
189        $this->params  = $params;
190
191        if (isset($params['pdo'])) {
192            $this->_conn       = $params['pdo'];
193            $this->isConnected = true;
194            unset($this->params['pdo']);
195        }
196
197        if (isset($params['platform'])) {
198            if (! $params['platform'] instanceof Platforms\AbstractPlatform) {
199                throw DBALException::invalidPlatformType($params['platform']);
200            }
201
202            $this->platform = $params['platform'];
203        }
204
205        // Create default config and event manager if none given
206        if (! $config) {
207            $config = new Configuration();
208        }
209
210        if (! $eventManager) {
211            $eventManager = new EventManager();
212        }
213
214        $this->_config       = $config;
215        $this->_eventManager = $eventManager;
216
217        $this->_expr = new Query\Expression\ExpressionBuilder($this);
218
219        $this->autoCommit = $config->getAutoCommit();
220    }
221
222    /**
223     * Gets the parameters used during instantiation.
224     *
225     * @return mixed[]
226     */
227    public function getParams()
228    {
229        return $this->params;
230    }
231
232    /**
233     * Gets the name of the database this Connection is connected to.
234     *
235     * @return string
236     */
237    public function getDatabase()
238    {
239        return $this->_driver->getDatabase($this);
240    }
241
242    /**
243     * Gets the hostname of the currently connected database.
244     *
245     * @deprecated
246     *
247     * @return string|null
248     */
249    public function getHost()
250    {
251        return $this->params['host'] ?? null;
252    }
253
254    /**
255     * Gets the port of the currently connected database.
256     *
257     * @deprecated
258     *
259     * @return mixed
260     */
261    public function getPort()
262    {
263        return $this->params['port'] ?? null;
264    }
265
266    /**
267     * Gets the username used by this connection.
268     *
269     * @deprecated
270     *
271     * @return string|null
272     */
273    public function getUsername()
274    {
275        return $this->params['user'] ?? null;
276    }
277
278    /**
279     * Gets the password used by this connection.
280     *
281     * @deprecated
282     *
283     * @return string|null
284     */
285    public function getPassword()
286    {
287        return $this->params['password'] ?? null;
288    }
289
290    /**
291     * Gets the DBAL driver instance.
292     *
293     * @return Driver
294     */
295    public function getDriver()
296    {
297        return $this->_driver;
298    }
299
300    /**
301     * Gets the Configuration used by the Connection.
302     *
303     * @return Configuration
304     */
305    public function getConfiguration()
306    {
307        return $this->_config;
308    }
309
310    /**
311     * Gets the EventManager used by the Connection.
312     *
313     * @return EventManager
314     */
315    public function getEventManager()
316    {
317        return $this->_eventManager;
318    }
319
320    /**
321     * Gets the DatabasePlatform for the connection.
322     *
323     * @return AbstractPlatform
324     *
325     * @throws DBALException
326     */
327    public function getDatabasePlatform()
328    {
329        if ($this->platform === null) {
330            $this->detectDatabasePlatform();
331        }
332
333        return $this->platform;
334    }
335
336    /**
337     * Gets the ExpressionBuilder for the connection.
338     *
339     * @return ExpressionBuilder
340     */
341    public function getExpressionBuilder()
342    {
343        return $this->_expr;
344    }
345
346    /**
347     * Establishes the connection with the database.
348     *
349     * @return bool TRUE if the connection was successfully established, FALSE if
350     *              the connection is already open.
351     */
352    public function connect()
353    {
354        if ($this->isConnected) {
355            return false;
356        }
357
358        $driverOptions = $this->params['driverOptions'] ?? [];
359        $user          = $this->params['user'] ?? null;
360        $password      = $this->params['password'] ?? null;
361
362        $this->_conn       = $this->_driver->connect($this->params, $user, $password, $driverOptions);
363        $this->isConnected = true;
364
365        $this->transactionNestingLevel = 0;
366
367        if ($this->autoCommit === false) {
368            $this->beginTransaction();
369        }
370
371        if ($this->_eventManager->hasListeners(Events::postConnect)) {
372            $eventArgs = new Event\ConnectionEventArgs($this);
373            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
374        }
375
376        return true;
377    }
378
379    /**
380     * Detects and sets the database platform.
381     *
382     * Evaluates custom platform class and version in order to set the correct platform.
383     *
384     * @throws DBALException If an invalid platform was specified for this connection.
385     */
386    private function detectDatabasePlatform()
387    {
388        $version = $this->getDatabasePlatformVersion();
389
390        if ($version !== null) {
391            assert($this->_driver instanceof VersionAwarePlatformDriver);
392
393            $this->platform = $this->_driver->createDatabasePlatformForVersion($version);
394        } else {
395            $this->platform = $this->_driver->getDatabasePlatform();
396        }
397
398        $this->platform->setEventManager($this->_eventManager);
399    }
400
401    /**
402     * Returns the version of the related platform if applicable.
403     *
404     * Returns null if either the driver is not capable to create version
405     * specific platform instances, no explicit server version was specified
406     * or the underlying driver connection cannot determine the platform
407     * version without having to query it (performance reasons).
408     *
409     * @return string|null
410     *
411     * @throws Exception
412     */
413    private function getDatabasePlatformVersion()
414    {
415        // Driver does not support version specific platforms.
416        if (! $this->_driver instanceof VersionAwarePlatformDriver) {
417            return null;
418        }
419
420        // Explicit platform version requested (supersedes auto-detection).
421        if (isset($this->params['serverVersion'])) {
422            return $this->params['serverVersion'];
423        }
424
425        // If not connected, we need to connect now to determine the platform version.
426        if ($this->_conn === null) {
427            try {
428                $this->connect();
429            } catch (Throwable $originalException) {
430                if (empty($this->params['dbname'])) {
431                    throw $originalException;
432                }
433
434                // The database to connect to might not yet exist.
435                // Retry detection without database name connection parameter.
436                $databaseName           = $this->params['dbname'];
437                $this->params['dbname'] = null;
438
439                try {
440                    $this->connect();
441                } catch (Throwable $fallbackException) {
442                    // Either the platform does not support database-less connections
443                    // or something else went wrong.
444                    // Reset connection parameters and rethrow the original exception.
445                    $this->params['dbname'] = $databaseName;
446
447                    throw $originalException;
448                }
449
450                // Reset connection parameters.
451                $this->params['dbname'] = $databaseName;
452                $serverVersion          = $this->getServerVersion();
453
454                // Close "temporary" connection to allow connecting to the real database again.
455                $this->close();
456
457                return $serverVersion;
458            }
459        }
460
461        return $this->getServerVersion();
462    }
463
464    /**
465     * Returns the database server version if the underlying driver supports it.
466     *
467     * @return string|null
468     */
469    private function getServerVersion()
470    {
471        $connection = $this->getWrappedConnection();
472
473        // Automatic platform version detection.
474        if ($connection instanceof ServerInfoAwareConnection && ! $connection->requiresQueryForServerVersion()) {
475            return $connection->getServerVersion();
476        }
477
478        // Unable to detect platform version.
479        return null;
480    }
481
482    /**
483     * Returns the current auto-commit mode for this connection.
484     *
485     * @see    setAutoCommit
486     *
487     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
488     */
489    public function isAutoCommit()
490    {
491        return $this->autoCommit === true;
492    }
493
494    /**
495     * Sets auto-commit mode for this connection.
496     *
497     * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
498     * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
499     * the method commit or the method rollback. By default, new connections are in auto-commit mode.
500     *
501     * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is
502     * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op.
503     *
504     * @see   isAutoCommit
505     *
506     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
507     */
508    public function setAutoCommit($autoCommit)
509    {
510        $autoCommit = (bool) $autoCommit;
511
512        // Mode not changed, no-op.
513        if ($autoCommit === $this->autoCommit) {
514            return;
515        }
516
517        $this->autoCommit = $autoCommit;
518
519        // Commit all currently active transactions if any when switching auto-commit mode.
520        if ($this->isConnected !== true || $this->transactionNestingLevel === 0) {
521            return;
522        }
523
524        $this->commitAll();
525    }
526
527    /**
528     * Sets the fetch mode.
529     *
530     * @param int $fetchMode
531     *
532     * @return void
533     */
534    public function setFetchMode($fetchMode)
535    {
536        $this->defaultFetchMode = $fetchMode;
537    }
538
539    /**
540     * Prepares and executes an SQL query and returns the first row of the result
541     * as an associative array.
542     *
543     * @param string         $statement The SQL query.
544     * @param mixed[]        $params    The query parameters.
545     * @param int[]|string[] $types     The query parameter types.
546     *
547     * @return mixed[]|false False is returned if no rows are found.
548     *
549     * @throws DBALException
550     */
551    public function fetchAssoc($statement, array $params = [], array $types = [])
552    {
553        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::ASSOCIATIVE);
554    }
555
556    /**
557     * Prepares and executes an SQL query and returns the first row of the result
558     * as a numerically indexed array.
559     *
560     * @param string         $statement The SQL query to be executed.
561     * @param mixed[]        $params    The prepared statement params.
562     * @param int[]|string[] $types     The query parameter types.
563     *
564     * @return mixed[]|false False is returned if no rows are found.
565     */
566    public function fetchArray($statement, array $params = [], array $types = [])
567    {
568        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::NUMERIC);
569    }
570
571    /**
572     * Prepares and executes an SQL query and returns the value of a single column
573     * of the first row of the result.
574     *
575     * @param string         $statement The SQL query to be executed.
576     * @param mixed[]        $params    The prepared statement params.
577     * @param int            $column    The 0-indexed column number to retrieve.
578     * @param int[]|string[] $types     The query parameter types.
579     *
580     * @return mixed|false False is returned if no rows are found.
581     *
582     * @throws DBALException
583     */
584    public function fetchColumn($statement, array $params = [], $column = 0, array $types = [])
585    {
586        return $this->executeQuery($statement, $params, $types)->fetchColumn($column);
587    }
588
589    /**
590     * Whether an actual connection to the database is established.
591     *
592     * @return bool
593     */
594    public function isConnected()
595    {
596        return $this->isConnected;
597    }
598
599    /**
600     * Checks whether a transaction is currently active.
601     *
602     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
603     */
604    public function isTransactionActive()
605    {
606        return $this->transactionNestingLevel > 0;
607    }
608
609    /**
610     * Adds identifier condition to the query components
611     *
612     * @param mixed[]  $identifier Map of key columns to their values
613     * @param string[] $columns    Column names
614     * @param mixed[]  $values     Column values
615     * @param string[] $conditions Key conditions
616     *
617     * @throws DBALException
618     */
619    private function addIdentifierCondition(
620        array $identifier,
621        array &$columns,
622        array &$values,
623        array &$conditions
624    ) : void {
625        $platform = $this->getDatabasePlatform();
626
627        foreach ($identifier as $columnName => $value) {
628            if ($value === null) {
629                $conditions[] = $platform->getIsNullExpression($columnName);
630                continue;
631            }
632
633            $columns[]    = $columnName;
634            $values[]     = $value;
635            $conditions[] = $columnName . ' = ?';
636        }
637    }
638
639    /**
640     * Executes an SQL DELETE statement on a table.
641     *
642     * Table expression and columns are not escaped and are not safe for user-input.
643     *
644     * @param string         $tableExpression The expression of the table on which to delete.
645     * @param mixed[]        $identifier      The deletion criteria. An associative array containing column-value pairs.
646     * @param int[]|string[] $types           The types of identifiers.
647     *
648     * @return int The number of affected rows.
649     *
650     * @throws DBALException
651     * @throws InvalidArgumentException
652     */
653    public function delete($tableExpression, array $identifier, array $types = [])
654    {
655        if (empty($identifier)) {
656            throw InvalidArgumentException::fromEmptyCriteria();
657        }
658
659        $columns = $values = $conditions = [];
660
661        $this->addIdentifierCondition($identifier, $columns, $values, $conditions);
662
663        return $this->executeUpdate(
664            'DELETE FROM ' . $tableExpression . ' WHERE ' . implode(' AND ', $conditions),
665            $values,
666            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
667        );
668    }
669
670    /**
671     * Closes the connection.
672     *
673     * @return void
674     */
675    public function close()
676    {
677        $this->_conn = null;
678
679        $this->isConnected = false;
680    }
681
682    /**
683     * Sets the transaction isolation level.
684     *
685     * @param int $level The level to set.
686     *
687     * @return int
688     */
689    public function setTransactionIsolation($level)
690    {
691        $this->transactionIsolationLevel = $level;
692
693        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
694    }
695
696    /**
697     * Gets the currently active transaction isolation level.
698     *
699     * @return int The current transaction isolation level.
700     */
701    public function getTransactionIsolation()
702    {
703        if ($this->transactionIsolationLevel === null) {
704            $this->transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
705        }
706
707        return $this->transactionIsolationLevel;
708    }
709
710    /**
711     * Executes an SQL UPDATE statement on a table.
712     *
713     * Table expression and columns are not escaped and are not safe for user-input.
714     *
715     * @param string         $tableExpression The expression of the table to update quoted or unquoted.
716     * @param mixed[]        $data            An associative array containing column-value pairs.
717     * @param mixed[]        $identifier      The update criteria. An associative array containing column-value pairs.
718     * @param int[]|string[] $types           Types of the merged $data and $identifier arrays in that order.
719     *
720     * @return int The number of affected rows.
721     *
722     * @throws DBALException
723     */
724    public function update($tableExpression, array $data, array $identifier, array $types = [])
725    {
726        $columns = $values = $conditions = $set = [];
727
728        foreach ($data as $columnName => $value) {
729            $columns[] = $columnName;
730            $values[]  = $value;
731            $set[]     = $columnName . ' = ?';
732        }
733
734        $this->addIdentifierCondition($identifier, $columns, $values, $conditions);
735
736        if (is_string(key($types))) {
737            $types = $this->extractTypeValues($columns, $types);
738        }
739
740        $sql = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
741                . ' WHERE ' . implode(' AND ', $conditions);
742
743        return $this->executeUpdate($sql, $values, $types);
744    }
745
746    /**
747     * Inserts a table row with specified data.
748     *
749     * Table expression and columns are not escaped and are not safe for user-input.
750     *
751     * @param string         $tableExpression The expression of the table to insert data into, quoted or unquoted.
752     * @param mixed[]        $data            An associative array containing column-value pairs.
753     * @param int[]|string[] $types           Types of the inserted data.
754     *
755     * @return int The number of affected rows.
756     *
757     * @throws DBALException
758     */
759    public function insert($tableExpression, array $data, array $types = [])
760    {
761        if (empty($data)) {
762            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' () VALUES ()');
763        }
764
765        $columns = [];
766        $values  = [];
767        $set     = [];
768
769        foreach ($data as $columnName => $value) {
770            $columns[] = $columnName;
771            $values[]  = $value;
772            $set[]     = '?';
773        }
774
775        return $this->executeUpdate(
776            'INSERT INTO ' . $tableExpression . ' (' . implode(', ', $columns) . ')' .
777            ' VALUES (' . implode(', ', $set) . ')',
778            $values,
779            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
780        );
781    }
782
783    /**
784     * Extract ordered type list from an ordered column list and type map.
785     *
786     * @param int[]|string[] $columnList
787     * @param int[]|string[] $types
788     *
789     * @return int[]|string[]
790     */
791    private function extractTypeValues(array $columnList, array $types)
792    {
793        $typeValues = [];
794
795        foreach ($columnList as $columnIndex => $columnName) {
796            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
797        }
798
799        return $typeValues;
800    }
801
802    /**
803     * Quotes a string so it can be safely used as a table or column name, even if
804     * it is a reserved name.
805     *
806     * Delimiting style depends on the underlying database platform that is being used.
807     *
808     * NOTE: Just because you CAN use quoted identifiers does not mean
809     * you SHOULD use them. In general, they end up causing way more
810     * problems than they solve.
811     *
812     * @param string $str The name to be quoted.
813     *
814     * @return string The quoted name.
815     */
816    public function quoteIdentifier($str)
817    {
818        return $this->getDatabasePlatform()->quoteIdentifier($str);
819    }
820
821    /**
822     * {@inheritDoc}
823     */
824    public function quote($input, $type = null)
825    {
826        $connection = $this->getWrappedConnection();
827
828        [$value, $bindingType] = $this->getBindingInfo($input, $type);
829
830        return $connection->quote($value, $bindingType);
831    }
832
833    /**
834     * Prepares and executes an SQL query and returns the result as an associative array.
835     *
836     * @param string         $sql    The SQL query.
837     * @param mixed[]        $params The query parameters.
838     * @param int[]|string[] $types  The query parameter types.
839     *
840     * @return mixed[]
841     */
842    public function fetchAll($sql, array $params = [], $types = [])
843    {
844        return $this->executeQuery($sql, $params, $types)->fetchAll();
845    }
846
847    /**
848     * Prepares an SQL statement.
849     *
850     * @param string $statement The SQL statement to prepare.
851     *
852     * @return DriverStatement The prepared statement.
853     *
854     * @throws DBALException
855     */
856    public function prepare($statement)
857    {
858        try {
859            $stmt = new Statement($statement, $this);
860        } catch (Throwable $ex) {
861            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
862        }
863
864        $stmt->setFetchMode($this->defaultFetchMode);
865
866        return $stmt;
867    }
868
869    /**
870     * Executes an, optionally parametrized, SQL query.
871     *
872     * If the query is parametrized, a prepared statement is used.
873     * If an SQLLogger is configured, the execution is logged.
874     *
875     * @param string                 $query  The SQL query to execute.
876     * @param mixed[]                $params The parameters to bind to the query, if any.
877     * @param int[]|string[]         $types  The types the previous parameters are in.
878     * @param QueryCacheProfile|null $qcp    The query cache profile, optional.
879     *
880     * @return ResultStatement The executed statement.
881     *
882     * @throws DBALException
883     */
884    public function executeQuery($query, array $params = [], $types = [], ?QueryCacheProfile $qcp = null)
885    {
886        if ($qcp !== null) {
887            return $this->executeCacheQuery($query, $params, $types, $qcp);
888        }
889
890        $connection = $this->getWrappedConnection();
891
892        $logger = $this->_config->getSQLLogger();
893        if ($logger) {
894            $logger->startQuery($query, $params, $types);
895        }
896
897        try {
898            if ($params) {
899                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
900
901                $stmt = $connection->prepare($query);
902                if ($types) {
903                    $this->_bindTypedValues($stmt, $params, $types);
904                    $stmt->execute();
905                } else {
906                    $stmt->execute($params);
907                }
908            } else {
909                $stmt = $connection->query($query);
910            }
911        } catch (Throwable $ex) {
912            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
913        }
914
915        $stmt->setFetchMode($this->defaultFetchMode);
916
917        if ($logger) {
918            $logger->stopQuery();
919        }
920
921        return $stmt;
922    }
923
924    /**
925     * Executes a caching query.
926     *
927     * @param string            $query  The SQL query to execute.
928     * @param mixed[]           $params The parameters to bind to the query, if any.
929     * @param int[]|string[]    $types  The types the previous parameters are in.
930     * @param QueryCacheProfile $qcp    The query cache profile.
931     *
932     * @return ResultStatement
933     *
934     * @throws CacheException
935     */
936    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp)
937    {
938        $resultCache = $qcp->getResultCacheDriver() ?? $this->_config->getResultCacheImpl();
939
940        if ($resultCache === null) {
941            throw CacheException::noResultDriverConfigured();
942        }
943
944        $connectionParams = $this->getParams();
945        unset($connectionParams['platform']);
946
947        [$cacheKey, $realKey] = $qcp->generateCacheKeys($query, $params, $types, $connectionParams);
948
949        // fetch the row pointers entry
950        $data = $resultCache->fetch($cacheKey);
951
952        if ($data !== false) {
953            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
954            if (isset($data[$realKey])) {
955                $stmt = new ArrayStatement($data[$realKey]);
956            } elseif (array_key_exists($realKey, $data)) {
957                $stmt = new ArrayStatement([]);
958            }
959        }
960
961        if (! isset($stmt)) {
962            $stmt = new ResultCacheStatement($this->executeQuery($query, $params, $types), $resultCache, $cacheKey, $realKey, $qcp->getLifetime());
963        }
964
965        $stmt->setFetchMode($this->defaultFetchMode);
966
967        return $stmt;
968    }
969
970    /**
971     * Executes an, optionally parametrized, SQL query and returns the result,
972     * applying a given projection/transformation function on each row of the result.
973     *
974     * @param string  $query    The SQL query to execute.
975     * @param mixed[] $params   The parameters, if any.
976     * @param Closure $function The transformation function that is applied on each row.
977     *                           The function receives a single parameter, an array, that
978     *                           represents a row of the result set.
979     *
980     * @return mixed[] The projected result of the query.
981     */
982    public function project($query, array $params, Closure $function)
983    {
984        $result = [];
985        $stmt   = $this->executeQuery($query, $params);
986
987        while ($row = $stmt->fetch()) {
988            $result[] = $function($row);
989        }
990
991        $stmt->closeCursor();
992
993        return $result;
994    }
995
996    /**
997     * Executes an SQL statement, returning a result set as a Statement object.
998     *
999     * @return \Doctrine\DBAL\Driver\Statement
1000     *
1001     * @throws DBALException
1002     */
1003    public function query()
1004    {
1005        $connection = $this->getWrappedConnection();
1006
1007        $args = func_get_args();
1008
1009        $logger = $this->_config->getSQLLogger();
1010        if ($logger) {
1011            $logger->startQuery($args[0]);
1012        }
1013
1014        try {
1015            $statement = $connection->query(...$args);
1016        } catch (Throwable $ex) {
1017            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $args[0]);
1018        }
1019
1020        $statement->setFetchMode($this->defaultFetchMode);
1021
1022        if ($logger) {
1023            $logger->stopQuery();
1024        }
1025
1026        return $statement;
1027    }
1028
1029    /**
1030     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
1031     * and returns the number of affected rows.
1032     *
1033     * This method supports PDO binding types as well as DBAL mapping types.
1034     *
1035     * @param string         $query  The SQL query.
1036     * @param mixed[]        $params The query parameters.
1037     * @param int[]|string[] $types  The parameter types.
1038     *
1039     * @return int The number of affected rows.
1040     *
1041     * @throws DBALException
1042     */
1043    public function executeUpdate($query, array $params = [], array $types = [])
1044    {
1045        $connection = $this->getWrappedConnection();
1046
1047        $logger = $this->_config->getSQLLogger();
1048        if ($logger) {
1049            $logger->startQuery($query, $params, $types);
1050        }
1051
1052        try {
1053            if ($params) {
1054                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
1055
1056                $stmt = $connection->prepare($query);
1057
1058                if ($types) {
1059                    $this->_bindTypedValues($stmt, $params, $types);
1060                    $stmt->execute();
1061                } else {
1062                    $stmt->execute($params);
1063                }
1064                $result = $stmt->rowCount();
1065            } else {
1066                $result = $connection->exec($query);
1067            }
1068        } catch (Throwable $ex) {
1069            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
1070        }
1071
1072        if ($logger) {
1073            $logger->stopQuery();
1074        }
1075
1076        return $result;
1077    }
1078
1079    /**
1080     * Executes an SQL statement and return the number of affected rows.
1081     *
1082     * @param string $statement
1083     *
1084     * @return int The number of affected rows.
1085     *
1086     * @throws DBALException
1087     */
1088    public function exec($statement)
1089    {
1090        $connection = $this->getWrappedConnection();
1091
1092        $logger = $this->_config->getSQLLogger();
1093        if ($logger) {
1094            $logger->startQuery($statement);
1095        }
1096
1097        try {
1098            $result = $connection->exec($statement);
1099        } catch (Throwable $ex) {
1100            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1101        }
1102
1103        if ($logger) {
1104            $logger->stopQuery();
1105        }
1106
1107        return $result;
1108    }
1109
1110    /**
1111     * Returns the current transaction nesting level.
1112     *
1113     * @return int The nesting level. A value of 0 means there's no active transaction.
1114     */
1115    public function getTransactionNestingLevel()
1116    {
1117        return $this->transactionNestingLevel;
1118    }
1119
1120    /**
1121     * Fetches the SQLSTATE associated with the last database operation.
1122     *
1123     * @return string|null The last error code.
1124     */
1125    public function errorCode()
1126    {
1127        return $this->getWrappedConnection()->errorCode();
1128    }
1129
1130    /**
1131     * {@inheritDoc}
1132     */
1133    public function errorInfo()
1134    {
1135        return $this->getWrappedConnection()->errorInfo();
1136    }
1137
1138    /**
1139     * Returns the ID of the last inserted row, or the last value from a sequence object,
1140     * depending on the underlying driver.
1141     *
1142     * Note: This method may not return a meaningful or consistent result across different drivers,
1143     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
1144     * columns or sequences.
1145     *
1146     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
1147     *
1148     * @return string A string representation of the last inserted ID.
1149     */
1150    public function lastInsertId($seqName = null)
1151    {
1152        return $this->getWrappedConnection()->lastInsertId($seqName);
1153    }
1154
1155    /**
1156     * Executes a function in a transaction.
1157     *
1158     * The function gets passed this Connection instance as an (optional) parameter.
1159     *
1160     * If an exception occurs during execution of the function or transaction commit,
1161     * the transaction is rolled back and the exception re-thrown.
1162     *
1163     * @param Closure $func The function to execute transactionally.
1164     *
1165     * @return mixed The value returned by $func
1166     *
1167     * @throws Exception
1168     * @throws Throwable
1169     */
1170    public function transactional(Closure $func)
1171    {
1172        $this->beginTransaction();
1173        try {
1174            $res = $func($this);
1175            $this->commit();
1176
1177            return $res;
1178        } catch (Exception $e) {
1179            $this->rollBack();
1180            throw $e;
1181        } catch (Throwable $e) {
1182            $this->rollBack();
1183            throw $e;
1184        }
1185    }
1186
1187    /**
1188     * Sets if nested transactions should use savepoints.
1189     *
1190     * @param bool $nestTransactionsWithSavepoints
1191     *
1192     * @return void
1193     *
1194     * @throws ConnectionException
1195     */
1196    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
1197    {
1198        if ($this->transactionNestingLevel > 0) {
1199            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
1200        }
1201
1202        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1203            throw ConnectionException::savepointsNotSupported();
1204        }
1205
1206        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1207    }
1208
1209    /**
1210     * Gets if nested transactions should use savepoints.
1211     *
1212     * @return bool
1213     */
1214    public function getNestTransactionsWithSavepoints()
1215    {
1216        return $this->nestTransactionsWithSavepoints;
1217    }
1218
1219    /**
1220     * Returns the savepoint name to use for nested transactions are false if they are not supported
1221     * "savepointFormat" parameter is not set
1222     *
1223     * @return mixed A string with the savepoint name or false.
1224     */
1225    protected function _getNestedTransactionSavePointName()
1226    {
1227        return 'DOCTRINE2_SAVEPOINT_' . $this->transactionNestingLevel;
1228    }
1229
1230    /**
1231     * {@inheritDoc}
1232     */
1233    public function beginTransaction()
1234    {
1235        $connection = $this->getWrappedConnection();
1236
1237        ++$this->transactionNestingLevel;
1238
1239        $logger = $this->_config->getSQLLogger();
1240
1241        if ($this->transactionNestingLevel === 1) {
1242            if ($logger) {
1243                $logger->startQuery('"START TRANSACTION"');
1244            }
1245
1246            $connection->beginTransaction();
1247
1248            if ($logger) {
1249                $logger->stopQuery();
1250            }
1251        } elseif ($this->nestTransactionsWithSavepoints) {
1252            if ($logger) {
1253                $logger->startQuery('"SAVEPOINT"');
1254            }
1255            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1256            if ($logger) {
1257                $logger->stopQuery();
1258            }
1259        }
1260
1261        return true;
1262    }
1263
1264    /**
1265     * {@inheritDoc}
1266     *
1267     * @throws ConnectionException If the commit failed due to no active transaction or
1268     *                                            because the transaction was marked for rollback only.
1269     */
1270    public function commit()
1271    {
1272        if ($this->transactionNestingLevel === 0) {
1273            throw ConnectionException::noActiveTransaction();
1274        }
1275        if ($this->isRollbackOnly) {
1276            throw ConnectionException::commitFailedRollbackOnly();
1277        }
1278
1279        $result = true;
1280
1281        $connection = $this->getWrappedConnection();
1282
1283        $logger = $this->_config->getSQLLogger();
1284
1285        if ($this->transactionNestingLevel === 1) {
1286            if ($logger) {
1287                $logger->startQuery('"COMMIT"');
1288            }
1289
1290            $result = $connection->commit();
1291
1292            if ($logger) {
1293                $logger->stopQuery();
1294            }
1295        } elseif ($this->nestTransactionsWithSavepoints) {
1296            if ($logger) {
1297                $logger->startQuery('"RELEASE SAVEPOINT"');
1298            }
1299            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1300            if ($logger) {
1301                $logger->stopQuery();
1302            }
1303        }
1304
1305        --$this->transactionNestingLevel;
1306
1307        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1308            return $result;
1309        }
1310
1311        $this->beginTransaction();
1312
1313        return $result;
1314    }
1315
1316    /**
1317     * Commits all current nesting transactions.
1318     */
1319    private function commitAll()
1320    {
1321        while ($this->transactionNestingLevel !== 0) {
1322            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
1323                // When in no auto-commit mode, the last nesting commit immediately starts a new transaction.
1324                // Therefore we need to do the final commit here and then leave to avoid an infinite loop.
1325                $this->commit();
1326
1327                return;
1328            }
1329
1330            $this->commit();
1331        }
1332    }
1333
1334    /**
1335     * Cancels any database changes done during the current transaction.
1336     *
1337     * @throws ConnectionException If the rollback operation failed.
1338     */
1339    public function rollBack()
1340    {
1341        if ($this->transactionNestingLevel === 0) {
1342            throw ConnectionException::noActiveTransaction();
1343        }
1344
1345        $connection = $this->getWrappedConnection();
1346
1347        $logger = $this->_config->getSQLLogger();
1348
1349        if ($this->transactionNestingLevel === 1) {
1350            if ($logger) {
1351                $logger->startQuery('"ROLLBACK"');
1352            }
1353            $this->transactionNestingLevel = 0;
1354            $connection->rollBack();
1355            $this->isRollbackOnly = false;
1356            if ($logger) {
1357                $logger->stopQuery();
1358            }
1359
1360            if ($this->autoCommit === false) {
1361                $this->beginTransaction();
1362            }
1363        } elseif ($this->nestTransactionsWithSavepoints) {
1364            if ($logger) {
1365                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
1366            }
1367            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
1368            --$this->transactionNestingLevel;
1369            if ($logger) {
1370                $logger->stopQuery();
1371            }
1372        } else {
1373            $this->isRollbackOnly = true;
1374            --$this->transactionNestingLevel;
1375        }
1376    }
1377
1378    /**
1379     * Creates a new savepoint.
1380     *
1381     * @param string $savepoint The name of the savepoint to create.
1382     *
1383     * @return void
1384     *
1385     * @throws ConnectionException
1386     */
1387    public function createSavepoint($savepoint)
1388    {
1389        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1390            throw ConnectionException::savepointsNotSupported();
1391        }
1392
1393        $this->getWrappedConnection()->exec($this->platform->createSavePoint($savepoint));
1394    }
1395
1396    /**
1397     * Releases the given savepoint.
1398     *
1399     * @param string $savepoint The name of the savepoint to release.
1400     *
1401     * @return void
1402     *
1403     * @throws ConnectionException
1404     */
1405    public function releaseSavepoint($savepoint)
1406    {
1407        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1408            throw ConnectionException::savepointsNotSupported();
1409        }
1410
1411        if (! $this->platform->supportsReleaseSavepoints()) {
1412            return;
1413        }
1414
1415        $this->getWrappedConnection()->exec($this->platform->releaseSavePoint($savepoint));
1416    }
1417
1418    /**
1419     * Rolls back to the given savepoint.
1420     *
1421     * @param string $savepoint The name of the savepoint to rollback to.
1422     *
1423     * @return void
1424     *
1425     * @throws ConnectionException
1426     */
1427    public function rollbackSavepoint($savepoint)
1428    {
1429        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1430            throw ConnectionException::savepointsNotSupported();
1431        }
1432
1433        $this->getWrappedConnection()->exec($this->platform->rollbackSavePoint($savepoint));
1434    }
1435
1436    /**
1437     * Gets the wrapped driver connection.
1438     *
1439     * @return DriverConnection
1440     */
1441    public function getWrappedConnection()
1442    {
1443        $this->connect();
1444
1445        return $this->_conn;
1446    }
1447
1448    /**
1449     * Gets the SchemaManager that can be used to inspect or change the
1450     * database schema through the connection.
1451     *
1452     * @return AbstractSchemaManager
1453     */
1454    public function getSchemaManager()
1455    {
1456        if ($this->_schemaManager === null) {
1457            $this->_schemaManager = $this->_driver->getSchemaManager($this);
1458        }
1459
1460        return $this->_schemaManager;
1461    }
1462
1463    /**
1464     * Marks the current transaction so that the only possible
1465     * outcome for the transaction to be rolled back.
1466     *
1467     * @return void
1468     *
1469     * @throws ConnectionException If no transaction is active.
1470     */
1471    public function setRollbackOnly()
1472    {
1473        if ($this->transactionNestingLevel === 0) {
1474            throw ConnectionException::noActiveTransaction();
1475        }
1476        $this->isRollbackOnly = true;
1477    }
1478
1479    /**
1480     * Checks whether the current transaction is marked for rollback only.
1481     *
1482     * @return bool
1483     *
1484     * @throws ConnectionException If no transaction is active.
1485     */
1486    public function isRollbackOnly()
1487    {
1488        if ($this->transactionNestingLevel === 0) {
1489            throw ConnectionException::noActiveTransaction();
1490        }
1491
1492        return $this->isRollbackOnly;
1493    }
1494
1495    /**
1496     * Converts a given value to its database representation according to the conversion
1497     * rules of a specific DBAL mapping type.
1498     *
1499     * @param mixed  $value The value to convert.
1500     * @param string $type  The name of the DBAL mapping type.
1501     *
1502     * @return mixed The converted value.
1503     */
1504    public function convertToDatabaseValue($value, $type)
1505    {
1506        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1507    }
1508
1509    /**
1510     * Converts a given value to its PHP representation according to the conversion
1511     * rules of a specific DBAL mapping type.
1512     *
1513     * @param mixed  $value The value to convert.
1514     * @param string $type  The name of the DBAL mapping type.
1515     *
1516     * @return mixed The converted type.
1517     */
1518    public function convertToPHPValue($value, $type)
1519    {
1520        return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform());
1521    }
1522
1523    /**
1524     * Binds a set of parameters, some or all of which are typed with a PDO binding type
1525     * or DBAL mapping type, to a given statement.
1526     *
1527     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
1528     *           raw PDOStatement instances.
1529     *
1530     * @param \Doctrine\DBAL\Driver\Statement $stmt   The statement to bind the values to.
1531     * @param mixed[]                         $params The map/list of named/positional parameters.
1532     * @param int[]|string[]                  $types  The parameter types (PDO binding types or DBAL mapping types).
1533     *
1534     * @return void
1535     */
1536    private function _bindTypedValues($stmt, array $params, array $types)
1537    {
1538        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
1539        if (is_int(key($params))) {
1540            // Positional parameters
1541            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1542            $bindIndex  = 1;
1543            foreach ($params as $value) {
1544                $typeIndex = $bindIndex + $typeOffset;
1545                if (isset($types[$typeIndex])) {
1546                    $type                  = $types[$typeIndex];
1547                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1548                    $stmt->bindValue($bindIndex, $value, $bindingType);
1549                } else {
1550                    $stmt->bindValue($bindIndex, $value);
1551                }
1552                ++$bindIndex;
1553            }
1554        } else {
1555            // Named parameters
1556            foreach ($params as $name => $value) {
1557                if (isset($types[$name])) {
1558                    $type                  = $types[$name];
1559                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1560                    $stmt->bindValue($name, $value, $bindingType);
1561                } else {
1562                    $stmt->bindValue($name, $value);
1563                }
1564            }
1565        }
1566    }
1567
1568    /**
1569     * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type.
1570     *
1571     * @param mixed           $value The value to bind.
1572     * @param int|string|null $type  The type to bind (PDO or DBAL).
1573     *
1574     * @return mixed[] [0] => the (escaped) value, [1] => the binding type.
1575     */
1576    private function getBindingInfo($value, $type)
1577    {
1578        if (is_string($type)) {
1579            $type = Type::getType($type);
1580        }
1581        if ($type instanceof Type) {
1582            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1583            $bindingType = $type->getBindingType();
1584        } else {
1585            $bindingType = $type;
1586        }
1587
1588        return [$value, $bindingType];
1589    }
1590
1591    /**
1592     * Resolves the parameters to a format which can be displayed.
1593     *
1594     * @internal This is a purely internal method. If you rely on this method, you are advised to
1595     *           copy/paste the code as this method may change, or be removed without prior notice.
1596     *
1597     * @param mixed[]        $params
1598     * @param int[]|string[] $types
1599     *
1600     * @return mixed[]
1601     */
1602    public function resolveParams(array $params, array $types)
1603    {
1604        $resolvedParams = [];
1605
1606        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
1607        if (is_int(key($params))) {
1608            // Positional parameters
1609            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1610            $bindIndex  = 1;
1611            foreach ($params as $value) {
1612                $typeIndex = $bindIndex + $typeOffset;
1613                if (isset($types[$typeIndex])) {
1614                    $type                       = $types[$typeIndex];
1615                    [$value]                    = $this->getBindingInfo($value, $type);
1616                    $resolvedParams[$bindIndex] = $value;
1617                } else {
1618                    $resolvedParams[$bindIndex] = $value;
1619                }
1620                ++$bindIndex;
1621            }
1622        } else {
1623            // Named parameters
1624            foreach ($params as $name => $value) {
1625                if (isset($types[$name])) {
1626                    $type                  = $types[$name];
1627                    [$value]               = $this->getBindingInfo($value, $type);
1628                    $resolvedParams[$name] = $value;
1629                } else {
1630                    $resolvedParams[$name] = $value;
1631                }
1632            }
1633        }
1634
1635        return $resolvedParams;
1636    }
1637
1638    /**
1639     * Creates a new instance of a SQL query builder.
1640     *
1641     * @return QueryBuilder
1642     */
1643    public function createQueryBuilder()
1644    {
1645        return new Query\QueryBuilder($this);
1646    }
1647
1648    /**
1649     * Ping the server
1650     *
1651     * When the server is not available the method returns FALSE.
1652     * It is responsibility of the developer to handle this case
1653     * and abort the request or reconnect manually:
1654     *
1655     * @return bool
1656     *
1657     * @example
1658     *
1659     *   if ($conn->ping() === false) {
1660     *      $conn->close();
1661     *      $conn->connect();
1662     *   }
1663     *
1664     * It is undefined if the underlying driver attempts to reconnect
1665     * or disconnect when the connection is not available anymore
1666     * as long it returns TRUE when a reconnect succeeded and
1667     * FALSE when the connection was dropped.
1668     */
1669    public function ping()
1670    {
1671        $connection = $this->getWrappedConnection();
1672
1673        if ($connection instanceof PingableConnection) {
1674            return $connection->ping();
1675        }
1676
1677        try {
1678            $this->query($this->getDatabasePlatform()->getDummySelectSQL());
1679
1680            return true;
1681        } catch (DBALException $e) {
1682            return false;
1683        }
1684    }
1685}
1686