diff --git a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php index 817e30baf..f2869baa2 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -28,11 +28,6 @@ use PDO, Doctrine\DBAL\Connection, Doctrine\ORM\Mapping\ClassMetadata; * @since 2.0 * @author Roman Borschel * @author Guilherme Blanco - * - * @todo General behavior is "wrong" if you define an alias to selected IdentificationVariable. - * Example: SELECT u AS user FROM User u - * The result should contains an array where each array index is an array: array('user' => [User object]) - * Problem must be solved somehow by removing the isMixed in ResultSetMapping */ class ArrayHydrator extends AbstractHydrator { diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index ff3656b62..b63af9ac8 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -2607,7 +2607,7 @@ class Parser return $this->InputParameter(); } - return $this->IdentificationVariable(); + return $this->StateFieldPathExpression(); } /** diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index c12ea0690..5826b82d4 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -411,7 +411,8 @@ class SqlWalker implements TreeWalker { $this->_useSqlTableAliases = false; - return $this->walkUpdateClause($AST->updateClause) . $this->walkWhereClause($AST->whereClause); + return $this->walkUpdateClause($AST->updateClause) + . $this->walkWhereClause($AST->whereClause); } /** @@ -424,9 +425,29 @@ class SqlWalker implements TreeWalker { $this->_useSqlTableAliases = false; - return $this->walkDeleteClause($AST->deleteClause) . $this->walkWhereClause($AST->whereClause); + return $this->walkDeleteClause($AST->deleteClause) + . $this->walkWhereClause($AST->whereClause); } + /** + * Walks down an IdentificationVariable AST node, thereby generating the appropriate SQL. + * This one differs of ->walkIdentificationVariable() because it generates the entity identifiers. + * + * @param string $identVariable + * @return string + */ + public function walkEntityIdentificationVariable($identVariable) + { + $class = $this->_queryComponents[$identVariable]['metadata']; + $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable); + $sqlParts = array(); + + foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { + $sqlParts[] = $tableAlias . '.' . $columnName; + } + + return implode(', ', $sqlParts); + } /** * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL. @@ -689,7 +710,7 @@ class SqlWalker implements TreeWalker $expr = $orderByItem->expression; $sql = ($expr instanceof AST\PathExpression) ? $this->walkPathExpression($expr) - : $this->_scalarResultAliasMap[$this->_queryComponents[$expr]['token']['value']]; + : $this->walkResultVariable($this->_queryComponents[$expr]['token']['value']); return $sql . ' ' . strtoupper($orderByItem->type); } @@ -1287,15 +1308,7 @@ class SqlWalker implements TreeWalker break; default: // IdentificationVariable - $class = $this->_queryComponents[$expr]['metadata']; - $tableAlias = $this->getSQLTableAlias($class->getTableName(), $expr); - $sqlParts = array(); - - foreach ($class->getQuotedIdentifierColumnNames($this->_platform) as $columnName) { - $sqlParts[] = $tableAlias . '.' . $columnName; - } - - $sql .= implode(', ', $sqlParts); + $sql .= $this->walkEntityIdentificationVariable($expr); break; } @@ -1558,20 +1571,30 @@ class SqlWalker implements TreeWalker { $sql = $collMemberExpr->not ? 'NOT ' : ''; $sql .= 'EXISTS (SELECT 1 FROM '; - $entityExpr = $collMemberExpr->entityExpression; + + $entityExpr = $collMemberExpr->entityExpression; $collPathExpr = $collMemberExpr->collectionValuedPathExpression; $fieldName = $collPathExpr->field; - $dqlAlias = $collPathExpr->identificationVariable; + $dqlAlias = $collPathExpr->identificationVariable; $class = $this->_queryComponents[$dqlAlias]['metadata']; - if ($entityExpr instanceof AST\InputParameter) { - $dqlParamKey = $entityExpr->name; - $entity = $this->_query->getParameter($dqlParamKey); - } else { - //TODO - throw new \BadMethodCallException("Not implemented"); + switch (true) { + // InputParameter + case ($entityExpr instanceof AST\InputParameter): + $dqlParamKey = $entityExpr->name; + $entity = $this->_query->getParameter($dqlParamKey); + $entitySql = '?'; + break; + + // SingleValuedAssociationPathExpression | IdentificationVariable + case ($entityExpr instanceof AST\PathExpression): + $entitySql = $this->walkPathExpression($entityExpr); + break; + + default: + throw new \BadMethodCallException("Not implemented"); } $assoc = $class->associationMappings[$fieldName]; @@ -1584,25 +1607,23 @@ class SqlWalker implements TreeWalker $sql .= $targetClass->getQuotedTableName($this->_platform) . ' ' . $targetTableAlias . ' WHERE '; $owningAssoc = $targetClass->associationMappings[$assoc['mappedBy']]; - $first = true; + $sqlParts = array(); foreach ($owningAssoc['targetToSourceKeyColumns'] as $targetColumn => $sourceColumn) { - if ($first) $first = false; else $sql .= ' AND '; + $targetColumn = $class->getQuotedColumnName($class->fieldNames[$targetColumn], $this->_platform); - $sql .= $sourceTableAlias . '.' . $class->getQuotedColumnName($class->fieldNames[$targetColumn], $this->_platform) - . ' = ' - . $targetTableAlias . '.' . $sourceColumn; + $sqlParts[] = $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; } - $sql .= ' AND '; - $first = true; - foreach ($targetClass->getQuotedIdentifierColumnNames($this->_platform) as $targetColumnName) { - if ($first) $first = false; else $sql .= ' AND '; + if (isset($dqlParamKey)) { + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + } - $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); - $sql .= $targetTableAlias . '.' . $targetColumnName . ' = ?'; + $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' = ' . $entitySql; } + + $sql .= implode(' AND ', $sqlParts); } else { // many-to-many $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); @@ -1619,39 +1640,42 @@ class SqlWalker implements TreeWalker . ' INNER JOIN ' . $targetClass->getQuotedTableName($this->_platform) . ' ' . $targetTableAlias . ' ON '; // join conditions - $joinColumns = $assoc['isOwningSide'] - ? $joinTable['inverseJoinColumns'] - : $joinTable['joinColumns']; + $joinColumns = $assoc['isOwningSide'] ? $joinTable['inverseJoinColumns'] : $joinTable['joinColumns']; + $joinSqlParts = array(); - $first = true; foreach ($joinColumns as $joinColumn) { - if ($first) $first = false; else $sql .= ' AND '; + $targetColumn = $targetClass->getQuotedColumnName( + $targetClass->fieldNames[$joinColumn['referencedColumnName']], + $this->_platform + ); - $sql .= $joinTableAlias . '.' . $joinColumn['name'] . ' = ' - . $targetTableAlias . '.' . $targetClass->getQuotedColumnName($targetClass->fieldNames[$joinColumn['referencedColumnName']], $this->_platform); + $joinSqlParts[] = $joinTableAlias . '.' . $joinColumn['name'] . ' = ' . $targetTableAlias . '.' . $targetColumn; } + $sql .= implode(' AND ', $joinSqlParts); $sql .= ' WHERE '; $joinColumns = $assoc['isOwningSide'] ? $joinTable['joinColumns'] : $joinTable['inverseJoinColumns']; - $first = true; + $sqlParts = array(); foreach ($joinColumns as $joinColumn) { - if ($first) $first = false; else $sql .= ' AND '; + $targetColumn = $class->getQuotedColumnName( + $class->fieldNames[$joinColumn['referencedColumnName']], + $this->_platform + ); - $sql .= $joinTableAlias . '.' . $joinColumn['name'] . ' = ' - . $sourceTableAlias . '.' . $class->getQuotedColumnName($class->fieldNames[$joinColumn['referencedColumnName']], $this->_platform); + $sqlParts[] = $joinTableAlias . '.' . $joinColumn['name'] . ' = ' . $sourceTableAlias . '.' . $targetColumn; } - $sql .= ' AND '; - $first = true; - foreach ($targetClass->getQuotedIdentifierColumnNames($this->_platform) as $targetColumnName) { - if ($first) $first = false; else $sql .= ' AND '; + if (isset($dqlParamKey)) { + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + } - $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); - $sql .= $targetTableAlias . '.' . $targetColumnName . ' = ?'; + $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' = ' . $entitySql; } + + $sql .= implode(' AND ', $sqlParts); } return $sql . ')'; @@ -1946,7 +1970,7 @@ class SqlWalker implements TreeWalker { if (is_string($term)) { return (isset($this->_queryComponents[$term])) - ? $this->_scalarResultAliasMap[$this->_queryComponents[$term]['token']['value']] + ? $this->walkResultVariable($this->_queryComponents[$term]['token']['value']) : $term; } @@ -1998,8 +2022,7 @@ class SqlWalker implements TreeWalker return $primary->dispatch($this); } - // TODO: We need to deal with IdentificationVariable here - return ''; + return $this->walkEntityIdentificationVariable($primary); } /** diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index b1ba714d8..66d79a837 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -196,7 +196,7 @@ public function () if ($this->_backupExisting && file_exists($path)) { $backupPath = dirname($path) . DIRECTORY_SEPARATOR . basename($path) . "~"; if (!copy($path, $backupPath)) { - throw new \RuntimeException("Attempt to backup overwritten entitiy file but copy operation failed."); + throw new \RuntimeException("Attempt to backup overwritten entity file but copy operation failed."); } } @@ -404,11 +404,11 @@ public function () $collections[] = '$this->'.$mapping['fieldName'].' = new \Doctrine\Common\Collections\ArrayCollection();'; } } - + if ($collections) { return $this->_prefixCodeWithSpaces(str_replace("", implode("\n".$this->_spaces, $collections), self::$_constructorMethodTemplate)); } - + return ''; } @@ -576,7 +576,7 @@ public function () if (isset($metadata->table['schema'])) { $table[] = 'schema="' . $metadata->table['schema'] . '"'; } - + if (isset($metadata->table['name'])) { $table[] = 'name="' . $metadata->table['name'] . '"'; } @@ -754,7 +754,7 @@ public function () '' => Inflector::camelize($fieldName), '' => $methodName, '' => $fieldName, - '' => ($defaultValue !== null ) ? ('='.$defaultValue) : '', + '' => ($defaultValue !== null ) ? (' = '.$defaultValue) : '', '' => $this->_getClassName($metadata) ); diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 50de767db..031ec2138 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -536,43 +536,71 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ); } - public function testSupportsMemberOfExpression() + public function testSupportsMemberOfExpressionOneToMany() { // "Get all users who have $phone as a phonenumber." (*cough* doesnt really make sense...) - $q1 = $this->_em->createQuery('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE :param MEMBER OF u.phonenumbers'); - $q1->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); + $q = $this->_em->createQuery('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE :param MEMBER OF u.phonenumbers'); + $q->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); $phone = new \Doctrine\Tests\Models\CMS\CmsPhonenumber; $phone->phonenumber = 101; - $q1->setParameter('param', $phone); + $q->setParameter('param', $phone); $this->assertEquals( 'SELECT c0_.id AS id0 FROM cms_users c0_ WHERE EXISTS (SELECT 1 FROM cms_phonenumbers c1_ WHERE c0_.id = c1_.user_id AND c1_.phonenumber = ?)', - $q1->getSql() + $q->getSql() ); + } + public function testSupportsMemberOfExpressionManyToMany() + { // "Get all users who are members of $group." - $q2 = $this->_em->createQuery('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE :param MEMBER OF u.groups'); - $q2->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); + $q = $this->_em->createQuery('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE :param MEMBER OF u.groups'); + $q->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); $group = new \Doctrine\Tests\Models\CMS\CmsGroup; $group->id = 101; - $q2->setParameter('param', $group); + $q->setParameter('param', $group); $this->assertEquals( 'SELECT c0_.id AS id0 FROM cms_users c0_ WHERE EXISTS (SELECT 1 FROM cms_users_groups c1_ INNER JOIN cms_groups c2_ ON c1_.group_id = c2_.id WHERE c1_.user_id = c0_.id AND c2_.id = ?)', - $q2->getSql() + $q->getSql() ); + } + public function testSupportsMemberOfExpressionSelfReferencing() + { // "Get all persons who have $person as a friend." // Tough one: Many-many self-referencing ("friends") with class table inheritance - $q3 = $this->_em->createQuery('SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p WHERE :param MEMBER OF p.friends'); + $q = $this->_em->createQuery('SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p WHERE :param MEMBER OF p.friends'); $person = new \Doctrine\Tests\Models\Company\CompanyPerson; $this->_em->getClassMetadata(get_class($person))->setIdentifierValues($person, array('id' => 101)); - $q3->setParameter('param', $person); + $q->setParameter('param', $person); $this->assertEquals( 'SELECT c0_.id AS id0, c0_.name AS name1, c1_.title AS title2, c2_.salary AS salary3, c2_.department AS department4, c2_.startDate AS startDate5, c0_.discr AS discr6, c0_.spouse_id AS spouse_id7, c1_.car_id AS car_id8 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id WHERE EXISTS (SELECT 1 FROM company_persons_friends c3_ INNER JOIN company_persons c4_ ON c3_.friend_id = c4_.id WHERE c3_.person_id = c0_.id AND c4_.id = ?)', - $q3->getSql() + $q->getSql() + ); + } + + public function testSupportsMemberOfWithSingleValuedAssociation() + { + // Impossible example, but it illustrates the purpose + $q = $this->_em->createQuery('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.email MEMBER OF u.groups'); + + $this->assertEquals( + 'SELECT c0_.id AS id0 FROM cms_users c0_ WHERE EXISTS (SELECT 1 FROM cms_users_groups c1_ INNER JOIN cms_groups c2_ ON c1_.group_id = c2_.id WHERE c1_.user_id = c0_.id AND c2_.id = c0_.email_id)', + $q->getSql() + ); + } + + public function testSupportsMemberOfWithIdentificationVariable() + { + // Impossible example, but it illustrates the purpose + $q = $this->_em->createQuery('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u MEMBER OF u.groups'); + + $this->assertEquals( + 'SELECT c0_.id AS id0 FROM cms_users c0_ WHERE EXISTS (SELECT 1 FROM cms_users_groups c1_ INNER JOIN cms_groups c2_ ON c1_.group_id = c2_.id WHERE c1_.user_id = c0_.id AND c2_.id = c0_.id)', + $q->getSql() ); }