1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Database\Table;
9:
10: use Nette;
11: use Nette\Database\ISupplementalDriver;
12: use Nette\Database\SqlLiteral;
13: use Nette\Database\IConventions;
14: use Nette\Database\Context;
15: use Nette\Database\IStructure;
16:
17:
18: 19: 20: 21:
22: class SqlBuilder
23: {
24: use Nette\SmartObject;
25:
26:
27: protected $tableName;
28:
29:
30: protected $conventions;
31:
32:
33: protected $delimitedTable;
34:
35:
36: protected $select = [];
37:
38:
39: protected $where = [];
40:
41:
42: protected $joinCondition = [];
43:
44:
45: protected $conditions = [];
46:
47:
48: protected $parameters = [
49: 'select' => [],
50: 'joinCondition' => [],
51: 'where' => [],
52: 'group' => [],
53: 'having' => [],
54: 'order' => [],
55: ];
56:
57:
58: protected $order = [];
59:
60:
61: protected $limit = NULL;
62:
63:
64: protected $offset = NULL;
65:
66:
67: protected $group = '';
68:
69:
70: protected $having = '';
71:
72:
73: protected $reservedTableNames = [];
74:
75:
76: protected $aliases = [];
77:
78:
79: protected $currentAlias = NULL;
80:
81:
82: private $driver;
83:
84:
85: private $structure;
86:
87:
88: private $cacheTableList;
89:
90:
91: private $expandingJoins = [];
92:
93:
94: public function __construct($tableName, Context $context)
95: {
96: $this->tableName = $tableName;
97: $this->driver = $context->getConnection()->getSupplementalDriver();
98: $this->conventions = $context->getConventions();
99: $this->structure = $context->getStructure();
100: $tableNameParts = explode('.', $tableName);
101: $this->delimitedTable = implode('.', array_map([$this->driver, 'delimite'], $tableNameParts));
102: $this->checkUniqueTableName(end($tableNameParts), $tableName);
103: }
104:
105:
106: 107: 108:
109: public function getTableName()
110: {
111: return $this->tableName;
112: }
113:
114:
115: public function buildInsertQuery()
116: {
117: return "INSERT INTO {$this->delimitedTable}";
118: }
119:
120:
121: public function buildUpdateQuery()
122: {
123: if ($this->limit !== NULL || $this->offset) {
124: throw new Nette\NotSupportedException('LIMIT clause is not supported in UPDATE query.');
125: }
126: return "UPDATE {$this->delimitedTable} SET ?set" . $this->tryDelimite($this->buildConditions());
127: }
128:
129:
130: public function buildDeleteQuery()
131: {
132: if ($this->limit !== NULL || $this->offset) {
133: throw new Nette\NotSupportedException('LIMIT clause is not supported in DELETE query.');
134: }
135: return "DELETE FROM {$this->delimitedTable}" . $this->tryDelimite($this->buildConditions());
136: }
137:
138:
139: 140: 141: 142:
143: public function getSelectQueryHash($columns = NULL)
144: {
145: $parts = [
146: 'delimitedTable' => $this->delimitedTable,
147: 'queryCondition' => $this->buildConditions(),
148: 'queryEnd' => $this->buildQueryEnd(),
149: $this->aliases,
150: $this->limit, $this->offset,
151: ];
152: if ($this->select) {
153: $parts[] = $this->select;
154: } elseif ($columns) {
155: $parts[] = [$this->delimitedTable, $columns];
156: } elseif ($this->group && !$this->driver->isSupported(ISupplementalDriver::SUPPORT_SELECT_UNGROUPED_COLUMNS)) {
157: $parts[] = [$this->group];
158: } else {
159: $parts[] = "{$this->delimitedTable}.*";
160: }
161: return $this->getConditionHash(json_encode($parts), [
162: $this->parameters['select'],
163: $this->parameters['joinCondition'],
164: $this->parameters['where'],
165: $this->parameters['group'],
166: $this->parameters['having'],
167: $this->parameters['order']
168: ]);
169: }
170:
171:
172: 173: 174: 175: 176:
177: public function buildSelectQuery($columns = NULL)
178: {
179: if (!$this->order && ($this->limit !== NULL || $this->offset)) {
180: $this->order = array_map(
181: function ($col) { return "$this->tableName.$col"; },
182: (array) $this->conventions->getPrimary($this->tableName)
183: );
184: }
185:
186: $queryJoinConditions = $this->buildJoinConditions();
187: $queryCondition = $this->buildConditions();
188: $queryEnd = $this->buildQueryEnd();
189:
190: $joins = [];
191: $finalJoinConditions = $this->parseJoinConditions($joins, $queryJoinConditions);
192: $this->parseJoins($joins, $queryCondition);
193: $this->parseJoins($joins, $queryEnd);
194:
195: if ($this->select) {
196: $querySelect = $this->buildSelect($this->select);
197: $this->parseJoins($joins, $querySelect);
198:
199: } elseif ($columns) {
200: $prefix = $joins ? "{$this->delimitedTable}." : '';
201: $cols = [];
202: foreach ($columns as $col) {
203: $cols[] = $prefix . $col;
204: }
205: $querySelect = $this->buildSelect($cols);
206:
207: } elseif ($this->group && !$this->driver->isSupported(ISupplementalDriver::SUPPORT_SELECT_UNGROUPED_COLUMNS)) {
208: $querySelect = $this->buildSelect([$this->group]);
209: $this->parseJoins($joins, $querySelect);
210:
211: } else {
212: $prefix = $joins ? "{$this->delimitedTable}." : '';
213: $querySelect = $this->buildSelect([$prefix . '*']);
214: }
215:
216: $queryJoins = $this->buildQueryJoins($joins, $finalJoinConditions);
217: $query = "{$querySelect} FROM {$this->delimitedTable}{$queryJoins}{$queryCondition}{$queryEnd}";
218:
219: $this->driver->applyLimit($query, $this->limit, $this->offset);
220:
221: return $this->tryDelimite($query);
222: }
223:
224:
225: public function getParameters()
226: {
227: if (!isset($this->parameters['joinConditionSorted'])) {
228: $this->buildSelectQuery();
229: }
230: return array_merge(
231: $this->parameters['select'],
232: $this->parameters['joinConditionSorted'] ? call_user_func_array('array_merge', $this->parameters['joinConditionSorted']) : [],
233: $this->parameters['where'],
234: $this->parameters['group'],
235: $this->parameters['having'],
236: $this->parameters['order']
237: );
238: }
239:
240:
241: public function importConditions(SqlBuilder $builder)
242: {
243: $this->where = $builder->where;
244: $this->joinCondition = $builder->joinCondition;
245: $this->parameters['where'] = $builder->parameters['where'];
246: $this->parameters['joinCondition'] = $builder->parameters['joinCondition'];
247: $this->conditions = $builder->conditions;
248: $this->aliases = $builder->aliases;
249: $this->reservedTableNames = $builder->reservedTableNames;
250: }
251:
252:
253:
254:
255:
256: public function addSelect($columns, ...$params)
257: {
258: if (is_array($columns)) {
259: throw new Nette\InvalidArgumentException('Select column must be a string.');
260: }
261: $this->select[] = $columns;
262: $this->parameters['select'] = array_merge($this->parameters['select'], $params);
263: }
264:
265:
266: public function getSelect()
267: {
268: return $this->select;
269: }
270:
271:
272: public function addWhere($condition, ...$params)
273: {
274: return $this->addCondition($condition, $params, $this->where, $this->parameters['where']);
275: }
276:
277:
278: public function addJoinCondition($tableChain, $condition, ...$params)
279: {
280: $this->parameters['joinConditionSorted'] = NULL;
281: if (!isset($this->joinCondition[$tableChain])) {
282: $this->joinCondition[$tableChain] = $this->parameters['joinCondition'][$tableChain] = [];
283: }
284: return $this->addCondition($condition, $params, $this->joinCondition[$tableChain], $this->parameters['joinCondition'][$tableChain]);
285: }
286:
287:
288: protected function addCondition($condition, array $params, array & $conditions, array & $conditionsParameters)
289: {
290: if (is_array($condition) && !empty($params[0]) && is_array($params[0])) {
291: return $this->addConditionComposition($condition, $params[0], $conditions, $conditionsParameters);
292: }
293:
294: $hash = $this->getConditionHash($condition, $params);
295: if (isset($this->conditions[$hash])) {
296: return FALSE;
297: }
298:
299: $this->conditions[$hash] = $condition;
300: $placeholderCount = substr_count($condition, '?');
301: if ($placeholderCount > 1 && count($params) === 1 && is_array($params[0])) {
302: $params = $params[0];
303: }
304:
305: $condition = trim($condition);
306: if ($placeholderCount === 0 && count($params) === 1) {
307: $condition .= ' ?';
308: } elseif ($placeholderCount !== count($params)) {
309: throw new Nette\InvalidArgumentException('Argument count does not match placeholder count.');
310: }
311:
312: $replace = NULL;
313: $placeholderNum = 0;
314: foreach ($params as $arg) {
315: preg_match('#(?:.*?\?.*?){' . $placeholderNum . '}(((?:&|\||^|~|\+|-|\*|/|%|\(|,|<|>|=|(?<=\W|^)(?:REGEXP|ALL|AND|ANY|BETWEEN|EXISTS|IN|[IR]?LIKE|OR|NOT|SOME|INTERVAL))\s*)?(?:\(\?\)|\?))#s', $condition, $match, PREG_OFFSET_CAPTURE);
316: $hasOperator = ($match[1][0] === '?' && $match[1][1] === 0) ? TRUE : !empty($match[2][0]);
317:
318: if ($arg === NULL) {
319: $replace = 'IS NULL';
320: if ($hasOperator) {
321: if (trim($match[2][0]) === 'NOT') {
322: $replace = 'IS NOT NULL';
323: } else {
324: throw new Nette\InvalidArgumentException('Column operator does not accept NULL argument.');
325: }
326: }
327: } elseif (is_array($arg) || $arg instanceof Selection) {
328: if ($hasOperator) {
329: if (trim($match[2][0]) === 'NOT') {
330: $match[2][0] = rtrim($match[2][0]) . ' IN ';
331: } elseif (trim($match[2][0]) !== 'IN') {
332: throw new Nette\InvalidArgumentException('Column operator does not accept array argument.');
333: }
334: } else {
335: $match[2][0] = 'IN ';
336: }
337:
338: if ($arg instanceof Selection) {
339: $clone = clone $arg;
340: if (!$clone->getSqlBuilder()->select) {
341: try {
342: $clone->select($clone->getPrimary());
343: } catch (\LogicException $e) {
344: throw new Nette\InvalidArgumentException('Selection argument must have defined a select column.', 0, $e);
345: }
346: }
347:
348: if ($this->driver->isSupported(ISupplementalDriver::SUPPORT_SUBSELECT)) {
349: $arg = NULL;
350: $replace = $match[2][0] . '(' . $clone->getSql() . ')';
351: $conditionsParameters = array_merge($conditionsParameters, $clone->getSqlBuilder()->getParameters());
352: } else {
353: $arg = [];
354: foreach ($clone as $row) {
355: $arg[] = array_values(iterator_to_array($row));
356: }
357: }
358: }
359:
360: if ($arg !== NULL) {
361: if (!$arg) {
362: $hasBrackets = strpos($condition, '(') !== FALSE;
363: $hasOperators = preg_match('#AND|OR#', $condition);
364: $hasNot = strpos($condition, 'NOT') !== FALSE;
365: $hasPrefixNot = strpos($match[2][0], 'NOT') !== FALSE;
366: if (!$hasBrackets && ($hasOperators || ($hasNot && !$hasPrefixNot))) {
367: throw new Nette\InvalidArgumentException('Possible SQL query corruption. Add parentheses around operators.');
368: }
369: if ($hasPrefixNot) {
370: $replace = 'IS NULL OR TRUE';
371: } else {
372: $replace = 'IS NULL AND FALSE';
373: }
374: $arg = NULL;
375: } else {
376: $replace = $match[2][0] . '(?)';
377: $conditionsParameters[] = $arg;
378: }
379: }
380: } elseif ($arg instanceof SqlLiteral) {
381: $conditionsParameters[] = $arg;
382: } else {
383: if (!$hasOperator) {
384: $replace = '= ?';
385: }
386: $conditionsParameters[] = $arg;
387: }
388:
389: if ($replace) {
390: $condition = substr_replace($condition, $replace, $match[1][1], strlen($match[1][0]));
391: $replace = NULL;
392: }
393:
394: if ($arg !== NULL) {
395: $placeholderNum++;
396: }
397: }
398:
399: $conditions[] = $condition;
400: return TRUE;
401: }
402:
403:
404: public function getConditions()
405: {
406: return array_values($this->conditions);
407: }
408:
409:
410: 411: 412: 413:
414: public function addAlias($chain, $alias)
415: {
416: if (isset($chain[0]) && $chain[0] !== '.' && $chain[0] !== ':') {
417: $chain = '.' . $chain;
418: }
419: $this->checkUniqueTableName($alias, $chain);
420: $this->aliases[$alias] = $chain;
421: }
422:
423:
424: protected function checkUniqueTableName($tableName, $chain)
425: {
426: if (isset($this->aliases[$tableName]) && ('.' . $tableName === $chain)) {
427: $chain = $this->aliases[$tableName];
428: }
429: if (isset($this->reservedTableNames[$tableName])) {
430: if ($this->reservedTableNames[$tableName] === $chain) {
431: return;
432: }
433: 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.");
434: }
435: $this->reservedTableNames[$tableName] = $chain;
436: }
437:
438:
439: public function addOrder($columns, ...$params)
440: {
441: $this->order[] = $columns;
442: $this->parameters['order'] = array_merge($this->parameters['order'], $params);
443: }
444:
445:
446: public function setOrder(array $columns, array $parameters)
447: {
448: $this->order = $columns;
449: $this->parameters['order'] = $parameters;
450: }
451:
452:
453: public function getOrder()
454: {
455: return $this->order;
456: }
457:
458:
459: public function setLimit($limit, $offset)
460: {
461: $this->limit = $limit;
462: $this->offset = $offset;
463: }
464:
465:
466: public function getLimit()
467: {
468: return $this->limit;
469: }
470:
471:
472: public function getOffset()
473: {
474: return $this->offset;
475: }
476:
477:
478: public function setGroup($columns, ...$params)
479: {
480: $this->group = $columns;
481: $this->parameters['group'] = $params;
482: }
483:
484:
485: public function getGroup()
486: {
487: return $this->group;
488: }
489:
490:
491: public function setHaving($having, ...$params)
492: {
493: $this->having = $having;
494: $this->parameters['having'] = $params;
495: }
496:
497:
498: public function getHaving()
499: {
500: return $this->having;
501: }
502:
503:
504:
505:
506:
507: protected function buildSelect(array $columns)
508: {
509: return 'SELECT ' . implode(', ', $columns);
510: }
511:
512:
513: protected function parseJoinConditions(& $joins, $joinConditions)
514: {
515: $tableJoins = $leftJoinDependency = $finalJoinConditions = [];
516: foreach ($joinConditions as $tableChain => & $joinCondition) {
517: $fooQuery = $tableChain . '.foo';
518: $requiredJoins = [];
519: $this->parseJoins($requiredJoins, $fooQuery);
520: $tableAlias = substr($fooQuery, 0, -4);
521: $tableJoins[$tableAlias] = $requiredJoins;
522: $leftJoinDependency[$tableAlias] = [];
523: $finalJoinConditions[$tableAlias] = preg_replace_callback($this->getColumnChainsRegxp(), function ($match) use ($tableAlias, & $tableJoins, & $leftJoinDependency) {
524: $requiredJoins = [];
525: $query = $this->parseJoinsCb($requiredJoins, $match);
526: $queryParts = explode('.', $query);
527: $tableJoins[$queryParts[0]] = $requiredJoins;
528: if ($queryParts[0] !== $tableAlias) {
529: foreach (array_keys($requiredJoins) as $requiredTable) {
530: $leftJoinDependency[$tableAlias][$requiredTable] = $requiredTable;
531: }
532: }
533: return $query;
534: }, $joinCondition);
535: }
536: $this->parameters['joinConditionSorted'] = [];
537: if (count($joinConditions)) {
538: while (reset($tableJoins)) {
539: $this->getSortedJoins(key($tableJoins), $leftJoinDependency, $tableJoins, $joins);
540: }
541: }
542: return $finalJoinConditions;
543: }
544:
545:
546: protected function getSortedJoins($table, & $leftJoinDependency, & $tableJoins, & $finalJoins)
547: {
548: if (isset($this->expandingJoins[$table])) {
549: $path = implode("' => '", array_map(function($value) { return $this->reservedTableNames[$value]; }, array_merge(array_keys($this->expandingJoins), [$table])));
550: throw new Nette\InvalidArgumentException("Circular reference detected at left join conditions (tables '$path').");
551: }
552: if (isset($tableJoins[$table])) {
553: $this->expandingJoins[$table] = TRUE;
554: if (isset($leftJoinDependency[$table])) {
555: foreach ($leftJoinDependency[$table] as $requiredTable) {
556: if ($requiredTable === $table) {
557: continue;
558: }
559: $this->getSortedJoins($requiredTable, $leftJoinDependency, $tableJoins, $finalJoins);
560: }
561: }
562: if ($tableJoins[$table]) {
563: foreach ($tableJoins[$table] as $requiredTable => $tmp) {
564: if ($requiredTable === $table) {
565: continue;
566: }
567: $this->getSortedJoins($requiredTable, $leftJoinDependency, $tableJoins, $finalJoins);
568: }
569: }
570: $finalJoins += $tableJoins[$table];
571: $this->parameters['joinConditionSorted'] += isset($this->parameters['joinCondition'][$this->reservedTableNames[$table]])
572: ? [$table => $this->parameters['joinCondition'][$this->reservedTableNames[$table]]]
573: : [];
574: unset($tableJoins[$table]);
575: unset($this->expandingJoins[$table]);
576: }
577: }
578:
579:
580: protected function parseJoins(& $joins, & $query)
581: {
582: $query = preg_replace_callback($this->getColumnChainsRegxp(), function ($match) use (& $joins) {
583: return $this->parseJoinsCb($joins, $match);
584: }, $query);
585: }
586:
587:
588: private function getColumnChainsRegxp()
589: {
590: return '~
591: (?(DEFINE)
592: (?P<word> [\w_]*[a-z][\w_]* )
593: (?P<del> [.:] )
594: (?P<node> (?&del)? (?&word) (\((?&word)\))? )
595: )
596: (?P<chain> (?!\.) (?&node)*) \. (?P<column> (?&word) | \* )
597: ~xi';
598: }
599:
600:
601: public function parseJoinsCb(& $joins, $match)
602: {
603: $chain = $match['chain'];
604: if (!empty($chain[0]) && ($chain[0] !== '.' && $chain[0] !== ':')) {
605: $chain = '.' . $chain;
606: }
607:
608: preg_match_all('~
609: (?(DEFINE)
610: (?P<word> [\w_]*[a-z][\w_]* )
611: )
612: (?P<del> [.:])?(?P<key> (?&word))(\((?P<throughColumn> (?&word))\))?
613: ~xi', $chain, $keyMatches, PREG_SET_ORDER);
614:
615: $parent = $this->tableName;
616: $parentAlias = preg_replace('#^(.*\.)?(.*)$#', '$2', $this->tableName);
617:
618:
619: if ($this->driver->isSupported(ISupplementalDriver::SUPPORT_SCHEMA) && count($keyMatches) > 1) {
620: $tables = $this->getCachedTableList();
621: if (!isset($tables[$keyMatches[0]['key']]) && isset($tables[$keyMatches[0]['key'] . '.' . $keyMatches[1]['key']])) {
622: $keyMatch = array_shift($keyMatches);
623: $keyMatches[0]['key'] = $keyMatch['key'] . '.' . $keyMatches[0]['key'];
624: $keyMatches[0]['del'] = $keyMatch['del'];
625: }
626: }
627:
628:
629:
630: if ($keyMatches[0]['del'] === '.') {
631: if (count($keyMatches) > 1 && ($parent === $keyMatches[0]['key'] || $parentAlias === $keyMatches[0]['key'])) {
632: 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.");
633: }
634: if ($parent === $keyMatches[0]['key']) {
635: return "{$parent}.{$match['column']}";
636: } elseif ($parentAlias === $keyMatches[0]['key']) {
637: return "{$parentAlias}.{$match['column']}";
638: }
639: }
640: $tableChain = NULL;
641: foreach ($keyMatches as $index => $keyMatch) {
642: $isLast = !isset($keyMatches[$index + 1]);
643: if (!$index && isset($this->aliases[$keyMatch['key']])) {
644: if ($keyMatch['del'] === ':') {
645: throw new Nette\InvalidArgumentException("You are using has many syntax with alias (':{$keyMatch['key']}'). You have to move it to alias definition.");
646: } else {
647: $previousAlias = $this->currentAlias;
648: $this->currentAlias = $keyMatch['key'];
649: $requiredJoins = [];
650: $query = $this->aliases[$keyMatch['key']] . '.foo';
651: $this->parseJoins($requiredJoins, $query);
652: $aliasJoin = array_pop($requiredJoins);
653: $joins += $requiredJoins;
654: list($table, , $parentAlias, $column, $primary) = $aliasJoin;
655: $this->currentAlias = $previousAlias;
656: }
657: } elseif ($keyMatch['del'] === ':') {
658: if (isset($keyMatch['throughColumn'])) {
659: $table = $keyMatch['key'];
660: $belongsTo = $this->conventions->getBelongsToReference($table, $keyMatch['throughColumn']);
661: if (!$belongsTo) {
662: throw new Nette\InvalidArgumentException("No reference found for \${$parent}->{$keyMatch['key']}.");
663: }
664: list(, $primary) = $belongsTo;
665:
666: } else {
667: $hasMany = $this->conventions->getHasManyReference($parent, $keyMatch['key']);
668: if (!$hasMany) {
669: throw new Nette\InvalidArgumentException("No reference found for \${$parent}->related({$keyMatch['key']}).");
670: }
671: list($table, $primary) = $hasMany;
672: }
673: $column = $this->conventions->getPrimary($parent);
674:
675: } else {
676: $belongsTo = $this->conventions->getBelongsToReference($parent, $keyMatch['key']);
677: if (!$belongsTo) {
678: throw new Nette\InvalidArgumentException("No reference found for \${$parent}->{$keyMatch['key']}.");
679: }
680: list($table, $column) = $belongsTo;
681: $primary = $this->conventions->getPrimary($table);
682: }
683:
684: if ($this->currentAlias && $isLast) {
685: $tableAlias = $this->currentAlias;
686: } elseif ($parent === $table) {
687: $tableAlias = $parentAlias . '_ref';
688: } elseif ($keyMatch['key']) {
689: $tableAlias = $keyMatch['key'];
690: } else {
691: $tableAlias = preg_replace('#^(.*\.)?(.*)$#', '$2', $table);
692: }
693:
694: $tableChain .= $keyMatch[0];
695: if (!$isLast || !$this->currentAlias) {
696: $this->checkUniqueTableName($tableAlias, $tableChain);
697: }
698: $joins[$tableAlias] = [$table, $tableAlias, $parentAlias, $column, $primary];
699: $parent = $table;
700: $parentAlias = $tableAlias;
701: }
702:
703: return $tableAlias . ".{$match['column']}";
704: }
705:
706:
707: protected function buildQueryJoins(array $joins, array $leftJoinConditions = [])
708: {
709: $return = '';
710: foreach ($joins as list($joinTable, $joinAlias, $table, $tableColumn, $joinColumn)) {
711: $return .=
712: " LEFT JOIN {$joinTable}" . ($joinTable !== $joinAlias ? " {$joinAlias}" : '') .
713: " ON {$table}.{$tableColumn} = {$joinAlias}.{$joinColumn}" .
714: (isset($leftJoinConditions[$joinAlias]) ? " {$leftJoinConditions[$joinAlias]}" : '');
715: }
716: return $return;
717: }
718:
719:
720: protected function buildJoinConditions()
721: {
722: $conditions = [];
723: foreach ($this->joinCondition as $tableChain => $joinConditions) {
724: $conditions[$tableChain] = 'AND (' . implode(') AND (', $joinConditions) . ')';
725: }
726: return $conditions;
727: }
728:
729:
730: protected function buildConditions()
731: {
732: return $this->where ? ' WHERE (' . implode(') AND (', $this->where) . ')' : '';
733: }
734:
735:
736: protected function buildQueryEnd()
737: {
738: $return = '';
739: if ($this->group) {
740: $return .= ' GROUP BY '. $this->group;
741: }
742: if ($this->having) {
743: $return .= ' HAVING '. $this->having;
744: }
745: if ($this->order) {
746: $return .= ' ORDER BY ' . implode(', ', $this->order);
747: }
748: return $return;
749: }
750:
751:
752: protected function tryDelimite($s)
753: {
754: return preg_replace_callback('#(?<=[^\w`"\[?]|^)[a-z_][a-z0-9_]*(?=[^\w`"(\]]|\z)#i', function ($m) {
755: return strtoupper($m[0]) === $m[0] ? $m[0] : $this->driver->delimite($m[0]);
756: }, $s);
757: }
758:
759:
760: protected function addConditionComposition(array $columns, array $parameters, array & $conditions, array & $conditionsParameters)
761: {
762: if ($this->driver->isSupported(ISupplementalDriver::SUPPORT_MULTI_COLUMN_AS_OR_COND)) {
763: $conditionFragment = '(' . implode(' = ? AND ', $columns) . ' = ?) OR ';
764: $condition = substr(str_repeat($conditionFragment, count($parameters)), 0, -4);
765: return $this->addCondition($condition, [Nette\Utils\Arrays::flatten($parameters)], $conditions, $conditionsParameters);
766: } else {
767: return $this->addCondition('(' . implode(', ', $columns) . ') IN', [$parameters], $conditions, $conditionsParameters);
768: }
769: }
770:
771:
772: private function getConditionHash($condition, $parameters)
773: {
774: foreach ($parameters as $key => & $parameter) {
775: if ($parameter instanceof Selection) {
776: $parameter = $this->getConditionHash($parameter->getSql(), $parameter->getSqlBuilder()->getParameters());
777: } elseif ($parameter instanceof SqlLiteral) {
778: $parameter = $this->getConditionHash($parameter->__toString(), $parameter->getParameters());
779: } elseif (is_object($parameter) && method_exists($parameter, '__toString')) {
780: $parameter = $parameter->__toString();
781: } elseif (is_array($parameter) || $parameter instanceof \ArrayAccess) {
782: $parameter = $this->getConditionHash($key, $parameter);
783: }
784: }
785: return md5($condition . json_encode($parameters));
786: }
787:
788:
789: private function getCachedTableList()
790: {
791: if (!$this->cacheTableList) {
792: $this->cacheTableList = array_flip(array_map(function ($pair) {
793: return isset($pair['fullName']) ? $pair['fullName'] : $pair['name'];
794: }, $this->structure->getTables()));
795: }
796:
797: return $this->cacheTableList;
798: }
799:
800: }
801: