Skip to content

Commit

Permalink
Merge pull request #1012 from hrach/database-join-parser
Browse files Browse the repository at this point in the history
Database join parser
  • Loading branch information
dg committed Apr 1, 2013
2 parents 7171052 + 0a52fdc commit 1767148
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 76 deletions.
195 changes: 125 additions & 70 deletions Nette/Database/Table/SqlBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,63 @@ public function buildDeleteQuery()



/**
* Returns SQL query.
* @param list of columns
* @return string
*/
public function buildSelectQuery($columns = NULL)
{
$queryCondition = $this->buildConditions();
$queryEnd = $this->buildQueryEnd();

$joins = array();
$this->parseJoins($joins, $queryCondition, TRUE);
$this->parseJoins($joins, $queryEnd);

if ($this->select) {
$querySelect = $this->buildSelect($this->select);
$this->parseJoins($joins, $querySelect);

} elseif ($columns) {
$prefix = $joins ? "{$this->delimitedTable}." : '';
$cols = array();
foreach ($columns as $col) {
$cols[] = $prefix . $col;
}
$querySelect = $this->buildSelect($cols);

} elseif ($this->group && !$this->driver->isSupported(ISupplementalDriver::SUPPORT_SELECT_UNGROUPED_COLUMNS)) {
$querySelect = $this->buildSelect(array($this->group));
$this->parseJoins($joins, $querySelect);

} else {
$prefix = $joins ? "{$this->delimitedTable}." : '';
$querySelect = $this->buildSelect(array($prefix . '*'));

}

$queryJoins = $this->buildQueryJoins($joins);
$query = "{$querySelect} FROM {$this->delimitedTable}{$queryJoins}{$queryCondition}{$queryEnd}";

return $this->tryDelimite($query);
}



public function getParameters()
{
return array_merge(
$this->parameters['select'],
$this->parameters['where'],
$this->parameters['group'],
$this->parameters['having'],
$this->parameters['order']
);
}



public function importConditions(SqlBuilder $builder)
{
$this->where = $builder->where;
Expand Down Expand Up @@ -150,9 +207,6 @@ public function addWhere($condition, $parameters = array())
}

$this->conditions[$hash] = $condition;
$condition = $this->removeExtraTables($condition);
$condition = $this->tryDelimite($condition);

