Skip to content

Add optional type casting to DataReaderInterface using columns #992

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

Merged
merged 11 commits into from
Jun 24, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
- Enh #982: Reduce binding parameters (@Tigrov)
- New #984: Add `createQuery()` and `select()` methods to `ConnectionInterface` (@Tigrov)
- Chg #985: Rename `insertWithReturningPks()` to `insertReturningPks()` in `CommandInterface` and `DMLQueryBuilderInterface` (@Tigrov)
- Enh #992: Add optional type casting to `DataReaderInterface` using columns (@Tigrov)

## 1.3.0 March 21, 2024

Expand Down
1 change: 1 addition & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace
- `SchemaInterface::getResultColumn()` - returns the column instance for the column metadata received from the query;
- `AbstractSchema::getResultColumnCacheKey()` - returns the cache key for the column metadata received from the query;
- `AbstractSchema::loadResultColumn()` - creates a new column instance according to the column metadata from the query;
- `DataReaderInterface::typecastColumns()` - sets columns for type casting the query results;

### Remove methods

Expand Down
9 changes: 8 additions & 1 deletion src/Driver/Pdo/AbstractPdoCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,14 @@ protected function internalGetQueryResult(int $queryMode): mixed
{
if ($queryMode === self::QUERY_MODE_CURSOR) {
/** @psalm-suppress PossiblyNullArgument */
return new PdoDataReader($this->pdoStatement);
$dataReader = new PdoDataReader($this->pdoStatement);

if ($this->phpTypecasting && ($row = $dataReader->current()) !== false) {
/** @var array $row */
$dataReader->typecastColumns($this->getResultColumns(array_keys($row)));
}

return $dataReader;
}

if ($queryMode === self::QUERY_MODE_EXECUTE) {
Expand Down
27 changes: 24 additions & 3 deletions src/Driver/Pdo/PdoDataReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Yiisoft\Db\Exception\InvalidCallException;
use Yiisoft\Db\Query\DataReaderInterface;
use Yiisoft\Db\Query\QueryInterface;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function is_string;

Expand All @@ -36,6 +37,8 @@ final class PdoDataReader implements DataReaderInterface
/** @psalm-var ResultCallbackOne|null $resultCallback */
private Closure|null $resultCallback = null;
private array|false $row;
/** @var ColumnInterface[] */
private array $typecastColumns = [];

/**
* @param PDOStatement $statement The PDO statement object that contains the result of the query.
Expand Down Expand Up @@ -99,11 +102,23 @@ public function key(): int|string|null

public function current(): array|object|false
{
if ($this->resultCallback === null || $this->row === false) {
return $this->row;
$row = $this->row;

if ($row === false) {
return false;
}

if (!empty($this->typecastColumns)) {
foreach ($this->typecastColumns as $key => $column) {
$row[$key] = $column->phpTypecast($row[$key]);
}
}

if ($this->resultCallback === null) {
return $row;
}

return ($this->resultCallback)($this->row);
return ($this->resultCallback)($row);
}

/**
Expand Down Expand Up @@ -139,4 +154,10 @@ public function resultCallback(Closure|null $resultCallback): static
$this->resultCallback = $resultCallback;
return $this;
}

public function typecastColumns(array $typecastColumns): static
{
$this->typecastColumns = $typecastColumns;
return $this;
}
}
9 changes: 9 additions & 0 deletions src/Query/DataReaderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Closure;
use Countable;
use Iterator;
use Yiisoft\Db\Schema\Column\ColumnInterface;

/**
* This interface represents a forward-only stream of rows from a query result set.
Expand Down Expand Up @@ -85,4 +86,12 @@ public function indexBy(Closure|string|null $indexBy): static;
* @psalm-param ResultCallbackOne|null $resultCallback
*/
public function resultCallback(Closure|null $resultCallback): static;

/**
* Sets the columns for type casting the query results.
* Do not use this method if you want to get the raw data from the query.
*
* @param ColumnInterface[] $typecastColumns
*/
public function typecastColumns(array $typecastColumns): static;
}
86 changes: 86 additions & 0 deletions tests/Common/CommonColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,92 @@ abstract class CommonColumnTest extends AbstractColumnTest

protected const COLUMN_BUILDER = ColumnBuilder::class;

abstract protected function insertTypeValues(ConnectionInterface $db): void;

abstract protected function assertTypecastedValues(array $result, bool $allTypecasted = false): void;

public function testQueryWithTypecasting(): void
{
$db = $this->getConnection(true);

$this->insertTypeValues($db);

$query = $db->createQuery()->from('type')->withTypecasting();

$result = $query->one();

$this->assertTypecastedValues($result);

$result = $query->all();

$this->assertTypecastedValues($result[0]);

$result = iterator_to_array($query->each());

$this->assertTypecastedValues($result[0]);

$result = iterator_to_array($query->batch());

$this->assertTypecastedValues($result[0][0]);

$result = $db->select(['float_col'])->from('type')->withTypecasting()->column();

$this->assertSame(1.234, $result[0]);

$db->close();
}

public function testCommandWithPhpTypecasting(): void
{
$db = $this->getConnection(true);

$this->insertTypeValues($db);

$quotedTableName = $db->getQuoter()->quoteSimpleTableName('type');
$command = $db->createCommand("SELECT * FROM $quotedTableName")->withPhpTypecasting();

$result = $command->queryOne();

$this->assertTypecastedValues($result);

$result = $command->queryAll();

$this->assertTypecastedValues($result[0]);

$result = iterator_to_array($command->query());

$this->assertTypecastedValues($result[0]);

$quotedColumnName = $db->getQuoter()->quoteSimpleColumnName('float_col');
$result = $db->createCommand("SELECT $quotedColumnName FROM $quotedTableName")
->withPhpTypecasting()
->queryColumn();

$this->assertSame(1.234, $result[0]);

$db->close();
}

public function testPhpTypecast(): void
{
$db = $this->getConnection(true);
$columns = $db->getTableSchema('type')->getColumns();

$this->insertTypeValues($db);

$query = $db->createQuery()->from('type')->one();

$result = [];

foreach ($columns as $columnName => $column) {
$result[$columnName] = $column->phpTypecast($query[$columnName]);
}

$this->assertTypecastedValues($result, true);

$db->close();
}

public function createDateTimeColumnTable(ConnectionInterface $db): void
{
$schema = $db->getSchema();
Expand Down