Skip to content

Commit f88cb81

Browse files
committed
PDOStatement replaced by Result
1 parent 8fe50f9 commit f88cb81

8 files changed

Lines changed: 158 additions & 62 deletions

File tree

src/Database/Drivers/Connection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
interface Connection
1717
{
18-
function query(string $sql, array $params = []);
18+
function query(string $sql, array $params = []): Result;
1919

2020
function getNativeConnection(): mixed;
2121

src/Database/Drivers/PDO/Connection.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function __construct(
3030
}
3131

3232

33-
public function query(string $sql, array $params = [])
33+
public function query(string $sql, array $params = []): Result
3434
{
3535
$types = ['boolean' => PDO::PARAM_BOOL, 'integer' => PDO::PARAM_INT, 'resource' => PDO::PARAM_LOB, 'NULL' => PDO::PARAM_NULL];
3636

@@ -39,9 +39,8 @@ public function query(string $sql, array $params = [])
3939
$statement->bindValue(is_int($key) ? $key + 1 : $key, $value, $types[gettype($value)] ?? PDO::PARAM_STR);
4040
}
4141

42-
$statement->setFetchMode(PDO::FETCH_ASSOC);
4342
$statement->execute();
44-
return $statement;
43+
return new Result($statement, $this);
4544
}
4645

