Skip to content
Closed
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
14 changes: 14 additions & 0 deletions src/Database/Table/Selection.php
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,20 @@ public function having($having, ...$params)
}


/**
* Alias table.
* @example ':book:book_tag.tag', 'tg'
* @param string
* @param string
* @return self
*/
public function alias($tableChain, $alias)
{
$this->sqlBuilder->addAlias($tableChain, $alias);
return $this;
}


/********************* aggregations ****************d*g**/


Expand Down
85 changes: 76 additions & 9 deletions src/Database/Table/SqlBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ class SqlBuilder extends Nette\Object
/** @var string grouping condition */
protected $having = '';

/** @var array of reserved table names associated with chain */
protected $reservedTableNames = [];

/** @var array of table aliases */
protected $aliases = [];

/** @var string currently parsing alias for joins */
protected $currentAlias = NULL;

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

Expand All @@ -80,7 +89,9 @@ public function __construct($tableName, Context $context)
$this->driver = $context->getConnection()->getSupplementalDriver();
$this->conventions = $context->getConventions();
$this->structure = $context->getStructure();
$this->delimitedTable = implode('.', array_map([$this->driver, 'delimite'], explode('.', $tableName)));
$tableNameParts = explode('.', $tableName);
$this->delimitedTable = implode('.', array_map([$this->driver, 'delimite'], $tableNameParts));
$this->checkUniqueTableName(end($tableNameParts), $tableName);
}


Expand Down Expand Up @@ -329,6 +340,37 @@ public function getConditions()
}


/**
* Add alias.
* @param string
* @param string
* @return void
* @throws \Nette\InvalidArgumentException
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@return void

*/
public function addAlias($chain, $alias)
{
if (!empty($chain[0]) && ($chain[0] !== '.' && $chain[0] !== ':')) {
$chain = '.' . $chain; // unified chain format
}
$this->checkUniqueTableName($alias, $chain);
$this->aliases[$alias] = $chain;
}

protected function checkUniqueTableName($tableName, $chain)
{
if (isset($this->aliases[$tableName]) && ('.' . $tableName === $chain)) {
$chain = $this->aliases[$tableName];
}
if (isset($this->reservedTableNames[$tableName])) {
if ($this->reservedTableNames[$tableName] === $chain) {
return;
}
throw new \Nette\InvalidArgumentException("Table alias '$tableName' from chain '$chain' is already in use by chain '{$this->reservedTableNames[$tableName]}'. Please add/change alias for one of them.");
}
$this->reservedTableNames[$tableName] = $chain;
}


