Skip to content

Commit bbad472

Browse files
norbedg
authored andcommitted
SqlBuilder: Added ability to specify left join conditions
1 parent a86fdfb commit bbad472

File tree

6 files changed

+309
-24
lines changed

6 files changed

+309
-24
lines changed

src/Database/Table/Selection.php

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,22 +316,51 @@ public function wherePrimary($key)
316316
* Adds where condition, more calls appends with AND.
317317
* @param string condition possibly containing ?
318318
* @param mixed
319-
* @param mixed ...
320319
* @return self
321320
*/
322321
public function where($condition, ...$params)
322+
{
323+
$this->condition($condition, $params);
324+
return $this;
325+
}
326+
327+
328+
/**
329+
* Adds ON condition when joining specified table, more calls appends with AND.
330+
* @param string table chain or table alias for which you need additional left join condition
331+
* @param string condition possibly containing ?
332+
* @param mixed
333+
* @return self
334+
*/
335+
public function joinWhere($tableChain, $condition, ...$params)
336+
{
337+
$this->condition($condition, $params, $tableChain);
338+
return $this;
339+
}
340+
341+
342+
/**
343+
* Adds condition, more calls appends with AND.
344+
* @param string condition possibly containing ?
345+
* @return self
346+
*/
347+
protected function condition($condition, array $params, $tableChain = NULL)
323348
{
324349
$this->emptyResultSet();
325350
if (is_array($condition) && $params === []) { // where(array('column1' => 1, 'column2 > ?' => 2))
326351
foreach ($condition as $key => $val) {
327352
if (is_int($key)) {
328-
$this->sqlBuilder->addWhere($val); // where('full condition')
353+
$this->condition($val, [], $tableChain); // where('full condition')
329354
} else {
330-
$this->sqlBuilder->addWhere($key, $val); // where('column', 1)
355+
$this->condition($key, [$val], $tableChain); // where('column', 1)
331356
}
332357
}
333358
} else {
334-
$this->sqlBuilder->addWhere($condition, ...$params);
359+
if ($tableChain) {
360+
$this->sqlBuilder->addJoinCondition($tableChain, $condition, ...$params);
361+
} else {
362+
$this->sqlBuilder->addWhere($condition, ...$params);
363+
}
335364
}
336365
return $this;
337366
}

src/Database/Table/SqlBuilder.php

Lines changed: 131 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,16 @@ class SqlBuilder
3838
/** @var array of where conditions */
3939
protected $where = [];
4040

41+
/** @var array of array of join conditions */
42+
protected $joinCondition = [];
43+
4144
/** @var array of where conditions for caching */
4245
protected $conditions = [];
4346

4447
/** @var array of parameters passed to where conditions */
4548
protected $parameters = [
4649
'select' => [],
50+
'joinCondition' => [],
4751
'where' => [],
4852
'group' => [],
4953
'having' => [],
@@ -83,6 +87,9 @@ class SqlBuilder
8387
/** @var array */
8488
private $cacheTableList;
8589

90+
/** @var array of expanding joins */
91+
private $expandingJoins = [];
92+
8693

8794
public function __construct($tableName, Context $context)
8895
{
@@ -143,10 +150,12 @@ function ($col) { return "$this->tableName.$col"; },
143150
);
144151
}
145152

153+
$queryJoinConditions = $this->buildJoinConditions();
146154
$queryCondition = $this->buildConditions();
147155
$queryEnd = $this->buildQueryEnd();
148156

149157
$joins = [];
158+
$finalJoinConditions = $this->parseJoinConditions($joins, $queryJoinConditions);
150159
$this->parseJoins($joins, $queryCondition);
151160
$this->parseJoins($joins, $queryEnd);
152161

@@ -171,7 +180,7 @@ function ($col) { return "$this->tableName.$col"; },
171180
$querySelect = $this->buildSelect([$prefix . '*']);
172181
}
173182

