Skip to content

Commit

Permalink
Add the Result::getColumnName method
Browse files Browse the repository at this point in the history
This method allows introspecting the shape of results by knowing the
name of columns.
  • Loading branch information
stof committed Jun 7, 2024
1 parent 754e3ee commit fac07c7
Show file tree
Hide file tree
Showing 20 changed files with 284 additions and 0 deletions.
5 changes: 5 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ awareness about deprecated code.
* Upgrade to MariaDB 10.5 or later.
* Upgrade to MySQL 8.0 or later.

## Add `Result::getColumnName()`

Driver and middleware results need to implement a new method `getColumnName()` that gives access to the
column name. Not doing so is deprecated.

# Upgrade to 4.0

## BC BREAK: removed `AbstractMySQLPlatform` methods.
Expand Down
4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ parameters:

# Required for Psalm compatibility
- '~^Property Doctrine\\DBAL\\Tests\\Types\\BaseDateTypeTestCase\:\:\$currentTimezone \(non-empty-string\) does not accept string\.$~'

# Type check for legacy implementations of the Result interface
# TODO: remove in 5.0.0
- '~^Call to function method_exists\(\) with Doctrine\\DBAL\\Driver\\Result and ''getColumnName'' will always evaluate to true\.$~'
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
Expand Down
2 changes: 2 additions & 0 deletions psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@
</TypeDoesNotContainNull>
<TypeDoesNotContainType>
<errorLevel type="suppress">
<!-- See https://github.com/vimeo/psalm/issues/11008 -->
<file name="src/Driver/SQLite3/Result.php"/>
<!-- Ignore isset() checks in destructors. -->
<file name="src/Driver/PgSQL/Connection.php"/>
<file name="src/Driver/PgSQL/Statement.php"/>
Expand Down
11 changes: 11 additions & 0 deletions src/Cache/ArrayResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Exception\InvalidColumnIndex;

use function array_keys;
use function array_values;
use function count;
use function reset;
Expand Down Expand Up @@ -84,6 +86,15 @@ public function columnCount(): int
return $this->columnCount;
}

public function getColumnName(int $index): string
{
if ($this->data === [] || $index > count($this->data[0])) {
throw InvalidColumnIndex::new($index);

Check warning on line 92 in src/Cache/ArrayResult.php

View check run for this annotation

Codecov / codecov/patch

src/Cache/ArrayResult.php#L92

Added line #L92 was not covered by tests
}

return array_keys($this->data[0])[$index];
}

public function free(): void
{
$this->data = [];
Expand Down
12 changes: 12 additions & 0 deletions src/Driver/IBMDB2/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;

use function db2_fetch_array;
use function db2_fetch_assoc;
Expand Down Expand Up @@ -99,6 +100,17 @@ public function columnCount(): int
return 0;
}

public function getColumnName(int $index): string

Check warning on line 103 in src/Driver/IBMDB2/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/IBMDB2/Result.php#L103

Added line #L103 was not covered by tests
{
$name = db2_field_name($this->statement, $index);

Check warning on line 105 in src/Driver/IBMDB2/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/IBMDB2/Result.php#L105

Added line #L105 was not covered by tests

if ($name === false) {
throw InvalidColumnIndex::new($index);

Check warning on line 108 in src/Driver/IBMDB2/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/IBMDB2/Result.php#L107-L108

Added lines #L107 - L108 were not covered by tests
}

return $name;

Check warning on line 111 in src/Driver/IBMDB2/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/IBMDB2/Result.php#L111

Added line #L111 was not covered by tests
}

