| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\DataAccess; |
|
|
| use Exception; |
| use Piwik\DataAccess\LogQueryBuilder\JoinGenerator; |
| use Piwik\DataAccess\LogQueryBuilder\JoinTables; |
| use Piwik\Plugin\LogTablesProvider; |
| use Piwik\Segment\SegmentExpression; |
|
|
| class LogQueryBuilder |
| { |
| public const FORCE_INNER_GROUP_BY_NO_SUBSELECT = '__##nosubquery##__'; |
|
|
| |
| |
| |
| private $logTableProvider; |
|
|
| |
| |
| |
| |
| private $forcedInnerGroupBy = ''; |
|
|
| public function __construct(LogTablesProvider $logTablesProvider) |
| { |
| $this->logTableProvider = $logTablesProvider; |
| } |
|
|
| |
| |
| |
| |
| |
| public function forceInnerGroupBySubselect($innerGroupBy) |
| { |
| $this->forcedInnerGroupBy = $innerGroupBy; |
| } |
|
|
| public function getForcedInnerGroupBySubselect() |
| { |
| return $this->forcedInnerGroupBy; |
| } |
|
|
| public function getSelectQueryString( |
| SegmentExpression $segmentExpression, |
| $select, |
| $from, |
| $where, |
| $bind, |
| $groupBy, |
| $orderBy, |
| $limitAndOffset, |
| bool $withRollup = false |
| ) { |
| if (!is_array($from)) { |
| $from = array($from); |
| } |
|
|
| $fromInitially = $from; |
|
|
| if (!$segmentExpression->isEmpty()) { |
| $segmentExpression->parseSubExpressionsIntoSqlExpressions($from); |
| $segmentSql = $segmentExpression->getSql(); |
| $where = $this->getWhereMatchBoth($where, $segmentSql['where']); |
| $bind = array_merge($bind, $segmentSql['bind']); |
| } |
|
|
| |
| |
| if ($from === ['log_link_visit_action', 'log_visit']) { |
| $from[1] = ['table' => 'log_visit', 'join' => 'INNER JOIN']; |
| } |
|
|
| $tables = new JoinTables($this->logTableProvider, $from); |
| $join = new JoinGenerator($tables); |
| $join->generate(); |
| $from = $join->getJoinString(); |
| $joinWithSubSelect = $join->shouldJoinWithSelect(); |
|
|
| |
| $useSpecialConversionGroupBy = (!empty($segmentSql) |
| && strpos($groupBy, 'log_conversion.idgoal') !== false |
| && $fromInitially == array('log_conversion') |
| && strpos($from, 'log_link_visit_action') !== false); |
|
|
| if (!empty($this->forcedInnerGroupBy)) { |
| if ($this->forcedInnerGroupBy === self::FORCE_INNER_GROUP_BY_NO_SUBSELECT) { |
| $sql = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset); |
| } else { |
| $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $tables, $this->forcedInnerGroupBy); |
| } |
| } elseif ($useSpecialConversionGroupBy) { |
| $innerGroupBy = "CONCAT(log_conversion.idvisit, '_' , log_conversion.idgoal, '_', log_conversion.buster)"; |
| $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $tables, $innerGroupBy); |
| } elseif ($joinWithSubSelect) { |
| $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $tables); |
| } else { |
| $sql = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $withRollup); |
| } |
| return array( |
| 'sql' => $sql, |
| 'bind' => $bind, |
| ); |
| } |
|
|
| private function getKnownTables() |
| { |
| $names = array(); |
| foreach ($this->logTableProvider->getAllLogTablesWithTemporary() as $logTable) { |
| $names[] = $logTable->getName(); |
| } |
| return $names; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private function buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, JoinTables $tables, $innerGroupBy = null) |
| { |
| $matchTables = $this->getKnownTables(); |
| foreach ($tables as $table) { |
| if (is_array($table) && isset($table['tableAlias']) && !in_array($table['tableAlias'], $matchTables, $strict = true)) { |
| $matchTables[] = $table['tableAlias']; |
| } elseif (is_array($table) && isset($table['table']) && !in_array($table['table'], $matchTables, $strict = true)) { |
| $matchTables[] = $table['table']; |
| } elseif (is_string($table) && !in_array($table, $matchTables, $strict = true)) { |
| $matchTables[] = $table; |
| } |
| } |
|
|
| $matchTables = '(' . implode('|', $matchTables) . ')'; |
| preg_match_all("/" . $matchTables . "\.[a-z0-9_\*]+/", $select, $matches); |
| $neededFields = array_unique($matches[0]); |
|
|
| if (count($neededFields) == 0) { |
| throw new Exception("No needed fields found in select expression. " |
| . "Please use a table prefix."); |
| } |
|
|
| $fieldNames = array(); |
| $toBeReplaced = array(); |
| $epregReplace = array(); |
| foreach ($neededFields as &$neededField) { |
| $parts = explode('.', $neededField); |
| if (count($parts) === 2 && !empty($parts[1])) { |
| if (in_array($parts[1], $fieldNames, $strict = true)) { |
| |
| $columnAs = $parts[1] . md5($neededField); |
| $fieldNames[] = $columnAs; |
| |
| $toBeReplaced[$neededField . ' '] = $parts[0] . '.' . $columnAs . ' '; |
| $toBeReplaced[$neededField . ')'] = $parts[0] . '.' . $columnAs . ')'; |
| $toBeReplaced[$neededField . '`'] = $parts[0] . '.' . $columnAs . '`'; |
| $toBeReplaced[$neededField . ','] = $parts[0] . '.' . $columnAs . ','; |
| |
| $epregReplace["/(" . $neededField . ")$/"] = $parts[0] . '.' . $columnAs; |
| $neededField .= ' as ' . $columnAs; |
| } else { |
| $fieldNames[] = $parts[1]; |
| } |
| } |
| } |
|
|
| preg_match_all("/" . $matchTables . "/", $from, $matchesFrom); |
|
|
| $innerSelect = implode(", \n", $neededFields); |
| $innerFrom = $from; |
| $innerWhere = $where; |
|
|
| $innerLimitAndOffset = $limitAndOffset; |
|
|
| $innerOrderBy = "NULL"; |
| if ($innerLimitAndOffset && $orderBy) { |
| |
| $innerOrderBy = $orderBy; |
| } |
| if ($innerLimitAndOffset) { |
| |
| $innerGroupBy = false; |
| } |
|
|
| if (!isset($innerGroupBy) && in_array('log_visit', $matchesFrom[1])) { |
| $innerGroupBy = "log_visit.idvisit"; |
| } elseif (!isset($innerGroupBy)) { |
| throw new Exception('Cannot use subselect for join as no group by rule is specified'); |
| } |
|
|
| if (!empty($toBeReplaced)) { |
| $select = preg_replace(array_keys($epregReplace), array_values($epregReplace), $select); |
| $select = str_replace(array_keys($toBeReplaced), array_values($toBeReplaced), $select); |
| if (!empty($groupBy)) { |
| $groupBy = preg_replace(array_keys($epregReplace), array_values($epregReplace), $groupBy); |
| $groupBy = str_replace(array_keys($toBeReplaced), array_values($toBeReplaced), $groupBy); |
| } |
| if (!empty($orderBy)) { |
| $orderBy = preg_replace(array_keys($epregReplace), array_values($epregReplace), $orderBy); |
| $orderBy = str_replace(array_keys($toBeReplaced), array_values($toBeReplaced), $orderBy); |
| } |
| } |
|
|
| $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerGroupBy, $innerOrderBy, $innerLimitAndOffset); |
|
|
| $select = preg_replace('/' . $matchTables . '\./', 'log_inner.', $select); |
|
|
| $from = " |
| ( |
| $innerQuery |
| ) AS log_inner"; |
| $where = false; |
| $orderBy = preg_replace('/' . $matchTables . '\./', 'log_inner.', $orderBy); |
| $groupBy = preg_replace('/' . $matchTables . '\./', 'log_inner.', $groupBy); |
|
|
| $outerLimitAndOffset = null; |
| $query = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $outerLimitAndOffset); |
| return $query; |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private function buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, bool $withRollup = false) |
| { |
| $sql = " |
| SELECT |
| $select |
| FROM |
| $from"; |
|
|
| if ($where) { |
| $sql .= " |
| WHERE |
| $where"; |
| } |
|
|
| if ($groupBy) { |
| $sql .= " |
| GROUP BY |
| $groupBy"; |
|
|
| if ($withRollup) { |
| $sql .= " |
| WITH ROLLUP"; |
| } |
| } |
|
|
| if ($orderBy) { |
| if ($withRollup) { |
| $sql = " |
| SELECT * FROM ( |
| $sql |
| ) AS rollupQuery"; |
| } |
| $sql .= " |
| ORDER BY |
| $orderBy"; |
| } |
|
|
| $sql = $this->appendLimitClauseToQuery($sql, $limitAndOffset); |
|
|
| return $sql; |
| } |
|
|
| |
| |
| |
| |
| |
| private function appendLimitClauseToQuery($sql, $limit) |
| { |
| $limitParts = explode(',', (string) $limit); |
| $isLimitWithOffset = 2 === count($limitParts); |
|
|
| if ($isLimitWithOffset) { |
| |
| $offset = trim($limitParts[0]); |
| $limit = trim($limitParts[1]); |
| $sql .= sprintf(' LIMIT %d, %d', $offset, $limit); |
| } else { |
| |
| $limit = (int)$limit; |
| if ($limit >= 1) { |
| $sql .= " LIMIT $limit"; |
| } |
| } |
|
|
| return $sql; |
| } |
|
|
| |
| |
| |
| |
| |
| protected function getWhereMatchBoth($where, $segmentWhere) |
| { |
| if (empty($segmentWhere) && empty($where)) { |
| throw new \Exception("Segment where clause should be non empty."); |
| } |
| if (empty($segmentWhere)) { |
| return $where; |
| } |
| if (empty($where)) { |
| return $segmentWhere; |
| } |
| return "( $where ) |
| AND |
| ($segmentWhere)"; |
| } |
| } |
|
|