Connection.php 47.9 KB
Newer Older
1
<?php
romanb's avatar
romanb committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
Benjamin Eberlei's avatar
Benjamin Eberlei committed
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
romanb's avatar
romanb committed
18 19
 */

20
namespace Doctrine\DBAL;
romanb's avatar
romanb committed
21

22
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
23
use Doctrine\DBAL\Exception\InvalidArgumentException;
Benjamin Morel's avatar
Benjamin Morel committed
24 25 26 27 28 29 30 31 32 33
use PDO;
use Closure;
use Exception;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Cache\ResultCacheStatement;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Cache\ArrayStatement;
use Doctrine\DBAL\Cache\CacheException;
34
use Doctrine\DBAL\Driver\PingableConnection;
35 36

/**
37
 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
38 39
 * events, transaction isolation levels, configuration, emulated transaction nesting,
 * lazy connecting and more.
40
 *
Benjamin Morel's avatar
Benjamin Morel committed
41 42 43 44 45 46 47 48
 * @link   www.doctrine-project.org
 * @since  2.0
 * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @author Jonathan Wage <jonwage@gmail.com>
 * @author Roman Borschel <roman@code-factory.org>
 * @author Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @author Lukas Smith <smith@pooteeweet.org> (MDB2 library)
 * @author Benjamin Eberlei <kontakt@beberlei.de>
49
 */