public function free(): void
{
db2_free_result($this->statement);
Expand Down
17 changes: 17 additions & 0 deletions src/Driver/Middleware/AbstractResultMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
namespace Doctrine\DBAL\Driver\Middleware;

use Doctrine\DBAL\Driver\Result;
use LogicException;

use function get_debug_type;
use function method_exists;
use function sprintf;

abstract class AbstractResultMiddleware implements Result
{
Expand Down Expand Up @@ -61,6 +66,18 @@ public function columnCount(): int
return $this->wrappedResult->columnCount();
}

public function getColumnName(int $index): string
{
if (! method_exists($this->wrappedResult, 'getColumnName')) {
throw new LogicException(sprintf(
'The driver result %s does not support accessing the column name.',
get_debug_type($this->wrappedResult),
));

Check warning on line 75 in src/Driver/Middleware/AbstractResultMiddleware.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/Middleware/AbstractResultMiddleware.php#L72-L75

Added lines #L72 - L75 were not covered by tests
}

return $this->wrappedResult->getColumnName($index);
}

public function free(): void
{
$this->wrappedResult->free();
Expand Down
6 changes: 6 additions & 0 deletions src/Driver/Mysqli/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use mysqli_sql_exception;
use mysqli_stmt;

Expand Down Expand Up @@ -157,6 +158,11 @@ public function columnCount(): int
return $this->statement->field_count;
}

public function getColumnName(int $index): string

Check warning on line 161 in src/Driver/Mysqli/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/Mysqli/Result.php#L161

Added line #L161 was not covered by tests
{
return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index);

Check warning on line 163 in src/Driver/Mysqli/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/Mysqli/Result.php#L163

Added line #L163 was not covered by tests
}

public function free(): void
{
$this->statement->free_result();
Expand Down
13 changes: 13 additions & 0 deletions src/Driver/OCI8/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;

use function oci_cancel;
use function oci_error;
Expand Down Expand Up @@ -95,6 +96,18 @@ public function columnCount(): int
return 0;
}

public function getColumnName(int $index): string

Check warning on line 99 in src/Driver/OCI8/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Result.php#L99

Added line #L99 was not covered by tests
{
// OCI expects a 1-based index while DBAL works with a O-based index.
$name = oci_field_name($this->statement, $index + 1);

Check warning on line 102 in src/Driver/OCI8/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Result.php#L102

Added line #L102 was not covered by tests

if ($name === false) {
throw InvalidColumnIndex::new($index);

Check warning on line 105 in src/Driver/OCI8/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Result.php#L104-L105

Added lines #L104 - L105 were not covered by tests
}

return $name;

Check warning on line 108 in src/Driver/OCI8/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Result.php#L108

Added line #L108 was not covered by tests
}

public function free(): void
{
oci_cancel($this->statement);
Expand Down
16 changes: 16 additions & 0 deletions src/Driver/PDO/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Doctrine\DBAL\Driver\PDO;

use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use PDO;
use PDOException;
use PDOStatement;
Expand Down Expand Up @@ -73,6 +74,21 @@ public function columnCount(): int
}
}

public function getColumnName(int $index): string
{
try {
$meta = $this->statement->getColumnMeta($index);

if ($meta === false) {
throw InvalidColumnIndex::new($index);
}

return $meta['name'];
} catch (PDOException $exception) {
throw Exception::new($exception);

Check warning on line 88 in src/Driver/PDO/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/PDO/Result.php#L88

Added line #L88 was not covered by tests
}
}

public function free(): void
{
$this->statement->closeCursor();
Expand Down
15 changes: 15 additions & 0 deletions src/Driver/PgSQL/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\PgSQL\Exception\UnexpectedValue;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use PgSql\Result as PgSqlResult;
use ValueError;

use function array_keys;
use function array_map;
Expand Down Expand Up @@ -145,6 +147,19 @@ public function columnCount(): int
return pg_num_fields($this->result);
}

public function getColumnName(int $index): string

Check warning on line 150 in src/Driver/PgSQL/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/PgSQL/Result.php#L150

Added line #L150 was not covered by tests
{
if ($this->result === null) {
throw InvalidColumnIndex::new($index);

Check warning on line 153 in src/Driver/PgSQL/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/PgSQL/Result.php#L152-L153

Added lines #L152 - L153 were not covered by tests
}

try {
return pg_field_name($this->result, $index);
} catch (ValueError) {
throw InvalidColumnIndex::new($index);

Check warning on line 159 in src/Driver/PgSQL/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/PgSQL/Result.php#L157-L159

Added lines #L157 - L159 were not covered by tests
}
}

