Commit 6e5f2278 authored by Benjamin Eberlei's avatar Benjamin Eberlei

DBAL-12 - Fully test SQL QueryBuilder

parent c393e108
...@@ -33,7 +33,6 @@ use PDO, Closure, Exception, ...@@ -33,7 +33,6 @@ use PDO, Closure, Exception,
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org * @link www.doctrine-project.org
* @since 2.0 * @since 2.0
* @version $Revision: 3938 $
* @author Guilherme Blanco <guilhermeblanco@hotmail.com> * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com> * @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org> * @author Roman Borschel <roman@code-factory.org>
......
...@@ -24,7 +24,7 @@ namespace Doctrine\DBAL\Query\Expression; ...@@ -24,7 +24,7 @@ namespace Doctrine\DBAL\Query\Expression;
* *
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org * @link www.doctrine-project.org
* @since 2.0 * @since 2.1
* @author Guilherme Blanco <guilhermeblanco@hotmail.com> * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Benjamin Eberlei <kontakt@beberlei.de> * @author Benjamin Eberlei <kontakt@beberlei.de>
*/ */
...@@ -111,7 +111,7 @@ class CompositeExpression implements \Countable ...@@ -111,7 +111,7 @@ class CompositeExpression implements \Countable
*/ */
public function __toString() public function __toString()
{ {
if ($this->count() === 1) { if (count($this->parts) === 1) {
return (string) $this->parts[0]; return (string) $this->parts[0];
} }
......
...@@ -26,8 +26,7 @@ use Doctrine\DBAL\Connection; ...@@ -26,8 +26,7 @@ use Doctrine\DBAL\Connection;
* *
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.com * @link www.doctrine-project.com
* @since 1.0 * @since 2.1
* @version $Revision$
* @author Guilherme Blanco <guilhermeblanco@hotmail.com> * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Benjamin Eberlei <kontakt@beberlei.de> * @author Benjamin Eberlei <kontakt@beberlei.de>
*/ */
......
...@@ -25,10 +25,16 @@ use Doctrine\DBAL\Query\Expression\CompositeExpression, ...@@ -25,10 +25,16 @@ use Doctrine\DBAL\Query\Expression\CompositeExpression,
/** /**
* QueryBuilder class is responsible to dynamically create SQL queries. * QueryBuilder class is responsible to dynamically create SQL queries.
* *
* Important: Verify that every feature you use will work with your database vendor.
* SQL Query Builder does not attempt to validate the generated SQL at all.
*
* The query builder does no validation whatsoever if certain features even work with the
* underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements
* even if some vendors such as MySQL support it.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.com * @link www.doctrine-project.com
* @since 1.0 * @since 2.1
* @version $Revision$
* @author Guilherme Blanco <guilhermeblanco@hotmail.com> * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Benjamin Eberlei <kontakt@beberlei.de> * @author Benjamin Eberlei <kontakt@beberlei.de>
*/ */
...@@ -97,6 +103,13 @@ class QueryBuilder ...@@ -97,6 +103,13 @@ class QueryBuilder
*/ */
private $maxResults = null; private $maxResults = null;
/**
* The counter of bound parameters used with {@see bindValue)
*
* @var int
*/
private $boundCounter = 0;
/** /**
* Initializes a new <tt>QueryBuilder</tt>. * Initializes a new <tt>QueryBuilder</tt>.
* *
...@@ -158,6 +171,23 @@ class QueryBuilder ...@@ -158,6 +171,23 @@ class QueryBuilder
return $this->state; return $this->state;
} }
/**
* Execute this query using the bound parameters and their types.
*
* Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate}
* for insert, update and delete statements.
*
* @return mixed
*/
public function execute()
{
if ($this->type == self::SELECT) {
return $this->connection->executeQuery($this->getSQL(), $this->params, $this->paramTypes);
} else {
return $this->connection->executeUpdate($this->getSQL(), $this->params, $this->paramTypes);
}
}
/** /**
* Get the complete SQL string formed by the current specifications of this QueryBuilder. * Get the complete SQL string formed by the current specifications of this QueryBuilder.
* *
...@@ -280,6 +310,7 @@ class QueryBuilder ...@@ -280,6 +310,7 @@ class QueryBuilder
*/ */
public function setFirstResult($firstResult) public function setFirstResult($firstResult)
{ {
$this->state = self::STATE_DIRTY;
$this->firstResult = $firstResult; $this->firstResult = $firstResult;
return $this; return $this;
} }
...@@ -303,6 +334,7 @@ class QueryBuilder ...@@ -303,6 +334,7 @@ class QueryBuilder
*/ */
public function setMaxResults($maxResults) public function setMaxResults($maxResults)
{ {
$this->state = self::STATE_DIRTY;
$this->maxResults = $maxResults; $this->maxResults = $maxResults;
return $this; return $this;
} }
...@@ -331,20 +363,22 @@ class QueryBuilder ...@@ -331,20 +363,22 @@ class QueryBuilder
*/ */
public function add($sqlPartName, $sqlPart, $append = false) public function add($sqlPartName, $sqlPart, $append = false)
{ {
$isArray = is_array($sqlPart);
$isMultiple = is_array($this->sqlParts[$sqlPartName]); $isMultiple = is_array($this->sqlParts[$sqlPartName]);
if ($isMultiple && ! is_array($sqlPart)) { if ($isMultiple && !$isArray) {
$sqlPart = array($sqlPart); $sqlPart = array($sqlPart);
} }
$this->state = self::STATE_DIRTY; $this->state = self::STATE_DIRTY;
if ($append) { if ($append) {
if ($sqlPartName == "orderBy" || $sqlPartName == "groupBy" || $sqlPartName == "select" || $sqlPartName == "set") {
foreach ($sqlPart AS $part) {
$this->sqlParts[$sqlPartName][] = $part;
}
} else if ($isArray && is_array($sqlPart[key($sqlPart)])) {
$key = key($sqlPart); $key = key($sqlPart);
if ($sqlPartName == 'where' || $sqlPartName == 'having') {
$this->sqlParts[$sqlPartName] = $sqlPart;
} else if (is_array($sqlPart[$key])) {
$this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key]; $this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key];
} else if ($isMultiple) { } else if ($isMultiple) {
$this->sqlParts[$sqlPartName][] = $sqlPart; $this->sqlParts[$sqlPartName][] = $sqlPart;
...@@ -615,7 +649,7 @@ class QueryBuilder ...@@ -615,7 +649,7 @@ class QueryBuilder
*/ */
public function set($key, $value) public function set($key, $value)
{ {
return $this->add('set', $this->expr()->eq($key, '=', $value), true); return $this->add('set', $key .' = ' . $value, true);
} }
/** /**
...@@ -938,7 +972,7 @@ class QueryBuilder ...@@ -938,7 +972,7 @@ class QueryBuilder
. ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '') . ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '')
. ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : ''); . ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : '');
return ($this->maxResults === null) return ($this->maxResults === null && $this->firstResult == null)
? $query ? $query
: $this->connection->getDatabasePlatform()->modifyLimitQuery($query, $this->maxResults, $this->firstResult); : $this->connection->getDatabasePlatform()->modifyLimitQuery($query, $this->maxResults, $this->firstResult);
} }
...@@ -950,7 +984,10 @@ class QueryBuilder ...@@ -950,7 +984,10 @@ class QueryBuilder
*/ */
private function getSQLForUpdate() private function getSQLForUpdate()
{ {
$query = 'UPDATE '; $table = $this->sqlParts['from']['table'] . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');
$query = 'UPDATE ' . $table
. ' SET ' . implode(", ", $this->sqlParts['set'])
. ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
return $query; return $query;
} }
...@@ -962,7 +999,8 @@ class QueryBuilder ...@@ -962,7 +999,8 @@ class QueryBuilder
*/ */
private function getSQLForDelete() private function getSQLForDelete()
{ {
$query = 'DELETE '; $table = $this->sqlParts['from']['table'] . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');
$query = 'DELETE FROM ' . $table . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
return $query; return $query;
} }
...@@ -977,4 +1015,70 @@ class QueryBuilder ...@@ -977,4 +1015,70 @@ class QueryBuilder
{ {
return $this->getSQL(); return $this->getSQL();
} }
/**
* Create a new named parameter and bind the value $value to it.
*
* This method provides a shortcut for PDOStatement::bindValue
* when using prepared statements.
*
* The parameter $value specifies the value that you want to bind. If
* $placeholder is not provided bindValue() will automatically create a
* placeholder for you. An automatic placeholder will be of the name
* ':dcValue1', ':dcValue2' etc.
*
* For more information see {@link http://php.net/pdostatement-bindparam}
*
* Example:
* <code>
* $value = 2;
* $q->eq( 'id', $q->bindValue( $value ) );
* $stmt = $q->executeQuery(); // executed with 'id = 2'
* </code>
*
* @license New BSD License
* @link http://www.zetacomponents.org
* @param mixed $value
* @param mixed $type
* @param string $placeHolder the name to bind with. The string must start with a colon ':'.
* @return string the placeholder name used.
*/
public function createNamedParameter( $value, $type = \PDO::PARAM_STR, $placeHolder = null )
{
if ( $placeHolder === null ) {
$this->boundCounter++;
$placeHolder = ":dcValue" . $this->boundCounter;
}
$this->setParameter(substr($placeHolder, 1), $value, $type);
return $placeHolder;
}
/**
* Create a new positional parameter and bind the given value to it.
*
* Attention: If you are using positional parameters with the query builder you have
* to be very careful to bind all parameters in the order they appear in the SQL
* statement , otherwise they get bound in the wrong order which can lead to serious
* bugs in your code.
*
* Example:
* <code>
* $qb = $conn->createQueryBuilder();
* $qb->select('u.*')
* ->from('users', 'u')
* ->where('u.username = ' . $qb->createPositionalParameter('Foo', PDO::PARAM_STR))
* ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', PDO::PARAM_STR))
* </code>
*
* @param mixed $value
* @param mixed $type
* @return string
*/
public function createPositionalParameter($value, $type = \PDO::PARAM_STR)
{
$this->boundCounter++;
$this->setParameter($this->boundCounter, $value, $type);
return "?";
}
} }
\ No newline at end of file
...@@ -331,4 +331,221 @@ class QueryBuilderTest extends \Doctrine\Tests\DbalTestCase ...@@ -331,4 +331,221 @@ class QueryBuilderTest extends \Doctrine\Tests\DbalTestCase
$this->assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name ASC, u.username DESC', (string) $qb); $this->assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name ASC, u.username DESC', (string) $qb);
} }
public function testEmptySelect()
{
$qb = new QueryBuilder($this->conn);
$qb2 = $qb->select();
$this->assertSame($qb, $qb2);
$this->assertEquals(QueryBuilder::SELECT, $qb->getType());
}
public function testSelectAddSelect()
{
$qb = new QueryBuilder($this->conn);
$expr = $qb->expr();
$qb->select('u.*')
->addSelect('p.*')
->from('users', 'u');
$this->assertEquals('SELECT u.*, p.* FROM users u', (string) $qb);
}
public function testEmptyAddSelect()
{
$qb = new QueryBuilder($this->conn);
$qb2 = $qb->addSelect();
$this->assertSame($qb, $qb2);
$this->assertEquals(QueryBuilder::SELECT, $qb->getType());
}
public function testSelectMultipleFrom()
{
$qb = new QueryBuilder($this->conn);
$expr = $qb->expr();
$qb->select('u.*')
->addSelect('p.*')
->from('users', 'u')
->from('phonenumbers', 'p');
$this->assertEquals('SELECT u.*, p.* FROM users u, phonenumbers p', (string) $qb);
}
public function testUpdate()
{
$qb = new QueryBuilder($this->conn);
$qb->update('users', 'u')
->set('u.foo', '?')
->set('u.bar', '?');
$this->assertEquals(QueryBuilder::UPDATE, $qb->getType());
$this->assertEquals('UPDATE users u SET u.foo = ?, u.bar = ?', (string) $qb);
}
public function testUpdateWithoutAlias()
{
$qb = new QueryBuilder($this->conn);
$qb->update('users')
->set('foo', '?')
->set('bar', '?');
$this->assertEquals('UPDATE users SET foo = ?, bar = ?', (string) $qb);
}
public function testUpdateWhere()
{
$qb = new QueryBuilder($this->conn);
$qb->update('users', 'u')
->set('u.foo', '?')
->where('u.foo = ?');
$this->assertEquals('UPDATE users u SET u.foo = ? WHERE u.foo = ?', (string) $qb);
}
public function testEmptyUpdate()
{
$qb = new QueryBuilder($this->conn);
$qb2 = $qb->update();
$this->assertEquals(QueryBuilder::UPDATE, $qb->getType());
$this->assertSame($qb2, $qb);
}
public function testDelete()
{
$qb = new QueryBuilder($this->conn);
$qb->delete('users', 'u');
$this->assertEquals(QueryBuilder::DELETE, $qb->getType());
$this->assertEquals('DELETE FROM users u', (string) $qb);
}
public function testDeleteWithoutAlias()
{
$qb = new QueryBuilder($this->conn);
$qb->delete('users');
$this->assertEquals(QueryBuilder::DELETE, $qb->getType());
$this->assertEquals('DELETE FROM users', (string) $qb);
}
public function testDeleteWhere()
{
$qb = new QueryBuilder($this->conn);
$qb->delete('users', 'u')
->where('u.foo = ?');
$this->assertEquals('DELETE FROM users u WHERE u.foo = ?', (string) $qb);
}
public function testEmptyDelete()
{
$qb = new QueryBuilder($this->conn);
$qb2 = $qb->delete();
$this->assertEquals(QueryBuilder::DELETE, $qb->getType());
$this->assertSame($qb2, $qb);
}
public function testGetConnection()
{
$qb = new QueryBuilder($this->conn);
$this->assertSame($this->conn, $qb->getConnection());
}
public function testGetState()
{
$qb = new QueryBuilder($this->conn);
$this->assertEquals(QueryBuilder::STATE_CLEAN, $qb->getState());
$qb->select('u.*')->from('users', 'u');
$this->assertEquals(QueryBuilder::STATE_DIRTY, $qb->getState());
$sql1 = $qb->getSQL();
$this->assertEquals(QueryBuilder::STATE_CLEAN, $qb->getState());
$this->assertEquals($sql1, $qb->getSQL());
}
public function testSetMaxResults()
{
$qb = new QueryBuilder($this->conn);
$qb->setMaxResults(10);
$this->assertEquals(QueryBuilder::STATE_DIRTY, $qb->getState());
$this->assertEQuals(10, $qb->getMaxResults());
}
public function testSetFirstResult()
{
$qb = new QueryBuilder($this->conn);
$qb->setFirstResult(10);
$this->assertEquals(QueryBuilder::STATE_DIRTY, $qb->getState());
$this->assertEQuals(10, $qb->getFirstResult());
}
public function testResetQueryPart()
{
$qb = new QueryBuilder($this->conn);
$qb->select('u.*')->from('users', 'u')->where('u.name = ?');
$this->assertEquals('SELECT u.* FROM users u WHERE u.name = ?', (string)$qb);
$qb->resetQueryPart('where');
$this->assertEquals('SELECT u.* FROM users u', (string)$qb);
}
public function testResetQueryParts()
{
$qb = new QueryBuilder($this->conn);
$qb->select('u.*')->from('users', 'u')->where('u.name = ?')->orderBy('u.name');
$this->assertEquals('SELECT u.* FROM users u WHERE u.name = ? ORDER BY u.name ASC', (string)$qb);
$qb->resetQueryParts(array('where', 'orderBy'));
$this->assertEquals('SELECT u.* FROM users u', (string)$qb);
}
public function testCreateNamedParameter()
{
$qb = new QueryBuilder($this->conn);
$qb->select('u.*')->from('users', 'u')->where(
$qb->expr()->eq('u.name', $qb->createNamedParameter(10, \PDO::PARAM_INT))
);
$this->assertEquals('SELECT u.* FROM users u WHERE u.name = :dcValue1', (string)$qb);
$this->assertEquals(10, $qb->getParameter('dcValue1'));
}
public function testCreateNamedParameterCustomPlaceholder()
{
$qb = new QueryBuilder($this->conn);
$qb->select('u.*')->from('users', 'u')->where(
$qb->expr()->eq('u.name', $qb->createNamedParameter(10, \PDO::PARAM_INT, ':test'))
);
$this->assertEquals('SELECT u.* FROM users u WHERE u.name = :test', (string)$qb);
$this->assertEquals(10, $qb->getParameter('test'));
}
public function testCreatePositionalParameter()
{
$qb = new QueryBuilder($this->conn);
$qb->select('u.*')->from('users', 'u')->where(
$qb->expr()->eq('u.name', $qb->createPositionalParameter(10, \PDO::PARAM_INT))
);
$this->assertEquals('SELECT u.* FROM users u WHERE u.name = ?', (string)$qb);
$this->assertEquals(10, $qb->getParameter(1));
}
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment