diff --git a/docs/en/reference/data-retrieval-and-manipulation.rst b/docs/en/reference/data-retrieval-and-manipulation.rst index 8a88deb6a64..0efba4c466c 100644 --- a/docs/en/reference/data-retrieval-and-manipulation.rst +++ b/docs/en/reference/data-retrieval-and-manipulation.rst @@ -379,6 +379,25 @@ Execute the query and fetch all results into an array: ) */ +fetchAllKeyValue() +~~~~~~~~~~~~~~~~~~ + +Execute the query and fetch the first two columns into an associative array as keys and values respectively: + +.. code-block:: php + + fetchAllKeyValue('SELECT username, password FROM user'); + + /* + array( + 'jwage' => 'changeme', + ) + */ + +.. note:: + All additional columns will be ignored and are only allowed to be selected by DBAL for its internal purposes. + fetchNumeric() ~~~~~~~~~~~~~~ @@ -425,6 +444,21 @@ Retrieve associative array of the first result row. There are also convenience methods for data manipulation queries: +iterateKeyValue() +~~~~~~~~~~~~~~~~~ + +Execute the query and iterate over the first two columns as keys and values respectively: + +.. code-block:: php + + iterateKeyValue('SELECT username, password FROM user') as $username => $password) { + // ... + } + +.. note:: + All additional columns will be ignored and are only allowed to be selected by DBAL for its internal purposes. + delete() ~~~~~~~~~ diff --git a/lib/Doctrine/DBAL/Connection.php b/lib/Doctrine/DBAL/Connection.php index cd5f6e4da7e..1d9477431f9 100644 --- a/lib/Doctrine/DBAL/Connection.php +++ b/lib/Doctrine/DBAL/Connection.php @@ -15,6 +15,7 @@ use Doctrine\DBAL\Driver\ServerInfoAwareConnection; use Doctrine\DBAL\Exception\ConnectionLost; use Doctrine\DBAL\Exception\InvalidArgumentException; +use Doctrine\DBAL\Exception\NoKeyValue; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; use Doctrine\DBAL\Query\QueryBuilder; @@ -985,6 +986,33 @@ public function fetchAllAssociative(string $query, array $params = [], array $ty } } + /** + * Prepares and executes an SQL query and returns the result as an associative array with the keys + * mapped to the first column and the values mapped to the second column. + * + * @param string $query The SQL query. + * @param array|array $params The query parameters. + * @param array|array $types The query parameter types. + * + * @return array + * + * @throws Exception + */ + public function fetchAllKeyValue(string $query, array $params = [], array $types = []): array + { + $stmt = $this->executeQuery($query, $params, $types); + + $this->ensureHasKeyValue($stmt); + + $data = []; + + foreach ($stmt->fetchAll(FetchMode::NUMERIC) as [$key, $value]) { + $data[$key] = $value; + } + + return $data; + } + /** * Prepares and executes an SQL query and returns the result as an array of the first column values. * @@ -1068,6 +1096,29 @@ public function iterateAssociative(string $query, array $params = [], array $typ } } + /** + * Prepares and executes an SQL query and returns the result as an iterator with the keys + * mapped to the first column and the values mapped to the second column. + * + * @param string $query The SQL query. + * @param array|array $params The query parameters. + * @param array|array $types The query parameter types. + * + * @return Traversable + * + * @throws Exception + */ + public function iterateKeyValue(string $query, array $params = [], array $types = []): Traversable + { + $stmt = $this->executeQuery($query, $params, $types); + + $this->ensureHasKeyValue($stmt); + + while (($row = $stmt->fetch(FetchMode::NUMERIC)) !== false) { + yield $row[0] => $row[1]; + } + } + /** * Prepares and executes an SQL query and returns the result as an iterator over the first column values. * @@ -2055,4 +2106,13 @@ private function throw(Exception $e): void throw $e; } + + private function ensureHasKeyValue(ResultStatement $stmt): void + { + $columnCount = $stmt->columnCount(); + + if ($columnCount < 2) { + throw NoKeyValue::fromColumnCount($columnCount); + } + } } diff --git a/lib/Doctrine/DBAL/Exception/NoKeyValue.php b/lib/Doctrine/DBAL/Exception/NoKeyValue.php new file mode 100644 index 00000000000..34093704aa1 --- /dev/null +++ b/lib/Doctrine/DBAL/Exception/NoKeyValue.php @@ -0,0 +1,25 @@ + + * + * @throws Exception + */ + public function fetchAllKeyValue(): array + { + $this->ensureHasKeyValue(); + + $data = []; + + foreach ($this->fetchAllNumeric() as [$key, $value]) { + $data[$key] = $value; + } + + return $data; + } + /** * {@inheritdoc} * @@ -441,6 +464,25 @@ public function iterateAssociative(): Traversable } } + /** + * Returns an iterator over the result set with the keys mapped to the first column + * and the values mapped to the second column. + * + * The result must contain at least two columns. + * + * @return Traversable + * + * @throws Exception + */ + public function iterateKeyValue(): Traversable + { + $this->ensureHasKeyValue(); + + foreach ($this->iterateNumeric() as [$key, $value]) { + yield $key => $value; + } + } + /** * {@inheritDoc} * @@ -495,4 +537,13 @@ public function getWrappedStatement() { return $this->stmt; } + + private function ensureHasKeyValue(): void + { + $columnCount = $this->columnCount(); + + if ($columnCount < 2) { + throw NoKeyValue::fromColumnCount($columnCount); + } + } } diff --git a/tests/Doctrine/Tests/DBAL/Functional/Connection/FetchTest.php b/tests/Doctrine/Tests/DBAL/Functional/Connection/FetchTest.php index d33fc1708e9..49854b3e121 100644 --- a/tests/Doctrine/Tests/DBAL/Functional/Connection/FetchTest.php +++ b/tests/Doctrine/Tests/DBAL/Functional/Connection/FetchTest.php @@ -2,6 +2,8 @@ namespace Doctrine\Tests\DBAL\Functional\Connection; +use Doctrine\DBAL\Exception\NoKeyValue; +use Doctrine\DBAL\Platforms\SQLServer2012Platform; use Doctrine\Tests\DbalFunctionalTestCase; use Doctrine\Tests\TestUtil; @@ -77,6 +79,53 @@ public function testFetchAllAssociative(): void ], $this->connection->fetchAllAssociative($this->query)); } + public function testFetchAllKeyValue(): void + { + self::assertEquals([ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ], $this->connection->fetchAllKeyValue($this->query)); + } + + public function testStatementFetchAllKeyValue(): void + { + $stmt = $this->connection->prepare($this->query); + $stmt->execute(); + + self::assertEquals([ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ], $stmt->fetchAllKeyValue()); + } + + /** + * This test covers the requirement for the statement result to have at least two columns, + * not exactly two as PDO requires. + */ + public function testFetchAllKeyValueWithLimit(): void + { + $platform = $this->connection->getDatabasePlatform(); + + if ($platform instanceof SQLServer2012Platform) { + self::markTestSkipped('See https://github.com/doctrine/dbal/issues/2374'); + } + + $query = $platform->modifyLimitQuery($this->query, 1, 1); + + self::assertEquals(['bar' => 2], $this->connection->fetchAllKeyValue($query)); + } + + public function testFetchAllKeyValueOneColumn(): void + { + $sql = $this->connection->getDatabasePlatform() + ->getDummySelectSQL(); + + $this->expectException(NoKeyValue::class); + $this->connection->fetchAllKeyValue($sql); + } + public function testFetchFirstColumn(): void { self::assertEquals([ @@ -113,6 +162,36 @@ public function testIterateAssociative(): void ], iterator_to_array($this->connection->iterateAssociative($this->query))); } + public function testIterateKeyValue(): void + { + self::assertEquals([ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ], iterator_to_array($this->connection->iterateKeyValue($this->query))); + } + + public function testStatementKeyValue(): void + { + $stmt = $this->connection->prepare($this->query); + $stmt->execute(); + + self::assertEquals([ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ], iterator_to_array($stmt->iterateKeyValue())); + } + + public function testIterateKeyValueOneColumn(): void + { + $sql = $this->connection->getDatabasePlatform() + ->getDummySelectSQL(); + + $this->expectException(NoKeyValue::class); + iterator_to_array($this->connection->iterateKeyValue($sql)); + } + public function testIterateColumn(): void { self::assertEquals([