public function free(): void
{
if ($this->result === null) {
Expand Down
2 changes: 2 additions & 0 deletions src/Driver/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

/**
* Driver-level statement execution result.
*
* @method string getColumnName(int $index)
*/
interface Result
{
Expand Down
13 changes: 13 additions & 0 deletions src/Driver/SQLSrv/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;

use function sqlsrv_fetch;
use function sqlsrv_fetch_array;
use function sqlsrv_field_metadata;
use function sqlsrv_num_fields;
use function sqlsrv_rows_affected;

Expand Down Expand Up @@ -87,6 +89,17 @@ public function columnCount(): int
return 0;
}

public function getColumnName(int $index): string
{
$meta = sqlsrv_field_metadata($this->statement);

if ($meta === false || ! isset($meta[$index])) {
throw InvalidColumnIndex::new($index);
}

return $meta[$index]['Name'];
}

public function free(): void
{
// emulate it by fetching and discarding rows, similarly to what PDO does in this case
Expand Down
16 changes: 16 additions & 0 deletions src/Driver/SQLite3/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use SQLite3Result;

use const SQLITE3_ASSOC;
Expand Down Expand Up @@ -76,6 +77,21 @@ public function columnCount(): int
return $this->result->numColumns();
}

public function getColumnName(int $index): string

Check warning on line 80 in src/Driver/SQLite3/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/SQLite3/Result.php#L80

Added line #L80 was not covered by tests
{
if ($this->result === null) {
throw InvalidColumnIndex::new($index);

Check warning on line 83 in src/Driver/SQLite3/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/SQLite3/Result.php#L82-L83

Added lines #L82 - L83 were not covered by tests
}

$name = $this->result->columnName($index);

Check warning on line 86 in src/Driver/SQLite3/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/SQLite3/Result.php#L86

Added line #L86 was not covered by tests

if ($name === false) {
throw InvalidColumnIndex::new($index);

Check warning on line 89 in src/Driver/SQLite3/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/SQLite3/Result.php#L88-L89

Added lines #L88 - L89 were not covered by tests
}

return $name;

Check warning on line 92 in src/Driver/SQLite3/Result.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/SQLite3/Result.php#L92

Added line #L92 was not covered by tests
}

public function free(): void
{
if ($this->result === null) {
Expand Down
19 changes: 19 additions & 0 deletions src/Exception/InvalidColumnIndex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Exception;

use Doctrine\DBAL\Exception;
use LogicException;

use function sprintf;

/** @psalm-immutable */
final class InvalidColumnIndex extends LogicException implements Exception
{
public static function new(int $index): self
{
return new self(sprintf('Invalid column index "%s".', $index));
}
}
14 changes: 14 additions & 0 deletions src/Portability/Converter.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use function array_reduce;
use function is_string;
use function rtrim;
use function strtolower;
use function strtoupper;

use const CASE_LOWER;
use const CASE_UPPER;
Expand All @@ -26,6 +28,7 @@ final class Converter
private readonly Closure $convertAllNumeric;
private readonly Closure $convertAllAssociative;
private readonly Closure $convertFirstColumn;
private readonly Closure $convertColumnName;

/**
* @param bool $convertEmptyStringToNull Whether each empty string should
Expand All @@ -48,6 +51,12 @@ public function __construct(bool $convertEmptyStringToNull, bool $rightTrimStrin
$this->convertAllNumeric = $this->createConvertAll($convertNumeric);
$this->convertAllAssociative = $this->createConvertAll($convertAssociative);
$this->convertFirstColumn = $this->createConvertAll($convertValue);

$this->convertColumnName = match ($case) {
null => static fn (string $name) => $name,
self::CASE_LOWER => strtolower(...),
self::CASE_UPPER => strtoupper(...),
};
}

/**
Expand Down Expand Up @@ -105,6 +114,11 @@ public function convertFirstColumn(array $data): array
return ($this->convertFirstColumn)($data);
}

public function convertColumnName(string $name): string
{
return ($this->convertColumnName)($name);
}

/**
* @param T $value
*
Expand Down
7 changes: 7 additions & 0 deletions src/Portability/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,11 @@ public function fetchFirstColumn(): array
parent::fetchFirstColumn(),
);
}

public function getColumnName(int $index): string
{
return $this->converter->convertColumnName(
parent::getColumnName($index)

Check failure on line 72 in src/Portability/Result.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.3)

Multi-line function calls must have a trailing comma after the last parameter.
);
}
}
Loading

0 comments on commit fac07c7

Please sign in to comment.