Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 621 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

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