diff --git a/lib/Doctrine/Hydrator/ArrayDriver.php b/lib/Doctrine/Hydrator/ArrayDriver.php index 0850ed0e2..e1fdc21a9 100644 --- a/lib/Doctrine/Hydrator/ArrayDriver.php +++ b/lib/Doctrine/Hydrator/ArrayDriver.php @@ -89,4 +89,37 @@ public function setLastElement(&$prev, &$coll, $index, $dqlAlias, $oneToOne) } } } -} \ No newline at end of file + + protected function beforeAddingAggregateValue($rowData, $cache, $dqlAlias, $value) + { + $rowData = $this->addSelectedRelationToRowData($rowData, $dqlAlias, $cache); + + $rowData = $this->addIdentifierColumnToRowData($rowData, $cache, $dqlAlias, $value); + + return $rowData; + } + + private function addSelectedRelationToRowData($rowData, $dqlAlias, $cache) + { + if (!isset($rowData[$dqlAlias])) { + if ($cache['isRelation']) { + $rowData[$dqlAlias] = array(); + + foreach ($cache['identifiers'] as $identifierField) { + $rowData[$dqlAlias][$identifierField] = null; + } + } + } + + return $rowData; + } + + private function addIdentifierColumnToRowData($rowData, $cache, $dqlAlias, $value) + { + if ($cache['isIdentifier'] && !isset($rowData[$dqlAlias][$cache['columnName']])) { + $rowData[$dqlAlias][$cache['columnName']] = $value; + } + + return $rowData; + } +} diff --git a/lib/Doctrine/Hydrator/Graph.php b/lib/Doctrine/Hydrator/Graph.php index b227f3874..f37f7ad49 100644 --- a/lib/Doctrine/Hydrator/Graph.php +++ b/lib/Doctrine/Hydrator/Graph.php @@ -312,11 +312,25 @@ protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents) $fieldName = $table->getFieldName($last); $cache[$key]['fieldName'] = $fieldName; + $cache[$key]['identifiers'] = (array) $table->getIdentifier(); + + $cache[$key]['isRelation'] = isset($this->_queryComponents[$cache[$key]['dqlAlias']]['relation']); + + if (isset($this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$fieldName])) { + $cache[$key]['isAgg'] = true; + $cache[$key]['aliasName'] = $this->_queryComponents[$cache[$key]['dqlAlias']]['agg'][$fieldName]; + } else { + $cache[$key]['isAgg'] = false; + $cache[$key]['aliasName'] = $fieldName; + } + if (isset($this->_queryComponents[$cache[$key]['dqlAlias']]['agg_field'][$last])) { - $fieldName = $this->_queryComponents[$cache[$key]['dqlAlias']]['agg_field'][$last]; + $cache[$key]['columnName'] = $this->_queryComponents[$cache[$key]['dqlAlias']]['agg_field'][$last]; + } else { + $cache[$key]['columnName'] = $fieldName; } - if ($table->isIdentifier($fieldName)) { + if ($table->isIdentifier($cache[$key]['columnName'])) { $cache[$key]['isIdentifier'] = true; } else { $cache[$key]['isIdentifier'] = false; @@ -334,12 +348,8 @@ protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents) $map = $this->_queryComponents[$cache[$key]['dqlAlias']]; $table = $map['table']; $dqlAlias = $cache[$key]['dqlAlias']; - $fieldName = $cache[$key]['fieldName']; - $agg = false; - if (isset($this->_queryComponents[$dqlAlias]['agg'][$fieldName])) { - $fieldName = $this->_queryComponents[$dqlAlias]['agg'][$fieldName]; - $agg = true; - } + $fieldName = $cache[$key]['aliasName']; + $agg = $cache[$key]['isAgg']; if ($cache[$key]['isIdentifier']) { $id[$dqlAlias] .= '|' . $value; @@ -355,7 +365,10 @@ protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents) // Hydrate aggregates in to the root component as well. // So we know that all aggregate values will always be available in the root component if ($agg) { + $rowData = $this->beforeAddingAggregateValue($rowData, $cache[$key], $dqlAlias, $preparedValue); + $rowData[$this->_rootAlias][$fieldName] = $preparedValue; + if (isset($rowData[$dqlAlias])) { $rowData[$dqlAlias][$fieldName] = $preparedValue; } @@ -371,6 +384,11 @@ protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents) return $rowData; } + protected function beforeAddingAggregateValue($rowData, $cache, $dqlAlias, $value) + { + return $rowData; + } + abstract public function getElementCollection($component); abstract public function registerCollection($coll); diff --git a/lib/Doctrine/Query.php b/lib/Doctrine/Query.php index a4d0b40f8..498da9283 100644 --- a/lib/Doctrine/Query.php +++ b/lib/Doctrine/Query.php @@ -470,11 +470,7 @@ public function processPendingFields($componentAlias) if (in_array('*', $fields)) { $fields = $table->getFieldNames(); } else { - $driverClassName = $this->_hydrator->getHydratorDriverClassName(); - // only auto-add the primary key fields if this query object is not - // a subquery of another query object or we're using a child of the Object Graph - // hydrator - if ( ! $this->_isSubquery && is_subclass_of($driverClassName, 'Doctrine_Hydrator_Graph')) { + if ($this->shouldAutoSelectIdentifiers()) { $fields = array_unique(array_merge((array) $table->getIdentifier(), $fields)); } } @@ -504,6 +500,18 @@ public function processPendingFields($componentAlias) return implode(', ', $sql); } + /* + * only auto-add the primary key fields if this query object is not + * a subquery of another query object or we're using + * a child of the Object Graph hydrator + */ + private function shouldAutoSelectIdentifiers() + { + $driverClassName = $this->_hydrator->getHydratorDriverClassName(); + + return ! $this->_isSubquery && is_subclass_of($driverClassName, 'Doctrine_Hydrator_Graph'); + } + /** * Parses a nested field * @@ -615,6 +623,7 @@ public function parseSelect($dql) $terms = $this->_tokenizer->sqlExplode($reference, ' '); $pos = strpos($terms[0], '('); + $isColumnSelect = $pos === false; if (count($terms) > 1 || $pos !== false) { $expression = array_shift($terms); @@ -647,6 +656,8 @@ public function parseSelect($dql) $this->_expressionMap[$alias][0] = $expression; $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; + $this->_queryComponents[$componentAlias]['has_selected_column'] ??= false; + $this->_queryComponents[$componentAlias]['has_selected_column'] |= $isColumnSelect; if (preg_match('/^([^\(]+)\.(\'?)(.*?)(\'?)$/', $expression, $field)) { $this->_queryComponents[$componentAlias]['agg_field'][$index] = $field[3]; @@ -668,6 +679,27 @@ public function parseSelect($dql) $this->_pendingFields[$componentAlias][] = $field; } } + + $this->appendRelationIdentifierOnSqlSelect(); + } + + private function appendRelationIdentifierOnSqlSelect() + { + if ($this->shouldAutoSelectIdentifiers()) { + foreach ($this->_queryComponents as $componentAlias => $queryComponent) { + if ( + isset($queryComponent['relation']) + && isset($queryComponent['agg']) + && !empty($queryComponent['has_selected_column']) + ) { + $table = $queryComponent['table']; + + foreach ((array) $table->getIdentifier() as $field) { + $this->_pendingFields[$componentAlias][] = $field; + } + } + } + } } /** diff --git a/tests/Ticket/585TestCase.php b/tests/Ticket/585TestCase.php index 9c2ed8f5c..f75631d7a 100644 --- a/tests/Ticket/585TestCase.php +++ b/tests/Ticket/585TestCase.php @@ -63,7 +63,7 @@ public function test_hydrateArrayShallow_withAllColumnsAliased_thenResultsHasAll public function test_hydrateArray_withAllColumnsAliased_thenResultsHasAllRecords() { $hydrateType = Doctrine_Core::HYDRATE_ARRAY; - $expectedKeys = array('aliasId', 'aliasName'); + $expectedKeys = array('id', 'aliasId', 'aliasName'); $this->doTestWithAllColumnsAliased($hydrateType, $expectedKeys); } diff --git a/tests/Ticket/GH134TestCase.php b/tests/Ticket/GH134TestCase.php new file mode 100644 index 000000000..f6cbf0d17 --- /dev/null +++ b/tests/Ticket/GH134TestCase.php @@ -0,0 +1,177 @@ +provideIdentifierAndRelationWithAliasData() as [$hydrateType, $expectedSql, $expectedFirstResult]) { + $query = Doctrine_Query::create() + ->select('u.id, e.address as aliasAddress') + ->from('User u') + ->innerJoin('u.Email e') + ; + + $results = $query->execute(array(), $hydrateType); + + $this->assertEqual($expectedSql, $query->getSqlQuery()); + + foreach ($results as $firstResult) { + break; + } + + if (Doctrine_Core::HYDRATE_RECORD === $hydrateType + || Doctrine_Core::HYDRATE_ON_DEMAND === $hydrateType + ) { + $firstResult = $firstResult->toArray(); + + $firstResult = array_intersect_key($firstResult, $expectedFirstResult); + ksort($firstResult); + ksort($expectedFirstResult); + + if (isset($firstResult['Email'])) { + $firstResult['Email'] = array_intersect_key($firstResult['Email'], $expectedFirstResult['Email']); + ksort($firstResult['Email']); + ksort($expectedFirstResult['Email']); + } + } + + $this->assertEqual($expectedFirstResult, $firstResult); + + if (Doctrine_Core::HYDRATE_ON_DEMAND !== $hydrateType) { + $this->assertEqual(count($this->users), count($results)); + } + } + } + + private function provideIdentifierAndRelationWithAliasData() + { + yield [ + Doctrine_Core::HYDRATE_ARRAY, + 'SELECT e.id AS e__id, e2.id AS e2__id, e2.address AS e2__0 FROM entity e INNER JOIN email e2 ON e.email_id = e2.id WHERE (e.type = 0)', + array ( + 'id' => '4', + 'aliasAddress' => 'zYne@example.com', + 'Email' => array ( + 'id' => 1, + 'aliasAddress' => 'zYne@example.com', + ), + ), + ]; + + yield [ + Doctrine_Core::HYDRATE_ARRAY_SHALLOW, + 'SELECT e.id AS e__id, e2.address AS e2__0 FROM entity e INNER JOIN email e2 ON e.email_id = e2.id WHERE (e.type = 0)', + array ( + 'id' => '4', + 'aliasAddress' => 'zYne@example.com', + ), + ]; + + yield [ + Doctrine_Core::HYDRATE_SCALAR, + 'SELECT e.id AS e__id, e2.address AS e2__0 FROM entity e INNER JOIN email e2 ON e.email_id = e2.id WHERE (e.type = 0)', + array ( + 'u_id' => '4', + 'e_aliasAddress' => 'zYne@example.com', + ), + ]; + + yield [ + Doctrine_Core::HYDRATE_RECORD, + 'SELECT e.id AS e__id, e2.id AS e2__id, e2.address AS e2__0 FROM entity e INNER JOIN email e2 ON e.email_id = e2.id WHERE (e.type = 0)', + array ( + 'id' => '4', + 'aliasAddress' => 'zYne@example.com', + 'Email' => array ( + 'id' => 1, + 'aliasAddress' => 'zYne@example.com', + ), + ), + ]; + + yield [ + Doctrine_Core::HYDRATE_ON_DEMAND, + 'SELECT e.id AS e__id, e2.id AS e2__id, e2.address AS e2__0 FROM entity e INNER JOIN email e2 ON e.email_id = e2.id WHERE (e.type = 0)', + array ( + 'id' => '4', + 'aliasAddress' => 'zYne@example.com', + 'Email' => array ( + 'id' => 1, + 'aliasAddress' => 'zYne@example.com', + ), + ), + ]; + } + + public function test_addIdentifierForSelectedRelation_withoutAlias() + { + foreach ($this->provideIdentifierAndRelationWithoutAliasData() as [$hydrateType, $expectedSql, $expectedFirstResult]) { + $query = Doctrine_Query::create() + ->select('u.id, e.address') + ->from('User u') + ->innerJoin('u.Email e') + ; + + $results = $query->execute(array(), $hydrateType); + + $this->assertEqual($expectedSql, $query->getSqlQuery()); + + $this->assertEqual($expectedFirstResult, $results[0]); + $this->assertEqual(count($this->users), count($results)); + } + } + + private function provideIdentifierAndRelationWithoutAliasData() + { + yield [ + Doctrine_Core::HYDRATE_ARRAY, + 'SELECT e.id AS e__id, e2.id AS e2__id, e2.address AS e2__address FROM entity e INNER JOIN email e2 ON e.email_id = e2.id WHERE (e.type = 0)', + array ( + 'id' => '4', + 'Email' => array ( + 'id' => 1, + 'address' => 'zYne@example.com', + ), + ), + ]; + + yield [ + Doctrine_Core::HYDRATE_ARRAY_SHALLOW, + 'SELECT e.id AS e__id, e2.address AS e2__address FROM entity e INNER JOIN email e2 ON e.email_id = e2.id WHERE (e.type = 0)', + array ( + 'id' => '4', + 'address' => 'zYne@example.com', + ), + ]; + + yield [ + Doctrine_Core::HYDRATE_SCALAR, + 'SELECT e.id AS e__id, e2.address AS e2__address FROM entity e INNER JOIN email e2 ON e.email_id = e2.id WHERE (e.type = 0)', + array ( + 'u_id' => '4', + 'e_address' => 'zYne@example.com', + ), + ]; + } + + public function test_columnAliasWithSameNameAsIdentifier_willKeepOverwriteIdentifierByAlias() + { + foreach ($this->provideKeepOverwriteIdentifierWithAliasData() as [$hydrateType, $checkArrayIndex]) { + $query = Doctrine_Query::create() + ->select('99 as id, u.id as aliasId') + ->from('User u') + ; + + $results = $query->execute(array(), $hydrateType); + + $this->assertEqual(99, $results[0][$checkArrayIndex]); + } + } + + private function provideKeepOverwriteIdentifierWithAliasData() + { + yield [Doctrine_Core::HYDRATE_SCALAR, 'u_id']; + yield [Doctrine_Core::HYDRATE_ARRAY, 'id']; + yield [Doctrine_Core::HYDRATE_ARRAY_SHALLOW, 'id']; + } +}