Skip to content

Commit 000e079

Browse files
committed
Add the Result::getColumnName method
This method allows introspecting the shape of results by knowing the name of columns.
1 parent 754e3ee commit 000e079

File tree

20 files changed

+284
-0
lines changed

20 files changed

+284
-0
lines changed

UPGRADE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ awareness about deprecated code.
1313
* Upgrade to MariaDB 10.5 or later.
1414
* Upgrade to MySQL 8.0 or later.
1515

16+
## Add `Result::getColumnName()`
17+
18+
Driver and middleware results need to implement a new method `getColumnName()` that gives access to the
19+
column name. Not doing so is deprecated.
20+
1621
# Upgrade to 4.0
1722

1823
## BC BREAK: removed `AbstractMySQLPlatform` methods.

phpstan.neon.dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ parameters:
103103

104104
# Required for Psalm compatibility
105105
- '~^Property Doctrine\\DBAL\\Tests\\Types\\BaseDateTypeTestCase\:\:\$currentTimezone \(non-empty-string\) does not accept string\.$~'
106+
107+
# Type check for legacy implementations of the Result interface
108+
# TODO: remove in 5.0.0
109+
- '~^Call to function method_exists\(\) with Doctrine\\DBAL\\Driver\\Result and ''getColumnName'' will always evaluate to true\.$~'
106110
includes:
107111
- vendor/phpstan/phpstan-phpunit/extension.neon
108112
- vendor/phpstan/phpstan-phpunit/rules.neon

psalm.xml.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@
282282
</TypeDoesNotContainNull>
283283
<TypeDoesNotContainType>
284284
<errorLevel type="suppress">
285+
<!-- See https://github.com/vimeo/psalm/issues/11008 -->
286+
<file name="src/Driver/SQLite3/Result.php"/>
285287
<!-- Ignore isset() checks in destructors. -->
286288
<file name="src/Driver/PgSQL/Connection.php"/>
287289
<file name="src/Driver/PgSQL/Statement.php"/>

src/Cache/ArrayResult.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
use Doctrine\DBAL\Driver\FetchUtils;
88
use Doctrine\DBAL\Driver\Result;
9+
use Doctrine\DBAL\Exception\InvalidColumnIndex;
910

11+
use function array_keys;
1012
use function array_values;
1113
use function count;
1214
use function reset;
@@ -84,6 +86,15 @@ public function columnCount(): int
8486
return $this->columnCount;
8587
}
8688

89+
public function getColumnName(int $index): string
90+
{
91+
if ($this->data === [] || $index > count($this->data[0])) {
92+
throw InvalidColumnIndex::new($index);
93+
}
94+
95+
return array_keys($this->data[0])[$index];
96+
}
97+
8798
public function free(): void
8899
{
89100
$this->data = [];

src/Driver/IBMDB2/Result.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Doctrine\DBAL\Driver\FetchUtils;
88
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
99
use Doctrine\DBAL\Driver\Result as ResultInterface;
10+
use Doctrine\DBAL\Exception\InvalidColumnIndex;
1011

1112
use function db2_fetch_array;
1213
use function db2_fetch_assoc;
@@ -99,6 +100,17 @@ public function columnCount(): int
99100
return 0;
100101
}
101102

