From 652d293784e8f9637312102e4456f27a202a2979 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 17 Aug 2024 19:58:20 +0200 Subject: [PATCH] added Mapper, replaces rowNormalizer (BC break) --- src/Database/Connection.php | 24 +++++-- src/Database/Helpers.php | 17 ----- src/Database/Mapping/Mapper.php | 19 ++++++ src/Database/ResultSet.php | 63 ++++++++++--------- src/Database/Table/Selection.php | 2 +- .../Database/ResultSet.customNormalizer.phpt | 5 +- tests/Database/ResultSet.mapper.phpt | 58 +++++++++++++++++ 7 files changed, 134 insertions(+), 54 deletions(-) create mode 100644 src/Database/Mapping/Mapper.php create mode 100644 tests/Database/ResultSet.mapper.phpt diff --git a/src/Database/Connection.php b/src/Database/Connection.php index 8be9a443a..febff982b 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -30,9 +30,7 @@ class Connection private ?Drivers\Engine $engine; private ?SqlPreprocessor $preprocessor; private TypeConverter $typeConverter; - - /** @var callable(array, ResultSet): array */ - private $rowNormalizer = [Helpers::class, 'normalizeRow']; + private ?Mapping\Mapper $mapper = null; private ?string $sql = null; private int $transactionDepth = 0; @@ -143,13 +141,27 @@ public function getTypeConverter(): TypeConverter } - public function setRowNormalizer(?callable $normalizer): static + /** @internal experimental feature */ + public function setMapper(?Mapping\Mapper $mapper): static { - $this->rowNormalizer = $normalizer; + $this->mapper = $mapper; return $this; } + public function getMapper(): ?Mapping\Mapper + { + return $this->mapper; + } + + + /** @deprecated use setMapper() */ + public function setRowNormalizer(?callable $normalizer): static + { + throw new Nette\DeprecatedException(__METHOD__ . "() is deprecated, use setMapper() or configure 'convert*' options."); + } + + public function getInsertId(?string $sequence = null): int|string { return $this->getConnectionDriver()->getInsertId($sequence); @@ -230,7 +242,7 @@ public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] $time = microtime(true); $result = $this->connection->query($this->sql, $params); $time = microtime(true) - $time; - $resultSet = new ResultSet($this, $result, new SqlLiteral($this->sql, $params), $this->rowNormalizer, $time); + $resultSet = new ResultSet($this, $result, new SqlLiteral($this->sql, $params), $time); } catch (DriverException $e) { Arrays::invoke($this->onQuery, $this, $e); throw $e; diff --git a/src/Database/Helpers.php b/src/Database/Helpers.php index 79e84f86a..2ef57fc6a 100644 --- a/src/Database/Helpers.php +++ b/src/Database/Helpers.php @@ -151,23 +151,6 @@ public static function dumpSql(string $sql, ?array $params = null, ?Connection $ } - /** @internal */ - public static function normalizeRow( - array $row, - ResultSet $resultSet, - $dateTimeClass = DateTime::class, - ): array - { - foreach ($resultSet->resolveColumnConverters() as $key => $converter) { - $value = $row[$key]; - $row[$key] = isset($value, $converter) - ? $converter($value) - : $value; - } - return $row; - } - - /** * Import SQL dump from file - extremely fast. * @param ?array $onProgress diff --git a/src/Database/Mapping/Mapper.php b/src/Database/Mapping/Mapper.php new file mode 100644 index 000000000..6dc6b920d --- /dev/null +++ b/src/Database/Mapping/Mapper.php @@ -0,0 +1,19 @@ +normalizer = $normalizer; } @@ -76,15 +72,6 @@ public function getTime(): float } - /** @internal */ - public function normalizeRow(array $row): array - { - return $this->normalizer - ? ($this->normalizer)($row, $this) - : $row; - } - - /********************* misc tools ****************d*g**/ @@ -136,26 +123,25 @@ public function valid(): bool } + /********************* fetch ****************d*g**/ + + /** * Fetches single row object. */ - public function fetch(): ?Row + public function fetch(): mixed { - $data = $this->result->fetch(); - if ($data === null) { + $row = $this->result->fetch(); + if ($row === null) { return null; - } elseif ($this->lastRow === null && count($data) !== $this->result->getColumnCount()) { + } elseif ($this->lastRow === null && count($row) !== $this->result->getColumnCount()) { $duplicates = array_filter(array_count_values(array_column($this->result->getColumnsInfo(), 'name')), fn($val) => $val > 1); trigger_error("Found duplicate columns in database result set: '" . implode("', '", array_keys($duplicates)) . "'."); } - $row = new Row; - foreach ($this->normalizeRow($data) as $key => $value) { - if ($key !== '') { - $row->$key = $value; - } - } + $row = $this->convertTypes($row); + $row = $this->connection->getMapper()?->mapRow($row, $this) ?? $this->mapRow($row); $this->lastRowKey++; return $this->lastRow = $row; @@ -218,12 +204,23 @@ public function fetchAssoc(string $path): array } - public function resolveColumnConverters(): array - { - if (isset($this->converters)) { - return $this->converters; + /** @internal */ + public function convertTypes(array $row): array + { + $converters = $this->converters ??= $this->resolveColumnConverters(); + foreach ($row as $key => $value) { + $converter = $converters[$key]; + $row[$key] = isset($value, $converter) + ? $converter($value) + : $value; } + return $row; + } + + + private function resolveColumnConverters(): array + { $res = []; $engine = $this->connection->getDatabaseEngine(); $converter = $this->connection->getTypeConverter(); @@ -232,6 +229,16 @@ public function resolveColumnConverters(): array ? $engine->resolveColumnConverter($meta, $converter) : null; } - return $this->converters = $res; + return $res; + } + + + private function mapRow(?array $row): Row + { + $res = new Row; + foreach ($row as $key => $value) { + $res->$key = $value; + } + return $res; } } diff --git a/src/Database/Table/Selection.php b/src/Database/Table/Selection.php index ab049caba..6eefd28ba 100644 --- a/src/Database/Table/Selection.php +++ b/src/Database/Table/Selection.php @@ -521,7 +521,7 @@ protected function execute(): void $usedPrimary = true; $key = 0; while ($row = $result->fetchAssociative()) { - $row = $this->createRow($result->normalizeRow($row)); + $row = $this->createRow($result->convertTypes($row)); $primary = $row->getSignature(false); $usedPrimary = $usedPrimary && $primary !== ''; $this->rows[$usedPrimary ? $primary : $key] = $row; diff --git a/tests/Database/ResultSet.customNormalizer.phpt b/tests/Database/ResultSet.customNormalizer.phpt index 4da4189ae..463b99512 100644 --- a/tests/Database/ResultSet.customNormalizer.phpt +++ b/tests/Database/ResultSet.customNormalizer.phpt @@ -16,10 +16,11 @@ Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName $connection->query('UPDATE author SET born=?', new DateTime('2022-01-23')); +// TODO test('disabled normalization', function () use ($connection) { $driverName = $GLOBALS['driverName']; - $connection->setRowNormalizer(null); + $connection->getTypeConverter()->convertDateTime = false; $res = $connection->query('SELECT * FROM author'); $asInt = $driverName === 'pgsql' || ($driverName !== 'sqlsrv' && PHP_VERSION_ID >= 80100); Assert::same([ @@ -34,7 +35,7 @@ test('disabled normalization', function () use ($connection) { test('custom normalization', function () use ($connection) { $driverName = $GLOBALS['driverName']; - $connection->setRowNormalizer(function (array $row, Nette\Database\ResultSet $resultSet) { + @$connection->setRowNormalizer(function (array $row, Nette\Database\ResultSet $resultSet) { // deprecated foreach ($row as $key => $value) { unset($row[$key]); $row['_' . $key . '_'] = (string) $value; diff --git a/tests/Database/ResultSet.mapper.phpt b/tests/Database/ResultSet.mapper.phpt new file mode 100644 index 000000000..c24032292 --- /dev/null +++ b/tests/Database/ResultSet.mapper.phpt @@ -0,0 +1,58 @@ +getConnection(); +Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); + +$connection->query('UPDATE author SET born=?', new DateTime('2022-01-23')); + + +test('disabled normalization', function () use ($connection) { + $driverName = $GLOBALS['driverName']; + + $connection->setMapper(null); + $res = $connection->query('SELECT * FROM author'); + $asInt = $driverName === 'pgsql' || ($driverName !== 'sqlsrv' && PHP_VERSION_ID >= 80100); + Assert::same([ + 'id' => $asInt ? 11 : '11', + 'name' => 'Jakub Vrana', + 'web' => 'http://www.vrana.cz/', + 'born' => $driverName === 'sqlite' ? ($asInt ? 1_642_892_400 : '1642892400') : '2022-01-23', + ], (array) $res->fetch()); +}); + + +class TestMapper implements Mapping\Mapper +{ + public function mapRow(array $row): array + { + foreach ($row as $key => $value) { + unset($row[$key]); + $row['_' . $key . '_'] = (string) $value; + } + return $row; + } +} + +test('custom normalization', function () use ($connection) { + $driverName = $GLOBALS['driverName']; + + $connection->setMapper(new TestMapper); + $res = $connection->query('SELECT * FROM author'); + Assert::same([ + '_id_' => '11', + '_name_' => 'Jakub Vrana', + '_web_' => 'http://www.vrana.cz/', + '_born_' => $driverName === 'sqlite' ? '1642892400' : '2022-01-23', + ], (array) $res->fetch()); +});