From da6764dac70377e5000a3827a4893e64e2d95b6d Mon Sep 17 00:00:00 2001 From: Unlink Date: Thu, 2 Jun 2016 11:48:27 +0200 Subject: [PATCH 1/2] ResultSet: configurable row normalization --- src/Database/Connection.php | 8 +- src/Database/IRowNormalizer.php | 23 +++++ src/Database/ResultSet.php | 87 ++++++++++--------- src/Database/RowNormalizer.php | 65 ++++++++++++++ src/Database/Table/Selection.php | 10 ++- .../ResultSet.customNormalization.phpt | 34 ++++++++ tests/Database/Table/Table.join.phpt | 4 +- 7 files changed, 183 insertions(+), 48 deletions(-) create mode 100644 src/Database/IRowNormalizer.php create mode 100644 src/Database/RowNormalizer.php create mode 100644 tests/Database/ResultSet.customNormalization.phpt diff --git a/src/Database/Connection.php b/src/Database/Connection.php index 27f1cbdd5..e4d0bb678 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -40,6 +40,9 @@ class Connection /** @var PDO */ private $pdo; + /** @var IRowNormalizer */ + private $rowNormalizer; + public function __construct($dsn, $user = NULL, $password = NULL, array $options = NULL) { @@ -69,6 +72,9 @@ public function connect() throw ConnectionException::from($e); } + $rowNormalizer = empty($this->options['rowNormalizer']) ? 'Nette\Database\RowNormalizer' : $this->options['rowNormalizer']; + $this->rowNormalizer = new $rowNormalizer; + $class = empty($this->options['driverClass']) ? 'Nette\Database\Drivers\\' . ucfirst(str_replace('sql', 'Sql', $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME))) . 'Driver' : $this->options['driverClass']; @@ -175,7 +181,7 @@ public function query($sql, ...$params) { list($sql, $params) = $this->preprocess($sql, ...$params); try { - $result = new ResultSet($this, $sql, $params); + $result = new ResultSet($this, $sql, $params, $this->rowNormalizer); } catch (PDOException $e) { $this->onQuery($this, $e); throw $e; diff --git a/src/Database/IRowNormalizer.php b/src/Database/IRowNormalizer.php new file mode 100644 index 000000000..9b4d919fe --- /dev/null +++ b/src/Database/IRowNormalizer.php @@ -0,0 +1,23 @@ +connection = $connection; $this->supplementalDriver = $connection->getSupplementalDriver(); $this->queryString = $queryString; $this->params = $params; + $this->rowNormalizer = $normalizer; try { if (substr($queryString, 0, 2) === '::') { @@ -116,6 +123,16 @@ public function getParameters() return $this->params; } + /** + * @return array + */ + public function getColumnTypes() + { + if ($this->types === NULL) { + $this->types = (array) $this->supplementalDriver->getColumnTypes($this->pdoStatement); + } + return $this->types; + } /** * @return int @@ -145,47 +162,24 @@ public function getTime() /** - * Normalizes result row. - * @param array - * @return array + * @param IRowNormalizer|NULL + * @return static */ - public function normalizeRow($row) + public function setRowNormalizer($normalizer) { - if ($this->types === NULL) { - $this->types = (array) $this->supplementalDriver->getColumnTypes($this->pdoStatement); - } - - foreach ($this->types as $key => $type) { - $value = $row[$key]; - if ($value === NULL || $value === FALSE || $type === IStructure::FIELD_TEXT) { - - } elseif ($type === IStructure::FIELD_INTEGER) { - $row[$key] = is_float($tmp = $value * 1) ? $value : $tmp; - - } elseif ($type === IStructure::FIELD_FLOAT) { - if (($pos = strpos($value, '.')) !== FALSE) { - $value = rtrim(rtrim($pos === 0 ? "0$value" : $value, '0'), '.'); - } - $float = (float) $value; - $row[$key] = (string) $float === $value ? $float : $value; - - } elseif ($type === IStructure::FIELD_BOOL) { - $row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F'; - - } elseif ($type === IStructure::FIELD_DATETIME || $type === IStructure::FIELD_DATE || $type === IStructure::FIELD_TIME) { - $row[$key] = new Nette\Utils\DateTime($value); - - } elseif ($type === IStructure::FIELD_TIME_INTERVAL) { - preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)\z#', $value, $m); - $row[$key] = new \DateInterval("PT$m[2]H$m[3]M$m[4]S"); - $row[$key]->invert = (int) (bool) $m[1]; + $this->rowNormalizer = $normalizer; + return $this; + } - } elseif ($type === IStructure::FIELD_UNIX_TIMESTAMP) { - $row[$key] = Nette\Utils\DateTime::from($value); - } - } - return $this->supplementalDriver->normalizeRow($row); + /** + * Set a factory to create fetched object instances. These should implements the IRow interface. + * @return self + */ + public function setRowFactory(callable $callback) + { + $this->rowFactory = $callback; + return $this; } @@ -255,10 +249,19 @@ public function fetch() return FALSE; } - $row = new Row; - foreach ($this->normalizeRow($data) as $key => $value) { - if ($key !== '') { - $row->$key = $value; + if ($this->rowNormalizer !== NULL) { + $data = $this->rowNormalizer->normalizeRow($data, $this); + } + + if ($this->rowFactory) { + $row = call_user_func($this->rowFactory, $data); + } + else { + $row = new Row; + foreach ($data as $key => $value) { + if ($key !== '') { + $row->$key = $value; + } } } diff --git a/src/Database/RowNormalizer.php b/src/Database/RowNormalizer.php new file mode 100644 index 000000000..24330fefa --- /dev/null +++ b/src/Database/RowNormalizer.php @@ -0,0 +1,65 @@ +getColumnTypes() as $key => $type) { + $value = $row[$key]; + if ($value === NULL || $value === FALSE || $type === IStructure::FIELD_TEXT) { + + } elseif ($type === IStructure::FIELD_INTEGER) { + $row[$key] = is_float($tmp = $value * 1) ? $value : $tmp; + + } elseif ($type === IStructure::FIELD_FLOAT) { + if (($pos = strpos($value, '.')) !== FALSE) { + $value = rtrim(rtrim($pos === 0 ? "0$value" : $value, '0'), '.'); + } + $float = (float) $value; + $row[$key] = (string) $float === $value ? $float : $value; + + } elseif ($type === IStructure::FIELD_BOOL) { + $row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F'; + + } elseif ($type === IStructure::FIELD_DATETIME || $type === IStructure::FIELD_DATE || $type === IStructure::FIELD_TIME) { + $row[$key] = new Nette\Utils\DateTime($value); + + } elseif ($type === IStructure::FIELD_TIME_INTERVAL) { + preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)\z#', $value, $m); + $row[$key] = new \DateInterval("PT$m[2]H$m[3]M$m[4]S"); + $row[$key]->invert = (int) (bool) $m[1]; + + } elseif ($type === IStructure::FIELD_UNIX_TIMESTAMP) { + $row[$key] = Nette\Utils\DateTime::from($value); + } + } + + if ($this->supplementalDriver === NULL) { + $this->supplementalDriver = $resultSet->getConnection()->getSupplementalDriver(); + } + + return $this->supplementalDriver->normalizeRow($row); + } +} diff --git a/src/Database/Table/Selection.php b/src/Database/Table/Selection.php index 2349c3ac0..c5eabd9ba 100644 --- a/src/Database/Table/Selection.php +++ b/src/Database/Table/Selection.php @@ -575,11 +575,9 @@ protected function execute() throw $exception; } } - $this->rows = []; $usedPrimary = TRUE; - foreach ($result->getPdoStatement() as $key => $row) { - $row = $this->createRow($result->normalizeRow($row)); + foreach ($result as $key => $row) { $primary = $row->getSignature(FALSE); $usedPrimary = $usedPrimary && (string) $primary !== ''; $this->rows[$usedPrimary ? $primary : $key] = $row; @@ -614,7 +612,11 @@ protected function createGroupedSelectionInstance($table, $column) protected function query($query) { - return $this->context->queryArgs($query, $this->sqlBuilder->getParameters()); + $result = $this->context->queryArgs($query, $this->sqlBuilder->getParameters()); + $result->setRowFactory(function($row) { + return $this->createRow($row); + }); + return $result; } diff --git a/tests/Database/ResultSet.customNormalization.phpt b/tests/Database/ResultSet.customNormalization.phpt new file mode 100644 index 000000000..f8c40f397 --- /dev/null +++ b/tests/Database/ResultSet.customNormalization.phpt @@ -0,0 +1,34 @@ + $value) { + unset($row[$key]); + $row['_'.$key.'_'] = (string) $value; + } + return $row; + } +} + +test(function() use ($context) { + $res = $context->query('SELECT * FROM author'); + $res->setRowNormalizer(new CustomRowNormalizer()); + Assert::equal([ + '_id_' => '11', + '_name_' => 'Jakub Vrana', + '_web_' => 'http://www.vrana.cz/', + '_born_' => '' + ], (array)$res->fetch()); +}); diff --git a/tests/Database/Table/Table.join.phpt b/tests/Database/Table/Table.join.phpt index eb1d8a9c4..cd100ee5c 100644 --- a/tests/Database/Table/Table.join.phpt +++ b/tests/Database/Table/Table.join.phpt @@ -90,5 +90,7 @@ test(function () use ($connection, $structure) { ); $books = $context->table('book')->select('book.*, author.name, translator.name'); - iterator_to_array($books); + Assert::error(function() use($books) { + iterator_to_array($books); + }, E_USER_NOTICE, 'Found duplicate columns in database result set.'); }); From 5ed5dfd03037e7d63ee645e264254dede03369f6 Mon Sep 17 00:00:00 2001 From: Unlink Date: Thu, 2 Jun 2016 12:46:23 +0200 Subject: [PATCH 2/2] ResultSet: improved error message on duplicated names in select statement --- src/Database/Helpers.php | 24 ++++++++++++++++++++++++ src/Database/ResultSet.php | 3 ++- tests/Database/ResultSet.fetch().phpt | 11 ++++++++++- tests/Database/Table/Table.join.phpt | 4 ++-- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/Database/Helpers.php b/src/Database/Helpers.php index c7e76f32d..50bdf9d64 100644 --- a/src/Database/Helpers.php +++ b/src/Database/Helpers.php @@ -267,4 +267,28 @@ public static function toPairs(array $rows, $key = NULL, $value = NULL) return $return; } + + /** + * Finds duplicate columns in select statement + * @param \PDOStatement + * @return string + */ + public static function findDuplicates(\PDOStatement $statement) + { + $cols = []; + for ($i=0; $i<$statement->columnCount(); $i++) { + $meta = $statement->getColumnMeta($i); + $tableName = isset($meta['table']) ? $meta['table'] : ''; + $cols[$meta['name']][] = $tableName; + } + $duplicates = []; + foreach ($cols as $name => $tables) { + if (count($tables) > 1) { + $tableNames = implode(', ', array_unique($tables)); + $duplicates[] = "'$name'".($tableNames !== '' ? " from $tableNames" : ''); + } + } + return implode('; ', $duplicates); + } + } diff --git a/src/Database/ResultSet.php b/src/Database/ResultSet.php index deeeffadb..84a7c11ae 100644 --- a/src/Database/ResultSet.php +++ b/src/Database/ResultSet.php @@ -266,7 +266,8 @@ public function fetch() } if ($this->result === NULL && count($data) !== $this->pdoStatement->columnCount()) { - trigger_error('Found duplicate columns in database result set.', E_USER_NOTICE); + $duplicates = Helpers::findDuplicates($this->pdoStatement); + trigger_error("Found duplicate columns in database result set: $duplicates.", E_USER_NOTICE); } $this->resultKey++; diff --git a/tests/Database/ResultSet.fetch().phpt b/tests/Database/ResultSet.fetch().phpt index 1dbac5a36..679cf5650 100644 --- a/tests/Database/ResultSet.fetch().phpt +++ b/tests/Database/ResultSet.fetch().phpt @@ -17,7 +17,7 @@ test(function () use ($context) { Assert::error(function () use ($res) { $res->fetch(); - }, E_USER_NOTICE, 'Found duplicate columns in database result set.'); + }, E_USER_NOTICE); $res->fetch(); }); @@ -35,3 +35,12 @@ test(function () use ($context, $driverName) { // tests closeCursor() foreach ($res as $row) {} } }); + +test(function () use ($context, $driverName) { + + $result = $context->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)'); + //Found duplicate columns in database result set: 'id' from book, author; 'name' from author, translator. + Assert::error(function() use($result) { + iterator_to_array($result); + }, E_USER_NOTICE); +}); diff --git a/tests/Database/Table/Table.join.phpt b/tests/Database/Table/Table.join.phpt index cd100ee5c..cf06abd5d 100644 --- a/tests/Database/Table/Table.join.phpt +++ b/tests/Database/Table/Table.join.phpt @@ -82,7 +82,7 @@ test(function () use ($context) { }); -test(function () use ($connection, $structure) { +test(function () use ($connection, $structure, $driverName) { $context = new Nette\Database\Context( $connection, $structure, @@ -92,5 +92,5 @@ test(function () use ($connection, $structure) { $books = $context->table('book')->select('book.*, author.name, translator.name'); Assert::error(function() use($books) { iterator_to_array($books); - }, E_USER_NOTICE, 'Found duplicate columns in database result set.'); + }, E_USER_NOTICE); });