50
class Connection implements DriverConnection
51
{
52 53 54 55
    /**
     * Constant for transaction isolation level READ UNCOMMITTED.
     */
    const TRANSACTION_READ_UNCOMMITTED = 1;
56

57 58 59 60
    /**
     * Constant for transaction isolation level READ COMMITTED.
     */
    const TRANSACTION_READ_COMMITTED = 2;
61

62 63 64 65
    /**
     * Constant for transaction isolation level REPEATABLE READ.
     */
    const TRANSACTION_REPEATABLE_READ = 3;
66

67 68 69 70
    /**
     * Constant for transaction isolation level SERIALIZABLE.
     */
    const TRANSACTION_SERIALIZABLE = 4;
71

72 73
    /**
     * Represents an array of ints to be expanded by Doctrine SQL parsing.
74
     *
Benjamin Morel's avatar
Benjamin Morel committed
75
     * @var integer
76 77
     */
    const PARAM_INT_ARRAY = 101;
78

79 80
    /**
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
81
     *
Benjamin Morel's avatar
Benjamin Morel committed
82
     * @var integer
83 84
     */
    const PARAM_STR_ARRAY = 102;
85

86 87
    /**
     * Offset by which PARAM_* constants are detected as arrays of the param type.
88
     *
Benjamin Morel's avatar
Benjamin Morel committed
89
     * @var integer
90 91
     */
    const ARRAY_PARAM_OFFSET = 100;
92

romanb's avatar
romanb committed
93 94 95
    /**
     * The wrapped driver connection.
     *
96
     * @var \Doctrine\DBAL\Driver\Connection
romanb's avatar
romanb committed
97 98
     */
    protected $_conn;
99

romanb's avatar
romanb committed
100
    /**
101
     * @var \Doctrine\DBAL\Configuration
romanb's avatar
romanb committed
102 103
     */
    protected $_config;
104

romanb's avatar
romanb committed
105
    /**
106
     * @var \Doctrine\Common\EventManager
romanb's avatar
romanb committed
107 108
     */
    protected $_eventManager;
109

110
    /**
111
     * @var \Doctrine\DBAL\Query\Expression\ExpressionBuilder
112 113
     */
    protected $_expr;
114

romanb's avatar
romanb committed
115 116 117 118 119
    /**
     * Whether or not a connection has been established.
     *
     * @var boolean
     */
120
    private $_isConnected = false;
121

122
    /**
123
     * The current auto-commit mode of this connection.
124 125 126 127 128
     *
     * @var boolean
     */
    private $autoCommit = true;

129 130 131 132 133 134 135 136 137 138 139 140 141 142
    /**
     * The transaction nesting level.
     *
     * @var integer
     */
    private $_transactionNestingLevel = 0;

    /**
     * The currently active transaction isolation level.
     *
     * @var integer
     */
    private $_transactionIsolationLevel;

143
    /**
Benjamin Morel's avatar
Benjamin Morel committed
144
     * If nested transactions should use savepoints.
145
     *
146
     * @var boolean
147
     */
148
    private $_nestTransactionsWithSavepoints = false;
Lukas Kahwe Smith's avatar
Lukas Kahwe Smith committed
149

romanb's avatar
romanb committed
150 151 152 153 154
    /**
     * The parameters used during creation of the Connection instance.
     *
     * @var array
     */
155
    private $_params = array();
156

romanb's avatar
romanb committed
157 158 159 160
    /**
     * The DatabasePlatform object that provides information about the
     * database platform used by the connection.
     *
161
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
romanb's avatar
romanb committed
162
     */
163
    private $platform;
164

romanb's avatar
romanb committed
165 166 167
    /**
     * The schema manager.
     *
168
     * @var \Doctrine\DBAL\Schema\AbstractSchemaManager
romanb's avatar
romanb committed
169 170
     */
    protected $_schemaManager;
171

romanb's avatar
romanb committed
172
    /**
romanb's avatar
romanb committed
173 174
     * The used DBAL driver.
     *
175
     * @var \Doctrine\DBAL\Driver
romanb's avatar
romanb committed
176 177
     */
    protected $_driver;
178

179
    /**
180
     * Flag that indicates whether the current transaction is marked for rollback only.
181
     *
182
     * @var boolean
183
     */
184 185
    private $_isRollbackOnly = false;

Benjamin Morel's avatar
Benjamin Morel committed
186 187 188
    /**
     * @var integer
     */
189
    protected $defaultFetchMode = PDO::FETCH_ASSOC;
190

romanb's avatar
romanb committed
191 192 193
    /**
     * Initializes a new instance of the Connection class.
     *
Benjamin Morel's avatar
Benjamin Morel committed
194 195 196 197 198 199
     * @param array                              $params       The connection parameters.
     * @param \Doctrine\DBAL\Driver              $driver       The driver to use.
     * @param \Doctrine\DBAL\Configuration|null  $config       The configuration, optional.
     * @param \Doctrine\Common\EventManager|null $eventManager The event manager, optional.
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
200 201 202 203 204 205
     */
    public function __construct(array $params, Driver $driver, Configuration $config = null,
            EventManager $eventManager = null)
    {
        $this->_driver = $driver;
        $this->_params = $params;
206

romanb's avatar
romanb committed
207 208 209
        if (isset($params['pdo'])) {
            $this->_conn = $params['pdo'];
            $this->_isConnected = true;
210
            unset($this->_params['pdo']);
romanb's avatar
romanb committed
211
        }
212

romanb's avatar
romanb committed
213 214 215
        // Create default config and event manager if none given
        if ( ! $config) {
            $config = new Configuration();
romanb's avatar
romanb committed
216
        }
217

romanb's avatar
romanb committed
218
        if ( ! $eventManager) {
romanb's avatar
romanb committed
219
            $eventManager = new EventManager();
romanb's avatar
romanb committed
220
        }
221

romanb's avatar
romanb committed
222
        $this->_config = $config;
romanb's avatar
romanb committed
223
        $this->_eventManager = $eventManager;
224

225
        $this->_expr = new Query\Expression\ExpressionBuilder($this);
226

227
        $this->autoCommit = $config->getAutoCommit();
romanb's avatar
romanb committed
228
    }
romanb's avatar
romanb committed
229

230
    /**
romanb's avatar
romanb committed
231
     * Gets the parameters used during instantiation.
232
     *
Benjamin Morel's avatar
Benjamin Morel committed
233
     * @return array
234 235 236 237 238 239
     */
    public function getParams()
    {
        return $this->_params;
    }

romanb's avatar
romanb committed
240
    /**
romanb's avatar
romanb committed
241
     * Gets the name of the database this Connection is connected to.
romanb's avatar
romanb committed
242
     *
Benjamin Morel's avatar
Benjamin Morel committed
243
     * @return string
romanb's avatar
romanb committed
244 245 246 247 248
     */
    public function getDatabase()
    {
        return $this->_driver->getDatabase($this);
    }
249

250 251
    /**
     * Gets the hostname of the currently connected database.
252
     *
Benjamin Morel's avatar
Benjamin Morel committed
253
     * @return string|null
254 255 256
     */
    public function getHost()
    {
jwage's avatar
jwage committed
257
        return isset($this->_params['host']) ? $this->_params['host'] : null;
258
    }
259

260 261
    /**
     * Gets the port of the currently connected database.
262
     *
263 264 265 266
     * @return mixed
     */
    public function getPort()
    {
jwage's avatar
jwage committed
267
        return isset($this->_params['port']) ? $this->_params['port'] : null;
268
    }
269

270 271
    /**
     * Gets the username used by this connection.
272
     *
Benjamin Morel's avatar
Benjamin Morel committed
273
     * @return string|null
274 275 276
     */
    public function getUsername()
    {
jwage's avatar
jwage committed
277
        return isset($this->_params['user']) ? $this->_params['user'] : null;
278
    }
279

280 281
    /**
     * Gets the password used by this connection.
282
     *
Benjamin Morel's avatar
Benjamin Morel committed
283
     * @return string|null
284 285 286
     */
    public function getPassword()
    {
jwage's avatar
jwage committed
287
        return isset($this->_params['password']) ? $this->_params['password'] : null;
288
    }
romanb's avatar
romanb committed
289 290 291 292

    /**
     * Gets the DBAL driver instance.
     *
293
     * @return \Doctrine\DBAL\Driver
romanb's avatar
romanb committed
294 295 296 297 298 299 300 301 302
     */
    public function getDriver()
    {
        return $this->_driver;
    }

    /**
     * Gets the Configuration used by the Connection.
     *
303
     * @return \Doctrine\DBAL\Configuration
romanb's avatar
romanb committed
304 305 306 307 308 309 310 311 312
     */
    public function getConfiguration()
    {
        return $this->_config;
    }

    /**
     * Gets the EventManager used by the Connection.
     *
313
     * @return \Doctrine\Common\EventManager
romanb's avatar
romanb committed
314 315 316 317 318 319 320 321 322
     */
    public function getEventManager()
    {
        return $this->_eventManager;
    }

    /**
     * Gets the DatabasePlatform for the connection.
     *
323
     * @return \Doctrine\DBAL\Platforms\AbstractPlatform
romanb's avatar
romanb committed
324 325 326
     */
    public function getDatabasePlatform()
    {
327 328
        if (null == $this->platform) {
            $this->detectDatabasePlatform();
329 330 331
        }

        return $this->platform;
romanb's avatar
romanb committed
332
    }
333

334 335 336
    /**
     * Gets the ExpressionBuilder for the connection.
     *
337
     * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
338 339 340 341 342
     */
    public function getExpressionBuilder()
    {
        return $this->_expr;
    }
343

romanb's avatar
romanb committed
344 345 346
    /**
     * Establishes the connection with the database.
     *
347 348
     * @return boolean TRUE if the connection was successfully established, FALSE if
     *                 the connection is already open.
romanb's avatar
romanb committed
349 350 351
     */
    public function connect()
    {
352 353 354
        if ($this->_isConnected) {
            return false;
        }
romanb's avatar
romanb committed
355 356

        $driverOptions = isset($this->_params['driverOptions']) ?
romanb's avatar
romanb committed
357 358
                $this->_params['driverOptions'] : array();
        $user = isset($this->_params['user']) ? $this->_params['user'] : null;
romanb's avatar
romanb committed
359
        $password = isset($this->_params['password']) ?
romanb's avatar
romanb committed
360 361 362
                $this->_params['password'] : null;

        $this->_conn = $this->_driver->connect($this->_params, $user, $password, $driverOptions);
romanb's avatar
romanb committed
363 364
        $this->_isConnected = true;

365 366 367 368
        if (null === $this->platform) {
            $this->detectDatabasePlatform();
        }

369 370 371 372
        if (false === $this->autoCommit) {
            $this->beginTransaction();
        }

373
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
374
            $eventArgs = new Event\ConnectionEventArgs($this);
375 376 377
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
        }

romanb's avatar
romanb committed
378 379 380
        return true;
    }