174-
$queryJoins = $this->buildQueryJoins($joins);
183+
$queryJoins = $this->buildQueryJoins($joins, $finalJoinConditions);
175184
$query = "{$querySelect} FROM {$this->delimitedTable}{$queryJoins}{$queryCondition}{$queryEnd}";
176185

177186
$this->driver->applyLimit($query, $this->limit, $this->offset);
@@ -182,8 +191,12 @@ function ($col) { return "$this->tableName.$col"; },
182191

183192
public function getParameters()
184193
{
194+
if (!isset($this->parameters['joinConditionSorted'])) {
195+
$this->buildSelectQuery();
196+
}
185197
return array_merge(
186198
$this->parameters['select'],
199+
$this->parameters['joinConditionSorted'] ? call_user_func_array('array_merge', $this->parameters['joinConditionSorted']) : [],
187200
$this->parameters['where'],
188201
$this->parameters['group'],
189202
$this->parameters['having'],
@@ -195,7 +208,9 @@ public function getParameters()
195208
public function importConditions(SqlBuilder $builder)
196209
{
197210
$this->where = $builder->where;
211+
$this->joinCondition = $builder->joinCondition;
198212
$this->parameters['where'] = $builder->parameters['where'];
213+
$this->parameters['joinCondition'] = $builder->parameters['joinCondition'];
199214
$this->conditions = $builder->conditions;
200215
$this->aliases = $builder->aliases;
201216
$this->reservedTableNames = $builder->reservedTableNames;
@@ -222,9 +237,25 @@ public function getSelect()
222237

223238

224239
public function addWhere($condition, ...$params)
240+
{
241+
return $this->addCondition($condition, $params, $this->where, $this->parameters['where']);
242+
}
243+
244+
245+
public function addJoinCondition($tableChain, $condition, ...$params)
246+
{
247+
$this->parameters['joinConditionSorted'] = NULL;
248+
if (!isset($this->joinCondition[$tableChain])) {
249+
$this->joinCondition[$tableChain] = $this->parameters['joinCondition'][$tableChain] = [];
250+
}
251+
return $this->addCondition($condition, $params, $this->joinCondition[$tableChain], $this->parameters['joinCondition'][$tableChain]);
252+
}
253+
254+
255+
protected function addCondition($condition, array $params, array & $conditions, array & $conditionsParameters)
225256
{
226257
if (is_array($condition) && !empty($params[0]) && is_array($params[0])) {
227-
return $this->addWhereComposition($condition, $params[0]);
258+
return $this->addConditionComposition($condition, $params[0], $conditions, $conditionsParameters);
228259
}
229260

230261
$hash = $this->getConditionHash($condition, $params);
@@ -284,7 +315,7 @@ public function addWhere($condition, ...$params)
284315
if ($this->driver->isSupported(ISupplementalDriver::SUPPORT_SUBSELECT)) {
285316
$arg = NULL;
286317
$replace = $match[2][0] . '(' . $clone->getSql() . ')';
287-
$this->parameters['where'] = array_merge($this->parameters['where'], $clone->getSqlBuilder()->getParameters());
318+
$conditionsParameters = array_merge($conditionsParameters, $clone->getSqlBuilder()->getParameters());
288319
} else {
289320
$arg = [];
290321
foreach ($clone as $row) {
@@ -310,16 +341,16 @@ public function addWhere($condition, ...$params)
310341
$arg = NULL;
311342
} else {
312343
$replace = $match[2][0] . '(?)';
313-
$this->parameters['where'][] = $arg;
344+
$conditionsParameters[] = $arg;
314345
}
315346
}
316347
} elseif ($arg instanceof SqlLiteral) {
317-
$this->parameters['where'][] = $arg;
348+
$conditionsParameters[] = $arg;
318349
} else {
319350
if (!$hasOperator) {
320351
$replace = '= ?';
321352
}
322-
$this->parameters['where'][] = $arg;
353+
$conditionsParameters[] = $arg;
323354
}
324355

325356
if ($replace) {
@@ -332,7 +363,7 @@ public function addWhere($condition, ...$params)
332363
}
333364
}
334365

335-
$this->where[] = $condition;
366+
$conditions[] = $condition;
336367
return TRUE;
337368
}
338369

@@ -446,18 +477,91 @@ protected function buildSelect(array $columns)
446477
}
447478

