Skip to content

Commit

Permalink
Merge pull request #4338 from morozov/issues/4289
Browse files Browse the repository at this point in the history
Add Statement::fetchAllKeyValue() and ::iterateKeyValue()
  • Loading branch information
morozov authored Oct 13, 2020
2 parents 4e3d016 + f1a901e commit e79dcb8
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 0 deletions.
34 changes: 34 additions & 0 deletions docs/en/reference/data-retrieval-and-manipulation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php
$users = $conn->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()
~~~~~~~~~~~~~~

Expand Down Expand Up @@ -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
<?php
foreach ($conn->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()
~~~~~~~~~

Expand Down
60 changes: 60 additions & 0 deletions lib/Doctrine/DBAL/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<int, mixed>|array<string, mixed> $params The query parameters.
* @param array<int, int|string>|array<string, int|string> $types The query parameter types.
*
* @return array<mixed,mixed>
*
* @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.
*
Expand Down Expand Up @@ -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<int, mixed>|array<string, mixed> $params The query parameters.
* @param array<int, int|string>|array<string, int|string> $types The query parameter types.
*
* @return Traversable<mixed,mixed>
*
* @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.
*
Expand Down Expand Up @@ -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);
}
}
}
25 changes: 25 additions & 0 deletions lib/Doctrine/DBAL/Exception/NoKeyValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Doctrine\DBAL\Exception;

use Doctrine\DBAL\Exception;

use function sprintf;

/**
* @internal
*
* @psalm-immutable
*/
final class NoKeyValue extends Exception
{
public static function fromColumnCount(int $columnCount): self
{
return new self(
sprintf(
'Fetching as key-value pairs requires the result to contain at least 2 columns, %d given.',
$columnCount
)
);
}
}
51 changes: 51 additions & 0 deletions lib/Doctrine/DBAL/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Doctrine\DBAL\Abstraction\Result;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\Statement as DriverStatement;
use Doctrine\DBAL\Exception\NoKeyValue;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use IteratorAggregate;
Expand Down Expand Up @@ -375,6 +376,28 @@ public function fetchAllAssociative(): array
}
}

/**
* Returns an associative array 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 array<mixed,mixed>
*
* @throws Exception
*/
public function fetchAllKeyValue(): array
{
$this->ensureHasKeyValue();

$data = [];

foreach ($this->fetchAllNumeric() as [$key, $value]) {
$data[$key] = $value;
}

return $data;
}

/**
* {@inheritdoc}
*
Expand Down Expand Up @@ -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<mixed,mixed>
*
* @throws Exception
*/
public function iterateKeyValue(): Traversable
{
$this->ensureHasKeyValue();

foreach ($this->iterateNumeric() as [$key, $value]) {
yield $key => $value;
}
}

/**
* {@inheritDoc}
*
Expand Down Expand Up @@ -495,4 +537,13 @@ public function getWrappedStatement()
{
return $this->stmt;
}

private function ensureHasKeyValue(): void
{
$columnCount = $this->columnCount();

if ($columnCount < 2) {
throw NoKeyValue::fromColumnCount($columnCount);
}
}
}
79 changes: 79 additions & 0 deletions tests/Doctrine/Tests/DBAL/Functional/Connection/FetchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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([
Expand Down

0 comments on commit e79dcb8

Please sign in to comment.