381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
    /**
     * Detects and sets the database platform.
     *
     * Evaluates custom platform class and version in order to set the correct platform.
     *
     * @throws DBALException if an invalid platform was specified for this connection.
     */
    private function detectDatabasePlatform()
    {
        if ( ! isset($this->_params['platform'])) {
            $version = $this->getDatabasePlatformVersion();

            if (null !== $version) {
                $this->platform = $this->_driver->createDatabasePlatformForVersion($version);
            } else {
                $this->platform = $this->_driver->getDatabasePlatform();
            }
        } elseif ($this->_params['platform'] instanceof Platforms\AbstractPlatform) {
            $this->platform = $this->_params['platform'];
        } else {
            throw DBALException::invalidPlatformSpecified();
        }

        $this->platform->setEventManager($this->_eventManager);
    }

    /**
     * Returns the version of the related platform if applicable.
     *
     * Returns null if either the driver is not capable to create version
     * specific platform instances, no explicit server version was specified
     * or the underlying driver connection cannot determine the platform
     * version without having to query it (performance reasons).
     *
     * @return string|null
     */
    private function getDatabasePlatformVersion()
    {
        // Driver does not support version specific platforms.
        if ( ! $this->_driver instanceof VersionAwarePlatformDriver) {
            return null;
        }

        // Explicit platform version requested (supersedes auto-detection).
        if (isset($this->_params['serverVersion'])) {
            return $this->_params['serverVersion'];
        }

        // If not connected, we need to connect now to determine the platform version.
        if (null === $this->_conn) {
            $this->connect();
        }

        // Automatic platform version detection.
        if ($this->_conn instanceof ServerInfoAwareConnection &&
            ! $this->_conn->requiresQueryForServerVersion()
        ) {
            return $this->_conn->getServerVersion();
        }

        // Unable to detect platform version.
        return null;
    }

445 446 447 448 449 450 451
    /**
     * Returns the current auto-commit mode for this connection.
     *
     * @return boolean True if auto-commit mode is currently enabled for this connection, false otherwise.
     *
     * @see    setAutoCommit
     */
452
    public function isAutoCommit()
453
    {
454
        return true === $this->autoCommit;
455 456 457 458 459 460 461 462 463 464 465 466 467 468
    }

    /**
     * Sets auto-commit mode for this connection.
     *
     * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
     * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
     * the method commit or the method rollback. By default, new connections are in auto-commit mode.
     *
     * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is
     * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op.
     *
     * @param boolean $autoCommit True to enable auto-commit mode; false to disable it.
     *
469
     * @see   isAutoCommit
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
     */
    public function setAutoCommit($autoCommit)
    {
        $autoCommit = (boolean) $autoCommit;

        // Mode not changed, no-op.
        if ($autoCommit === $this->autoCommit) {
            return;
        }

        $this->autoCommit = $autoCommit;

        // Commit all currently active transactions if any when switching auto-commit mode.
        if (true === $this->_isConnected && 0 !== $this->_transactionNestingLevel) {
            $this->commitAll();
        }
    }

488
    /**
Benjamin Morel's avatar
Benjamin Morel committed
489
     * Sets the fetch mode.
490
     *
491
     * @param integer $fetchMode
Benjamin Morel's avatar
Benjamin Morel committed
492 493
     *
     * @return void
494
     */
495
    public function setFetchMode($fetchMode)
496
    {
497
        $this->defaultFetchMode = $fetchMode;
498 499
    }

romanb's avatar
romanb committed
500
    /**
501 502
     * Prepares and executes an SQL query and returns the first row of the result
     * as an associative array.
503
     *
romanb's avatar
romanb committed
504
     * @param string $statement The SQL query.
Benjamin Morel's avatar
Benjamin Morel committed
505
     * @param array  $params    The query parameters.
506
     * @param array  $types     The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
507
     *
romanb's avatar
romanb committed
508 509
     * @return array
     */
510
    public function fetchAssoc($statement, array $params = array(), array $types = array())
romanb's avatar
romanb committed
511
    {
512
        return $this->executeQuery($statement, $params, $types)->fetch(PDO::FETCH_ASSOC);
romanb's avatar
romanb committed
513 514 515
    }

    /**
516 517
     * Prepares and executes an SQL query and returns the first row of the result
     * as a numerically indexed array.
romanb's avatar
romanb committed
518
     *
Benjamin Morel's avatar
Benjamin Morel committed
519 520
     * @param string $statement The SQL query to be executed.
     * @param array  $params    The prepared statement params.
521
     * @param array  $types     The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
522
     *
romanb's avatar
romanb committed
523 524
     * @return array
     */
525
    public function fetchArray($statement, array $params = array(), array $types = array())
romanb's avatar
romanb committed
526
    {
527
        return $this->executeQuery($statement, $params, $types)->fetch(PDO::FETCH_NUM);
romanb's avatar
romanb committed
528 529 530
    }

    /**
531 532
     * Prepares and executes an SQL query and returns the value of a single column
     * of the first row of the result.
533
     *
Benjamin Morel's avatar
Benjamin Morel committed
534 535
     * @param string  $statement The SQL query to be executed.
     * @param array   $params    The prepared statement params.
Bilge's avatar
Bilge committed
536
     * @param integer $column    The 0-indexed column number to retrieve.
537
     * @param array  $types      The query parameter types.
Benjamin Morel's avatar
Benjamin Morel committed
538
     *
539
     * @return mixed
romanb's avatar
romanb committed
540
     */
541
    public function fetchColumn($statement, array $params = array(), $column = 0, array $types = array())
romanb's avatar
romanb committed
542
    {
543
        return $this->executeQuery($statement, $params, $types)->fetchColumn($column);
romanb's avatar
romanb committed
544 545 546 547 548 549 550 551 552 553 554 555
    }

    /**
     * Whether an actual connection to the database is established.
     *
     * @return boolean
     */
    public function isConnected()
    {
        return $this->_isConnected;
    }

556 557
    /**
     * Checks whether a transaction is currently active.
558
     *
559 560 561 562 563 564 565
     * @return boolean TRUE if a transaction is currently active, FALSE otherwise.
     */
    public function isTransactionActive()
    {
        return $this->_transactionNestingLevel > 0;
    }

566 567
    /**
     * Executes an SQL DELETE statement on a table.
romanb's avatar
romanb committed
568
     *
569 570 571
     * Table expression and columns are not escaped and are not safe for user-input.
     *
     * @param string $tableExpression  The expression of the table on which to delete.
Benjamin Morel's avatar
Benjamin Morel committed
572 573 574
     * @param array  $identifier The deletion criteria. An associative array containing column-value pairs.
     * @param array  $types      The types of identifiers.
     *
575
     * @return integer The number of affected rows.
576
     *
577
     * @throws InvalidArgumentException
578
     */
579
    public function delete($tableExpression, array $identifier, array $types = array())
580 581
    {
        if (empty($identifier)) {
582
            throw InvalidArgumentException::fromEmptyCriteria();
583 584 585 586 587 588 589 590
        }

        $criteria = array();

        foreach (array_keys($identifier) as $columnName) {
            $criteria[] = $columnName . ' = ?';
        }

591 592 593 594 595
        return $this->executeUpdate(
            'DELETE FROM ' . $tableExpression . ' WHERE ' . implode(' AND ', $criteria),
            array_values($identifier),
            is_string(key($types)) ? $this->extractTypeValues($identifier, $types) : $types
        );
596 597
    }