public function addOrder($columns, ...$params)
{
$this->order[] = $columns;
Expand Down Expand Up @@ -448,15 +490,33 @@ public function parseJoinsCb(& $joins, $match)
// do not make a join when referencing to the current table column - inner conditions
// check it only when not making backjoin on itself - outer condition
if ($keyMatches[0]['del'] === '.') {
if (count($keyMatches) > 1 && ($parent === $keyMatches[0]['key'] || $parentAlias === $keyMatches[0]['key'])) {
throw new Nette\InvalidArgumentException("Do not prefix table chain with origin table name '{$keyMatches[0]['key']}'. If you want to make self reference, please add alias.");
}
if ($parent === $keyMatches[0]['key']) {
return "{$parent}.{$match['column']}";
} elseif ($parentAlias === $keyMatches[0]['key']) {
return "{$parentAlias}.{$match['column']}";
}
}

foreach ($keyMatches as $keyMatch) {
if ($keyMatch['del'] === ':') {
$tableChain = NULL;
foreach ($keyMatches as $index => $keyMatch) {
$isLast = !isset($keyMatches[$index+1]);
if (!$index && isset($this->aliases[$keyMatch['key']])) {
if ($keyMatch['del'] === ':') {
throw new Nette\InvalidArgumentException("You are using has many syntax with alias (':{$keyMatch['key']}'). You have to move it to alias definition.");
} else {
$previousAlias = $this->currentAlias;
$this->currentAlias = $keyMatch['key'];
$requiredJoins = [];
$query = $this->aliases[$keyMatch['key']] . '.foo';
$this->parseJoins($requiredJoins, $query);
$aliasJoin = array_pop($requiredJoins);
$joins += $requiredJoins;
list($table, , $parentAlias, $column, $primary) = $aliasJoin;
$this->currentAlias = $previousAlias;
}
} elseif ($keyMatch['del'] === ':') {
if (isset($keyMatch['throughColumn'])) {
$table = $keyMatch['key'];
$belongsTo = $this->conventions->getBelongsToReference($table, $keyMatch['throughColumn']);
Expand All @@ -483,14 +543,21 @@ public function parseJoinsCb(& $joins, $match)
$primary = $this->conventions->getPrimary($table);
}

$tableAlias = $keyMatch['key'] ?: preg_replace('#^(.*\.)?(.*)$#', '$2', $table);

// if we are joining itself (parent table), we must alias joining table
if ($parent === $table) {
if ($this->currentAlias && $isLast) {
$tableAlias = $this->currentAlias;
} elseif ($parent === $table) {
$tableAlias = $parentAlias . '_ref';
} elseif ($keyMatch['key']) {
$tableAlias = $keyMatch['key'];
} else {
$tableAlias = preg_replace('#^(.*\.)?(.*)$#', '$2', $table);
}

$joins[$tableAlias . $column] = [$table, $tableAlias, $parentAlias, $column, $primary];
$tableChain .= $keyMatch['del'] . $tableAlias;
if (!$isLast || !$this->currentAlias) {
$this->checkUniqueTableName($tableAlias, $tableChain);
}
$joins[$tableAlias] = [$table, $tableAlias, $parentAlias, $column, $primary];
$parent = $table;
$parentAlias = $tableAlias;
}
Expand Down
103 changes: 103 additions & 0 deletions tests/Database/Table/SqlBuilder.addAlias().phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

/**
* Test: Nette\Database\Table\SqlBuilder: addAlias().
* @dataProvider? ../databases.ini
*/

use Tester\Assert;
use Nette\Database\ISupplementalDriver;
use Nette\Database\Table\SqlBuilder;

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

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

class SqlBuilderMock extends SqlBuilder
{
public function parseJoins(& $joins, & $query, $inner = FALSE)
{
parent::parseJoins($joins, $query);
}
public function buildQueryJoins(array $joins, $leftConditions = [])
{
return parent::buildQueryJoins($joins, $leftConditions);
}
}

$driver = $connection->getSupplementalDriver();


test(function() use ($context, $driver) { // test duplicated table names throw exception
if ($driver->isSupported(ISupplementalDriver::SUPPORT_SCHEMA)) {
$sqlBuilder = new SqlBuilderMock('public.author', $context);
} else {
$sqlBuilder = new SqlBuilderMock('author', $context);
}
$sqlBuilder->addAlias(':book(translator)', 'book1');
$sqlBuilder->addAlias(':book:book_tag', 'book2');
Assert::exception(function() use ($sqlBuilder) {
$sqlBuilder->addAlias(':book', 'book1');
}, '\Nette\InvalidArgumentException');

Assert::exception(function() use ($sqlBuilder) { // reserved by base table name
$sqlBuilder->addAlias(':book', 'author');
}, '\Nette\InvalidArgumentException');

Assert::exception(function() use ($sqlBuilder) {
$sqlBuilder->addAlias(':book', 'book1');
}, '\Nette\InvalidArgumentException');

$sqlBuilder->addAlias(':book', 'tag');
Assert::exception(function() use ($sqlBuilder) {
$query = 'WHERE book1:book_tag.tag.id IS NULL';
$joins = [];
$sqlBuilder->parseJoins($joins, $query);
}, '\Nette\InvalidArgumentException');
});


test(function() use ($context, $driver) { // test same table chain with another alias
$sqlBuilder = new SqlBuilderMock('author', $context);
$sqlBuilder->addAlias(':book(translator)', 'translated_book');
$sqlBuilder->addAlias(':book(translator)', 'translated_book2');
$query = 'WHERE translated_book.translator_id IS NULL AND translated_book2.id IS NULL';
$joins = [];
$sqlBuilder->parseJoins($joins, $query);
$join = $sqlBuilder->buildQueryJoins($joins);

Assert::same(
'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' .
'LEFT JOIN book translated_book2 ON author.id = translated_book2.translator_id',
trim($join)
);
});


test(function() use ($context, $driver) { // test nested alias
if ($driver->isSupported(ISupplementalDriver::SUPPORT_SCHEMA)) {
$sqlBuilder = new SqlBuilderMock('public.author', $context);
} else {
$sqlBuilder = new SqlBuilderMock('author', $context);
}
$sqlBuilder->addAlias(':book(translator)', 'translated_book');
$sqlBuilder->addAlias('translated_book.next_volume', 'next');
$query = 'WHERE next.translator_id IS NULL';
$joins = [];
$sqlBuilder->parseJoins($joins, $query);
$join = $sqlBuilder->buildQueryJoins($joins);
if ($driver->isSupported(ISupplementalDriver::SUPPORT_SCHEMA)) {
Assert::same(
'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' .
'LEFT JOIN public.book next ON translated_book.next_volume = next.id',
trim($join)
);

} else {
Assert::same(
'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' .
'LEFT JOIN book next ON translated_book.next_volume = next.id',
trim($join)
);
}
});