Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Statement::fetchAllKeyValue() and ::iterateKeyValue() #4338

Merged
merged 1 commit into from
Oct 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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