romanb's avatar
romanb committed
598 599 600 601 602 603 604
    /**
     * Closes the connection.
     *
     * @return void
     */
    public function close()
    {
605
        $this->_conn = null;
606

romanb's avatar
romanb committed
607 608 609
        $this->_isConnected = false;
    }

610 611 612
    /**
     * Sets the transaction isolation level.
     *
613
     * @param integer $level The level to set.
Benjamin Morel's avatar
Benjamin Morel committed
614
     *
615
     * @return integer
616 617 618 619
     */
    public function setTransactionIsolation($level)
    {
        $this->_transactionIsolationLevel = $level;
620

621
        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
622 623 624 625 626
    }

    /**
     * Gets the currently active transaction isolation level.
     *
627
     * @return integer The current transaction isolation level.
628 629 630
     */
    public function getTransactionIsolation()
    {
631 632 633 634
        if (null === $this->_transactionIsolationLevel) {
            $this->_transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
        }

635 636 637
        return $this->_transactionIsolationLevel;
    }

romanb's avatar
romanb committed
638
    /**
639
     * Executes an SQL UPDATE statement on a table.
romanb's avatar
romanb committed
640
     *
641 642 643
     * Table expression and columns are not escaped and are not safe for user-input.
     *
     * @param string $tableExpression  The expression of the table to update quoted or unquoted.
Benjamin Morel's avatar
Benjamin Morel committed
644 645 646 647
     * @param array  $data       An associative array containing column-value pairs.
     * @param array  $identifier The update criteria. An associative array containing column-value pairs.
     * @param array  $types      Types of the merged $data and $identifier arrays in that order.
     *
648
     * @return integer The number of affected rows.
romanb's avatar
romanb committed
649
     */
650
    public function update($tableExpression, array $data, array $identifier, array $types = array())
romanb's avatar
romanb committed
651 652
    {
        $set = array();
653

romanb's avatar
romanb committed
654
        foreach ($data as $columnName => $value) {
655
            $set[] = $columnName . ' = ?';
romanb's avatar
romanb committed
656 657
        }

658
        if (is_string(key($types))) {
659 660 661
            $types = $this->extractTypeValues(array_merge($data, $identifier), $types);
        }

romanb's avatar
romanb committed
662 663
        $params = array_merge(array_values($data), array_values($identifier));

664
        $sql  = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
665 666
                . ' WHERE ' . implode(' = ? AND ', array_keys($identifier))
                . ' = ?';
romanb's avatar
romanb committed
667

668
        return $this->executeUpdate($sql, $params, $types);
romanb's avatar
romanb committed
669 670 671 672 673
    }

    /**
     * Inserts a table row with specified data.
     *
674 675 676
     * Table expression and columns are not escaped and are not safe for user-input.
     *
     * @param string $tableExpression The expression of the table to insert data into, quoted or unquoted.
Benjamin Morel's avatar
Benjamin Morel committed
677 678 679
     * @param array  $data      An associative array containing column-value pairs.
     * @param array  $types     Types of the inserted data.
     *
680
     * @return integer The number of affected rows.
romanb's avatar
romanb committed
681
     */
682
    public function insert($tableExpression, array $data, array $types = array())
romanb's avatar
romanb committed
683
    {
684
        if (empty($data)) {
685
            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' ()' . ' VALUES ()');
686 687
        }

688
        return $this->executeUpdate(
689
            'INSERT INTO ' . $tableExpression . ' (' . implode(', ', array_keys($data)) . ')' .
690 691
            ' VALUES (' . implode(', ', array_fill(0, count($data), '?')) . ')',
            array_values($data),
692
            is_string(key($types)) ? $this->extractTypeValues($data, $types) : $types
693
        );
romanb's avatar
romanb committed
694 695
    }

696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716
    /**
     * Extract ordered type list from two associate key lists of data and types.
     *
     * @param array $data
     * @param array $types
     *
     * @return array
     */
    private function extractTypeValues(array $data, array $types)
    {
        $typeValues = array();

        foreach ($data as $k => $_) {
            $typeValues[] = isset($types[$k])
                ? $types[$k]
                : \PDO::PARAM_STR;
        }

        return $typeValues;
    }

romanb's avatar
romanb committed
717
    /**
Benjamin Morel's avatar
Benjamin Morel committed
718
     * Quotes a string so it can be safely used as a table or column name, even if
romanb's avatar
romanb committed
719 720 721 722
     * it is a reserved name.
     *
     * Delimiting style depends on the underlying database platform that is being used.
     *
723 724
     * NOTE: Just because you CAN use quoted identifiers does not mean
     * you SHOULD use them. In general, they end up causing way more
romanb's avatar
romanb committed
725 726
     * problems than they solve.
     *
727
     * @param string $str The name to be quoted.
Benjamin Morel's avatar
Benjamin Morel committed
728
     *
729
     * @return string The quoted name.
romanb's avatar
romanb committed
730 731 732
     */
    public function quoteIdentifier($str)
    {
733
        return $this->getDatabasePlatform()->quoteIdentifier($str);
romanb's avatar
romanb committed
734 735 736 737 738
    }

    /**
     * Quotes a given input parameter.
     *
Benjamin Morel's avatar
Benjamin Morel committed
739 740 741
     * @param mixed       $input The parameter to be quoted.
     * @param string|null $type  The type of the parameter.
     *
742
     * @return string The quoted parameter.
romanb's avatar
romanb committed
743 744 745
     */
    public function quote($input, $type = null)
    {
746
        $this->connect();
747 748

        list($value, $bindingType) = $this->getBindingInfo($input, $type);
749

750
        return $this->_conn->quote($value, $bindingType);
romanb's avatar
romanb committed
751 752 753
    }

    /**
754
     * Prepares and executes an SQL query and returns the result as an associative array.
romanb's avatar
romanb committed
755
     *
Benjamin Morel's avatar
Benjamin Morel committed
756 757 758 759
     * @param string $sql    The SQL query.
     * @param array  $params The query parameters.
     * @param array  $types  The query parameter types.
     *
romanb's avatar
romanb committed
760 761
     * @return array
     */
root's avatar
root committed
762
    public function fetchAll($sql, array $params = array(), $types = array())
