Skip to content

Commit 1329476

Browse files
norbedg
authored andcommitted
SqlBuilder: Added alias support [Closes #119]
1 parent cbf22d4 commit 1329476

File tree

3 files changed

+190
-9
lines changed

3 files changed

+190
-9
lines changed

src/Database/Table/Selection.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,19 @@ public function having($having, ...$params)
406406
}
407407

408408

409+
/**
410+
* Aliases table. Example ':book:book_tag.tag', 'tg'
411+
* @param string
412+
* @param string
413+
* @return self
414+
*/
415+
public function alias($tableChain, $alias)
416+
{
417+
$this->sqlBuilder->addAlias($tableChain, $alias);
418+
return $this;
419+
}
420+
421+
409422
/********************* aggregations ****************d*g**/
410423

411424

src/Database/Table/SqlBuilder.php

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ class SqlBuilder extends Nette\Object
6464
/** @var string grouping condition */
6565
protected $having = '';
6666

67+
/** @var array of reserved table names associated with chain */
68+
protected $reservedTableNames = [];
69+
70+
/** @var array of table aliases */
71+
protected $aliases = [];
72+
73+
/** @var string currently parsing alias for joins */
74+
protected $currentAlias = NULL;
75+
6776
/** @var ISupplementalDriver */
6877
private $driver;
6978

@@ -80,7 +89,9 @@ public function __construct($tableName, Context $context)
8089
$this->driver = $context->getConnection()->getSupplementalDriver();
8190
$this->conventions = $context->getConventions();
8291
$this->structure = $context->getStructure();
83-
$this->delimitedTable = implode('.', array_map([$this->driver, 'delimite'], explode('.', $tableName)));
92+
$tableNameParts = explode('.', $tableName);
93+
$this->delimitedTable = implode('.', array_map([$this->driver, 'delimite'], $tableNameParts));
94+
$this->checkUniqueTableName(end($tableNameParts), $tableName);
8495
}
8596

8697

@@ -329,6 +340,35 @@ public function getConditions()
329340
}
330341

331342

