1<?php
2
3namespace go\core\db;
4
5use Exception;
6use go\core\db\Column;
7use go\core\db\Criteria;
8use go\core\util\ArrayUtil;
9
10/**
11 * QueryBuilder
12 *
13 * Builds or executes an SQL string with a {@see Query} object anmd {@see AbstractRecord}
14 *
15 * @copyright (c) 2015, Intermesh BV http://www.intermesh.nl
16 * @author Merijn Schering <mschering@intermesh.nl>
17 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3
18 */
19class QueryBuilder {
20
21
22
23	/**
24	 *
25	 * @var Query
26	 */
27	private $query;
28
29	/**
30	 * The main table name
31	 *
32	 * @var string
33	 */
34	protected $tableName;
35
36	/**
37	 * The main table alias
38	 *
39	 * @var string
40	 */
41	protected $tableAlias;
42
43	/**
44	 * Key value array of parameters to bind to the SQL Statement
45	 *
46	 * Query::where() parameters will be put in here to bind.
47	 *
48	 * @var array[]
49	 */
50	private $buildBindParameters = [];
51
52	/**
53	 * To generate unique param tags for binding
54	 *
55	 * @var int
56	 */
57	private static $paramCount = 0;
58
59	/**
60	 * Prefix of the bind parameter tag
61	 * @var type
62	 */
63	private static $paramPrefix = ':go';
64
65	/**
66	 * Key value array with [tableAlias => Table()]
67	 *
68	 * Used to find the model that belongs to an alias to find column types
69	 * @var Table[]
70	 */
71	protected $aliasMap = [];
72
73	/**
74	 *
75	 * @var Table
76	 */
77	private $table;
78
79
80	/**
81	 * @var Connection
82	 */
83	private $conn;
84
85	public function __construct(Connection $conn) {
86		$this->conn = $conn;
87	}
88
89
90  /**
91   * Constructor
92   *
93   * @param string $tableName The table to operate on
94   * @throws Exception
95   */
96	public function setTableName($tableName) {
97
98		if(!isset($tableName)) {
99			throw new \Exception("No from() table set for the select query");
100
101		}
102		$this->tableName = $tableName;
103		$this->table = Table::getInstance($tableName, $this->conn);
104	}
105
106	/**
107	 * Used when building sub queries for when aliases of the main query are used
108	 * in the subquery.
109	 *
110	 * @param array $aliasMap
111	 */
112	public function mergeAliasMap($aliasMap) {
113		$this->aliasMap = array_merge($this->aliasMap, $aliasMap);
114	}
115
116	/**
117	 * Get the query parameters
118	 *
119	 * @return Query
120	 */
121	public function getQuery() {
122		return $this->query;
123	}
124
125	/**
126	 * Get the name of the record this query builder is for.
127	 *
128	 * @return string
129	 */
130	public function getTableName() {
131		return $this->tableName;
132	}
133
134  /**
135   * @param $tableName
136   * @param $data
137   * @param array $columns
138   * @param string $command
139   * @return array
140   * @throws Exception
141   */
142	public function buildInsert($tableName, $data, $columns = [], $command = "INSERT") {
143
144		$this->reset();
145		$this->setTableName($tableName);
146		$this->aliasMap[$tableName] = Table::getInstance($this->tableName, $this->conn);
147
148		$sql = $command . " ";
149
150		$sql .= "INTO `{$this->tableName}` ";
151
152		if ($data instanceof \go\core\db\Query) {
153			if(!empty($columns)) {
154				$sql .= " (`" . implode("`, `", $columns ) . "`)\n";
155			}
156
157			$build = $data->build();
158
159			$sql .= ' ' . $build['sql'];
160			$this->buildBindParameters = array_merge($this->buildBindParameters, $build['params']);
161		} else {
162			if(ArrayUtil::isAssociative($data)) {
163				$data = [$data];
164			}
165			if(empty($columns)) {
166				reset($data);
167				$columns = array_keys(current($data));
168			}
169			$sql .= " (\n\t`" . implode("`,\n\t`", $columns) . "`\n)\n" .
170				"VALUES \n";
171
172			foreach($data as $record) {
173				$tags = [];
174				foreach ($record as $colName => $value) {
175					if(is_int($colName)) {
176						$colName = $columns[$colName];
177					}
178
179					if($value instanceof Expression) {
180						$tags[] = (string) $value;
181					} else
182					{
183						$paramTag = $this->getParamTag();
184						$tags[] = $paramTag;
185						$this->addBuildBindParameter($paramTag, $value, $this->tableName, $colName);
186					}
187				}
188
189				$sql .= "(\n\t" . implode(",\n\t", $tags) . "\n), ";
190			}
191
192			$sql = substr($sql, 0, -2); //strip off last ', '
193		}
194
195		return ['sql' => $sql, 'params' => $this->buildBindParameters];
196	}
197
198	public function buildUpdate($tableName, $data, Query $query, $command = "UPDATE") {
199
200		$this->reset();
201
202		$this->setTableName($tableName);
203
204		$this->query = $query;
205		$this->buildBindParameters = $query->getBindParameters();
206		$this->tableAlias = $this->query->getTableAlias();
207		$this->aliasMap[$this->tableAlias] = Table::getInstance($this->tableName, $this->conn);
208
209		if (is_array($data)) {
210			$updates = [];
211			foreach ($data as $colName => $value) {
212
213				$tableAndCol = $this->splitTableAndColumn($colName);
214				$colName = '`' . $tableAndCol[0] .'`.`'.$tableAndCol[1].'`';
215				if($value instanceof Expression) {
216					$updates[] = $colName . ' = ' . $value;
217				} elseif($value instanceof Query) {
218					$build = $value->build();
219
220					$updates[] = $colName . ' = (' . $build['sql'] .')';
221
222					$this->buildBindParameters = array_merge($this->buildBindParameters, $build['params']);
223				} else
224				{
225					$paramTag = $this->getParamTag();
226					$updates[] = $colName . ' = ' . $paramTag;
227					$this->addBuildBindParameter($paramTag, $value, $tableAndCol[0], $tableAndCol[1]);
228				}
229			}
230			$set = implode(",\n\t", $updates);
231		} else if ($data instanceof Expression) {
232			$set = (string) $data;
233		}
234
235		$sql = $command . " `{$this->tableName}` `" . $this->tableAlias . "`";
236
237		foreach ($this->query->getJoins() as $join) {
238			$sql .= "\n" . $this->join($join, "");
239		}
240
241		$sql .= "\nSET\n\t" . $set;
242
243		$where = $this->buildWhere($this->getQuery()->getWhere());
244
245		if (!empty($where)) {
246			$sql .= "\nWHERE " . $where;
247		}
248
249		return ['sql' => $sql, 'params' => $this->buildBindParameters];
250	}
251
252	public function buildDelete($tableName, Query $query) {
253
254		$this->setTableName($tableName);
255		$this->reset();
256		$this->query = $query;
257		$this->buildBindParameters = $query->getBindParameters();
258		$this->tableAlias = $this->query->getTableAlias();
259		$this->aliasMap[$this->tableAlias] = Table::getInstance($this->tableName, $this->conn);
260
261		$sql = "DELETE FROM `" . $this->tableAlias . "` USING `" . $this->tableName . "` AS `" . $this->tableAlias . "` ";
262
263		foreach ($this->query->getJoins() as $join) {
264			$sql .= "\n" . $this->join($join, "");
265		}
266
267		$where = $this->buildWhere($this->getQuery()->getWhere());
268
269		if (!empty($where)) {
270			$sql .= "\nWHERE " . $where;
271		}
272
273		return ['sql' => $sql, 'params' => $this->buildBindParameters];
274	}
275
276	private function reset() {
277		$this->query = null;
278		$this->buildBindParameters = [];
279		$this->aliasMap = [];
280	}
281
282	/**
283	 * Build the select SQL and params
284	 */
285	public function buildSelect(Query $query = null, $prefix = "") {
286
287		$unions = $query->getUnions();
288
289		$r = $this->internalBuildSelect($query, empty($unions) ? $prefix : $prefix . "\t");
290
291		$unions = $query->getUnions();
292		if(empty($unions)) {
293			return $r;
294		}
295
296		$r['sql'] = "(\n" . $r['sql'];
297
298		foreach($unions as $q) {
299			$u = $this->internalBuildSelect($q, "\t");
300			$r['sql'] .=  "\n) UNION (\n" . $u['sql'];
301			$r['params'] = array_merge($r['params'], $u['params']);
302		}
303
304		$r['sql'] .= "\n)";
305
306		//reset to the main query object
307		$this->query = $query;
308		// Unions can't have aliases in the global scope
309		$this->aliasMap = [];
310
311		$orderBy = $this->buildOrderBy(true);
312		if(!empty($orderBy)) {
313			$r['sql'] .= "\n" . $orderBy;
314		}
315
316		if ($query->getUnionLimit() > 0) {
317			$r['sql'] .= "\nLIMIT " . $query->getUnionOffset() . ',' . $query->getUnionLimit();
318		}
319
320		return $r;
321
322	}
323
324	protected function internalBuildSelect(Query $query, $prefix = '') {
325		$this->reset();
326		$this->setTableName($query->getFrom());
327		$this->tableAlias = $query->getTableAlias();
328		$this->query = $query;
329		$this->buildBindParameters = $query->getBindParameters();
330
331		$this->aliasMap[$this->tableAlias] = Table::getInstance($this->tableName, $this->conn);
332
333		$joins = "";
334		foreach ($this->query->getJoins() as $join) {
335			$joins .= "\n" . $prefix . $this->join($join, $prefix);
336		}
337
338		$select = $prefix . $this->buildSelectFields();
339		$select .= "\n" . $prefix . "FROM `" . $this->tableName . '`';
340
341		if(isset($this->tableAlias) && $this->tableAlias != $this->tableName) {
342			$select .= ' `' . $this->tableAlias . "`";
343		}
344
345		$where = $this->buildWhere($this->query->getWhere(), $prefix);
346
347		if (!empty($where)) {
348			$where = "\n" . $prefix . "WHERE " . $where;
349		}
350		$group = $this->buildGroupBy();
351		if(!empty($group)) {
352			$group = "\n" . $prefix . $group;
353		}
354
355		$having = $this->buildHaving();
356		if(!empty($having)) {
357			$having = "\n" . $prefix . $having;
358		}
359		$orderBy = $this->buildOrderBy();
360		if(!empty($orderBy)) {
361			$orderBy = "\n" . $prefix . $orderBy;
362		}
363
364		$limit = "";
365		if ($this->query->getLimit() > 0) {
366			$limit .= "\n" . $prefix . "LIMIT " . $this->query->getOffset() . ',' . $this->query->getLimit();
367		}
368
369		$sql = $select . $joins . $where . $group . $having . $orderBy . $limit;
370
371		if ($this->query->getForUpdate()) {
372			$sql .= "\n" . $prefix . "FOR UPDATE";
373		}
374
375		return ['sql' => $sql, 'params' => $this->buildBindParameters];
376	}
377
378	/**
379	 * Will replace all :paramName tags with the values. Used for debugging the SQL string.
380	 *
381	 * @param array $build
382	 * @param string
383	 */
384	public static function debugBuild($build) {
385		$sql = $build['sql'];
386		$binds = [];
387		if(isset($build['params'])) {
388			foreach ($build['params'] as $p) {
389				if (is_string($p['value']) && !mb_check_encoding($p['value'], 'utf8')) {
390					$queryValue = "[NON UTF8 VALUE]";
391				} else {
392					$queryValue = var_export($p['value'], true);
393				}
394				$binds[$p['paramTag']] = $queryValue;
395			}
396		}
397
398		//sort so $binds :param1 does not replace :param11 first.
399		krsort($binds);
400
401		foreach ($binds as $tag => $value) {
402			$sql = str_replace($tag, $value, $sql);
403		}
404
405		return $sql;
406	}
407
408	protected function buildSelectFields() {
409		$select = "SELECT ";
410
411		if ($this->query->getCalcFoundRows()) {
412			$select .= "SQL_CALC_FOUND_ROWS ";
413		}
414
415		if ($this->query->getDistinct()) {
416			$select .= "DISTINCT ";
417		}
418
419		$s = $this->query->getSelect();
420		if (!empty($s)) {
421			$select .= implode(', ', $s);
422		} else {
423			$select .= '*';
424		}
425
426		return $select . ' ';
427	}
428
429	/**
430	 *
431	 * @param string $tableAlias
432	 * @param string $column
433	 * @return Column
434	 * @throws Exception
435	 */
436	private function findColumn($tableAlias, $column) {
437
438		if (!isset($this->aliasMap[$tableAlias])) {
439
440//			var_dump($this);
441			throw new Exception("Alias '" . $tableAlias . "'  not found in the aliasMap for " . $column);
442		}
443
444		if ($this->aliasMap[$tableAlias]->getColumn($column) == null) {
445			throw new Exception("Column '" . $column . "' not found in table " . $this->aliasMap[$tableAlias]->getName());
446		}
447		return $this->aliasMap[$tableAlias]->getColumn($column);
448	}
449
450	private function buildGroupBy() {
451
452		$qroupBy = $this->query->getGroupBy();
453
454		if (empty($qroupBy)) {
455			return '';
456		}
457
458		$groupBy = "GROUP BY ";
459
460		foreach (array_unique($qroupBy) as $column) {
461			if ($column instanceof Expression) {
462				$groupBy .= $column . ', ';
463			} else {
464				$groupBy .= $this->quoteTableAndColumnName($column) . ', ';
465			}
466		}
467
468		$groupBy = trim($groupBy, ' ,');
469
470		return $groupBy . "\n";
471	}
472	//Clear first AND or OR. Other wise WHERE AND ... will be generated
473	private $firstWhereCondition = true;
474
475	/**
476	 * Build the where part of the SQL string
477	 *
478	 * @param \go\core\db\Criteria $query
479	 * @param string $prefix Simple string prefix not really functional but only used to add some tab spaces to the SQL string for pretty formatting.
480	 * @param string
481	 */
482	protected function buildWhere(array $conditions, $prefix = "") {
483		$this->firstWhereCondition = true;
484
485		$where = "";
486		foreach ($conditions as $condition) {
487			$str = $this->buildCondition($condition, $prefix);
488			$where .= "\n" . $prefix . $str;
489		}
490
491		return rtrim($where);
492	}
493
494	/**
495	 * Convert where condition to string
496	 *
497	 * {@see Criteria::where()}
498	 *
499	 *
500	 * @param string|array|Criteria $condition
501	 * @param string $prefix
502	 * @return string
503	 * @throws Exception
504	 */
505	private function buildCondition($condition, $prefix = "") {
506
507		switch ($condition[0]) {
508			case 'column':
509				return $this->buildColumn($condition, $prefix);
510			default:
511				array_shift($condition);
512				return $this->buildTokens($condition, $prefix);
513		}
514	}
515
516	/**
517	 * Tokens is always:
518	 *
519	 * return ["tokens", AND/OR, string/expression/query/criteria];
520	 *
521	 * @param type $tokens
522	 * @param type $prefix
523	 * @return string
524	 */
525	private function buildTokens($tokens, $prefix) {
526		$str = "";
527
528		if(stripos($tokens[0], "NOT_OR_NULL") !== false) {
529			$tokens[0] = str_replace('NOT_OR_NULL', 'NOT IFNULL(', $tokens[0]);
530			$tokens[] = ', false)';
531		}
532
533		if($this->firstWhereCondition) {
534			//clear first AND/OR to avoid WHERE AND to be generated
535			$tokens[0] = str_ireplace(['AND', 'OR'], '', $tokens[0]);
536			$this->firstWhereCondition = false;
537		}
538
539		foreach ($tokens as $token) {
540			$str .= $this->buildToken($token, $prefix) . " ";
541		}
542
543		return $str;
544	}
545
546	private function buildToken($token, $prefix) {
547		if (is_string($token)) {
548			return $token;
549		} else {
550			if($token instanceof Expression) {
551				return (string) $token;
552			}
553
554			if($token instanceof Query) {
555				return $this->buildSubQuery($token, $prefix);
556			}
557
558			if($token instanceof Criteria) {
559				$this->buildBindParameters = array_merge($this->buildBindParameters, $token->getBindParameters());
560				$where = $this->buildWhere($token->getWhere(), $prefix . "\t");
561
562				return "(" . $where  . "\n" . $prefix . ")";
563			}
564
565			throw new \Exception("Invalid token?");
566		}
567	}
568
569	private function buildColumnArrayValue($logicalOperator, $columnName, $comparisonOperator, $value) {
570		//If the value is an array and it's not an IN query we do:
571		// (foo like array1 OR foo like array2)
572
573		if(empty($value)) {
574			return "";
575		}
576
577		if ($this->firstWhereCondition) {
578			//Clear first AND or OR. Other wise WHERE AND ... will be generated
579			$this->firstWhereCondition = false;
580			$logicalOperator = stripos($logicalOperator, "NOT") !== false ? "NOT" : "";
581		}
582
583		$str = $logicalOperator . " (";
584
585		for($i = 0, $c = count($value); $i < $c; $i++) {
586			$str .= $this->buildColumn([null, $i == 0 ? "" : "OR", $columnName, $comparisonOperator, $value[$i]], "");
587		}
588
589		return $str .= ")";
590	}
591
592	private function buildColumn($condition, $prefix) {
593
594		list(, $logicalOperator, $columnName, $comparisonOperator, $value) = $condition;
595
596
597		if(is_array($value) && $comparisonOperator != "=" && stripos($comparisonOperator, 'IN') === false) {
598			//single array value can be simplified and handled like non array value
599			if(count($value) == 1) {
600				$value = $value[0];
601			} else
602			{
603				return $prefix . $this->buildColumnArrayValue($logicalOperator, $columnName, $comparisonOperator, $value);
604			}
605		}
606
607		if ($this->firstWhereCondition) {
608			//Clear first AND or OR. Other wise WHERE AND ... will be generated
609			$this->firstWhereCondition = false;
610			$logicalOperator = stripos($logicalOperator, "NOT") !== false ? "NOT" : "";
611		}
612
613		$tokens = [$logicalOperator]; //AND / OR
614
615		$columnParts = $this->splitTableAndColumn($columnName);
616
617		if (empty($columnParts[0])) {
618			$tables = [];
619			foreach($this->aliasMap as $table) {
620				$tables[] = $table->getName();
621			}
622			throw new \Exception("Invalid column name '" . $columnName . "'. Not a column of any table: ".implode(', ', $tables));
623		}
624
625		$tokens[] = $this->quoteTableName($columnParts[0]) . '.' . $this->quoteColumnName($columnParts[1]); //column name
626
627		if (!isset($value)) {
628			if ($comparisonOperator == '=' || $comparisonOperator == 'IS') {
629				$tokens[] = "IS NULL";
630			} elseif ($comparisonOperator == '!=' || $comparisonOperator == 'IS NOT') {
631				$tokens[] = "IS NOT NULL";
632			} else {
633				throw new Exception('Null value not possible with comparator: ' . $comparisonOperator);
634			}
635		} else if (is_array($value)) {
636			$tokens[] = $comparisonOperator == "=" ? "IN" : $comparisonOperator;
637			$tokens[] = $this->buildInValues($columnParts, $value);
638		} else if ($value instanceof \go\core\db\Query) {
639			$tokens[] = $comparisonOperator;
640			$tokens[] = $value;
641		} else {
642			$paramTag = $this->getParamTag();
643
644			$this->addBuildBindParameter($paramTag, $value, $columnParts[0], $columnParts[1]);
645
646			$tokens[] = $comparisonOperator;
647			$tokens[] = $paramTag;
648		}
649
650		return $this->buildTokens($tokens, $prefix);
651	}
652
653	private function buildSubQuery(\go\core\db\Query $query, $prefix) {
654
655		//subquery
656		if ($query->getTableAlias() == 't' && $this->getQuery()->getTableAlias() == 't') {
657			$query->tableAlias('sub');
658		}
659
660		$builder = new QueryBuilder($this->conn);
661		$builder->aliasMap = $this->aliasMap;
662
663		$build = $builder->buildSelect($query, $prefix . "\t");
664
665		$str = "(\n" . $prefix . $build['sql'] . "\n" . $prefix . ")";
666
667		$this->buildBindParameters = array_merge($this->buildBindParameters, $build['params']);
668
669		return $str;
670	}
671
672	private function splitTableAndColumn($tableAndCol) {
673		$dot = strpos($tableAndCol, '.');
674
675		if ($dot !== false) {
676			$column = substr($tableAndCol, $dot + 1);
677			$alias = substr($tableAndCol, 0, $dot);
678			return [trim($alias, ' `'), trim($column, ' `')];
679		} else {
680			$colName = trim($tableAndCol, ' `');
681
682			//if column not found then don'use an alias. It could be an alias defined in the select part or a function.
683			$alias = null;
684
685			//find table for column
686			foreach($this->aliasMap as $tableAlias => $table) {
687				$columnObject = $table->getColumn($colName);
688				if ($columnObject) {
689					$alias = $tableAlias;
690					break;
691				}
692			}
693
694			return [$alias, $colName];
695		}
696	}
697
698	/**
699	 * Put's quotes around the table name and checks for injections
700	 *
701	 * @param string $tableName
702	 * @param string
703	 * @throws Exception
704	 */
705	protected function quoteTableName($tableName) {
706		return Utils::quoteTableName($tableName);
707	}
708
709	/**
710	 * Quotes a column name for use in a query.
711	 * If the column name contains prefix, the prefix will also be properly quoted.
712	 * If the column name is already quoted or contains '(', '[[' or '{{',
713	 * then this method will do nothing.
714	 *
715	 * @param string $columnName column name
716	 * @param string the properly quoted column name
717	 */
718	protected function quoteColumnName($columnName) {
719		return $this->quoteTableName($columnName);
720	}
721
722	/**
723	 * Splits table and column on the . separator and quotes them both.
724	 *
725	 * @param string $columnName
726	 * @param string
727	 */
728	protected function quoteTableAndColumnName($columnName) {
729
730		$parts = $this->splitTableAndColumn($columnName);
731
732		if (isset($parts[0])) {
733			return $this->quoteTableName($parts[0]) . '.' . $this->quoteColumnName($parts[1]);
734		} else {
735			return $this->quoteColumnName($parts[1]);
736		}
737	}
738
739	private function buildInValues($columnParts, $values) {
740
741		if (empty($values)) {
742			throw new \Exception("IN condition can not be empty!");
743		}
744
745		$str = "(";
746
747		foreach ($values as $value) {
748			$paramTag = $this->getParamTag();
749			$this->addBuildBindParameter($paramTag, $value, $columnParts[0], $columnParts[1]);
750
751			$str .= $paramTag . ', ';
752		}
753
754		$str = rtrim($str, ', ') . ")";
755
756		return $str;
757	}
758
759	private function buildOrderBy($forUnion = false) {
760		$oBy = $forUnion ? $this->query->getUnionOrderBy() : $this->query->getOrderBy();
761		if (empty($oBy)) {
762			return '';
763		}
764
765		$orderBy = "ORDER BY ";
766
767		foreach ($oBy as $column => $direction) {
768
769			if ($direction instanceof Expression) {
770				$orderBy .= $direction . ', ';
771			} else {
772				$direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC';
773				$orderBy .= $this->quoteTableAndColumnName($column) . ' ' . $direction . ', ';
774			}
775		}
776
777		return trim($orderBy, ' ,');
778	}
779
780	private function buildHaving() {
781
782		$h = $this->query->getHaving();
783		if (empty($h)) {
784			return '';
785		}
786
787		return "HAVING" . $this->buildWhere($h);
788	}
789
790	private function addBuildBindParameter($paramTag, $value, $tableAlias, $columnName) {
791
792		$columnObj = $this->findColumn($tableAlias, $columnName);
793
794		$this->buildBindParameters[] = [
795				'paramTag' => $paramTag,
796				'value' => $columnObj->castToDb($value),
797				'pdoType' => $columnObj->pdoType
798		];
799	}
800
801	/**
802	 * Private function to get the current parameter prefix.
803	 *
804	 * @param string The next available parameter prefix.
805	 */
806	private function getParamTag() {
807		self::$paramCount++;
808		return self::$paramPrefix . self::$paramCount;
809	}
810
811	private function join($config, $prefix) {
812		$join = "";
813
814		if ($config['src'] instanceof \go\core\db\Query) {
815			$builder = new QueryBuilder($this->conn);
816			$builder->aliasMap = $this->aliasMap;
817
818			$build = $builder->buildSelect($config['src'], $prefix . "\t");
819			$joinTableName = "(\n" . $prefix . "\t" . $build['sql'] . "\n" . $prefix . ')';
820
821			$this->buildBindParameters = array_merge($build['params']);
822		} else {
823			$this->aliasMap[$config['joinTableAlias']] = Table::getInstance($config['src'], $this->query->getDbConnection());
824			$joinTableName = '`' . $config['src'] . '`';
825		}
826
827		$join .= $config['type'] . ' JOIN ' . $joinTableName . ' ';
828
829		if (!empty($config['joinTableAlias'])) {
830			$join .= '`' . $config['joinTableAlias'] . '` ';
831		}
832
833
834		//import new params
835		$this->buildBindParameters = array_merge($this->buildBindParameters, $config['on']->getBindParameters());
836		$join .= 'ON ' . $this->buildWhere($config['on']->getWhere(), $prefix);
837
838		return $join;
839	}
840
841}
842