romanb's avatar
romanb committed
763
    {
root's avatar
root committed
764
        return $this->executeQuery($sql, $params, $types)->fetchAll();
romanb's avatar
romanb committed
765 766 767 768 769
    }

    /**
     * Prepares an SQL statement.
     *
770
     * @param string $statement The SQL statement to prepare.
Benjamin Morel's avatar
Benjamin Morel committed
771
     *
772
     * @return \Doctrine\DBAL\Statement The prepared statement.
Benjamin Morel's avatar
Benjamin Morel committed
773 774
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
775 776 777
     */
    public function prepare($statement)
    {
778 779 780
        try {
            $stmt = new Statement($statement, $this);
        } catch (\Exception $ex) {
781
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
782 783
        }

784
        $stmt->setFetchMode($this->defaultFetchMode);
785 786

        return $stmt;
romanb's avatar
romanb committed
787 788 789
    }

    /**
Pascal Borreli's avatar
Pascal Borreli committed
790
     * Executes an, optionally parametrized, SQL query.
romanb's avatar
romanb committed
791
     *
Pascal Borreli's avatar
Pascal Borreli committed
792
     * If the query is parametrized, a prepared statement is used.
793 794
     * If an SQLLogger is configured, the execution is logged.
     *
Benjamin Morel's avatar
Benjamin Morel committed
795 796 797 798 799
     * @param string                                      $query  The SQL query to execute.
     * @param array                                       $params The parameters to bind to the query, if any.
     * @param array                                       $types  The types the previous parameters are in.
     * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp    The query cache profile, optional.
     *
800
     * @return \Doctrine\DBAL\Driver\Statement The executed statement.
Benjamin Morel's avatar
Benjamin Morel committed
801 802
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
803
     */
804
    public function executeQuery($query, array $params = array(), $types = array(), QueryCacheProfile $qcp = null)
romanb's avatar
romanb committed
805
    {
806
        if ($qcp !== null) {
807
            return $this->executeCacheQuery($query, $params, $types, $qcp);
808 809
        }

romanb's avatar
romanb committed
810 811
        $this->connect();

812 813 814
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
815
        }
816

817 818 819
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
820

821 822 823 824 825 826 827
                $stmt = $this->_conn->prepare($query);
                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
828
            } else {
829
                $stmt = $this->_conn->query($query);
830
            }
831
        } catch (\Exception $ex) {
832
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
833
        }
834

835
        $stmt->setFetchMode($this->defaultFetchMode);
836

837 838
        if ($logger) {
            $logger->stopQuery();
839 840
        }

romanb's avatar
romanb committed
841
        return $stmt;
romanb's avatar
romanb committed
842
    }
843

844
    /**
Benjamin Morel's avatar
Benjamin Morel committed
845 846 847 848 849 850
     * Executes a caching query.
     *
     * @param string                                 $query  The SQL query to execute.
     * @param array                                  $params The parameters to bind to the query, if any.
     * @param array                                  $types  The types the previous parameters are in.
     * @param \Doctrine\DBAL\Cache\QueryCacheProfile $qcp    The query cache profile.
851 852
     *
     * @return \Doctrine\DBAL\Driver\ResultStatement
Benjamin Morel's avatar
Benjamin Morel committed
853 854
     *
     * @throws \Doctrine\DBAL\Cache\CacheException
855 856 857 858
     */
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp)
    {
        $resultCache = $qcp->getResultCacheDriver() ?: $this->_config->getResultCacheImpl();
859
        if ( ! $resultCache) {
860 861 862 863 864 865 866 867 868
            throw CacheException::noResultDriverConfigured();
        }

        list($cacheKey, $realKey) = $qcp->generateCacheKeys($query, $params, $types);

        // fetch the row pointers entry
        if ($data = $resultCache->fetch($cacheKey)) {
            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
            if (isset($data[$realKey])) {
869
                $stmt = new ArrayStatement($data[$realKey]);
Steve Müller's avatar
Steve Müller committed
870
            } elseif (array_key_exists($realKey, $data)) {
871
                $stmt = new ArrayStatement(array());
872 873
            }
        }
874 875 876 877 878

        if (!isset($stmt)) {
            $stmt = new ResultCacheStatement($this->executeQuery($query, $params, $types), $resultCache, $cacheKey, $realKey, $qcp->getLifetime());
        }

879
        $stmt->setFetchMode($this->defaultFetchMode);
880 881

        return $stmt;
882 883
    }

884
    /**
Pascal Borreli's avatar
Pascal Borreli committed
885
     * Executes an, optionally parametrized, SQL query and returns the result,
886
     * applying a given projection/transformation function on each row of the result.
887
     *
Benjamin Morel's avatar
Benjamin Morel committed
888 889 890 891 892 893
     * @param string   $query    The SQL query to execute.
     * @param array    $params   The parameters, if any.
     * @param \Closure $function The transformation function that is applied on each row.
     *                           The function receives a single parameter, an array, that
     *                           represents a row of the result set.
     *
894
     * @return array The projected result of the query.
895
     */
896
    public function project($query, array $params, Closure $function)
897 898
    {
        $result = array();
899
        $stmt = $this->executeQuery($query, $params);
900

901
        while ($row = $stmt->fetch()) {
902
            $result[] = $function($row);
903
        }
904

905
        $stmt->closeCursor();
906

907 908
        return $result;
    }
romanb's avatar
romanb committed
909 910

    /**
911
     * Executes an SQL statement, returning a result set as a Statement object.
912
     *
913
     * @return \Doctrine\DBAL\Driver\Statement
Benjamin Morel's avatar
Benjamin Morel committed
914 915
     *
     * @throws \Doctrine\DBAL\DBALException
916 917 918
     */
    public function query()
    {
919 920
        $this->connect();

921 922
        $args = func_get_args();

923
        $logger = $this->_config->getSQLLogger();
924 925 926 927
        if ($logger) {
            $logger->startQuery($args[0]);
        }

928
        try {
929 930 931 932 933 934 935 936 937 938 939
            switch (func_num_args()) {
                case 1:
                    $statement = $this->_conn->query($args[0]);
                    break;
                case 2:
                    $statement = $this->_conn->query($args[0], $args[1]);
                    break;
                default:
                    $statement = call_user_func_array(array($this->_conn, 'query'), $args);
                    break;
            }
940
        } catch (\Exception $ex) {
941
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $args[0]);
942 943
        }

944
        $statement->setFetchMode($this->defaultFetchMode);
945 946 947 948 949 950

        if ($logger) {
            $logger->stopQuery();
        }

        return $statement;
951 952 953 954 955
    }

    /**
     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
     * and returns the number of affected rows.
956
     *
957
     * This method supports PDO binding types as well as DBAL mapping types.
romanb's avatar
romanb committed
958
     *
Benjamin Morel's avatar
Benjamin Morel committed
959 960 961 962
     * @param string $query  The SQL query.
     * @param array  $params The query parameters.
     * @param array  $types  The parameter types.
     *
963
     * @return integer The number of affected rows.
Benjamin Morel's avatar
Benjamin Morel committed
964 965
     *
     * @throws \Doctrine\DBAL\DBALException
romanb's avatar
romanb committed
966
     */
