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\Exception\InvalidArgumentException;
16use Doctrine\DBAL\Platforms\AbstractPlatform;
17use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
18use Doctrine\DBAL\Query\QueryBuilder;
19use Doctrine\DBAL\Schema\AbstractSchemaManager;
20use Doctrine\DBAL\Types\Type;
21use Exception;
22use Throwable;
23
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(): void
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     * @return void
509     */
510    public function setAutoCommit($autoCommit)
511    {
512        $autoCommit = (bool) $autoCommit;
513
514        // Mode not changed, no-op.
515        if ($autoCommit === $this->autoCommit) {
516            return;
517        }
518
519        $this->autoCommit = $autoCommit;
520
521        // Commit all currently active transactions if any when switching auto-commit mode.
522        if ($this->isConnected !== true || $this->transactionNestingLevel === 0) {
523            return;
524        }
525
526        $this->commitAll();
527    }
528
529    /**
530     * Sets the fetch mode.
531     *
532     * @param int $fetchMode
533     *
534     * @return void
535     */
536    public function setFetchMode($fetchMode)
537    {
538        $this->defaultFetchMode = $fetchMode;
539    }
540
541    /**
542     * Prepares and executes an SQL query and returns the first row of the result
543     * as an associative array.
544     *
545     * @param string         $sql    The query SQL
546     * @param mixed[]        $params The query parameters
547     * @param int[]|string[] $types  The query parameter types
548     *
549     * @return mixed[]|false False is returned if no rows are found.
550     *
551     * @throws DBALException
552     */
553    public function fetchAssoc($sql, array $params = [], array $types = [])
554    {
555        return $this->executeQuery($sql, $params, $types)->fetch(FetchMode::ASSOCIATIVE);
556    }
557
558    /**
559     * Prepares and executes an SQL query and returns the first row of the result
560     * as a numerically indexed array.
561     *
562     * @param string         $sql    The query SQL
563     * @param mixed[]        $params The query parameters
564     * @param int[]|string[] $types  The query parameter types
565     *
566     * @return mixed[]|false False is returned if no rows are found.
567     */
568    public function fetchArray($sql, array $params = [], array $types = [])
569    {
570        return $this->executeQuery($sql, $params, $types)->fetch(FetchMode::NUMERIC);
571    }
572
573    /**
574     * Prepares and executes an SQL query and returns the value of a single column
575     * of the first row of the result.
576     *
577     * @param string         $sql    The query SQL
578     * @param mixed[]        $params The query parameters
579     * @param int            $column The 0-indexed column number to retrieve
580     * @param int[]|string[] $types  The query parameter types
581     *
582     * @return mixed|false False is returned if no rows are found.
583     *
584     * @throws DBALException
585     */
586    public function fetchColumn($sql, array $params = [], $column = 0, array $types = [])
587    {
588        return $this->executeQuery($sql, $params, $types)->fetchColumn($column);
589    }
590
591    /**
592     * Whether an actual connection to the database is established.
593     *
594     * @return bool
595     */
596    public function isConnected()
597    {
598        return $this->isConnected;
599    }
600
601    /**
602     * Checks whether a transaction is currently active.
603     *
604     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
605     */
606    public function isTransactionActive()
607    {
608        return $this->transactionNestingLevel > 0;
609    }
610
611    /**
612     * Adds identifier condition to the query components
613     *
614     * @param mixed[]  $identifier Map of key columns to their values
615     * @param string[] $columns    Column names
616     * @param mixed[]  $values     Column values
617     * @param string[] $conditions Key conditions
618     *
619     * @throws DBALException
620     */
621    private function addIdentifierCondition(
622        array $identifier,
623        array &$columns,
624        array &$values,
625        array &$conditions
626    ): void {
627        $platform = $this->getDatabasePlatform();
628
629        foreach ($identifier as $columnName => $value) {
630            if ($value === null) {
631                $conditions[] = $platform->getIsNullExpression($columnName);
632                continue;
633            }
634
635            $columns[]    = $columnName;
636            $values[]     = $value;
637            $conditions[] = $columnName . ' = ?';
638        }
639    }
640
641    /**
642     * Executes an SQL DELETE statement on a table.
643     *
644     * Table expression and columns are not escaped and are not safe for user-input.
645     *
646     * @param string         $table      The expression of the table on which to delete.
647     * @param mixed[]        $identifier The deletion criteria. An associative array containing column-value pairs.
648     * @param int[]|string[] $types      The types of identifiers.
649     *
650     * @return int The number of affected rows.
651     *
652     * @throws DBALException
653     * @throws InvalidArgumentException
654     */
655    public function delete($table, array $identifier, array $types = [])
656    {
657        if (empty($identifier)) {
658            throw InvalidArgumentException::fromEmptyCriteria();
659        }
660
661        $columns = $values = $conditions = [];
662
663        $this->addIdentifierCondition($identifier, $columns, $values, $conditions);
664
665        return $this->executeUpdate(
666            'DELETE FROM ' . $table . ' WHERE ' . implode(' AND ', $conditions),
667            $values,
668            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
669        );
670    }
671
672    /**
673     * Closes the connection.
674     *
675     * @return void
676     */
677    public function close()
678    {
679        $this->_conn = null;
680
681        $this->isConnected = false;
682    }
683
684    /**
685     * Sets the transaction isolation level.
686     *
687     * @param int $level The level to set.
688     *
689     * @return int
690     */
691    public function setTransactionIsolation($level)
692    {
693        $this->transactionIsolationLevel = $level;
694
695        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
696    }
697
698    /**
699     * Gets the currently active transaction isolation level.
700     *
701     * @return int The current transaction isolation level.
702     */
703    public function getTransactionIsolation()
704    {
705        if ($this->transactionIsolationLevel === null) {
706            $this->transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
707        }
708
709        return $this->transactionIsolationLevel;
710    }
711
712    /**
713     * Executes an SQL UPDATE statement on a table.
714     *
715     * Table expression and columns are not escaped and are not safe for user-input.
716     *
717     * @param string         $table      The expression of the table to update quoted or unquoted.
718     * @param mixed[]        $data       An associative array containing column-value pairs.
719     * @param mixed[]        $identifier The update criteria. An associative array containing column-value pairs.
720     * @param int[]|string[] $types      Types of the merged $data and $identifier arrays in that order.
721     *
722     * @return int The number of affected rows.
723     *
724     * @throws DBALException
725     */
726    public function update($table, array $data, array $identifier, array $types = [])
727    {
728        $columns = $values = $conditions = $set = [];
729
730        foreach ($data as $columnName => $value) {
731            $columns[] = $columnName;
732            $values[]  = $value;
733            $set[]     = $columnName . ' = ?';
734        }
735
736        $this->addIdentifierCondition($identifier, $columns, $values, $conditions);
737
738        if (is_string(key($types))) {
739            $types = $this->extractTypeValues($columns, $types);
740        }
741
742        $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $set)
743                . ' WHERE ' . implode(' AND ', $conditions);
744
745        return $this->executeUpdate($sql, $values, $types);
746    }
747
748    /**
749     * Inserts a table row with specified data.
750     *
751     * Table expression and columns are not escaped and are not safe for user-input.
752     *
753     * @param string         $table The expression of the table to insert data into, quoted or unquoted.
754     * @param mixed[]        $data  An associative array containing column-value pairs.
755     * @param int[]|string[] $types Types of the inserted data.
756     *
757     * @return int The number of affected rows.
758     *
759     * @throws DBALException
760     */
761    public function insert($table, array $data, array $types = [])
762    {
763        if (empty($data)) {
764            return $this->executeUpdate('INSERT INTO ' . $table . ' () VALUES ()');
765        }
766
767        $columns = [];
768        $values  = [];
769        $set     = [];
770
771        foreach ($data as $columnName => $value) {
772            $columns[] = $columnName;
773            $values[]  = $value;
774            $set[]     = '?';
775        }
776
777        return $this->executeUpdate(
778            'INSERT INTO ' . $table . ' (' . implode(', ', $columns) . ')' .
779            ' VALUES (' . implode(', ', $set) . ')',
780            $values,
781            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
782        );
783    }
784
785    /**
786     * Extract ordered type list from an ordered column list and type map.
787     *
788     * @param int[]|string[] $columnList
789     * @param int[]|string[] $types
790     *
791     * @return int[]|string[]
792     */
793    private function extractTypeValues(array $columnList, array $types)
794    {
795        $typeValues = [];
796
797        foreach ($columnList as $columnIndex => $columnName) {
798            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
799        }
800
801        return $typeValues;
802    }
803
804    /**
805     * Quotes a string so it can be safely used as a table or column name, even if
806     * it is a reserved name.
807     *
808     * Delimiting style depends on the underlying database platform that is being used.
809     *
810     * NOTE: Just because you CAN use quoted identifiers does not mean
811     * you SHOULD use them. In general, they end up causing way more
812     * problems than they solve.
813     *
814     * @param string $str The name to be quoted.
815     *
816     * @return string The quoted name.
817     */
818    public function quoteIdentifier($str)
819    {
820        return $this->getDatabasePlatform()->quoteIdentifier($str);
821    }
822
823    /**
824     * {@inheritDoc}
825     */
826    public function quote($value, $type = ParameterType::STRING)
827    {
828        $connection = $this->getWrappedConnection();
829
830        [$value, $bindingType] = $this->getBindingInfo($value, $type);
831
832        return $connection->quote($value, $bindingType);
833    }
834
835    /**
836     * Prepares and executes an SQL query and returns the result as an associative array.
837     *
838     * @param string         $sql    The SQL query.
839     * @param mixed[]        $params The query parameters.
840     * @param int[]|string[] $types  The query parameter types.
841     *
842     * @return mixed[]
843     */
844    public function fetchAll($sql, array $params = [], $types = [])
845    {
846        return $this->executeQuery($sql, $params, $types)->fetchAll();
847    }
848
849    /**
850     * Prepares an SQL statement.
851     *
852     * @param string $sql The SQL statement to prepare.
853     *
854     * @return Statement The prepared statement.
855     *
856     * @throws DBALException
857     */
858    public function prepare($sql)
859    {
860        try {
861            $stmt = new Statement($sql, $this);
862        } catch (Throwable $ex) {
863            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
864        }
865
866        $stmt->setFetchMode($this->defaultFetchMode);
867
868        return $stmt;
869    }
870
871    /**
872     * Executes an, optionally parametrized, SQL query.
873     *
874     * If the query is parametrized, a prepared statement is used.
875     * If an SQLLogger is configured, the execution is logged.
876     *
877     * @param string                 $sql    The SQL query to execute.
878     * @param mixed[]                $params The parameters to bind to the query, if any.
879     * @param int[]|string[]         $types  The types the previous parameters are in.
880     * @param QueryCacheProfile|null $qcp    The query cache profile, optional.
881     *
882     * @return ResultStatement The executed statement.
883     *
884     * @throws DBALException
885     */
886    public function executeQuery($sql, array $params = [], $types = [], ?QueryCacheProfile $qcp = null)
887    {
888        if ($qcp !== null) {
889            return $this->executeCacheQuery($sql, $params, $types, $qcp);
890        }
891
892        $connection = $this->getWrappedConnection();
893
894        $logger = $this->_config->getSQLLogger();
895        if ($logger) {
896            $logger->startQuery($sql, $params, $types);
897        }
898
899        try {
900            if ($params) {
901                [$sql, $params, $types] = SQLParserUtils::expandListParameters($sql, $params, $types);
902
903                $stmt = $connection->prepare($sql);
904                if ($types) {
905                    $this->_bindTypedValues($stmt, $params, $types);
906                    $stmt->execute();
907                } else {
908                    $stmt->execute($params);
909                }
910            } else {
911                $stmt = $connection->query($sql);
912            }
913        } catch (Throwable $ex) {
914            throw DBALException::driverExceptionDuringQuery(
915                $this->_driver,
916                $ex,
917                $sql,
918                $this->resolveParams($params, $types)
919            );
920        }
921
922        $stmt->setFetchMode($this->defaultFetchMode);
923
924        if ($logger) {
925            $logger->stopQuery();
926        }
927
928        return $stmt;
929    }
930
931    /**
932     * Executes a caching query.
933     *
934     * @param string            $sql    The SQL query to execute.
935     * @param mixed[]           $params The parameters to bind to the query, if any.
936     * @param int[]|string[]    $types  The types the previous parameters are in.
937     * @param QueryCacheProfile $qcp    The query cache profile.
938     *
939     * @return ResultStatement
940     *
941     * @throws CacheException
942     */
943    public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp)
944    {
945        $resultCache = $qcp->getResultCacheDriver() ?? $this->_config->getResultCacheImpl();
946
947        if ($resultCache === null) {
948            throw CacheException::noResultDriverConfigured();
949        }
950
951        $connectionParams = $this->getParams();
952        unset($connectionParams['platform']);
953
954        [$cacheKey, $realKey] = $qcp->generateCacheKeys($sql, $params, $types, $connectionParams);
955
956        // fetch the row pointers entry
957        $data = $resultCache->fetch($cacheKey);
958
959        if ($data !== false) {
960            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
961            if (isset($data[$realKey])) {
962                $stmt = new ArrayStatement($data[$realKey]);
963            } elseif (array_key_exists($realKey, $data)) {
964                $stmt = new ArrayStatement([]);
965            }
966        }
967
968        if (! isset($stmt)) {
969            $stmt = new ResultCacheStatement(
970                $this->executeQuery($sql, $params, $types),
971                $resultCache,
972                $cacheKey,
973                $realKey,
974                $qcp->getLifetime()
975            );
976        }
977
978        $stmt->setFetchMode($this->defaultFetchMode);
979
980        return $stmt;
981    }
982
983    /**
984     * Executes an, optionally parametrized, SQL query and returns the result,
985     * applying a given projection/transformation function on each row of the result.
986     *
987     * @param string  $sql      The SQL query to execute.
988     * @param mixed[] $params   The parameters, if any.
989     * @param Closure $function The transformation function that is applied on each row.
990     *                           The function receives a single parameter, an array, that
991     *                           represents a row of the result set.
992     *
993     * @return mixed[] The projected result of the query.
994     */
995    public function project($sql, array $params, Closure $function)
996    {
997        $result = [];
998        $stmt   = $this->executeQuery($sql, $params);
999
1000        while ($row = $stmt->fetch()) {
1001            $result[] = $function($row);
1002        }
1003
1004        $stmt->closeCursor();
1005
1006        return $result;
1007    }
1008
1009    /**
1010     * Executes an SQL statement, returning a result set as a Statement object.
1011     *
1012     * @return \Doctrine\DBAL\Driver\Statement
1013     *
1014     * @throws DBALException
1015     */
1016    public function query()
1017    {
1018        $connection = $this->getWrappedConnection();
1019
1020        $args = func_get_args();
1021
1022        $logger = $this->_config->getSQLLogger();
1023        if ($logger) {
1024            $logger->startQuery($args[0]);
1025        }
1026
1027        try {
1028            $statement = $connection->query(...$args);
1029        } catch (Throwable $ex) {
1030            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $args[0]);
1031        }
1032
1033        $statement->setFetchMode($this->defaultFetchMode);
1034
1035        if ($logger) {
1036            $logger->stopQuery();
1037        }
1038
1039        return $statement;
1040    }
1041
1042    /**
1043     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
1044     * and returns the number of affected rows.
1045     *
1046     * This method supports PDO binding types as well as DBAL mapping types.
1047     *
1048     * @param string                 $sql    The SQL query.
1049     * @param array<mixed>           $params The query parameters.
1050     * @param array<int|string|null> $types  The parameter types.
1051     *
1052     * @return int The number of affected rows.
1053     *
1054     * @throws DBALException
1055     */
1056    public function executeUpdate($sql, array $params = [], array $types = [])
1057    {
1058        $connection = $this->getWrappedConnection();
1059
1060        $logger = $this->_config->getSQLLogger();
1061        if ($logger) {
1062            $logger->startQuery($sql, $params, $types);
1063        }
1064
1065        try {
1066            if ($params) {
1067                [$sql, $params, $types] = SQLParserUtils::expandListParameters($sql, $params, $types);
1068
1069                $stmt = $connection->prepare($sql);
1070
1071                if ($types) {
1072                    $this->_bindTypedValues($stmt, $params, $types);
1073                    $stmt->execute();
1074                } else {
1075                    $stmt->execute($params);
1076                }
1077
1078                $result = $stmt->rowCount();
1079            } else {
1080                $result = $connection->exec($sql);
1081            }
1082        } catch (Throwable $ex) {
1083            throw DBALException::driverExceptionDuringQuery(
1084                $this->_driver,
1085                $ex,
1086                $sql,
1087                $this->resolveParams($params, $types)
1088            );
1089        }
1090
1091        if ($logger) {
1092            $logger->stopQuery();
1093        }
1094
1095        return $result;
1096    }
1097
1098    /**
1099     * Executes an SQL statement and return the number of affected rows.
1100     *
1101     * @param string $sql
1102     *
1103     * @return int The number of affected rows.
1104     *
1105     * @throws DBALException
1106     */
1107    public function exec($sql)
1108    {
1109        $connection = $this->getWrappedConnection();
1110
1111        $logger = $this->_config->getSQLLogger();
1112        if ($logger) {
1113            $logger->startQuery($sql);
1114        }
1115
1116        try {
1117            $result = $connection->exec($sql);
1118        } catch (Throwable $ex) {
1119            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
1120        }
1121
1122        if ($logger) {
1123            $logger->stopQuery();
1124        }
1125
1126        return $result;
1127    }
1128
1129    /**
1130     * Returns the current transaction nesting level.
1131     *
1132     * @return int The nesting level. A value of 0 means there's no active transaction.
1133     */
1134    public function getTransactionNestingLevel()
1135    {
1136        return $this->transactionNestingLevel;
1137    }
1138
1139    /**
1140     * Fetches the SQLSTATE associated with the last database operation.
1141     *
1142     * @return string|null The last error code.
1143     */
1144    public function errorCode()
1145    {
1146        return $this->getWrappedConnection()->errorCode();
1147    }
1148
1149    /**
1150     * {@inheritDoc}
1151     */
1152    public function errorInfo()
1153    {
1154        return $this->getWrappedConnection()->errorInfo();
1155    }
1156
1157    /**
1158     * Returns the ID of the last inserted row, or the last value from a sequence object,
1159     * depending on the underlying driver.
1160     *
1161     * Note: This method may not return a meaningful or consistent result across different drivers,
1162     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
1163     * columns or sequences.
1164     *
1165     * @param string|null $name Name of the sequence object from which the ID should be returned.
1166     *
1167     * @return string A string representation of the last inserted ID.
1168     */
1169    public function lastInsertId($name = null)
1170    {
1171        return $this->getWrappedConnection()->lastInsertId($name);
1172    }
1173
1174    /**
1175     * Executes a function in a transaction.
1176     *
1177     * The function gets passed this Connection instance as an (optional) parameter.
1178     *
1179     * If an exception occurs during execution of the function or transaction commit,
1180     * the transaction is rolled back and the exception re-thrown.
1181     *
1182     * @param Closure $func The function to execute transactionally.
1183     *
1184     * @return mixed The value returned by $func
1185     *
1186     * @throws Exception
1187     * @throws Throwable
1188     */
1189    public function transactional(Closure $func)
1190    {
1191        $this->beginTransaction();
1192        try {
1193            $res = $func($this);
1194            $this->commit();
1195
1196            return $res;
1197        } catch (Exception $e) {
1198            $this->rollBack();
1199
1200            throw $e;
1201        } catch (Throwable $e) {
1202            $this->rollBack();
1203
1204            throw $e;
1205        }
1206    }
1207
1208    /**
1209     * Sets if nested transactions should use savepoints.
1210     *
1211     * @param bool $nestTransactionsWithSavepoints
1212     *
1213     * @return void
1214     *
1215     * @throws ConnectionException
1216     */
1217    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
1218    {
1219        if ($this->transactionNestingLevel > 0) {
1220            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
1221        }
1222
1223        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1224            throw ConnectionException::savepointsNotSupported();
1225        }
1226
1227        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1228    }
1229
1230    /**
1231     * Gets if nested transactions should use savepoints.
1232     *
1233     * @return bool
1234     */
1235    public function getNestTransactionsWithSavepoints()
1236    {
1237        return $this->nestTransactionsWithSavepoints;
1238    }
1239
1240    /**
1241     * Returns the savepoint name to use for nested transactions are false if they are not supported
1242     * "savepointFormat" parameter is not set
1243     *
1244     * @return mixed A string with the savepoint name or false.
1245     */
1246    protected function _getNestedTransactionSavePointName()
1247    {
1248        return 'DOCTRINE2_SAVEPOINT_' . $this->transactionNestingLevel;
1249    }
1250
1251    /**
1252     * {@inheritDoc}
1253     */
1254    public function beginTransaction()
1255    {
1256        $connection = $this->getWrappedConnection();
1257
1258        ++$this->transactionNestingLevel;
1259
1260        $logger = $this->_config->getSQLLogger();
1261
1262        if ($this->transactionNestingLevel === 1) {
1263            if ($logger) {
1264                $logger->startQuery('"START TRANSACTION"');
1265            }
1266
1267            $connection->beginTransaction();
1268
1269            if ($logger) {
1270                $logger->stopQuery();
1271            }
1272        } elseif ($this->nestTransactionsWithSavepoints) {
1273            if ($logger) {
1274                $logger->startQuery('"SAVEPOINT"');
1275            }
1276
1277            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1278            if ($logger) {
1279                $logger->stopQuery();
1280            }
1281        }
1282
1283        return true;
1284    }
1285
1286    /**
1287     * {@inheritDoc}
1288     *
1289     * @throws ConnectionException If the commit failed due to no active transaction or
1290     *                                            because the transaction was marked for rollback only.
1291     */
1292    public function commit()
1293    {
1294        if ($this->transactionNestingLevel === 0) {
1295            throw ConnectionException::noActiveTransaction();
1296        }
1297
1298        if ($this->isRollbackOnly) {
1299            throw ConnectionException::commitFailedRollbackOnly();
1300        }
1301
1302        $result = true;
1303
1304        $connection = $this->getWrappedConnection();
1305
1306        $logger = $this->_config->getSQLLogger();
1307
1308        if ($this->transactionNestingLevel === 1) {
1309            if ($logger) {
1310                $logger->startQuery('"COMMIT"');
1311            }
1312
1313            $result = $connection->commit();
1314
1315            if ($logger) {
1316                $logger->stopQuery();
1317            }
1318        } elseif ($this->nestTransactionsWithSavepoints) {
1319            if ($logger) {
1320                $logger->startQuery('"RELEASE SAVEPOINT"');
1321            }
1322
1323            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1324            if ($logger) {
1325                $logger->stopQuery();
1326            }
1327        }
1328
1329        --$this->transactionNestingLevel;
1330
1331        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1332            return $result;
1333        }
1334
1335        $this->beginTransaction();
1336
1337        return $result;
1338    }
1339
1340    /**
1341     * Commits all current nesting transactions.
1342     */
1343    private function commitAll(): void
1344    {
1345        while ($this->transactionNestingLevel !== 0) {
1346            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
1347                // When in no auto-commit mode, the last nesting commit immediately starts a new transaction.
1348                // Therefore we need to do the final commit here and then leave to avoid an infinite loop.
1349                $this->commit();
1350
1351                return;
1352            }
1353
1354            $this->commit();
1355        }
1356    }
1357
1358    /**
1359     * Cancels any database changes done during the current transaction.
1360     *
1361     * @return bool
1362     *
1363     * @throws ConnectionException If the rollback operation failed.
1364     */
1365    public function rollBack()
1366    {
1367        if ($this->transactionNestingLevel === 0) {
1368            throw ConnectionException::noActiveTransaction();
1369        }
1370
1371        $connection = $this->getWrappedConnection();
1372
1373        $logger = $this->_config->getSQLLogger();
1374
1375        if ($this->transactionNestingLevel === 1) {
1376            if ($logger) {
1377                $logger->startQuery('"ROLLBACK"');
1378            }
1379
1380            $this->transactionNestingLevel = 0;
1381            $connection->rollBack();
1382            $this->isRollbackOnly = false;
1383            if ($logger) {
1384                $logger->stopQuery();
1385            }
1386
1387            if ($this->autoCommit === false) {
1388                $this->beginTransaction();
1389            }
1390        } elseif ($this->nestTransactionsWithSavepoints) {
1391            if ($logger) {
1392                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
1393            }
1394
1395            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
1396            --$this->transactionNestingLevel;
1397            if ($logger) {
1398                $logger->stopQuery();
1399            }
1400        } else {
1401            $this->isRollbackOnly = true;
1402            --$this->transactionNestingLevel;
1403        }
1404
1405        return true;
1406    }
1407
1408    /**
1409     * Creates a new savepoint.
1410     *
1411     * @param string $savepoint The name of the savepoint to create.
1412     *
1413     * @return void
1414     *
1415     * @throws ConnectionException
1416     */
1417    public function createSavepoint($savepoint)
1418    {
1419        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1420            throw ConnectionException::savepointsNotSupported();
1421        }
1422
1423        $this->getWrappedConnection()->exec($this->platform->createSavePoint($savepoint));
1424    }
1425
1426    /**
1427     * Releases the given savepoint.
1428     *
1429     * @param string $savepoint The name of the savepoint to release.
1430     *
1431     * @return void
1432     *
1433     * @throws ConnectionException
1434     */
1435    public function releaseSavepoint($savepoint)
1436    {
1437        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1438            throw ConnectionException::savepointsNotSupported();
1439        }
1440
1441        if (! $this->platform->supportsReleaseSavepoints()) {
1442            return;
1443        }
1444
1445        $this->getWrappedConnection()->exec($this->platform->releaseSavePoint($savepoint));
1446    }
1447
1448    /**
1449     * Rolls back to the given savepoint.
1450     *
1451     * @param string $savepoint The name of the savepoint to rollback to.
1452     *
1453     * @return void
1454     *
1455     * @throws ConnectionException
1456     */
1457    public function rollbackSavepoint($savepoint)
1458    {
1459        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1460            throw ConnectionException::savepointsNotSupported();
1461        }
1462
1463        $this->getWrappedConnection()->exec($this->platform->rollbackSavePoint($savepoint));
1464    }
1465
1466    /**
1467     * Gets the wrapped driver connection.
1468     *
1469     * @return DriverConnection
1470     */
1471    public function getWrappedConnection()
1472    {
1473        $this->connect();
1474
1475        assert($this->_conn !== null);
1476
1477        return $this->_conn;
1478    }
1479
1480    /**
1481     * Gets the SchemaManager that can be used to inspect or change the
1482     * database schema through the connection.
1483     *
1484     * @return AbstractSchemaManager
1485     */
1486    public function getSchemaManager()
1487    {
1488        if ($this->_schemaManager === null) {
1489            $this->_schemaManager = $this->_driver->getSchemaManager($this);
1490        }
1491
1492        return $this->_schemaManager;
1493    }
1494
1495    /**
1496     * Marks the current transaction so that the only possible
1497     * outcome for the transaction to be rolled back.
1498     *
1499     * @return void
1500     *
1501     * @throws ConnectionException If no transaction is active.
1502     */
1503    public function setRollbackOnly()
1504    {
1505        if ($this->transactionNestingLevel === 0) {
1506            throw ConnectionException::noActiveTransaction();
1507        }
1508
1509        $this->isRollbackOnly = true;
1510    }
1511
1512    /**
1513     * Checks whether the current transaction is marked for rollback only.
1514     *
1515     * @return bool
1516     *
1517     * @throws ConnectionException If no transaction is active.
1518     */
1519    public function isRollbackOnly()
1520    {
1521        if ($this->transactionNestingLevel === 0) {
1522            throw ConnectionException::noActiveTransaction();
1523        }
1524
1525        return $this->isRollbackOnly;
1526    }
1527
1528    /**
1529     * Converts a given value to its database representation according to the conversion
1530     * rules of a specific DBAL mapping type.
1531     *
1532     * @param mixed  $value The value to convert.
1533     * @param string $type  The name of the DBAL mapping type.
1534     *
1535     * @return mixed The converted value.
1536     */
1537    public function convertToDatabaseValue($value, $type)
1538    {
1539        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1540    }
1541
1542    /**
1543     * Converts a given value to its PHP representation according to the conversion
1544     * rules of a specific DBAL mapping type.
1545     *
1546     * @param mixed  $value The value to convert.
1547     * @param string $type  The name of the DBAL mapping type.
1548     *
1549     * @return mixed The converted type.
1550     */
1551    public function convertToPHPValue($value, $type)
1552    {
1553        return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform());
1554    }
1555
1556    /**
1557     * Binds a set of parameters, some or all of which are typed with a PDO binding type
1558     * or DBAL mapping type, to a given statement.
1559     *
1560     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
1561     *           raw PDOStatement instances.
1562     *
1563     * @param \Doctrine\DBAL\Driver\Statement $stmt   The statement to bind the values to.
1564     * @param mixed[]                         $params The map/list of named/positional parameters.
1565     * @param int[]|string[]                  $types  The parameter types (PDO binding types or DBAL mapping types).
1566     *
1567     * @return void
1568     */
1569    private function _bindTypedValues($stmt, array $params, array $types)
1570    {
1571        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
1572        if (is_int(key($params))) {
1573            // Positional parameters
1574            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1575            $bindIndex  = 1;
1576            foreach ($params as $value) {
1577                $typeIndex = $bindIndex + $typeOffset;
1578                if (isset($types[$typeIndex])) {
1579                    $type                  = $types[$typeIndex];
1580                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1581                    $stmt->bindValue($bindIndex, $value, $bindingType);
1582                } else {
1583                    $stmt->bindValue($bindIndex, $value);
1584                }
1585
1586                ++$bindIndex;
1587            }
1588        } else {
1589            // Named parameters
1590            foreach ($params as $name => $value) {
1591                if (isset($types[$name])) {
1592                    $type                  = $types[$name];
1593                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1594                    $stmt->bindValue($name, $value, $bindingType);
1595                } else {
1596                    $stmt->bindValue($name, $value);
1597                }
1598            }
1599        }
1600    }
1601
1602    /**
1603     * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type.
1604     *
1605     * @param mixed           $value The value to bind.
1606     * @param int|string|null $type  The type to bind (PDO or DBAL).
1607     *
1608     * @return mixed[] [0] => the (escaped) value, [1] => the binding type.
1609     */
1610    private function getBindingInfo($value, $type)
1611    {
1612        if (is_string($type)) {
1613            $type = Type::getType($type);
1614        }
1615
1616        if ($type instanceof Type) {
1617            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1618            $bindingType = $type->getBindingType();
1619        } else {
1620            $bindingType = $type;
1621        }
1622
1623        return [$value, $bindingType];
1624    }
1625
1626    /**
1627     * Resolves the parameters to a format which can be displayed.
1628     *
1629     * @internal This is a purely internal method. If you rely on this method, you are advised to
1630     *           copy/paste the code as this method may change, or be removed without prior notice.
1631     *
1632     * @param mixed[]        $params
1633     * @param int[]|string[] $types
1634     *
1635     * @return mixed[]
1636     */
1637    public function resolveParams(array $params, array $types)
1638    {
1639        $resolvedParams = [];
1640
1641        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
1642        if (is_int(key($params))) {
1643            // Positional parameters
1644            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1645            $bindIndex  = 1;
1646            foreach ($params as $value) {
1647                $typeIndex = $bindIndex + $typeOffset;
1648                if (isset($types[$typeIndex])) {
1649                    $type                       = $types[$typeIndex];
1650                    [$value]                    = $this->getBindingInfo($value, $type);
1651                    $resolvedParams[$bindIndex] = $value;
1652                } else {
1653                    $resolvedParams[$bindIndex] = $value;
1654                }
1655
1656                ++$bindIndex;
1657            }
1658        } else {
1659            // Named parameters
1660            foreach ($params as $name => $value) {
1661                if (isset($types[$name])) {
1662                    $type                  = $types[$name];
1663                    [$value]               = $this->getBindingInfo($value, $type);
1664                    $resolvedParams[$name] = $value;
1665                } else {
1666                    $resolvedParams[$name] = $value;
1667                }
1668            }
1669        }
1670
1671        return $resolvedParams;
1672    }
1673
1674    /**
1675     * Creates a new instance of a SQL query builder.
1676     *
1677     * @return QueryBuilder
1678     */
1679    public function createQueryBuilder()
1680    {
1681        return new Query\QueryBuilder($this);
1682    }
1683
1684    /**
1685     * Ping the server
1686     *
1687     * When the server is not available the method returns FALSE.
1688     * It is responsibility of the developer to handle this case
1689     * and abort the request or reconnect manually:
1690     *
1691     * @return bool
1692     *
1693     * @example
1694     *
1695     *   if ($conn->ping() === false) {
1696     *      $conn->close();
1697     *      $conn->connect();
1698     *   }
1699     *
1700     * It is undefined if the underlying driver attempts to reconnect
1701     * or disconnect when the connection is not available anymore
1702     * as long it returns TRUE when a reconnect succeeded and
1703     * FALSE when the connection was dropped.
1704     */
1705    public function ping()
1706    {
1707        $connection = $this->getWrappedConnection();
1708
1709        if ($connection instanceof PingableConnection) {
1710            return $connection->ping();
1711        }
1712
1713        try {
1714            $this->query($this->getDatabasePlatform()->getDummySelectSQL());
1715
1716            return true;
1717        } catch (DBALException $e) {
1718            return false;
1719        }
1720    }
1721}
1722