$placeholderCount = substr_count($condition, '?');
if ($placeholderCount > 1 && count($args) === 2 && is_array($parameters)) {
$args = $parameters;
Expand Down Expand Up @@ -323,80 +377,79 @@ public function getHaving()



/**
* Returns SQL query.
* @param list of columns
* @return string
*/
public function buildSelectQuery($columns = NULL)
protected function buildSelect($columns)
{
$join = $this->buildJoins(implode(',', $this->conditions), TRUE);
$join += $this->buildJoins(implode(',', $this->select) . ",{$this->group},{$this->having}," . implode(',', $this->order));
return "SELECT{$this->buildTopClause()} " . implode(', ', $columns);
}

$prefix = $join ? "{$this->delimitedTable}." : '';
if ($this->select) {
$cols = $this->tryDelimite($this->removeExtraTables(implode(', ', $this->select)));

} elseif ($columns) {
$cols = array_map(array($this->driver, 'delimite'), $columns);
$cols = $prefix . implode(', ' . $prefix, $cols);

} elseif ($this->group && !$this->driver->isSupported(ISupplementalDriver::SUPPORT_SELECT_UNGROUPED_COLUMNS)) {
$cols = $this->tryDelimite($this->removeExtraTables($this->group));
protected function parseJoins(& $joins, & $query, $inner = FALSE)
{
$builder = $this;
$query = preg_replace_callback('~
(?(DEFINE)
(?<word> [a-z][\w_]* )
(?<del> [.:] )
(?<node> (?&del)? (?&word) )
)
(?<chain> (?!\.) (?&node)*) \. (?<column> (?&word) | \* )
~xi', function($match) use (& $joins, $inner, $builder) {
return $builder->parseJoinsCb($joins, $match, $inner);
}, $query);
}

} else {
$cols = $prefix . '*';


public function parseJoinsCb(& $joins, $match, $inner)
{
$chain = $match['chain'];
if (!empty($chain[0]) && ($chain[0] !== '.' || $chain[0] !== ':')) {
$chain = '.' . $chain; // unified chain format
}

return "SELECT{$this->buildTopClause()} {$cols} FROM {$this->delimitedTable}" . implode($join) . $this->buildConditions();
}
$parent = $this->tableName;
if ($chain == ".{$parent}") { // case-sensitive
return "{$parent}.{$match['column']}";
}

preg_match_all('~
(?(DEFINE)
(?<word> [a-z][\w_]* )
)
(?<del> [.:])?(?<key> (?&word))
~xi', $chain, $keyMatches, PREG_SET_ORDER);

foreach ($keyMatches as $keyMatch) {
if ($keyMatch['del'] === ':') {
list($table, $primary) = $this->databaseReflection->getHasManyReference($parent, $keyMatch['key']);
$column = $this->databaseReflection->getPrimary($parent);
} else {
list($table, $column) = $this->databaseReflection->getBelongsToReference($parent, $keyMatch['key']);
$primary = $this->databaseReflection->getPrimary($table);
}

$joins[$table] = array($table, $keyMatch['key'] ?: $table, $parent, $column, $primary, !isset($joins[$table]) && $inner);
$parent = $table;
}

public function getParameters()
{
return array_merge(
$this->parameters['select'],
$this->parameters['where'],
$this->parameters['group'],
$this->parameters['having'],
$this->parameters['order']
);
return ($keyMatch['key'] ?: $table) . ".{$match['column']}";
}



protected function buildJoins($val, $inner = FALSE)
protected function buildQueryJoins($joins)
{
$joins = array();
preg_match_all('~\\b([a-z][\\w.:]*[.:])([a-z]\\w*|\*)(\\s+IS\\b|\\s*<=>)?~i', $val, $matches);
foreach ($matches[1] as $names) {
$parent = $parentAlias = $this->tableName;
if ($names !== "$parent.") { // case-sensitive
preg_match_all('~\\b([a-z][\\w]*|\*)([.:])~i', $names, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
list(, $name, $delimiter) = $match;

if ($delimiter === ':') {
list($table, $primary) = $this->databaseReflection->getHasManyReference($parent, $name);
$column = $this->databaseReflection->getPrimary($parent);
} else {
list($table, $column) = $this->databaseReflection->getBelongsToReference($parent, $name);
$primary = $this->databaseReflection->getPrimary($table);
}

$joins[$name] = ' '
. (!isset($joins[$name]) && $inner && !isset($match[3]) ? 'INNER' : 'LEFT')
. ' JOIN ' . $this->driver->delimite($table) . ($table !== $name ? ' AS ' . $this->driver->delimite($name) : '')
. ' ON ' . $this->driver->delimite($parentAlias) . '.' . $this->driver->delimite($column)
. ' = ' . $this->driver->delimite($name) . '.' . $this->driver->delimite($primary);
$return = '';
foreach ($joins as $join) {
list($joinTable, $joinAlias, $table, $tableColumn, $joinColumn, $inner) = $join;

$parent = $table;
$parentAlias = $name;
}
}
$return .= ' ' . ($inner ? 'INNER' : 'LEFT')
. " JOIN {$joinTable}" . ($joinTable !== $joinAlias ? " AS {$joinAlias}" : '')
. " ON {$table}.{$tableColumn} = {$joinAlias}.{$joinColumn}";
}
return $joins;

return $return;
}


Expand All @@ -411,14 +464,23 @@ protected function buildConditions()
if ($where) {
$return .= ' WHERE (' . implode(') AND (', $where) . ')';
}

return $return;
}



protected function buildQueryEnd()
{
$return = '';
if ($this->group) {
$return .= ' GROUP BY '. $this->tryDelimite($this->removeExtraTables($this->group));
$return .= ' GROUP BY '. $this->group;
}
if ($this->having) {
$return .= ' HAVING '. $this->tryDelimite($this->removeExtraTables($this->having));
$return .= ' HAVING '. $this->having;
}
if ($this->order) {
$return .= ' ORDER BY ' . $this->tryDelimite($this->removeExtraTables(implode(', ', $this->order)));
$return .= ' ORDER BY ' . implode(', ', $this->order);
}
if ($this->limit !== NULL && $this->driverName !== 'oci' && $this->driverName !== 'dblib') {
$return .= " LIMIT $this->limit";
Expand Down Expand Up @@ -449,11 +511,4 @@ protected function tryDelimite($s)
}, $s);
}



protected function removeExtraTables($expression)
{
return preg_replace('~(?:\\b[a-z_][a-z0-9_.:]*[.:])?([a-z_][a-z0-9_]*)[.:]([a-z_*])~i', '\\1.\\2', $expression); // rewrite tab1.tab2.col
}

}
2 changes: 1 addition & 1 deletion tests/Nette/Database/Table.aggregation.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ Assert::same(array(



$authors = $connection->table('author')->where('book:translator_id IS NOT NULL')->group('author.id'); // SELECT `author`.* FROM `author` INNER JOIN `book` ON `author`.`id` = `book`.`author_id` WHERE (`book`.`translator_id` IS NOT NULL) GROUP BY `author`.`id`
$authors = $connection->table('author')->where(':book.translator_id IS NOT NULL')->group('author.id'); // SELECT `author`.* FROM `author` INNER JOIN `book` ON `author`.`id` = `book`.`author_id` WHERE (`book`.`translator_id` IS NOT NULL) GROUP BY `author`.`id`
Assert::same(2, count($authors));
Assert::same(2, $authors->count('DISTINCT author.id')); // SELECT COUNT(DISTINCT author.id) FROM `author` INNER JOIN `book` ON `author`.`id` = `book`.`author_id` WHERE (`book`.`translator_id` IS NOT NULL)
11 changes: 9 additions & 2 deletions tests/Nette/Database/Table.backjoin.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ use Tester\Assert;
$authorTagsCount = array();
$authors = $connection
->table('author')
->select('author.name, COUNT(DISTINCT book:book_tag:tag_id) AS tagsCount')
->select('author.name, COUNT(DISTINCT :book:book_tag.tag_id) AS tagsCount')
->group('author.name')
->having('COUNT(DISTINCT book:book_tag:tag_id) < 3')
->having('COUNT(DISTINCT :book:book_tag.tag_id) < 3')
->order('tagsCount DESC');

foreach ($authors as $author) {
Expand All @@ -33,3 +33,10 @@ Assert::same(array(
'David Grudl' => 2,
'Geek' => 0,
), $authorTagsCount);



/*
$count = $connection->table('author')->where(':book.title LIKE ?', '%PHP%')->count('*'); // by translator_id
Assert::same(1, $count);
*/
7 changes: 7 additions & 0 deletions tests/Nette/Database/Table.discoveredReflection.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,10 @@ if (
'Dibi' => 'David Grudl',
), $books);
}



$count = $connection->table('book')->where('translator.name LIKE ?', '%David%')->count();
Assert::same(2, $count);
$count = $connection->table('book')->where('author.name LIKE ?', '%David%')->count();
Assert::same(2, $count);
2 changes: 1 addition & 1 deletion tests/Nette/Database/Table.join.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ Assert::same(array(



Assert::same(2, $connection->table('author')->where('author_id', 11)->count('book:id')); // SELECT COUNT(book.id) FROM `author` LEFT JOIN `book` ON `author`.`id` = `book`.`author_id` WHERE (`author_id` = 11)
Assert::same(2, $connection->table('author')->where('author_id', 11)->count(':book.id')); // SELECT COUNT(book.id) FROM `author` LEFT JOIN `book` ON `author`.`id` = `book`.`author_id` WHERE (`author_id` = 11)
4 changes: 2 additions & 2 deletions tests/Nette/Database/Table.placeholders.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ Assert::same((int) date('Y'), $row['col2']);
$bookTagsCount = array();
$books = $connection
->table('book')
->select('book.title, COUNT(DISTINCT book_tag:tag_id) AS tagsCount')
->select('book.title, COUNT(DISTINCT :book_tag.tag_id) AS tagsCount')
->group('book.title')
->having('COUNT(DISTINCT book_tag:tag_id) < ?', 2)
->having('COUNT(DISTINCT :book_tag.tag_id) < ?', 2)
->order('book.title');

foreach ($books as $book) {
Expand Down

0 comments on commit 1767148

Please sign in to comment.