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