4746

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Framework (https://nette.org)
5+
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Nette\Database\Drivers\PDO;
11+
12+
use Nette\Database\DriverException;
13+
use Nette\Database\Drivers;
14+
15+
16+
class Result implements Drivers\Result
17+
{
18+
private array $columns;
19+
20+
21+
public function __construct(
22+
protected readonly \PDOStatement $result,
23+
protected readonly Connection $connection,
24+
) {
25+
}
26+
27+
28+
public function fetch(): ?array
29+
{
30+
$row = $this->result->fetch(\PDO::FETCH_NUM);
31+
if ($row) {
32+
return $this->toAssociative($row);
33+
}
34+
$this->free();
35+
return null;
36+
}
37+
38+
39+
private function toAssociative(array $row): array
40+
{
41+
$res = [];
42+
foreach ($this->getColumnsInfo() as $i => $meta) {
43+
$res[$meta['name']] = $row[$i];
44+
}
45+
return $res;
46+
}
47+
48+
49+
public function getColumnCount(): int
50+
{
51+
return $this->result->columnCount();
52+
}
53+
54+
55+
public function getRowCount(): int
56+
{
57+
return $this->result->rowCount();
58+
}
59+
60+
61+
public function getColumnsInfo(): array
62+
{
63+
return $this->columns ??= $this->collectColumnsInfo();
64+
}
65+
66+
67+
protected function collectColumnsInfo(): array
68+
{
69+
$res = [];
70+
$count = $this->result->columnCount();
71+
for ($i = 0; $i < $count; $i++) {
72+
$meta = $this->result->getColumnMeta($i) ?: throw new DriverException('Cannot fetch column metadata');
73+
$res[] = [
74+
'name' => $meta['name'],
75+
'nativeType' => $meta[$this->connection->metaTypeKey] ?? null,
76+
'length' => $meta['len'],
77+
'precision' => $meta['precision'],
78+
];
79+
}
80+
return $res;
81+
}
82+
83+
84+
public function free(): void
85+
{
86+
$this->result->closeCursor();
87+
}
88+
}

src/Database/Drivers/Result.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Framework (https://nette.org)
5+
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Nette\Database\Drivers;
11+
12+
13+
interface Result
14+
{
15+
/**
16+
* Fetches the row at current position and moves the internal cursor to the next position.
17+
*/
18+
function fetch(): ?array;
19+
20+
/**
21+
* Returns the number of columns in a result set.
22+
*/
23+
function getColumnCount(): int;
24+
25+
/**
26+
* Returns the number of rows in a result set.
27+
*/
28+
function getRowCount(): int;
29+
30+
/**
31+
* Returns metadata for all columns in a result set.
32+
* @return list<array{name: string, nativeType: ?string, length: ?int, precision: ?int}>
33+
*/
34+
function getColumnsInfo(): array;
35+
36+
/**
37+
* Discards the remaining result set, allowing the statement to be re-executed.
38+
*/
39+
function free(): void;
40+
}

src/Database/Helpers.php

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -291,29 +291,6 @@ public static function toPairs(array $rows, string|int|\Closure|null $key, strin
291291
}
292292

293293

294-
/**
295-
* Finds duplicate columns in select statement
296-
*/
297-
public static function findDuplicates(\PDOStatement $statement): string
298-
{
299-
$cols = [];
300-
for ($i = 0; $i < $statement->columnCount(); $i++) {
301-
$meta = $statement->getColumnMeta($i);
302-
$cols[$meta['name']][] = $meta['table'] ?? '';
303-
}
304-
305-
$duplicates = [];
306-
foreach ($cols as $name => $tables) {
307-
if (count($tables) > 1) {
308-
$tables = array_filter(array_unique($tables));
309-
$duplicates[] = "'$name'" . ($tables ? ' (from ' . implode(', ', $tables) . ')' : '');
310-
}
311-
}
312-
313-
return implode(', ', $duplicates);
314-
}
315-
316-
317294
/** @return array{type: string, length: ?null, scale: ?null, parameters: ?string} */
318295
public static function parseColumnType(string $type): array
319296
{

src/Database/Result.php

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*/
1818
class Result implements \Iterator
1919
{
20-
private ?\PDOStatement $pdoStatement = null;
20+
private ?Drivers\Result $result = null;
2121

2222
/** @var callable(array, Result): array */
2323
private readonly mixed $normalizer;
@@ -44,7 +44,7 @@ public function __construct(
4444
if (str_starts_with($queryString, '::')) {
4545
$connection->getConnectionDriver()->{substr($queryString, 2)}();
4646
} else {
47-
$this->pdoStatement = $connection->getConnectionDriver()->query($queryString, $params);
47+
$this->result = $connection->getConnectionDriver()->query($queryString, $params);
4848
}
4949
} catch (\PDOException $e) {
5050
$e = $connection->getDatabaseEngine()->convertException($e);
@@ -65,15 +65,6 @@ public function getConnection(): Connection
6565
}
6666

6767

68-
/**
69-
* @internal
70-
*/
71-
public function getPdoStatement(): ?\PDOStatement
72-
{
73-
return $this->pdoStatement;
74-
}
75-
76-
7768
public function getQueryString(): string
7869
{
7970
return $this->queryString;
@@ -88,13 +79,13 @@ public function getParameters(): array
8879

8980
public function getColumnCount(): ?int
9081
{
91-
return $this->pdoStatement ? $this->pdoStatement->columnCount() : null;
82+
return $this->result?->getColumnCount();
9283
}
9384

9485

9586
public function getRowCount(): ?int
9687
{
97-
return $this->pdoStatement ? $this->pdoStatement->rowCount() : null;
88+
return $this->result?->getRowCount();
9889
}
9990

10091

@@ -169,14 +160,13 @@ public function valid(): bool
169160
*/
170161
public function fetch(): ?Row
171162
{
172-
$data = $this->pdoStatement ? $this->pdoStatement->fetch() : null;
173-
if (!$data) {
174-
$this->pdoStatement->closeCursor();
163+
$data = $this->result?->fetch();
164+
if ($data === null) {
175165
return null;
176166

177-
} elseif ($this->lastRow === null && count($data) !== $this->pdoStatement->columnCount()) {
178-
$duplicates = Helpers::findDuplicates($this->pdoStatement);
179-
trigger_error("Found duplicate columns in database result set: $duplicates.");
167+
} elseif ($this->lastRow === null && count($data) !== $this->result->getColumnCount()) {
168+
$duplicates = array_filter(array_count_values(array_column($this->result->getColumnsInfo(), 'name')), fn($val) => $val > 1);
169+
trigger_error("Found duplicate columns in database result set: '" . implode("', '", array_keys($duplicates)) . "'.");
180170
}
181171

182172
$row = new Row;
@@ -191,6 +181,13 @@ public function fetch(): ?Row
191181
}
192182

193183

184+
/** @internal */
185+
public function fetchAssociative(): ?array
186+
{
187+
return $this->result?->fetch();
188+
}
189+
190+
194191
/**
195192
* Fetches single field.
196193
*/
@@ -249,17 +246,10 @@ public function resolveColumnConverters(): array
249246
$res = [];
250247
$engine = $this->connection->getDatabaseEngine();
251248
$converter = $this->connection->getTypeConverter();
252-
$metaTypeKey = $this->connection->getConnectionDriver()->metaTypeKey;
253-
$count = $this->pdoStatement->columnCount();
254-
for ($i = 0; $i < $count; $i++) {
255-
$meta = $this->pdoStatement->getColumnMeta($i);
256-
if (isset($meta[$metaTypeKey])) {
257-
$res[$meta['name']] = $engine->resolveColumnConverter([
258-
'nativeType' => $meta[$metaTypeKey],
259-
'length' => $meta['len'],
260-
'precision' => $meta['precision'],
261-
], $converter);
262-
}
249+
foreach ($this->result->getColumnsInfo() as $meta) {
250+
$res[$meta['name']] = isset($meta['nativeType'])
251+
? $engine->resolveColumnConverter($meta, $converter)
252+
: null;
263253
}
264254
return $this->converters = $res;
265255
}

src/Database/Table/Selection.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,11 +519,13 @@ protected function execute(): void
519519

520520
$this->rows = [];
521521
$usedPrimary = true;
522-
foreach ($result->getPdoStatement() as $key => $row) {
522+
$key = 0;
523+
while ($row = $result->fetchAssociative()) {
523524
$row = $this->createRow($result->normalizeRow($row));
524525
$primary = $row->getSignature(false);
525526
$usedPrimary = $usedPrimary && $primary !== '';
526527
$this->rows[$usedPrimary ? $primary : $key] = $row;
528+
$key++;
527529
}
528530

529531
$this->data = $this->rows;

tests/Database/Result.fetch().phpt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName
1818
test('', function () use ($connection, $driverName) {
1919
$res = $connection->query('SELECT name, name FROM author');
2020
$message = match ($driverName) {
21-
'mysql' => "Found duplicate columns in database result set: 'name' (from author).",
21+
'mysql' => "Found duplicate columns in database result set: 'name'.",
2222
'pgsql' => "Found duplicate columns in database result set: 'name'%a%",
23-
'sqlite' => "Found duplicate columns in database result set: 'name' (from author).",
23+
'sqlite' => "Found duplicate columns in database result set: 'name'.",
2424
'sqlsrv' => "Found duplicate columns in database result set: 'name'.",
2525
default => Assert::fail("Unsupported driver $driverName"),
2626
};
@@ -54,9 +54,9 @@ test('tests closeCursor()', function () use ($connection, $driverName) {
5454
test('', function () use ($connection, $driverName) {
5555
$res = $connection->query('SELECT book.id, author.id, author.name, translator.name FROM book JOIN author ON (author.id = book.author_id) JOIN author translator ON (translator.id = book.translator_id)');
5656
$message = match ($driverName) {
57-
'mysql' => "Found duplicate columns in database result set: 'id' (from book, author), 'name' (from author, translator).",
57+
'mysql' => "Found duplicate columns in database result set: 'id', 'name'.",
5858
'pgsql' => "Found duplicate columns in database result set: 'id'%a% 'name'%a%",
59-
'sqlite' => "Found duplicate columns in database result set: 'id' (from book, author), 'name' (from author).",
59+
'sqlite' => "Found duplicate columns in database result set: 'id', 'name'.",
6060
'sqlsrv' => "Found duplicate columns in database result set: 'id', 'name'.",
6161
default => Assert::fail("Unsupported driver $driverName"),
6262
};

0 commit comments

Comments
 (0)