103+
public function getColumnName(int $index): string
104+
{
105+
$name = db2_field_name($this->statement, $index);
106+
107+
if ($name === false) {
108+
throw InvalidColumnIndex::new($index);
109+
}
110+
111+
return $name;
112+
}
113+
102114
public function free(): void
103115
{
104116
db2_free_result($this->statement);

src/Driver/Middleware/AbstractResultMiddleware.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
namespace Doctrine\DBAL\Driver\Middleware;
66

77
use Doctrine\DBAL\Driver\Result;
8+
use LogicException;
9+
10+
use function get_debug_type;
11+
use function method_exists;
12+
use function sprintf;
813

914
abstract class AbstractResultMiddleware implements Result
1015
{
@@ -61,6 +66,18 @@ public function columnCount(): int
6166
return $this->wrappedResult->columnCount();
6267
}
6368

69+
public function getColumnName(int $index): string
70+
{
71+
if (! method_exists($this->wrappedResult, 'getColumnName')) {
72+
throw new LogicException(sprintf(
73+
'The driver result %s does not support accessing the column name.',
74+
get_debug_type($this->wrappedResult),
75+
));
76+
}
77+
78+
return $this->wrappedResult->getColumnName($index);
79+
}
80+
6481
public function free(): void
6582
{
6683
$this->wrappedResult->free();

src/Driver/Mysqli/Result.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Doctrine\DBAL\Driver\FetchUtils;
99
use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
1010
use Doctrine\DBAL\Driver\Result as ResultInterface;
11+
use Doctrine\DBAL\Exception\InvalidColumnIndex;
1112
use mysqli_sql_exception;
1213
use mysqli_stmt;
1314

@@ -157,6 +158,11 @@ public function columnCount(): int
157158
return $this->statement->field_count;
158159
}
159160

161+
public function getColumnName(int $index): string
162+
{
163+
return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index);
164+
}
165+
160166
public function free(): void
161167
{
162168
$this->statement->free_result();

src/Driver/OCI8/Result.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Doctrine\DBAL\Driver\FetchUtils;
99
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
1010
use Doctrine\DBAL\Driver\Result as ResultInterface;
11+
use Doctrine\DBAL\Exception\InvalidColumnIndex;
1112

1213
use function oci_cancel;
1314
use function oci_error;
@@ -95,6 +96,18 @@ public function columnCount(): int
9596
return 0;
9697
}
9798

99+
public function getColumnName(int $index): string
100+
{
101+
// OCI expects a 1-based index while DBAL works with a O-based index.
102+
$name = @oci_field_name($this->statement, $index + 1);
103+
104+
if ($name === false) {
105+
throw InvalidColumnIndex::new($index);
106+
}
107+
108+
return $name;
109+
}
110+
98111
public function free(): void
99112
{
100113
oci_cancel($this->statement);

src/Driver/PDO/Result.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Doctrine\DBAL\Driver\PDO;
66

77
use Doctrine\DBAL\Driver\Result as ResultInterface;
8+
use Doctrine\DBAL\Exception\InvalidColumnIndex;
89
use PDO;
910
use PDOException;
1011
use PDOStatement;
@@ -73,6 +74,21 @@ public function columnCount(): int
7374
}
7475
}
7576

77+
public function getColumnName(int $index): string
78+
{
79+
try {
80+
$meta = $this->statement->getColumnMeta($index);
81+
82+
if ($meta === false) {
83+
throw InvalidColumnIndex::new($index);
84+
}
85+
86+
return $meta['name'];
87+
} catch (PDOException $exception) {
88+
throw Exception::new($exception);
89+
}
90+
}
91+
7692
public function free(): void
7793
{
7894
$this->statement->closeCursor();

src/Driver/PgSQL/Result.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
use Doctrine\DBAL\Driver\FetchUtils;
88
use Doctrine\DBAL\Driver\PgSQL\Exception\UnexpectedValue;
99
use Doctrine\DBAL\Driver\Result as ResultInterface;
10+
use Doctrine\DBAL\Exception\InvalidColumnIndex;
1011
use PgSql\Result as PgSqlResult;
12+
use ValueError;
1113

1214
use function array_keys;
1315
use function array_map;
@@ -145,6 +147,19 @@ public function columnCount(): int
145147
return pg_num_fields($this->result);
146148
}
147149