967
    public function executeUpdate($query, array $params = array(), array $types = array())
romanb's avatar
romanb committed
968 969 970
    {
        $this->connect();

971 972 973
        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($query, $params, $types);
romanb's avatar
romanb committed
974 975
        }

976 977 978
        try {
            if ($params) {
                list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types);
979

980 981 982 983 984 985 986 987
                $stmt = $this->_conn->prepare($query);
                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
                $result = $stmt->rowCount();
988
            } else {
989
                $result = $this->_conn->exec($query);
990
            }
991
        } catch (\Exception $ex) {
992
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
romanb's avatar
romanb committed
993
        }
994

995 996
        if ($logger) {
            $logger->stopQuery();
997 998
        }

romanb's avatar
romanb committed
999
        return $result;
romanb's avatar
romanb committed
1000 1001
    }

1002
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1003
     * Executes an SQL statement and return the number of affected rows.
1004
     *
1005
     * @param string $statement
Benjamin Morel's avatar
Benjamin Morel committed
1006
     *
1007
     * @return integer The number of affected rows.
Benjamin Morel's avatar
Benjamin Morel committed
1008 1009
     *
     * @throws \Doctrine\DBAL\DBALException
1010 1011 1012 1013
     */
    public function exec($statement)
    {
        $this->connect();
1014 1015 1016 1017 1018 1019

        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($statement);
        }

1020 1021 1022
        try {
            $result = $this->_conn->exec($statement);
        } catch (\Exception $ex) {
1023
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1024
        }
1025 1026 1027 1028 1029 1030

        if ($logger) {
            $logger->stopQuery();
        }

        return $result;
1031 1032
    }

1033 1034 1035 1036 1037 1038 1039 1040 1041 1042
    /**
     * Returns the current transaction nesting level.
     *
     * @return integer The nesting level. A value of 0 means there's no active transaction.
     */
    public function getTransactionNestingLevel()
    {
        return $this->_transactionNestingLevel;
    }

romanb's avatar
romanb committed
1043
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1044
     * Fetches the SQLSTATE associated with the last database operation.
romanb's avatar
romanb committed
1045
     *
1046
     * @return integer The last error code.
romanb's avatar
romanb committed
1047 1048 1049 1050
     */
    public function errorCode()
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1051

romanb's avatar
romanb committed
1052 1053 1054 1055
        return $this->_conn->errorCode();
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1056
     * Fetches extended error information associated with the last database operation.
romanb's avatar
romanb committed
1057
     *
1058
     * @return array The last error information.
romanb's avatar
romanb committed
1059 1060 1061 1062
     */
    public function errorInfo()
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1063

romanb's avatar
romanb committed
1064 1065 1066 1067 1068 1069 1070 1071
        return $this->_conn->errorInfo();
    }

    /**
     * Returns the ID of the last inserted row, or the last value from a sequence object,
     * depending on the underlying driver.
     *
     * Note: This method may not return a meaningful or consistent result across different drivers,
1072 1073
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
     * columns or sequences.
romanb's avatar
romanb committed
1074
     *
Benjamin Morel's avatar
Benjamin Morel committed
1075 1076
     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
     *
1077
     * @return string A string representation of the last inserted ID.
romanb's avatar
romanb committed
1078
     */
romanb's avatar
romanb committed
1079 1080 1081
    public function lastInsertId($seqName = null)
    {
        $this->connect();
Benjamin Morel's avatar
Benjamin Morel committed
1082

romanb's avatar
romanb committed
1083 1084
        return $this->_conn->lastInsertId($seqName);
    }
1085

1086 1087 1088 1089 1090 1091 1092 1093
    /**
     * Executes a function in a transaction.
     *
     * The function gets passed this Connection instance as an (optional) parameter.
     *
     * If an exception occurs during execution of the function or transaction commit,
     * the transaction is rolled back and the exception re-thrown.
     *
Benjamin Morel's avatar
Benjamin Morel committed
1094 1095
     * @param \Closure $func The function to execute transactionally.
     *
1096
     * @return mixed The value returned by $func
Benjamin Morel's avatar
Benjamin Morel committed
1097 1098
     *
     * @throws \Exception
1099 1100 1101 1102 1103
     */
    public function transactional(Closure $func)
    {
        $this->beginTransaction();
        try {
1104
            $res = $func($this);
1105
            $this->commit();
1106
            return $res;
1107
        } catch (Exception $e) {
1108
            $this->rollBack();
1109 1110 1111 1112
            throw $e;
        }
    }

1113
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1114
     * Sets if nested transactions should use savepoints.
1115
     *
1116
     * @param boolean $nestTransactionsWithSavepoints
Benjamin Morel's avatar
Benjamin Morel committed
1117
     *
1118
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1119 1120
     *
     * @throws \Doctrine\DBAL\ConnectionException
1121 1122 1123
     */
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
    {
1124 1125 1126 1127
        if ($this->_transactionNestingLevel > 0) {
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
        }

1128
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1129
            throw ConnectionException::savepointsNotSupported();
1130 1131
        }

1132
        $this->_nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1133 1134 1135
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1136
     * Gets if nested transactions should use savepoints.
1137 1138 1139 1140 1141 1142 1143 1144
     *
     * @return boolean
     */
    public function getNestTransactionsWithSavepoints()
    {
        return $this->_nestTransactionsWithSavepoints;
    }

1145 1146 1147 1148
    /**
     * Returns the savepoint name to use for nested transactions are false if they are not supported
     * "savepointFormat" parameter is not set
     *
Benjamin Morel's avatar
Benjamin Morel committed
1149
     * @return mixed A string with the savepoint name or false.
1150
     */
1151 1152 1153
    protected function _getNestedTransactionSavePointName()
    {
        return 'DOCTRINE2_SAVEPOINT_'.$this->_transactionNestingLevel;
1154 1155
    }