343+
/**
344+
* Adds alias.
345+
* @return void
346+
*/
347+
public function addAlias($chain, $alias)
348+
{
349+
if (isset($chain[0]) && $chain[0] !== '.' && $chain[0] !== ':') {
350+
$chain = '.' . $chain; // unified chain format
351+
}
352+
$this->checkUniqueTableName($alias, $chain);
353+
$this->aliases[$alias] = $chain;
354+
}
355+
356+
357+
protected function checkUniqueTableName($tableName, $chain)
358+
{
359+
if (isset($this->aliases[$tableName]) && ('.' . $tableName === $chain)) {
360+
$chain = $this->aliases[$tableName];
361+
}
362+
if (isset($this->reservedTableNames[$tableName])) {
363+
if ($this->reservedTableNames[$tableName] === $chain) {
364+
return;
365+
}
366+
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.");
367+
}
368+
$this->reservedTableNames[$tableName] = $chain;
369+
}
370+
371+
332372
public function addOrder($columns, ...$params)
333373
{
334374
$this->order[] = $columns;
@@ -448,15 +488,33 @@ public function parseJoinsCb(& $joins, $match)
448488
// do not make a join when referencing to the current table column - inner conditions
449489
// check it only when not making backjoin on itself - outer condition
450490
if ($keyMatches[0]['del'] === '.') {
491+
if (count($keyMatches) > 1 && ($parent === $keyMatches[0]['key'] || $parentAlias === $keyMatches[0]['key'])) {
492+
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.");
493+
}
451494
if ($parent === $keyMatches[0]['key']) {
452495
return "{$parent}.{$match['column']}";
453496
} elseif ($parentAlias === $keyMatches[0]['key']) {
454497
return "{$parentAlias}.{$match['column']}";
455498
}
456499
}
457-
458-
foreach ($keyMatches as $keyMatch) {
459-
if ($keyMatch['del'] === ':') {
500+
$tableChain = NULL;
501+
foreach ($keyMatches as $index => $keyMatch) {
502+
$isLast = !isset($keyMatches[$index + 1]);
503+
if (!$index && isset($this->aliases[$keyMatch['key']])) {
504+
if ($keyMatch['del'] === ':') {
505+
throw new Nette\InvalidArgumentException("You are using has many syntax with alias (':{$keyMatch['key']}'). You have to move it to alias definition.");
506+
} else {
507+
$previousAlias = $this->currentAlias;
508+
$this->currentAlias = $keyMatch['key'];
509+
$requiredJoins = [];
510+
$query = $this->aliases[$keyMatch['key']] . '.foo';
511+
$this->parseJoins($requiredJoins, $query);
512+
$aliasJoin = array_pop($requiredJoins);
513+
$joins += $requiredJoins;
514+
list($table, , $parentAlias, $column, $primary) = $aliasJoin;
515+
$this->currentAlias = $previousAlias;
516+
}
517+
} elseif ($keyMatch['del'] === ':') {
460518
if (isset($keyMatch['throughColumn'])) {
461519
$table = $keyMatch['key'];
462520
$belongsTo = $this->conventions->getBelongsToReference($table, $keyMatch['throughColumn']);
@@ -483,14 +541,21 @@ public function parseJoinsCb(& $joins, $match)
483541
$primary = $this->conventions->getPrimary($table);
484542
}
485543

486-
$tableAlias = $keyMatch['key'] ?: preg_replace('#^(.*\.)?(.*)$#', '$2', $table);
487-
488-
// if we are joining itself (parent table), we must alias joining table
489-
if ($parent === $table) {
544+
if ($this->currentAlias && $isLast) {
545+
$tableAlias = $this->currentAlias;
546+
} elseif ($parent === $table) {
490547
$tableAlias = $parentAlias . '_ref';
548+
} elseif ($keyMatch['key']) {
549+
$tableAlias = $keyMatch['key'];
550+
} else {
551+
$tableAlias = preg_replace('#^(.*\.)?(.*)$#', '$2', $table);
491552
}
492553

493-
$joins[$tableAlias . $column] = [$table, $tableAlias, $parentAlias, $column, $primary];
554+
$tableChain .= $keyMatch['del'] . $tableAlias;
555+
if (!$isLast || !$this->currentAlias) {
556+
$this->checkUniqueTableName($tableAlias, $tableChain);
557+
}
558+
$joins[$tableAlias] = [$table, $tableAlias, $parentAlias, $column, $primary];
494559
$parent = $table;
495560
$parentAlias = $tableAlias;
496561
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
/**
4+
* Test: Nette\Database\Table\SqlBuilder: addAlias().
5+
* @dataProvider? ../databases.ini
6+
*/
7+
8+
use Tester\Assert;
9+
use Nette\Database\ISupplementalDriver;
10+
use Nette\Database\Table\SqlBuilder;
11+
12+
require __DIR__ . '/../connect.inc.php'; // create $connection
13+
14+
Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql");
15+
16+
class SqlBuilderMock extends SqlBuilder
17+
{
18+
public function parseJoins(& $joins, & $query, $inner = FALSE)
19+
{
20+
parent::parseJoins($joins, $query);
21+
}
22+
public function buildQueryJoins(array $joins, $leftConditions = [])
23+
{
24+
return parent::buildQueryJoins($joins, $leftConditions);
25+
}
26+
}
27+
28+
$driver = $connection->getSupplementalDriver();
29+
30+
31+
test(function() use ($context, $driver) { // test duplicated table names throw exception
32+
if ($driver->isSupported(ISupplementalDriver::SUPPORT_SCHEMA)) {
33+
$sqlBuilder = new SqlBuilderMock('public.author', $context);
34+
} else {
35+
$sqlBuilder = new SqlBuilderMock('author', $context);
36+
}
37+
$sqlBuilder->addAlias(':book(translator)', 'book1');
38+
$sqlBuilder->addAlias(':book:book_tag', 'book2');
39+
Assert::exception(function() use ($sqlBuilder) {
40+
$sqlBuilder->addAlias(':book', 'book1');
41+
}, Nette\InvalidArgumentException::class, "Table alias 'book1' from chain ':book' is already in use by chain ':book(translator)'. Please add/change alias for one of them.");
42+
43+
Assert::exception(function() use ($sqlBuilder) { // reserved by base table name
44+
$sqlBuilder->addAlias(':book', 'author');
45+
}, Nette\InvalidArgumentException::class, "Table alias 'author' from chain ':book' is already in use by chain 'author'. Please add/change alias for one of them.");
46+
47+
Assert::exception(function() use ($sqlBuilder) {
48+
$sqlBuilder->addAlias(':book', 'book1');
49+
}, Nette\InvalidArgumentException::class, "Table alias 'book1' from chain ':book' is already in use by chain ':book(translator)'. Please add/change alias for one of them.");
50+
51+
$sqlBuilder->addAlias(':book', 'tag');
52+
Assert::exception(function() use ($sqlBuilder) {
53+
$query = 'WHERE book1:book_tag.tag.id IS NULL';
54+
$joins = [];
55+
$sqlBuilder->parseJoins($joins, $query);
56+
}, Nette\InvalidArgumentException::class, "Table alias 'tag' from chain '.book1:book_tag.tag' is already in use by chain ':book'. Please add/change alias for one of them.");
57+
});
58+
59+
60+
test(function() use ($context, $driver) { // test same table chain with another alias
61+
$sqlBuilder = new SqlBuilderMock('author', $context);
62+
$sqlBuilder->addAlias(':book(translator)', 'translated_book');
63+
$sqlBuilder->addAlias(':book(translator)', 'translated_book2');
64+
$query = 'WHERE translated_book.translator_id IS NULL AND translated_book2.id IS NULL';
65+
$joins = [];
66+
$sqlBuilder->parseJoins($joins, $query);
67+
$join = $sqlBuilder->buildQueryJoins($joins);
68+
69+
Assert::same(
70+
'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' .
71+
'LEFT JOIN book translated_book2 ON author.id = translated_book2.translator_id',
72+
trim($join)
73+
);
74+
});
75+
76+
77+
test(function() use ($context, $driver) { // test nested alias
78+
if ($driver->isSupported(ISupplementalDriver::SUPPORT_SCHEMA)) {
79+
$sqlBuilder = new SqlBuilderMock('public.author', $context);
80+
} else {
81+
$sqlBuilder = new SqlBuilderMock('author', $context);
82+
}
83+
$sqlBuilder->addAlias(':book(translator)', 'translated_book');
84+
$sqlBuilder->addAlias('translated_book.next_volume', 'next');
85+
$query = 'WHERE next.translator_id IS NULL';
86+
$joins = [];
87+
$sqlBuilder->parseJoins($joins, $query);
88+
$join = $sqlBuilder->buildQueryJoins($joins);
89+
if ($driver->isSupported(ISupplementalDriver::SUPPORT_SCHEMA)) {
90+
Assert::same(
91+
'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' .
92+
'LEFT JOIN public.book next ON translated_book.next_volume = next.id',
93+
trim($join)
94+
);
95+
96+
} else {
97+
Assert::same(
98+
'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' .
99+
'LEFT JOIN book next ON translated_book.next_volume = next.id',
100+
trim($join)
101+
);
102+
}
103+
});

0 commit comments

Comments
 (0)