150+
public function getColumnName(int $index): string
151+
{
152+
if ($this->result === null) {
153+
throw InvalidColumnIndex::new($index);
154+
}
155+
156+
try {
157+
return pg_field_name($this->result, $index);
158+
} catch (ValueError) {
159+
throw InvalidColumnIndex::new($index);
160+
}
161+
}
162+
148163
public function free(): void
149164
{
150165
if ($this->result === null) {

src/Driver/Result.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
/**
88
* Driver-level statement execution result.
9+
*
10+
* @method string getColumnName(int $index)
911
*/
1012
interface Result
1113
{

src/Driver/SQLSrv/Result.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

77
use Doctrine\DBAL\Driver\FetchUtils;
88
use Doctrine\DBAL\Driver\Result as ResultInterface;
9+
use Doctrine\DBAL\Exception\InvalidColumnIndex;
910

1011
use function sqlsrv_fetch;
1112
use function sqlsrv_fetch_array;
13+
use function sqlsrv_field_metadata;
1214
use function sqlsrv_num_fields;
1315
use function sqlsrv_rows_affected;
1416

@@ -87,6 +89,17 @@ public function columnCount(): int
8789
return 0;
8890
}
8991

92+
public function getColumnName(int $index): string
93+
{
94+
$meta = sqlsrv_field_metadata($this->statement);
95+
96+
if ($meta === false || ! isset($meta[$index])) {
97+
throw InvalidColumnIndex::new($index);
98+
}
99+
100+
return $meta[$index]['Name'];
101+
}
102+
90103
public function free(): void
91104
{
92105
// emulate it by fetching and discarding rows, similarly to what PDO does in this case

src/Driver/SQLite3/Result.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Doctrine\DBAL\Driver\FetchUtils;
88
use Doctrine\DBAL\Driver\Result as ResultInterface;
9+
use Doctrine\DBAL\Exception\InvalidColumnIndex;
910
use SQLite3Result;
1011

1112
use const SQLITE3_ASSOC;
@@ -76,6 +77,21 @@ public function columnCount(): int
7677
return $this->result->numColumns();
7778
}
7879

80+
public function getColumnName(int $index): string
81+
{
82+
if ($this->result === null) {
83+
throw InvalidColumnIndex::new($index);
84+
}
85+
86+
$name = $this->result->columnName($index);
87+
88+
if ($name === false) {
89+
throw InvalidColumnIndex::new($index);
90+
}
91+
92+
return $name;
93+
}
94+
7995
public function free(): void
8096
{
8197
if ($this->result === null) {

src/Exception/InvalidColumnIndex.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Exception;
6+
7+
use Doctrine\DBAL\Exception;
8+
use LogicException;
9+
10+
use function sprintf;
11+
12+
/** @psalm-immutable */
13+
final class InvalidColumnIndex extends LogicException implements Exception
14+
{
15+
public static function new(int $index): self
16+
{
17+
return new self(sprintf('Invalid column index "%s".', $index));
18+
}
19+
}

src/Portability/Converter.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use function array_reduce;
1212
use function is_string;
1313
use function rtrim;
14+
use function strtolower;
15+
use function strtoupper;
1416

1517
use const CASE_LOWER;
1618
use const CASE_UPPER;
@@ -26,6 +28,7 @@ final class Converter
2628
private readonly Closure $convertAllNumeric;
2729
private readonly Closure $convertAllAssociative;
2830
private readonly Closure $convertFirstColumn;
31+
private readonly Closure $convertColumnName;
2932

3033
/**
3134
* @param bool $convertEmptyStringToNull Whether each empty string should
@@ -48,6 +51,12 @@ public function __construct(bool $convertEmptyStringToNull, bool $rightTrimStrin
4851
$this->convertAllNumeric = $this->createConvertAll($convertNumeric);
4952
$this->convertAllAssociative = $this->createConvertAll($convertAssociative);
5053
$this->convertFirstColumn = $this->createConvertAll($convertValue);
54+
55+
$this->convertColumnName = match ($case) {
56+
null => static fn (string $name) => $name,
57+
self::CASE_LOWER => strtolower(...),
58+
self::CASE_UPPER => strtoupper(...),
59+
};
5160
}
5261

5362
/**
@@ -105,6 +114,11 @@ public function convertFirstColumn(array $data): array
105114
return ($this->convertFirstColumn)($data);
106115
}
107116

117+
public function convertColumnName(string $name): string
118+
{
119+
return ($this->convertColumnName)($name);
120+
}
121+
108122
/**
109123
* @param T $value
110124
*

src/Portability/Result.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,11 @@ public function fetchFirstColumn(): array
6565
parent::fetchFirstColumn(),
6666
);
6767
}
68+
69+
public function getColumnName(int $index): string
70+
{
71+
return $this->converter->convertColumnName(
72+
parent::getColumnName($index),
73+
);
74+
}
6875
}

0 commit comments

Comments
 (0)