1156 1157 1158 1159 1160 1161 1162 1163 1164
    /**
     * Starts a transaction by suspending auto-commit mode.
     *
     * @return void
     */
    public function beginTransaction()
    {
        $this->connect();

1165 1166
        ++$this->_transactionNestingLevel;

1167 1168
        $logger = $this->_config->getSQLLogger();

1169
        if ($this->_transactionNestingLevel == 1) {
1170 1171 1172
            if ($logger) {
                $logger->startQuery('"START TRANSACTION"');
            }
1173
            $this->_conn->beginTransaction();
1174 1175 1176
            if ($logger) {
                $logger->stopQuery();
            }
Steve Müller's avatar
Steve Müller committed
1177
        } elseif ($this->_nestTransactionsWithSavepoints) {
1178 1179 1180
            if ($logger) {
                $logger->startQuery('"SAVEPOINT"');
            }
1181
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1182 1183 1184
            if ($logger) {
                $logger->stopQuery();
            }
1185 1186 1187 1188 1189 1190 1191
        }
    }

    /**
     * Commits the current transaction.
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1192 1193 1194
     *
     * @throws \Doctrine\DBAL\ConnectionException If the commit failed due to no active transaction or
     *                                            because the transaction was marked for rollback only.
1195 1196 1197 1198
     */
    public function commit()
    {
        if ($this->_transactionNestingLevel == 0) {
1199
            throw ConnectionException::noActiveTransaction();
1200 1201 1202 1203 1204 1205 1206
        }
        if ($this->_isRollbackOnly) {
            throw ConnectionException::commitFailedRollbackOnly();
        }

        $this->connect();

1207 1208
        $logger = $this->_config->getSQLLogger();

1209
        if ($this->_transactionNestingLevel == 1) {
1210 1211 1212
            if ($logger) {
                $logger->startQuery('"COMMIT"');
            }
1213
            $this->_conn->commit();
1214 1215 1216
            if ($logger) {
                $logger->stopQuery();
            }
Steve Müller's avatar
Steve Müller committed
1217
        } elseif ($this->_nestTransactionsWithSavepoints) {
1218 1219 1220
            if ($logger) {
                $logger->startQuery('"RELEASE SAVEPOINT"');
            }
1221
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1222 1223 1224
            if ($logger) {
                $logger->stopQuery();
            }
1225 1226 1227
        }

        --$this->_transactionNestingLevel;
1228 1229 1230 1231 1232 1233 1234 1235 1236

        if (false === $this->autoCommit && 0 === $this->_transactionNestingLevel) {
            $this->beginTransaction();
        }
    }

    /**
     * Commits all current nesting transactions.
     */
1237
    private function commitAll()
1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249
    {
        while (0 !== $this->_transactionNestingLevel) {
            if (false === $this->autoCommit && 1 === $this->_transactionNestingLevel) {
                // When in no auto-commit mode, the last nesting commit immediately starts a new transaction.
                // Therefore we need to do the final commit here and then leave to avoid an infinite loop.
                $this->commit();

                return;
            }

            $this->commit();
        }
1250 1251 1252
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1253
     * Cancels any database changes done during the current transaction.
1254
     *
Benjamin Morel's avatar
Benjamin Morel committed
1255
     * @throws \Doctrine\DBAL\ConnectionException If the rollback operation failed.
1256
     */
1257
    public function rollBack()
1258 1259
    {
        if ($this->_transactionNestingLevel == 0) {
1260
            throw ConnectionException::noActiveTransaction();
1261 1262 1263 1264
        }

        $this->connect();

1265 1266
        $logger = $this->_config->getSQLLogger();

1267
        if ($this->_transactionNestingLevel == 1) {
1268 1269 1270
            if ($logger) {
                $logger->startQuery('"ROLLBACK"');
            }
1271
            $this->_transactionNestingLevel = 0;
1272
            $this->_conn->rollBack();
1273
            $this->_isRollbackOnly = false;
1274 1275 1276
            if ($logger) {
                $logger->stopQuery();
            }
1277 1278 1279 1280

            if (false === $this->autoCommit) {
                $this->beginTransaction();
            }
Steve Müller's avatar
Steve Müller committed
1281
        } elseif ($this->_nestTransactionsWithSavepoints) {
1282 1283 1284
            if ($logger) {
                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
            }
1285 1286
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
            --$this->_transactionNestingLevel;
1287 1288 1289
            if ($logger) {
                $logger->stopQuery();
            }
1290
        } else {
1291
            $this->_isRollbackOnly = true;
1292 1293 1294 1295
            --$this->_transactionNestingLevel;
        }
    }

1296
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1297 1298 1299
     * Creates a new savepoint.
     *
     * @param string $savepoint The name of the savepoint to create.
1300 1301
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1302 1303
     *
     * @throws \Doctrine\DBAL\ConnectionException
1304
     */
1305
    public function createSavepoint($savepoint)
1306
    {
1307
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1308
            throw ConnectionException::savepointsNotSupported();
1309 1310
        }

1311
        $this->_conn->exec($this->platform->createSavePoint($savepoint));
1312 1313 1314
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1315 1316 1317
     * Releases the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to release.
1318 1319
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1320 1321
     *
     * @throws \Doctrine\DBAL\ConnectionException
1322
     */
1323
    public function releaseSavepoint($savepoint)
1324
    {
1325
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1326
            throw ConnectionException::savepointsNotSupported();
1327 1328
        }

1329 1330
        if ($this->platform->supportsReleaseSavepoints()) {
            $this->_conn->exec($this->platform->releaseSavePoint($savepoint));
1331
        }
1332 1333 1334
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1335 1336 1337
     * Rolls back to the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to rollback to.
1338 1339
     *
     * @return void
Benjamin Morel's avatar
Benjamin Morel committed
1340 1341
     *
     * @throws \Doctrine\DBAL\ConnectionException
1342
     */
1343
    public function rollbackSavepoint($savepoint)
1344
    {
1345
        if ( ! $this->getDatabasePlatform()->supportsSavepoints()) {
1346
            throw ConnectionException::savepointsNotSupported();
1347 1348
        }

1349
        $this->_conn->exec($this->platform->rollbackSavePoint($savepoint));
1350 1351
    }

romanb's avatar
romanb committed
1352 1353 1354
    /**
     * Gets the wrapped driver connection.
     *
1355
     * @return \Doctrine\DBAL\Driver\Connection
romanb's avatar
romanb committed
1356 1357 1358 1359
     */
    public function getWrappedConnection()
    {
        $this->connect();
1360

romanb's avatar
romanb committed
1361 1362
        return $this->_conn;
    }
1363

romanb's avatar
romanb committed
1364 1365 1366 1367
    /**
     * Gets the SchemaManager that can be used to inspect or change the
     * database schema through the connection.
     *
1368
     * @return \Doctrine\DBAL\Schema\AbstractSchemaManager
romanb's avatar
romanb committed
1369 1370 1371 1372 1373 1374
     */
    public function getSchemaManager()
    {
        if ( ! $this->_schemaManager) {
            $this->_schemaManager = $this->_driver->getSchemaManager($this);
        }
1375

romanb's avatar
romanb committed
1376 1377
        return $this->_schemaManager;
    }
1378

1379 1380 1381
    /**
     * Marks the current transaction so that the only possible
     * outcome for the transaction to be rolled back.
1382
     *
Benjamin Morel's avatar
Benjamin Morel committed
1383 1384 1385
     * @return void
     *
     * @throws \Doctrine\DBAL\ConnectionException If no transaction is active.
1386 1387 1388 1389 1390 1391 1392 1393 1394 1395
     */
    public function setRollbackOnly()
    {
        if ($this->_transactionNestingLevel == 0) {
            throw ConnectionException::noActiveTransaction();
        }
        $this->_isRollbackOnly = true;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
1396
     * Checks whether the current transaction is marked for rollback only.
1397
     *
1398
     * @return boolean
Benjamin Morel's avatar
Benjamin Morel committed
1399 1400
     *
     * @throws \Doctrine\DBAL\ConnectionException If no transaction is active.
1401
     */
1402
    public function isRollbackOnly()
1403 1404 1405 1406
    {
        if ($this->_transactionNestingLevel == 0) {
            throw ConnectionException::noActiveTransaction();
        }
Benjamin Morel's avatar
Benjamin Morel committed
1407

1408 1409 1410
        return $this->_isRollbackOnly;
    }

1411 1412 1413
    /**
     * Converts a given value to its database representation according to the conversion
     * rules of a specific DBAL mapping type.
1414
     *
Benjamin Morel's avatar
Benjamin Morel committed
1415 1416 1417
     * @param mixed  $value The value to convert.
     * @param string $type  The name of the DBAL mapping type.
     *
1418 1419 1420 1421
     * @return mixed The converted value.
     */
    public function convertToDatabaseValue($value, $type)
    {
1422
        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1423 1424 1425 1426 1427
    }

    /**
     * Converts a given value to its PHP representation according to the conversion
     * rules of a specific DBAL mapping type.
1428
     *
Benjamin Morel's avatar
Benjamin Morel committed
1429 1430 1431
     * @param mixed  $value The value to convert.
     * @param string $type  The name of the DBAL mapping type.
     *
1432 1433 1434 1435
     * @return mixed The converted type.
     */
    public function convertToPHPValue($value, $type)
    {
1436
        return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform());
1437 1438 1439 1440 1441
    }

    /**
     * Binds a set of parameters, some or all of which are typed with a PDO binding type
     * or DBAL mapping type, to a given statement.
1442
     *
Benjamin Morel's avatar
Benjamin Morel committed
1443 1444 1445 1446 1447 1448
     * @param \Doctrine\DBAL\Driver\Statement $stmt   The statement to bind the values to.
     * @param array                           $params The map/list of named/positional parameters.
     * @param array                           $types  The parameter types (PDO binding types or DBAL mapping types).
     *
     * @return void
     *
1449 1450
     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
     *           raw PDOStatement instances.
1451
     */
1452
    private function _bindTypedValues($stmt, array $params, array $types)
1453 1454 1455 1456
    {
        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
        if (is_int(key($params))) {
            // Positional parameters
1457
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1458
            $bindIndex = 1;
1459
            foreach ($params as $value) {
1460 1461 1462
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
                    $type = $types[$typeIndex];
1463
                    list($value, $bindingType) = $this->getBindingInfo($value, $type);
1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474
                    $stmt->bindValue($bindIndex, $value, $bindingType);
                } else {
                    $stmt->bindValue($bindIndex, $value);
                }
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
                    $type = $types[$name];
1475
                    list($value, $bindingType) = $this->getBindingInfo($value, $type);
1476 1477 1478 1479 1480 1481 1482
                    $stmt->bindValue($name, $value, $bindingType);
                } else {
                    $stmt->bindValue($name, $value);
                }
            }
        }
    }
