Skip to content
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

Row normalization #138

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions src/Database/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
23 changes: 23 additions & 0 deletions src/Database/IRowNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

namespace Nette\Database;


/**
* Row normalizer interface.
*/
interface IRowNormalizer
{
/**
* Normalizes result row.
* @param array
* @param ResultSet
* @return array
*/
function normalizeRow($row, ResultSet $resultSet);
}
90 changes: 47 additions & 43 deletions src/Database/ResultSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class ResultSet implements \Iterator, IRowContainer
/** @var \PDOStatement|NULL */
private $pdoStatement;

/** @var IRowNormalizer */
private $rowNormalizer;

/** @var IRow */
private $result;

Expand All @@ -48,14 +51,18 @@ class ResultSet implements \Iterator, IRowContainer
/** @var array */
private $types;

/** @var callable|NULL */
private $rowFactory;


public function __construct(Connection $connection, $queryString, array $params)
public function __construct(Connection $connection, $queryString, array $params, IRowNormalizer $normalizer)
{
$time = microtime(TRUE);
$this->connection = $connection;
$this->supplementalDriver = $connection->getSupplementalDriver();
$this->queryString = $queryString;
$this->params = $params;
$this->rowNormalizer = $normalizer;

try {
if (substr($queryString, 0, 2) === '::') {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}


Expand Down Expand Up @@ -255,15 +249,25 @@ 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;
}
}
}

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++;
Expand Down
65 changes: 65 additions & 0 deletions src/Database/RowNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

namespace Nette\Database;

use Nette;


/**
* Default implementation for row normalization.
*/
class RowNormalizer implements IRowNormalizer
{
use Nette\SmartObject;

/** @var ISupplementalDriver */
private $supplementalDriver;


/**
* @inheritdoc
*/
public function normalizeRow($row, ResultSet $resultSet)
{
foreach ($resultSet->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);
}
}
10 changes: 6 additions & 4 deletions src/Database/Table/Selection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}


Expand Down
34 changes: 34 additions & 0 deletions tests/Database/ResultSet.customNormalization.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* @dataProvider? databases.ini
*/

use Tester\Assert;

require __DIR__ . '/connect.inc.php'; // create $connection

Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql");

class CustomRowNormalizer implements \Nette\Database\IRowNormalizer
{
function normalizeRow($row, \Nette\Database\ResultSet $resultSet)
{
foreach ($row as $key => $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());
});
11 changes: 10 additions & 1 deletion tests/Database/ResultSet.fetch().phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand All @@ -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);
});
6 changes: 4 additions & 2 deletions tests/Database/Table/Table.join.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ test(function () use ($context) {
});


test(function () use ($connection, $structure) {
test(function () use ($connection, $structure, $driverName) {
$context = new Nette\Database\Context(
$connection,
$structure,
new Nette\Database\Conventions\DiscoveredConventions($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);
});