@@ -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
0 commit comments