1483 1484 1485 1486

    /**
     * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type.
     *
Benjamin Morel's avatar
Benjamin Morel committed
1487 1488 1489 1490
     * @param mixed $value The value to bind.
     * @param mixed $type  The type to bind (PDO or DBAL).
     *
     * @return array [0] => the (escaped) value, [1] => the binding type.
1491 1492 1493 1494 1495 1496 1497
     */
    private function getBindingInfo($value, $type)
    {
        if (is_string($type)) {
            $type = Type::getType($type);
        }
        if ($type instanceof Type) {
1498
            $value = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1499 1500 1501 1502
            $bindingType = $type->getBindingType();
        } else {
            $bindingType = $type; // PDO::PARAM_* constants
        }
Benjamin Morel's avatar
Benjamin Morel committed
1503

1504 1505 1506
        return array($value, $bindingType);
    }

1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553
    /**
     * Resolves the parameters to a format which can be displayed.
     *
     * @internal This is a purely internal method. If you rely on this method, you are advised to
     *           copy/paste the code as this method may change, or be removed without prior notice.
     *
     * @param array $params
     * @param array $types
     *
     * @return array
     */
    public function resolveParams(array $params, array $types)
    {
        $resolvedParams = array();

        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
        if (is_int(key($params))) {
            // Positional parameters
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
            $bindIndex = 1;
            foreach ($params as $value) {
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
                    $type = $types[$typeIndex];
                    list($value,) = $this->getBindingInfo($value, $type);
                    $resolvedParams[$bindIndex] = $value;
                } else {
                    $resolvedParams[$bindIndex] = $value;
                }
                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
                    $type = $types[$name];
                    list($value,) = $this->getBindingInfo($value, $type);
                    $resolvedParams[$name] = $value;
                } else {
                    $resolvedParams[$name] = $value;
                }
            }
        }

        return $resolvedParams;
    }

1554
    /**
Benjamin Morel's avatar
Benjamin Morel committed
1555
     * Creates a new instance of a SQL query builder.
1556
     *
1557
     * @return \Doctrine\DBAL\Query\QueryBuilder
1558 1559 1560 1561 1562
     */
    public function createQueryBuilder()
    {
        return new Query\QueryBuilder($this);
    }
1563 1564

    /**
1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581
     * Ping the server
     *
     * When the server is not available the method returns FALSE.
     * It is responsibility of the developer to handle this case
     * and abort the request or reconnect manually:
     *
     * @example
     *
     *   if ($conn->ping() === false) {
     *      $conn->close();
     *      $conn->connect();
     *   }
     *
     * It is undefined if the underlying driver attempts to reconnect
     * or disconnect when the connection is not available anymore
     * as long it returns TRUE when a reconnect succeeded and
     * FALSE when the connection was dropped.
1582 1583 1584 1585 1586
     *
     * @return bool
     */
    public function ping()
    {
1587
        $this->connect();
1588

1589 1590 1591 1592 1593
        if ($this->_conn instanceof PingableConnection) {
            return $this->_conn->ping();
        }

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

1596 1597
            return true;
        } catch (DBALException $e) {
1598
            return false;
1599
        }
1600
    }
1601
}