448479

480+
protected function parseJoinConditions(& $joins, $joinConditions)
481+
{
482+
$tableJoins = $leftJoinDependency = $finalJoinConditions = [];
483+
foreach ($joinConditions as $tableChain => & $joinCondition) {
484+
$fooQuery = $tableChain . '.foo';
485+
$requiredJoins = [];
486+
$this->parseJoins($requiredJoins, $fooQuery);
487+
$tableAlias = substr($fooQuery, 0, -4);
488+
$tableJoins[$tableAlias] = $requiredJoins;
489+
$leftJoinDependency[$tableAlias] = [];
490+
$finalJoinConditions[$tableAlias] = preg_replace_callback($this->getColumnChainsRegxp(), function ($match) use ($tableAlias, & $tableJoins, & $leftJoinDependency) {
491+
$requiredJoins = [];
492+
$query = $this->parseJoinsCb($requiredJoins, $match);
493+
$queryParts = explode('.', $query);
494+
$tableJoins[$queryParts[0]] = $requiredJoins;
495+
if ($queryParts[0] !== $tableAlias) {
496+
foreach (array_keys($requiredJoins) as $requiredTable) {
497+
$leftJoinDependency[$tableAlias][$requiredTable] = $requiredTable;
498+
}
499+
}
500+
return $query;
501+
}, $joinCondition);
502+
}
503+
$this->parameters['joinConditionSorted'] = [];
504+
if (count($joinConditions)) {
505+
while (reset($tableJoins)) {
506+
$this->getSortedJoins(key($tableJoins), $leftJoinDependency, $tableJoins, $joins);
507+
}
508+
}
509+
return $finalJoinConditions;
510+
}
511+
512+
513+
protected function getSortedJoins($table, & $leftJoinDependency, & $tableJoins, & $finalJoins)
514+
{
515+
if (isset($this->expandingJoins[$table])) {
516+
$path = implode("' => '", array_map(function($value) { return $this->reservedTableNames[$value]; }, array_merge(array_keys($this->expandingJoins), [$table])));
517+
throw new Nette\InvalidArgumentException("Circular reference detected at left join conditions (tables '$path').");
518+
}
519+
if (isset($tableJoins[$table])) {
520+
$this->expandingJoins[$table] = TRUE;
521+
if (isset($leftJoinDependency[$table])) {
522+
foreach ($leftJoinDependency[$table] as $requiredTable) {
523+
if ($requiredTable === $table) {
524+
continue;
525+
}
526+
$this->getSortedJoins($requiredTable, $leftJoinDependency, $tableJoins, $finalJoins);
527+
}
528+
}
529+
if ($tableJoins[$table]) {
530+
foreach ($tableJoins[$table] as $requiredTable => $tmp) {
531+
if ($requiredTable === $table) {
532+
continue;
533+
}
534+
$this->getSortedJoins($requiredTable, $leftJoinDependency, $tableJoins, $finalJoins);
535+
}
536+
}
537+
$finalJoins += $tableJoins[$table];
538+
$this->parameters['joinConditionSorted'] += isset($this->parameters['joinCondition'][$this->reservedTableNames[$table]])
539+
? [$table => $this->parameters['joinCondition'][$this->reservedTableNames[$table]]]
540+
: [];
541+
unset($tableJoins[$table]);
542+
unset($this->expandingJoins[$table]);
543+
}
544+
}
545+
546+
449547
protected function parseJoins(& $joins, & $query)
450548
{
451-
$query = preg_replace_callback('~
549+
$query = preg_replace_callback($this->getColumnChainsRegxp(), function ($match) use (& $joins) {
550+
return $this->parseJoinsCb($joins, $match);
551+
}, $query);
552+
}
553+
554+
555+
private function getColumnChainsRegxp()
556+
{
557+
return '~
452558
(?(DEFINE)
453559
(?P<word> [\w_]*[a-z][\w_]* )
454560
(?P<del> [.:] )
455561
(?P<node> (?&del)? (?&word) (\((?&word)\))? )
456562
)
457563
(?P<chain> (?!\.) (?&node)*) \. (?P<column> (?&word) | \* )
458-
~xi', function ($match) use (& $joins) {
459-
return $this->parseJoinsCb($joins, $match);
460-
}, $query);
564+
~xi';
461565
}
462566

