From 5b19d31166660da6c406bbfee24ce4b9d5e58980 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 20 Jun 2025 18:33:28 +0700 Subject: [PATCH 1/9] Add optional type casting to `DataReaderInterface` using columns --- src/Driver/Pdo/AbstractPdoCommand.php | 8 ++- src/Driver/Pdo/PdoDataReader.php | 27 +++++++- src/Query/DataReaderInterface.php | 9 +++ tests/AbstractColumnTest.php | 89 +++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/src/Driver/Pdo/AbstractPdoCommand.php b/src/Driver/Pdo/AbstractPdoCommand.php index 2fa32836a..88f7ddac7 100644 --- a/src/Driver/Pdo/AbstractPdoCommand.php +++ b/src/Driver/Pdo/AbstractPdoCommand.php @@ -251,7 +251,13 @@ 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) { + $dataReader->columns($this->getResultColumns(array_keys($row))); + } + + return $dataReader; } if ($queryMode === self::QUERY_MODE_EXECUTE) { diff --git a/src/Driver/Pdo/PdoDataReader.php b/src/Driver/Pdo/PdoDataReader.php index 0b11348a0..7118c19c8 100644 --- a/src/Driver/Pdo/PdoDataReader.php +++ b/src/Driver/Pdo/PdoDataReader.php @@ -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; @@ -30,6 +31,8 @@ */ final class PdoDataReader implements DataReaderInterface { + /** @var ColumnInterface[] */ + private array $columns = []; /** @psalm-var IndexBy|null $indexBy */ private Closure|string|null $indexBy = null; private int $index = 0; @@ -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->columns)) { + foreach ($this->columns as $key => $column) { + $row[$key] = $column->phpTypecast($row[$key]); + } + } + + if ($this->resultCallback === null) { + return $row; } - return ($this->resultCallback)($this->row); + return ($this->resultCallback)($row); } /** @@ -139,4 +154,10 @@ public function resultCallback(Closure|null $resultCallback): static $this->resultCallback = $resultCallback; return $this; } + + public function columns(array $columns): static + { + $this->columns = $columns; + return $this; + } } diff --git a/src/Query/DataReaderInterface.php b/src/Query/DataReaderInterface.php index 250d5c4b8..4ed617f97 100644 --- a/src/Query/DataReaderInterface.php +++ b/src/Query/DataReaderInterface.php @@ -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. @@ -48,6 +49,14 @@ public function key(): int|string|null; */ public function current(): array|object|false; + /** + * 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[] $columns + */ + public function columns(array $columns): static; + /** * Sets `indexBy` property. * diff --git a/tests/AbstractColumnTest.php b/tests/AbstractColumnTest.php index 093abe252..d109b24ab 100644 --- a/tests/AbstractColumnTest.php +++ b/tests/AbstractColumnTest.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; +use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; use Yiisoft\Db\Schema\Column\ColumnInterface; use Yiisoft\Db\Tests\Provider\ColumnProvider; @@ -15,6 +16,94 @@ abstract class AbstractColumnTest extends TestCase { + abstract protected function getConnection(bool $fixture = false): PdoConnectionInterface; + + abstract protected function insertTypeValues(PdoConnectionInterface $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(); + } + #[DataProviderExternal(ColumnProvider::class, 'predefinedTypes')] public function testPredefinedType(string $className, string $type, string $phpType) { From 53db0a8ab5ee2de264f3cf604373f40289aa325d Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 20 Jun 2025 18:47:27 +0700 Subject: [PATCH 2/9] Fix tests --- tests/AbstractColumnTest.php | 88 ------------------------------- tests/Common/CommonColumnTest.php | 86 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 88 deletions(-) diff --git a/tests/AbstractColumnTest.php b/tests/AbstractColumnTest.php index d109b24ab..67932a94d 100644 --- a/tests/AbstractColumnTest.php +++ b/tests/AbstractColumnTest.php @@ -16,94 +16,6 @@ abstract class AbstractColumnTest extends TestCase { - abstract protected function getConnection(bool $fixture = false): PdoConnectionInterface; - - abstract protected function insertTypeValues(PdoConnectionInterface $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(); - } - #[DataProviderExternal(ColumnProvider::class, 'predefinedTypes')] public function testPredefinedType(string $className, string $type, string $phpType) { diff --git a/tests/Common/CommonColumnTest.php b/tests/Common/CommonColumnTest.php index a044878da..05a74cb4e 100644 --- a/tests/Common/CommonColumnTest.php +++ b/tests/Common/CommonColumnTest.php @@ -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(); From 8ff58f80d27df9dbb7f81f0a462c7a683af303c8 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 20 Jun 2025 11:48:41 +0000 Subject: [PATCH 3/9] Apply fixes from StyleCI --- tests/AbstractColumnTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/AbstractColumnTest.php b/tests/AbstractColumnTest.php index 67932a94d..093abe252 100644 --- a/tests/AbstractColumnTest.php +++ b/tests/AbstractColumnTest.php @@ -7,7 +7,6 @@ use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; -use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; use Yiisoft\Db\Schema\Column\ColumnInterface; use Yiisoft\Db\Tests\Provider\ColumnProvider; From 7b29a3acfb61b968ba0c903d39d447f358e0a9f4 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 20 Jun 2025 19:06:45 +0700 Subject: [PATCH 4/9] Fix psalm --- src/Driver/Pdo/AbstractPdoCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Driver/Pdo/AbstractPdoCommand.php b/src/Driver/Pdo/AbstractPdoCommand.php index 88f7ddac7..66d4cb736 100644 --- a/src/Driver/Pdo/AbstractPdoCommand.php +++ b/src/Driver/Pdo/AbstractPdoCommand.php @@ -254,6 +254,7 @@ protected function internalGetQueryResult(int $queryMode): mixed $dataReader = new PdoDataReader($this->pdoStatement); if ($this->phpTypecasting && ($row = $dataReader->current()) !== false) { + /** @var array $row */ $dataReader->columns($this->getResultColumns(array_keys($row))); } From 557731fa214949612e6504baa306690796261bb1 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 20 Jun 2025 19:09:58 +0700 Subject: [PATCH 5/9] Add lines to CHANGELOG.md and UPGRADE.md --- CHANGELOG.md | 1 + UPGRADE.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a222faa00..eb3ee456d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,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 diff --git a/UPGRADE.md b/UPGRADE.md index fec63ab92..6348a8e7f 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -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::columns()` - sets columns for type casting the query results; ### Remove methods From 72e851ff3e24f9c90e2f9a8050d5eb5565f56bcd Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 21 Jun 2025 01:08:13 +0700 Subject: [PATCH 6/9] Rename `DataReaderInterface::columns()` to `typecastColumns()` --- UPGRADE.md | 2 +- src/Driver/Pdo/AbstractPdoCommand.php | 2 +- src/Driver/Pdo/PdoDataReader.php | 2 +- src/Query/DataReaderInterface.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 6348a8e7f..8dd038e88 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -158,7 +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::columns()` - sets columns for type casting the query results; +- `DataReaderInterface::typecastColumns()` - sets columns for type casting the query results; ### Remove methods diff --git a/src/Driver/Pdo/AbstractPdoCommand.php b/src/Driver/Pdo/AbstractPdoCommand.php index 66d4cb736..1f914285c 100644 --- a/src/Driver/Pdo/AbstractPdoCommand.php +++ b/src/Driver/Pdo/AbstractPdoCommand.php @@ -255,7 +255,7 @@ protected function internalGetQueryResult(int $queryMode): mixed if ($this->phpTypecasting && ($row = $dataReader->current()) !== false) { /** @var array $row */ - $dataReader->columns($this->getResultColumns(array_keys($row))); + $dataReader->typecastColumns($this->getResultColumns(array_keys($row))); } return $dataReader; diff --git a/src/Driver/Pdo/PdoDataReader.php b/src/Driver/Pdo/PdoDataReader.php index 7118c19c8..755b22874 100644 --- a/src/Driver/Pdo/PdoDataReader.php +++ b/src/Driver/Pdo/PdoDataReader.php @@ -155,7 +155,7 @@ public function resultCallback(Closure|null $resultCallback): static return $this; } - public function columns(array $columns): static + public function typecastColumns(array $columns): static { $this->columns = $columns; return $this; diff --git a/src/Query/DataReaderInterface.php b/src/Query/DataReaderInterface.php index 4ed617f97..955b99657 100644 --- a/src/Query/DataReaderInterface.php +++ b/src/Query/DataReaderInterface.php @@ -55,7 +55,7 @@ public function current(): array|object|false; * * @param ColumnInterface[] $columns */ - public function columns(array $columns): static; + public function typecastColumns(array $columns): static; /** * Sets `indexBy` property. From 0bb906917fd849d2b65bcf17336871daea5ce31e Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 21 Jun 2025 01:10:37 +0700 Subject: [PATCH 7/9] Rename `PdoDataReader::columns` to `typecastColumns` property --- src/Driver/Pdo/PdoDataReader.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Driver/Pdo/PdoDataReader.php b/src/Driver/Pdo/PdoDataReader.php index 755b22874..867185f0f 100644 --- a/src/Driver/Pdo/PdoDataReader.php +++ b/src/Driver/Pdo/PdoDataReader.php @@ -32,7 +32,7 @@ final class PdoDataReader implements DataReaderInterface { /** @var ColumnInterface[] */ - private array $columns = []; + private array $typecastColumns = []; /** @psalm-var IndexBy|null $indexBy */ private Closure|string|null $indexBy = null; private int $index = 0; @@ -108,8 +108,8 @@ public function current(): array|object|false return false; } - if (!empty($this->columns)) { - foreach ($this->columns as $key => $column) { + if (!empty($this->typecastColumns)) { + foreach ($this->typecastColumns as $key => $column) { $row[$key] = $column->phpTypecast($row[$key]); } } @@ -155,9 +155,9 @@ public function resultCallback(Closure|null $resultCallback): static return $this; } - public function typecastColumns(array $columns): static + public function typecastColumns(array $typecastColumns): static { - $this->columns = $columns; + $this->typecastColumns = $typecastColumns; return $this; } } From 5cabc2f3f9c5f4a96af011e47bd1862a62fe3719 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 21 Jun 2025 01:11:30 +0700 Subject: [PATCH 8/9] Rearrange properties --- src/Driver/Pdo/PdoDataReader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Driver/Pdo/PdoDataReader.php b/src/Driver/Pdo/PdoDataReader.php index 867185f0f..b7127dcfa 100644 --- a/src/Driver/Pdo/PdoDataReader.php +++ b/src/Driver/Pdo/PdoDataReader.php @@ -31,14 +31,14 @@ */ final class PdoDataReader implements DataReaderInterface { - /** @var ColumnInterface[] */ - private array $typecastColumns = []; /** @psalm-var IndexBy|null $indexBy */ private Closure|string|null $indexBy = null; private int $index = 0; /** @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. From 0ce161bf04109fba83cb75b8da52b44b873053a6 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 21 Jun 2025 01:13:21 +0700 Subject: [PATCH 9/9] Rearrange methods and properties --- src/Query/DataReaderInterface.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Query/DataReaderInterface.php b/src/Query/DataReaderInterface.php index 955b99657..aa95e235a 100644 --- a/src/Query/DataReaderInterface.php +++ b/src/Query/DataReaderInterface.php @@ -49,14 +49,6 @@ public function key(): int|string|null; */ public function current(): array|object|false; - /** - * 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[] $columns - */ - public function typecastColumns(array $columns): static; - /** * Sets `indexBy` property. * @@ -94,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; }