463567

@@ -567,18 +671,29 @@ public function parseJoinsCb(& $joins, $match)
567671
}
568672

569673

570-
protected function buildQueryJoins(array $joins)
674+
protected function buildQueryJoins(array $joins, array $leftJoinConditions = [])
571675
{
572676
$return = '';
573677
foreach ($joins as list($joinTable, $joinAlias, $table, $tableColumn, $joinColumn)) {
574678
$return .=
575679
" LEFT JOIN {$joinTable}" . ($joinTable !== $joinAlias ? " {$joinAlias}" : '') .
576-
" ON {$table}.{$tableColumn} = {$joinAlias}.{$joinColumn}";
680+
" ON {$table}.{$tableColumn} = {$joinAlias}.{$joinColumn}" .
681+
(isset($leftJoinConditions[$joinAlias]) ? " {$leftJoinConditions[$joinAlias]}" : '');
577682
}
578683
return $return;
579684
}
580685

581686

687+
protected function buildJoinConditions()
688+
{
689+
$conditions = [];
690+
foreach ($this->joinCondition as $tableChain => $joinConditions) {
691+
$conditions[$tableChain] = 'AND (' . implode(') AND (', $joinConditions) . ')';
692+
}
693+
return $conditions;
694+
}
695+
696+
582697
protected function buildConditions()
583698
{
584699
return $this->where ? ' WHERE (' . implode(') AND (', $this->where) . ')' : '';
@@ -609,14 +724,14 @@ protected function tryDelimite($s)
609724
}
610725

611726

612-
protected function addWhereComposition(array $columns, array $parameters)
727+
protected function addConditionComposition(array $columns, array $parameters, array & $conditions, array & $conditionsParameters)
613728
{
614729
if ($this->driver->isSupported(ISupplementalDriver::SUPPORT_MULTI_COLUMN_AS_OR_COND)) {
615730
$conditionFragment = '(' . implode(' = ? AND ', $columns) . ' = ?) OR ';
616731
$condition = substr(str_repeat($conditionFragment, count($parameters)), 0, -4);
617-
return $this->addWhere($condition, Nette\Utils\Arrays::flatten($parameters));
732+
return $this->addCondition($condition, [Nette\Utils\Arrays::flatten($parameters)], $conditions, $conditionsParameters);
618733
} else {
619-
return $this->addWhere('(' . implode(', ', $columns) . ') IN', $parameters);
734+
return $this->addCondition('(' . implode(', ', $columns) . ') IN', [$parameters], $conditions, $conditionsParameters);
620735
}
621736
}
622737

tests/Database/Table/SqlBuilder.addAlias().phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ class SqlBuilderMock extends SqlBuilder
1919
{
2020
parent::parseJoins($joins, $query);
2121
}
22-
public function buildQueryJoins(array $joins, $leftConditions = [])
22+
public function buildQueryJoins(array $joins, array $leftJoinConditions = [])
2323
{
24-
return parent::buildQueryJoins($joins, $leftConditions);
24+
return parent::buildQueryJoins($joins, $leftJoinConditions);
2525
}
2626
}
2727

0 commit comments

Comments
 (0)