From 764a8c147f8992ae99a679573fc434d82dd7b0a6 Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 19:18:17 +1300 Subject: Moved SegmentExpression into namespace Piwik\Segment --- core/Segment.php | 1 + core/Segment/SegmentExpression.php | 378 +++++++++++++++++++++ core/SegmentExpression.php | 378 --------------------- core/Tracker/TableLogAction.php | 2 +- plugins/API/API.php | 2 +- plugins/Transitions/API.php | 2 +- plugins/VisitFrequency/API.php | 2 +- .../PHPUnit/Unit/Segment/SegmentExpressionTest.php | 132 +++++++ tests/PHPUnit/Unit/SegmentExpressionTest.php | 132 ------- 9 files changed, 515 insertions(+), 514 deletions(-) create mode 100644 core/Segment/SegmentExpression.php delete mode 100644 core/SegmentExpression.php create mode 100644 tests/PHPUnit/Unit/Segment/SegmentExpressionTest.php delete mode 100644 tests/PHPUnit/Unit/SegmentExpressionTest.php diff --git a/core/Segment.php b/core/Segment.php index 420946794d..d990ed16ff 100644 --- a/core/Segment.php +++ b/core/Segment.php @@ -10,6 +10,7 @@ namespace Piwik; use Exception; use Piwik\Plugins\API\API; +use Piwik\Segment\SegmentExpression; /** * Limits the set of visits Piwik uses when aggregating analytics data. diff --git a/core/Segment/SegmentExpression.php b/core/Segment/SegmentExpression.php new file mode 100644 index 0000000000..e32bbe8723 --- /dev/null +++ b/core/Segment/SegmentExpression.php @@ -0,0 +1,378 @@ +='; + const MATCH_LESS_OR_EQUAL = '<='; + const MATCH_GREATER = '>'; + const MATCH_LESS = '<'; + const MATCH_CONTAINS = '=@'; + const MATCH_DOES_NOT_CONTAIN = '!@'; + + // Note: you can't write this in the API, but access this feature + // via field!= <- IS NOT NULL + // or via field== <- IS NULL / empty + const MATCH_IS_NOT_NULL_NOR_EMPTY = '::NOT_NULL'; + const MATCH_IS_NULL_OR_EMPTY = '::NULL'; + + // Special case, since we look up Page URLs/Page titles in a sub SQL query + const MATCH_ACTIONS_CONTAINS = 'IN'; + + const INDEX_BOOL_OPERATOR = 0; + const INDEX_OPERAND = 1; + + function __construct($string) + { + $this->string = $string; + $this->tree = $this->parseTree(); + } + + protected $joins = array(); + protected $valuesBind = array(); + protected $parsedTree = array(); + protected $tree = array(); + protected $parsedSubExpressions = array(); + + /** + * Given the array of parsed filters containing, for each filter, + * the boolean operator (AND/OR) and the operand, + * Will return the array where the filters are in SQL representation + * + * @throws Exception + * @return array + */ + public function parseSubExpressions() + { + $parsedSubExpressions = array(); + foreach ($this->tree as $leaf) { + $operand = $leaf[self::INDEX_OPERAND]; + + $operand = urldecode($operand); + + $operator = $leaf[self::INDEX_BOOL_OPERATOR]; + $pattern = '/^(.+?)(' . self::MATCH_EQUAL . '|' + . self::MATCH_NOT_EQUAL . '|' + . self::MATCH_GREATER_OR_EQUAL . '|' + . self::MATCH_GREATER . '|' + . self::MATCH_LESS_OR_EQUAL . '|' + . self::MATCH_LESS . '|' + . self::MATCH_CONTAINS . '|' + . self::MATCH_DOES_NOT_CONTAIN + . '){1}(.*)/'; + $match = preg_match($pattern, $operand, $matches); + if ($match == 0) { + throw new Exception('The segment \'' . $operand . '\' is not valid.'); + } + + $leftMember = $matches[1]; + $operation = $matches[2]; + $valueRightMember = urldecode($matches[3]); + + // is null / is not null + if ($valueRightMember === '') { + if ($operation == self::MATCH_NOT_EQUAL) { + $operation = self::MATCH_IS_NOT_NULL_NOR_EMPTY; + } elseif ($operation == self::MATCH_EQUAL) { + $operation = self::MATCH_IS_NULL_OR_EMPTY; + } else { + throw new Exception('The segment \'' . $operand . '\' has no value specified. You can leave this value empty ' . + 'only when you use the operators: ' . self::MATCH_NOT_EQUAL . ' (is not) or ' . self::MATCH_EQUAL . ' (is)'); + } + } + + $parsedSubExpressions[] = array( + self::INDEX_BOOL_OPERATOR => $operator, + self::INDEX_OPERAND => array( + $leftMember, + $operation, + $valueRightMember, + )); + } + $this->parsedSubExpressions = $parsedSubExpressions; + return $parsedSubExpressions; + } + + /** + * Set the given expression + * @param $parsedSubExpressions + */ + public function setSubExpressionsAfterCleanup($parsedSubExpressions) + { + $this->parsedSubExpressions = $parsedSubExpressions; + } + + /** + * @param array $availableTables + */ + public function parseSubExpressionsIntoSqlExpressions(&$availableTables = array()) + { + $sqlSubExpressions = array(); + $this->valuesBind = array(); + $this->joins = array(); + + foreach ($this->parsedSubExpressions as $leaf) { + $operator = $leaf[self::INDEX_BOOL_OPERATOR]; + $operandDefinition = $leaf[self::INDEX_OPERAND]; + + $operand = $this->getSqlMatchFromDefinition($operandDefinition, $availableTables); + + if ($operand[1] !== null) { + $this->valuesBind[] = $operand[1]; + } + + $operand = $operand[0]; + $sqlSubExpressions[] = array( + self::INDEX_BOOL_OPERATOR => $operator, + self::INDEX_OPERAND => $operand, + ); + } + + $this->tree = $sqlSubExpressions; + } + + /** + * Given an array representing one filter operand ( left member , operation , right member) + * Will return an array containing + * - the SQL substring, + * - the values to bind to this substring + * + * @param array $def + * @param array $availableTables + * @throws Exception + * @return array + */ + protected function getSqlMatchFromDefinition($def, &$availableTables) + { + $field = $def[0]; + $matchType = $def[1]; + $value = $def[2]; + + $alsoMatchNULLValues = false; + switch ($matchType) { + case self::MATCH_EQUAL: + $sqlMatch = '='; + break; + case self::MATCH_NOT_EQUAL: + $sqlMatch = '<>'; + $alsoMatchNULLValues = true; + break; + case self::MATCH_GREATER: + $sqlMatch = '>'; + break; + case self::MATCH_LESS: + $sqlMatch = '<'; + break; + case self::MATCH_GREATER_OR_EQUAL: + $sqlMatch = '>='; + break; + case self::MATCH_LESS_OR_EQUAL: + $sqlMatch = '<='; + break; + case self::MATCH_CONTAINS: + $sqlMatch = 'LIKE'; + $value = '%' . $this->escapeLikeString($value) . '%'; + break; + case self::MATCH_DOES_NOT_CONTAIN: + $sqlMatch = 'NOT LIKE'; + $value = '%' . $this->escapeLikeString($value) . '%'; + $alsoMatchNULLValues = true; + break; + + case self::MATCH_IS_NOT_NULL_NOR_EMPTY: + $sqlMatch = 'IS NOT NULL AND (' . $field . ' <> \'\' OR ' . $field . ' = 0)'; + $value = null; + break; + + case self::MATCH_IS_NULL_OR_EMPTY: + $sqlMatch = 'IS NULL OR ' . $field . ' = \'\' '; + $value = null; + break; + + case self::MATCH_ACTIONS_CONTAINS: + // this match type is not accessible from the outside + // (it won't be matched in self::parseSubExpressions()) + // it can be used internally to inject sub-expressions into the query. + // see Segment::getCleanedExpression() + $sqlMatch = 'IN (' . $value['SQL'] . ')'; + $value = $this->escapeLikeString($value['bind']); + break; + default: + throw new Exception("Filter contains the match type '" . $matchType . "' which is not supported"); + break; + } + + // We match NULL values when rows are excluded only when we are not doing a + $alsoMatchNULLValues = $alsoMatchNULLValues && !empty($value); + + if ($matchType === self::MATCH_ACTIONS_CONTAINS + || is_null($value) + ) { + $sqlExpression = "( $field $sqlMatch )"; + } else { + if ($alsoMatchNULLValues) { + $sqlExpression = "( $field IS NULL OR $field $sqlMatch ? )"; + } else { + $sqlExpression = "$field $sqlMatch ?"; + } + } + + $this->checkFieldIsAvailable($field, $availableTables); + + return array($sqlExpression, $value); + } + + /** + * Check whether the field is available + * If not, add it to the available tables + * + * @param string $field + * @param array $availableTables + */ + private function checkFieldIsAvailable($field, &$availableTables) + { + $fieldParts = explode('.', $field); + + $table = count($fieldParts) == 2 ? $fieldParts[0] : false; + + // remove sql functions from field name + // example: `HOUR(log_visit.visit_last_action_time)` gets `HOUR(log_visit` => remove `HOUR(` + $table = preg_replace('/^[A-Z_]+\(/', '', $table); + $tableExists = !$table || in_array($table, $availableTables); + + if (!$tableExists) { + $availableTables[] = $table; + } + } + + /** + * Escape the characters % and _ in the given string + * @param string $str + * @return string + */ + private function escapeLikeString($str) + { + $str = str_replace("%", "\%", $str); + $str = str_replace("_", "\_", $str); + return $str; + } + + /** + * Given a filter string, + * will parse it into an array where each row contains the boolean operator applied to it, + * and the operand + * + * @return array + */ + protected function parseTree() + { + $string = $this->string; + if (empty($string)) { + return array(); + } + $tree = array(); + $i = 0; + $length = strlen($string); + $isBackslash = false; + $operand = ''; + while ($i <= $length) { + $char = $string[$i]; + + $isAND = ($char == self::AND_DELIMITER); + $isOR = ($char == self::OR_DELIMITER); + $isEnd = ($length == $i + 1); + + if ($isEnd) { + if ($isBackslash && ($isAND || $isOR)) { + $operand = substr($operand, 0, -1); + } + $operand .= $char; + $tree[] = array(self::INDEX_BOOL_OPERATOR => '', self::INDEX_OPERAND => $operand); + break; + } + + if ($isAND && !$isBackslash) { + $tree[] = array(self::INDEX_BOOL_OPERATOR => 'AND', self::INDEX_OPERAND => $operand); + $operand = ''; + } elseif ($isOR && !$isBackslash) { + $tree[] = array(self::INDEX_BOOL_OPERATOR => 'OR', self::INDEX_OPERAND => $operand); + $operand = ''; + } else { + if ($isBackslash && ($isAND || $isOR)) { + $operand = substr($operand, 0, -1); + } + $operand .= $char; + } + $isBackslash = ($char == "\\"); + $i++; + } + return $tree; + } + + /** + * Given the array of parsed boolean logic, will return + * an array containing the full SQL string representing the filter, + * the needed joins and the values to bind to the query + * + * @throws Exception + * @return array SQL Query, Joins and Bind parameters + */ + public function getSql() + { + if (count($this->tree) == 0) { + throw new Exception("Invalid segment, please specify a valid segment."); + } + $sql = ''; + $subExpression = false; + foreach ($this->tree as $expression) { + $operator = $expression[self::INDEX_BOOL_OPERATOR]; + $operand = $expression[self::INDEX_OPERAND]; + + if ($operator == 'OR' + && !$subExpression + ) { + $sql .= ' ('; + $subExpression = true; + } else { + $sql .= ' '; + } + + $sql .= $operand; + + if ($operator == 'AND' + && $subExpression + ) { + $sql .= ')'; + $subExpression = false; + } + + $sql .= " $operator"; + } + if ($subExpression) { + $sql .= ')'; + } + return array( + 'where' => $sql, + 'bind' => $this->valuesBind, + 'join' => implode(' ', $this->joins) + ); + } +} diff --git a/core/SegmentExpression.php b/core/SegmentExpression.php deleted file mode 100644 index cbd2a3eb9e..0000000000 --- a/core/SegmentExpression.php +++ /dev/null @@ -1,378 +0,0 @@ -='; - const MATCH_LESS_OR_EQUAL = '<='; - const MATCH_GREATER = '>'; - const MATCH_LESS = '<'; - const MATCH_CONTAINS = '=@'; - const MATCH_DOES_NOT_CONTAIN = '!@'; - - // Note: you can't write this in the API, but access this feature - // via field!= <- IS NOT NULL - // or via field== <- IS NULL / empty - const MATCH_IS_NOT_NULL_NOR_EMPTY = '::NOT_NULL'; - const MATCH_IS_NULL_OR_EMPTY = '::NULL'; - - // Special case, since we look up Page URLs/Page titles in a sub SQL query - const MATCH_ACTIONS_CONTAINS = 'IN'; - - const INDEX_BOOL_OPERATOR = 0; - const INDEX_OPERAND = 1; - - function __construct($string) - { - $this->string = $string; - $this->tree = $this->parseTree(); - } - - protected $joins = array(); - protected $valuesBind = array(); - protected $parsedTree = array(); - protected $tree = array(); - protected $parsedSubExpressions = array(); - - /** - * Given the array of parsed filters containing, for each filter, - * the boolean operator (AND/OR) and the operand, - * Will return the array where the filters are in SQL representation - * - * @throws Exception - * @return array - */ - public function parseSubExpressions() - { - $parsedSubExpressions = array(); - foreach ($this->tree as $leaf) { - $operand = $leaf[self::INDEX_OPERAND]; - - $operand = urldecode($operand); - - $operator = $leaf[self::INDEX_BOOL_OPERATOR]; - $pattern = '/^(.+?)(' . self::MATCH_EQUAL . '|' - . self::MATCH_NOT_EQUAL . '|' - . self::MATCH_GREATER_OR_EQUAL . '|' - . self::MATCH_GREATER . '|' - . self::MATCH_LESS_OR_EQUAL . '|' - . self::MATCH_LESS . '|' - . self::MATCH_CONTAINS . '|' - . self::MATCH_DOES_NOT_CONTAIN - . '){1}(.*)/'; - $match = preg_match($pattern, $operand, $matches); - if ($match == 0) { - throw new Exception('The segment \'' . $operand . '\' is not valid.'); - } - - $leftMember = $matches[1]; - $operation = $matches[2]; - $valueRightMember = urldecode($matches[3]); - - // is null / is not null - if ($valueRightMember === '') { - if ($operation == self::MATCH_NOT_EQUAL) { - $operation = self::MATCH_IS_NOT_NULL_NOR_EMPTY; - } elseif ($operation == self::MATCH_EQUAL) { - $operation = self::MATCH_IS_NULL_OR_EMPTY; - } else { - throw new Exception('The segment \'' . $operand . '\' has no value specified. You can leave this value empty ' . - 'only when you use the operators: ' . self::MATCH_NOT_EQUAL . ' (is not) or ' . self::MATCH_EQUAL . ' (is)'); - } - } - - $parsedSubExpressions[] = array( - self::INDEX_BOOL_OPERATOR => $operator, - self::INDEX_OPERAND => array( - $leftMember, - $operation, - $valueRightMember, - )); - } - $this->parsedSubExpressions = $parsedSubExpressions; - return $parsedSubExpressions; - } - - /** - * Set the given expression - * @param $parsedSubExpressions - */ - public function setSubExpressionsAfterCleanup($parsedSubExpressions) - { - $this->parsedSubExpressions = $parsedSubExpressions; - } - - /** - * @param array $availableTables - */ - public function parseSubExpressionsIntoSqlExpressions(&$availableTables = array()) - { - $sqlSubExpressions = array(); - $this->valuesBind = array(); - $this->joins = array(); - - foreach ($this->parsedSubExpressions as $leaf) { - $operator = $leaf[self::INDEX_BOOL_OPERATOR]; - $operandDefinition = $leaf[self::INDEX_OPERAND]; - - $operand = $this->getSqlMatchFromDefinition($operandDefinition, $availableTables); - - if ($operand[1] !== null) { - $this->valuesBind[] = $operand[1]; - } - - $operand = $operand[0]; - $sqlSubExpressions[] = array( - self::INDEX_BOOL_OPERATOR => $operator, - self::INDEX_OPERAND => $operand, - ); - } - - $this->tree = $sqlSubExpressions; - } - - /** - * Given an array representing one filter operand ( left member , operation , right member) - * Will return an array containing - * - the SQL substring, - * - the values to bind to this substring - * - * @param array $def - * @param array $availableTables - * @throws Exception - * @return array - */ - protected function getSqlMatchFromDefinition($def, &$availableTables) - { - $field = $def[0]; - $matchType = $def[1]; - $value = $def[2]; - - $alsoMatchNULLValues = false; - switch ($matchType) { - case self::MATCH_EQUAL: - $sqlMatch = '='; - break; - case self::MATCH_NOT_EQUAL: - $sqlMatch = '<>'; - $alsoMatchNULLValues = true; - break; - case self::MATCH_GREATER: - $sqlMatch = '>'; - break; - case self::MATCH_LESS: - $sqlMatch = '<'; - break; - case self::MATCH_GREATER_OR_EQUAL: - $sqlMatch = '>='; - break; - case self::MATCH_LESS_OR_EQUAL: - $sqlMatch = '<='; - break; - case self::MATCH_CONTAINS: - $sqlMatch = 'LIKE'; - $value = '%' . $this->escapeLikeString($value) . '%'; - break; - case self::MATCH_DOES_NOT_CONTAIN: - $sqlMatch = 'NOT LIKE'; - $value = '%' . $this->escapeLikeString($value) . '%'; - $alsoMatchNULLValues = true; - break; - - case self::MATCH_IS_NOT_NULL_NOR_EMPTY: - $sqlMatch = 'IS NOT NULL AND (' . $field . ' <> \'\' OR ' . $field . ' = 0)'; - $value = null; - break; - - case self::MATCH_IS_NULL_OR_EMPTY: - $sqlMatch = 'IS NULL OR ' . $field . ' = \'\' '; - $value = null; - break; - - case self::MATCH_ACTIONS_CONTAINS: - // this match type is not accessible from the outside - // (it won't be matched in self::parseSubExpressions()) - // it can be used internally to inject sub-expressions into the query. - // see Segment::getCleanedExpression() - $sqlMatch = 'IN (' . $value['SQL'] . ')'; - $value = $this->escapeLikeString($value['bind']); - break; - default: - throw new Exception("Filter contains the match type '" . $matchType . "' which is not supported"); - break; - } - - // We match NULL values when rows are excluded only when we are not doing a - $alsoMatchNULLValues = $alsoMatchNULLValues && !empty($value); - - if ($matchType === self::MATCH_ACTIONS_CONTAINS - || is_null($value) - ) { - $sqlExpression = "( $field $sqlMatch )"; - } else { - if ($alsoMatchNULLValues) { - $sqlExpression = "( $field IS NULL OR $field $sqlMatch ? )"; - } else { - $sqlExpression = "$field $sqlMatch ?"; - } - } - - $this->checkFieldIsAvailable($field, $availableTables); - - return array($sqlExpression, $value); - } - - /** - * Check whether the field is available - * If not, add it to the available tables - * - * @param string $field - * @param array $availableTables - */ - private function checkFieldIsAvailable($field, &$availableTables) - { - $fieldParts = explode('.', $field); - - $table = count($fieldParts) == 2 ? $fieldParts[0] : false; - - // remove sql functions from field name - // example: `HOUR(log_visit.visit_last_action_time)` gets `HOUR(log_visit` => remove `HOUR(` - $table = preg_replace('/^[A-Z_]+\(/', '', $table); - $tableExists = !$table || in_array($table, $availableTables); - - if (!$tableExists) { - $availableTables[] = $table; - } - } - - /** - * Escape the characters % and _ in the given string - * @param string $str - * @return string - */ - private function escapeLikeString($str) - { - $str = str_replace("%", "\%", $str); - $str = str_replace("_", "\_", $str); - return $str; - } - - /** - * Given a filter string, - * will parse it into an array where each row contains the boolean operator applied to it, - * and the operand - * - * @return array - */ - protected function parseTree() - { - $string = $this->string; - if (empty($string)) { - return array(); - } - $tree = array(); - $i = 0; - $length = strlen($string); - $isBackslash = false; - $operand = ''; - while ($i <= $length) { - $char = $string[$i]; - - $isAND = ($char == self::AND_DELIMITER); - $isOR = ($char == self::OR_DELIMITER); - $isEnd = ($length == $i + 1); - - if ($isEnd) { - if ($isBackslash && ($isAND || $isOR)) { - $operand = substr($operand, 0, -1); - } - $operand .= $char; - $tree[] = array(self::INDEX_BOOL_OPERATOR => '', self::INDEX_OPERAND => $operand); - break; - } - - if ($isAND && !$isBackslash) { - $tree[] = array(self::INDEX_BOOL_OPERATOR => 'AND', self::INDEX_OPERAND => $operand); - $operand = ''; - } elseif ($isOR && !$isBackslash) { - $tree[] = array(self::INDEX_BOOL_OPERATOR => 'OR', self::INDEX_OPERAND => $operand); - $operand = ''; - } else { - if ($isBackslash && ($isAND || $isOR)) { - $operand = substr($operand, 0, -1); - } - $operand .= $char; - } - $isBackslash = ($char == "\\"); - $i++; - } - return $tree; - } - - /** - * Given the array of parsed boolean logic, will return - * an array containing the full SQL string representing the filter, - * the needed joins and the values to bind to the query - * - * @throws Exception - * @return array SQL Query, Joins and Bind parameters - */ - public function getSql() - { - if (count($this->tree) == 0) { - throw new Exception("Invalid segment, please specify a valid segment."); - } - $sql = ''; - $subExpression = false; - foreach ($this->tree as $expression) { - $operator = $expression[self::INDEX_BOOL_OPERATOR]; - $operand = $expression[self::INDEX_OPERAND]; - - if ($operator == 'OR' - && !$subExpression - ) { - $sql .= ' ('; - $subExpression = true; - } else { - $sql .= ' '; - } - - $sql .= $operand; - - if ($operator == 'AND' - && $subExpression - ) { - $sql .= ')'; - $subExpression = false; - } - - $sql .= " $operator"; - } - if ($subExpression) { - $sql .= ')'; - } - return array( - 'where' => $sql, - 'bind' => $this->valuesBind, - 'join' => implode(' ', $this->joins) - ); - } -} diff --git a/core/Tracker/TableLogAction.php b/core/Tracker/TableLogAction.php index 709936f2a5..fe620035d7 100644 --- a/core/Tracker/TableLogAction.php +++ b/core/Tracker/TableLogAction.php @@ -10,7 +10,7 @@ namespace Piwik\Tracker; use Piwik\Common; -use Piwik\SegmentExpression; +use Piwik\Segment\SegmentExpression; use Piwik\Tracker; /** diff --git a/plugins/API/API.php b/plugins/API/API.php index f9f60dd261..f7aa20d08b 100644 --- a/plugins/API/API.php +++ b/plugins/API/API.php @@ -23,7 +23,7 @@ use Piwik\Period\Range; use Piwik\Piwik; use Piwik\Plugin\Dimension\VisitDimension; use Piwik\Plugins\CoreAdminHome\CustomLogo; -use Piwik\SegmentExpression; +use Piwik\Segment\SegmentExpression; use Piwik\Translate; use Piwik\Version; diff --git a/plugins/Transitions/API.php b/plugins/Transitions/API.php index 32abffdc06..44a39fdcf5 100644 --- a/plugins/Transitions/API.php +++ b/plugins/Transitions/API.php @@ -24,7 +24,7 @@ use Piwik\Plugins\Actions\Actions; use Piwik\Plugins\Actions\ArchivingHelper; use Piwik\RankingQuery; use Piwik\Segment; -use Piwik\SegmentExpression; +use Piwik\Segment\SegmentExpression; use Piwik\Site; use Piwik\Tracker\Action; use Piwik\Tracker\PageUrl; diff --git a/plugins/VisitFrequency/API.php b/plugins/VisitFrequency/API.php index 97a98f27f8..a6f9971480 100644 --- a/plugins/VisitFrequency/API.php +++ b/plugins/VisitFrequency/API.php @@ -13,7 +13,7 @@ use Piwik\Archive; use Piwik\DataTable; use Piwik\Piwik; use Piwik\Plugins\VisitsSummary\API as APIVisitsSummary; -use Piwik\SegmentExpression; +use Piwik\Segment\SegmentExpression; /** * VisitFrequency API lets you access a list of metrics related to Returning Visitors. diff --git a/tests/PHPUnit/Unit/Segment/SegmentExpressionTest.php b/tests/PHPUnit/Unit/Segment/SegmentExpressionTest.php new file mode 100644 index 0000000000..ede256707b --- /dev/null +++ b/tests/PHPUnit/Unit/Segment/SegmentExpressionTest.php @@ -0,0 +1,132 @@ + $expectedSql, 'bind' => array(), 'join' => ''); + $processed = $segment->getSql(); + $this->assertEquals($expected, $processed); + } + + /** + * Dataprovider for testSegmentSqlWithOperations + * @return array + */ + public function getOperationSegmentExpressions() + { + // Filter expression => SQL string + Bind values + return array( + array('A==B%', array('where' => " A = ? ", 'bind' => array('B%'))), + array('ABCDEF====B===', array('where' => " ABCDEF = ? ", 'bind' => array('==B==='))), + array('A===B;CDEF!=C!=', array('where' => " A = ? AND ( CDEF IS NULL OR CDEF <> ? ) ", 'bind' => array('=B', 'C!='))), + array('A==B,C==D', array('where' => " (A = ? OR C = ? )", 'bind' => array('B', 'D'))), + array('A!=B;C==D', array('where' => " ( A IS NULL OR A <> ? ) AND C = ? ", 'bind' => array('B', 'D'))), + array('A!=B;C==D,E!=Hello World!=', array('where' => " ( A IS NULL OR A <> ? ) AND (C = ? OR ( E IS NULL OR E <> ? ) )", 'bind' => array('B', 'D', 'Hello World!='))), + + array('A>B', array('where' => " A > ? ", 'bind' => array('B'))), + array('A " A < ? ", 'bind' => array('B'))), + array('A<=B', array('where' => " A <= ? ", 'bind' => array('B'))), + array('A>=B', array('where' => " A >= ? ", 'bind' => array('B'))), + array('ABCDEF>=>=>=B===', array('where' => " ABCDEF >= ? ", 'bind' => array('>=>=B==='))), + array('A>=<=B;CDEF>G;H>=I;J " A >= ? AND CDEF > ? AND H >= ? AND J < ? AND L <= ? ", 'bind' => array('<=B', 'G', 'I', 'K', 'M'))), + array('A>=B;C>=D,E " A >= ? AND (C >= ? OR E < ? )", 'bind' => array('B', 'D', 'w_ow great!'))), + + array('A=@B_', array('where' => " A LIKE ? ", 'bind' => array('%B\_%'))), + array('A!@B%', array('where' => " ( A IS NULL OR A NOT LIKE ? ) ", 'bind' => array('%B\%%'))), + ); + } + + /** + * @dataProvider getOperationSegmentExpressions + * @group Core + */ + public function testSegmentSqlWithOperations($expression, $expectedSql) + { + $segment = new SegmentExpression($expression); + $segment->parseSubExpressions(); + $segment->parseSubExpressionsIntoSqlExpressions(); + $processed = $segment->getSql(); + $expectedSql['join'] = ''; + $this->assertEquals($expectedSql, $processed); + } + + /** + * Dataprovider for testBogusFiltersExpectExceptionThrown + * @return array + */ + public function getBogusFilters() + { + return array( + array('A=B'), + array('C!D'), + array(''), + array(' '), + array(',;,'), + array(','), + array(',,'), + array('!='), + ); + } + + /** + * @dataProvider getBogusFilters + * @group Core + */ + public function testBogusFiltersExpectExceptionThrown($bogus) + { + try { + $segment = new SegmentExpression($bogus); + $segment->parseSubExpressions(); + $segment->getSql(); + } catch (\Exception $e) { + return; + } + $this->fail('Expected exception not raised for:' . var_export($segment->getSql(), true)); + } +} diff --git a/tests/PHPUnit/Unit/SegmentExpressionTest.php b/tests/PHPUnit/Unit/SegmentExpressionTest.php deleted file mode 100644 index aadecc8f0e..0000000000 --- a/tests/PHPUnit/Unit/SegmentExpressionTest.php +++ /dev/null @@ -1,132 +0,0 @@ - $expectedSql, 'bind' => array(), 'join' => ''); - $processed = $segment->getSql(); - $this->assertEquals($expected, $processed); - } - - /** - * Dataprovider for testSegmentSqlWithOperations - * @return array - */ - public function getOperationSegmentExpressions() - { - // Filter expression => SQL string + Bind values - return array( - array('A==B%', array('where' => " A = ? ", 'bind' => array('B%'))), - array('ABCDEF====B===', array('where' => " ABCDEF = ? ", 'bind' => array('==B==='))), - array('A===B;CDEF!=C!=', array('where' => " A = ? AND ( CDEF IS NULL OR CDEF <> ? ) ", 'bind' => array('=B', 'C!='))), - array('A==B,C==D', array('where' => " (A = ? OR C = ? )", 'bind' => array('B', 'D'))), - array('A!=B;C==D', array('where' => " ( A IS NULL OR A <> ? ) AND C = ? ", 'bind' => array('B', 'D'))), - array('A!=B;C==D,E!=Hello World!=', array('where' => " ( A IS NULL OR A <> ? ) AND (C = ? OR ( E IS NULL OR E <> ? ) )", 'bind' => array('B', 'D', 'Hello World!='))), - - array('A>B', array('where' => " A > ? ", 'bind' => array('B'))), - array('A " A < ? ", 'bind' => array('B'))), - array('A<=B', array('where' => " A <= ? ", 'bind' => array('B'))), - array('A>=B', array('where' => " A >= ? ", 'bind' => array('B'))), - array('ABCDEF>=>=>=B===', array('where' => " ABCDEF >= ? ", 'bind' => array('>=>=B==='))), - array('A>=<=B;CDEF>G;H>=I;J " A >= ? AND CDEF > ? AND H >= ? AND J < ? AND L <= ? ", 'bind' => array('<=B', 'G', 'I', 'K', 'M'))), - array('A>=B;C>=D,E " A >= ? AND (C >= ? OR E < ? )", 'bind' => array('B', 'D', 'w_ow great!'))), - - array('A=@B_', array('where' => " A LIKE ? ", 'bind' => array('%B\_%'))), - array('A!@B%', array('where' => " ( A IS NULL OR A NOT LIKE ? ) ", 'bind' => array('%B\%%'))), - ); - } - - /** - * @dataProvider getOperationSegmentExpressions - * @group Core - */ - public function testSegmentSqlWithOperations($expression, $expectedSql) - { - $segment = new SegmentExpression($expression); - $segment->parseSubExpressions(); - $segment->parseSubExpressionsIntoSqlExpressions(); - $processed = $segment->getSql(); - $expectedSql['join'] = ''; - $this->assertEquals($expectedSql, $processed); - } - - /** - * Dataprovider for testBogusFiltersExpectExceptionThrown - * @return array - */ - public function getBogusFilters() - { - return array( - array('A=B'), - array('C!D'), - array(''), - array(' '), - array(',;,'), - array(','), - array(',,'), - array('!='), - ); - } - - /** - * @dataProvider getBogusFilters - * @group Core - */ - public function testBogusFiltersExpectExceptionThrown($bogus) - { - try { - $segment = new SegmentExpression($bogus); - $segment->parseSubExpressions(); - $segment->getSql(); - } catch (\Exception $e) { - return; - } - $this->fail('Expected exception not raised for:' . var_export($segment->getSql(), true)); - } -} -- cgit v1.2.3 From 5c14cf8e5bff649230f91ef7e90dc3d3dd89f9a9 Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 19:31:25 +1300 Subject: move logic that creates the SQL query into LogQueryBuilder class --- core/DataAccess/LogQueryBuilder.php | 261 ++++++++++++++++++++++++++++++++++++ core/Segment.php | 249 +++------------------------------- core/Segment/SegmentExpression.php | 5 + 3 files changed, 284 insertions(+), 231 deletions(-) create mode 100644 core/DataAccess/LogQueryBuilder.php diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php new file mode 100644 index 0000000000..254334ca8c --- /dev/null +++ b/core/DataAccess/LogQueryBuilder.php @@ -0,0 +1,261 @@ +isEmpty()) { + $segmentExpression->parseSubExpressionsIntoSqlExpressions($from); + + $joins = $this->generateJoins($from); + $from = $joins['sql']; + $joinWithSubSelect = $joins['joinWithSubSelect']; + + $segmentSql = $segmentExpression->getSql(); + $segmentWhere = $segmentSql['where']; + if (!empty($segmentWhere)) { + if (!empty($where)) { + $where = "( $where ) + AND + ($segmentWhere)"; + } else { + $where = $segmentWhere; + } + } + + $bind = array_merge($bind, $segmentSql['bind']); + } else { + $joins = $this->generateJoins($from); + $from = $joins['sql']; + $joinWithSubSelect = $joins['joinWithSubSelect']; + } + + if ($joinWithSubSelect) { + $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy); + } else { + $sql = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); + } + return array( + 'sql' => $sql, + 'bind' => $bind + ); + } + /** + * Generate the join sql based on the needed tables + * @param array $tables tables to join + * @throws Exception if tables can't be joined + * @return array + */ + private function generateJoins($tables) + { + $knownTables = array("log_visit", "log_link_visit_action", "log_conversion", "log_conversion_item"); + $visitsAvailable = $actionsAvailable = $conversionsAvailable = $conversionItemAvailable = false; + $joinWithSubSelect = false; + $sql = ''; + + // make sure the tables are joined in the right order + // base table first, then action before conversion + // this way, conversions can be joined on idlink_va + $actionIndex = array_search("log_link_visit_action", $tables); + $conversionIndex = array_search("log_conversion", $tables); + if ($actionIndex > 0 && $conversionIndex > 0 && $actionIndex > $conversionIndex) { + $tables[$actionIndex] = "log_conversion"; + $tables[$conversionIndex] = "log_link_visit_action"; + } + + // same as above: action before visit + $actionIndex = array_search("log_link_visit_action", $tables); + $visitIndex = array_search("log_visit", $tables); + if ($actionIndex > 0 && $visitIndex > 0 && $actionIndex > $visitIndex) { + $tables[$actionIndex] = "log_visit"; + $tables[$visitIndex] = "log_link_visit_action"; + } + + foreach ($tables as $i => $table) { + if (is_array($table)) { + // join condition provided + $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table']; + $sql .= " + LEFT JOIN " . Common::prefixTable($table['table']) . " AS " . $alias + . " ON " . $table['joinOn']; + continue; + } + + if (!in_array($table, $knownTables)) { + throw new Exception("Table '$table' can't be used for segmentation"); + } + + $tableSql = Common::prefixTable($table) . " AS $table"; + + if ($i == 0) { + // first table + $sql .= $tableSql; + } else { + if ($actionsAvailable && $table == "log_conversion") { + // have actions, need conversions => join on idlink_va + $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va " + . "AND log_conversion.idsite = log_link_visit_action.idsite"; + } else if ($actionsAvailable && $table == "log_visit") { + // have actions, need visits => join on idvisit + $join = "log_visit.idvisit = log_link_visit_action.idvisit"; + } else if ($visitsAvailable && $table == "log_link_visit_action") { + // have visits, need actions => we have to use a more complex join + // we don't hande this here, we just return joinWithSubSelect=true in this case + $joinWithSubSelect = true; + $join = "log_link_visit_action.idvisit = log_visit.idvisit"; + } else if ($conversionsAvailable && $table == "log_link_visit_action") { + // have conversions, need actions => join on idlink_va + $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va"; + } else if (($visitsAvailable && $table == "log_conversion") + || ($conversionsAvailable && $table == "log_visit") + ) { + // have visits, need conversion (or vice versa) => join on idvisit + // notice that joining conversions on visits has lower priority than joining it on actions + $join = "log_conversion.idvisit = log_visit.idvisit"; + + // if conversions are joined on visits, we need a complex join + if ($table == "log_conversion") { + $joinWithSubSelect = true; + } + } elseif ($conversionItemAvailable && $table === 'log_visit') { + $join = "log_conversion_item.idvisit = log_visit.idvisit"; + } elseif ($conversionItemAvailable && $table === 'log_link_visit_action') { + $join = "log_conversion_item.idvisit = log_link_visit_action.idvisit"; + } elseif ($conversionItemAvailable && $table === 'log_conversion') { + $join = "log_conversion_item.idvisit = log_conversion.idvisit"; + } else { + throw new Exception("Table '$table' can't be joined for segmentation"); + } + + // the join sql the default way + $sql .= " + LEFT JOIN $tableSql ON $join"; + } + + // remember which tables are available + $visitsAvailable = ($visitsAvailable || $table == "log_visit"); + $actionsAvailable = ($actionsAvailable || $table == "log_link_visit_action"); + $conversionsAvailable = ($conversionsAvailable || $table == "log_conversion"); + $conversionItemAvailable = ($conversionItemAvailable || $table == "log_conversion_item"); + } + + $return = array( + 'sql' => $sql, + 'joinWithSubSelect' => $joinWithSubSelect + ); + return $return; + + } + + /** + * Build select query the normal way + * @param string $select fieldlist to be selected + * @param string $from tablelist to select from + * @param string $where where clause + * @param string $orderBy order by clause + * @param string $groupBy group by clause + * @return string + */ + private function buildSelectQuery($select, $from, $where, $orderBy, $groupBy) + { + $sql = " + SELECT + $select + FROM + $from"; + + if ($where) { + $sql .= " + WHERE + $where"; + } + + if ($groupBy) { + $sql .= " + GROUP BY + $groupBy"; + } + + if ($orderBy) { + $sql .= " + ORDER BY + $orderBy"; + } + + return $sql; + } + + /** + * Build a select query where actions have to be joined on visits (or conversions) + * In this case, the query gets wrapped in another query so that grouping by visit is possible + * @param string $select + * @param string $from + * @param string $where + * @param string $orderBy + * @param string $groupBy + * @throws Exception + * @return string + */ + private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy) + { + $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)"; + 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."); + } + + $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); + $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy); + $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy); + + $from = "( + SELECT + " . implode(", + ", $neededFields) . " + FROM + $from + WHERE + $where + GROUP BY log_visit.idvisit + ) AS log_inner"; + + $where = false; + $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); + return $query; + } + +} \ No newline at end of file diff --git a/core/Segment.php b/core/Segment.php index d990ed16ff..5229caf3c5 100644 --- a/core/Segment.php +++ b/core/Segment.php @@ -9,6 +9,7 @@ namespace Piwik; use Exception; +use Piwik\DataAccess\LogQueryBuilder; use Piwik\Plugins\API\API; use Piwik\Segment\SegmentExpression; @@ -60,6 +61,16 @@ class Segment */ protected $segment = null; + /** + * @var string + */ + protected $string = null; + + /** + * @var array + */ + protected $idSites = null; + /** * Truncate the Segments to 8k */ @@ -69,9 +80,8 @@ class Segment * Constructor. * * @param string $segmentCondition The segment condition, eg, `'browserCode=ff;countryCode=CA'`. - * @param array $idSites The list of sites the segment will be used with. Some segments are - * dependent on the site, such as goal segments. - * @throws Exception + * @param array $idSites The list of sites the st + * @throws */ public function __construct($segmentCondition, $idSites) { @@ -128,7 +138,7 @@ class Segment */ public function isEmpty() { - return empty($this->string); + return $this->segment->isEmpty(); } protected $availableSegments = array(); @@ -227,234 +237,10 @@ class Segment */ public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false) { - if (!is_array($from)) { - $from = array($from); - } - - if (!$this->isEmpty()) { - $this->segment->parseSubExpressionsIntoSqlExpressions($from); - - $joins = $this->generateJoins($from); - $from = $joins['sql']; - $joinWithSubSelect = $joins['joinWithSubSelect']; - - $segmentSql = $this->segment->getSql(); - $segmentWhere = $segmentSql['where']; - if (!empty($segmentWhere)) { - if (!empty($where)) { - $where = "( $where ) - AND - ($segmentWhere)"; - } else { - $where = $segmentWhere; - } - } - - $bind = array_merge($bind, $segmentSql['bind']); - } else { - $joins = $this->generateJoins($from); - $from = $joins['sql']; - $joinWithSubSelect = $joins['joinWithSubSelect']; - } - - if ($joinWithSubSelect) { - $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy); - } else { - $sql = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); - } - return array( - 'sql' => $sql, - 'bind' => $bind - ); + $segmentExpression = $this->segment; + $segmentQuery = new LogQueryBuilder(); + return $segmentQuery->getSelectQueryString($segmentExpression, $select, $from, $where, $bind, $orderBy, $groupBy); } - - /** - * Generate the join sql based on the needed tables - * @param array $tables tables to join - * @throws Exception if tables can't be joined - * @return array - */ - private function generateJoins($tables) - { - $knownTables = array("log_visit", "log_link_visit_action", "log_conversion", "log_conversion_item"); - $visitsAvailable = $actionsAvailable = $conversionsAvailable = $conversionItemAvailable = false; - $joinWithSubSelect = false; - $sql = ''; - - // make sure the tables are joined in the right order - // base table first, then action before conversion - // this way, conversions can be joined on idlink_va - $actionIndex = array_search("log_link_visit_action", $tables); - $conversionIndex = array_search("log_conversion", $tables); - if ($actionIndex > 0 && $conversionIndex > 0 && $actionIndex > $conversionIndex) { - $tables[$actionIndex] = "log_conversion"; - $tables[$conversionIndex] = "log_link_visit_action"; - } - - // same as above: action before visit - $actionIndex = array_search("log_link_visit_action", $tables); - $visitIndex = array_search("log_visit", $tables); - if ($actionIndex > 0 && $visitIndex > 0 && $actionIndex > $visitIndex) { - $tables[$actionIndex] = "log_visit"; - $tables[$visitIndex] = "log_link_visit_action"; - } - - foreach ($tables as $i => $table) { - if (is_array($table)) { - // join condition provided - $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table']; - $sql .= " - LEFT JOIN " . Common::prefixTable($table['table']) . " AS " . $alias - . " ON " . $table['joinOn']; - continue; - } - - if (!in_array($table, $knownTables)) { - throw new Exception("Table '$table' can't be used for segmentation"); - } - - $tableSql = Common::prefixTable($table) . " AS $table"; - - if ($i == 0) { - // first table - $sql .= $tableSql; - } else { - if ($actionsAvailable && $table == "log_conversion") { - // have actions, need conversions => join on idlink_va - $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va " - . "AND log_conversion.idsite = log_link_visit_action.idsite"; - } else if ($actionsAvailable && $table == "log_visit") { - // have actions, need visits => join on idvisit - $join = "log_visit.idvisit = log_link_visit_action.idvisit"; - } else if ($visitsAvailable && $table == "log_link_visit_action") { - // have visits, need actions => we have to use a more complex join - // we don't hande this here, we just return joinWithSubSelect=true in this case - $joinWithSubSelect = true; - $join = "log_link_visit_action.idvisit = log_visit.idvisit"; - } else if ($conversionsAvailable && $table == "log_link_visit_action") { - // have conversions, need actions => join on idlink_va - $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va"; - } else if (($visitsAvailable && $table == "log_conversion") - || ($conversionsAvailable && $table == "log_visit") - ) { - // have visits, need conversion (or vice versa) => join on idvisit - // notice that joining conversions on visits has lower priority than joining it on actions - $join = "log_conversion.idvisit = log_visit.idvisit"; - - // if conversions are joined on visits, we need a complex join - if ($table == "log_conversion") { - $joinWithSubSelect = true; - } - } elseif ($conversionItemAvailable && $table === 'log_visit') { - $join = "log_conversion_item.idvisit = log_visit.idvisit"; - } elseif ($conversionItemAvailable && $table === 'log_link_visit_action') { - $join = "log_conversion_item.idvisit = log_link_visit_action.idvisit"; - } elseif ($conversionItemAvailable && $table === 'log_conversion') { - $join = "log_conversion_item.idvisit = log_conversion.idvisit"; - } else { - throw new Exception("Table '$table' can't be joined for segmentation"); - } - - // the join sql the default way - $sql .= " - LEFT JOIN $tableSql ON $join"; - } - - // remember which tables are available - $visitsAvailable = ($visitsAvailable || $table == "log_visit"); - $actionsAvailable = ($actionsAvailable || $table == "log_link_visit_action"); - $conversionsAvailable = ($conversionsAvailable || $table == "log_conversion"); - $conversionItemAvailable = ($conversionItemAvailable || $table == "log_conversion_item"); - } - - $return = array( - 'sql' => $sql, - 'joinWithSubSelect' => $joinWithSubSelect - ); - return $return; - - } - - /** - * Build select query the normal way - * @param string $select fieldlist to be selected - * @param string $from tablelist to select from - * @param string $where where clause - * @param string $orderBy order by clause - * @param string $groupBy group by clause - * @return string - */ - private function buildSelectQuery($select, $from, $where, $orderBy, $groupBy) - { - $sql = " - SELECT - $select - FROM - $from"; - - if ($where) { - $sql .= " - WHERE - $where"; - } - - if ($groupBy) { - $sql .= " - GROUP BY - $groupBy"; - } - - if ($orderBy) { - $sql .= " - ORDER BY - $orderBy"; - } - - return $sql; - } - - /** - * Build a select query where actions have to be joined on visits (or conversions) - * In this case, the query gets wrapped in another query so that grouping by visit is possible - * @param string $select - * @param string $from - * @param string $where - * @param string $orderBy - * @param string $groupBy - * @throws Exception - * @return string - */ - private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy) - { - $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)"; - 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."); - } - - $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); - $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy); - $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy); - - $from = "( - SELECT - " . implode(", - ", $neededFields) . " - FROM - $from - WHERE - $where - GROUP BY log_visit.idvisit - ) AS log_inner"; - - $where = false; - $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); - return $query; - } - /** * Returns the segment string. * @@ -464,4 +250,5 @@ class Segment { return (string) $this->getString(); } + } \ No newline at end of file diff --git a/core/Segment/SegmentExpression.php b/core/Segment/SegmentExpression.php index e32bbe8723..4078321ce0 100644 --- a/core/Segment/SegmentExpression.php +++ b/core/Segment/SegmentExpression.php @@ -46,6 +46,11 @@ class SegmentExpression $this->tree = $this->parseTree(); } + public function isEmpty() + { + return empty($this->string); + } + protected $joins = array(); protected $valuesBind = array(); protected $parsedTree = array(); -- cgit v1.2.3 From 3980f9eeed4535032c1e563f39adf8efa4520914 Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 19:45:31 +1300 Subject: Renamed method to have distinct name --- core/RankingQuery.php | 4 ++-- plugins/Actions/Archiver.php | 4 ++-- plugins/Contents/Archiver.php | 2 +- plugins/Events/Archiver.php | 2 +- tests/PHPUnit/Unit/RankingQueryTest.php | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/RankingQuery.php b/core/RankingQuery.php index f44845f075..cd4f830669 100644 --- a/core/RankingQuery.php +++ b/core/RankingQuery.php @@ -214,7 +214,7 @@ class RankingQuery */ public function execute($innerQuery, $bind = array()) { - $query = $this->generateQuery($innerQuery); + $query = $this->generateRankingQuery($innerQuery); $data = Db::fetchAll($query, $bind); if ($this->columnToMarkExcludedRows !== false) { @@ -268,7 +268,7 @@ class RankingQuery * itself. * @return string The entire ranking query SQL. */ - public function generateQuery($innerQuery) + public function generateRankingQuery($innerQuery) { // +1 to include "Others" $limit = $this->limit + 1; diff --git a/plugins/Actions/Archiver.php b/plugins/Actions/Archiver.php index 67408ced64..ab5fea8739 100644 --- a/plugins/Actions/Archiver.php +++ b/plugins/Actions/Archiver.php @@ -254,7 +254,7 @@ class Archiver extends \Piwik\Plugin\Archiver return $this->isSiteSearchEnabled; } - protected function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, $sprintfField, $rankingQuery = false) + protected function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, $sprintfField, RankingQuery $rankingQuery = null) { $select = sprintf($select, $sprintfField); @@ -266,7 +266,7 @@ class Archiver extends \Piwik\Plugin\Archiver // apply ranking query if ($rankingQuery) { - $querySql = $rankingQuery->generateQuery($querySql); + $querySql = $rankingQuery->generateRankingQuery($querySql); } // get result diff --git a/plugins/Contents/Archiver.php b/plugins/Contents/Archiver.php index 55668b4c3a..45607bd7b7 100644 --- a/plugins/Contents/Archiver.php +++ b/plugins/Contents/Archiver.php @@ -188,7 +188,7 @@ class Archiver extends \Piwik\Plugin\Archiver // apply ranking query if ($rankingQuery) { - $query['sql'] = $rankingQuery->generateQuery($query['sql']); + $query['sql'] = $rankingQuery->generateRankingQuery($query['sql']); } // get result diff --git a/plugins/Events/Archiver.php b/plugins/Events/Archiver.php index eb33d4899f..b396401de0 100644 --- a/plugins/Events/Archiver.php +++ b/plugins/Events/Archiver.php @@ -189,7 +189,7 @@ class Archiver extends \Piwik\Plugin\Archiver // apply ranking query if ($rankingQuery) { - $query['sql'] = $rankingQuery->generateQuery($query['sql']); + $query['sql'] = $rankingQuery->generateRankingQuery($query['sql']); } // get result diff --git a/tests/PHPUnit/Unit/RankingQueryTest.php b/tests/PHPUnit/Unit/RankingQueryTest.php index 49c24e56be..c242732ee7 100644 --- a/tests/PHPUnit/Unit/RankingQueryTest.php +++ b/tests/PHPUnit/Unit/RankingQueryTest.php @@ -139,7 +139,7 @@ class RankingQueryTest extends \PHPUnit_Framework_TestCase */ private function checkQuery($rankingQuery, $innerQuerySql, $expected) { - $query = $rankingQuery->generateQuery($innerQuerySql); + $query = $rankingQuery->generateRankingQuery($innerQuerySql); $queryNoWhitespace = preg_replace("/\s+/", "", $query); $expectedNoWhitespace = preg_replace("/\s+/", "", $expected); -- cgit v1.2.3 From 2cd889046e16ed3b737e4734b70ac240f6515b3a Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 20:40:25 +1300 Subject: Refactoring without changing logic, so I can understand the code --- core/DataAccess/LogQueryBuilder.php | 136 ++++++++++++++++++------------------ core/Segment.php | 8 +-- core/Segment/SegmentExpression.php | 4 +- 3 files changed, 73 insertions(+), 75 deletions(-) diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php index 254334ca8c..2be8052d4c 100644 --- a/core/DataAccess/LogQueryBuilder.php +++ b/core/DataAccess/LogQueryBuilder.php @@ -16,51 +16,23 @@ use Piwik\Segment\SegmentExpression; class LogQueryBuilder { - - - /** - * @param $segmentExpression - * @param $select - * @param $from - * @param $where - * @param $bind - * @param $orderBy - * @param $groupBy - * @return array - * @throws Exception - */ public function getSelectQueryString(SegmentExpression $segmentExpression, $select, $from, $where, $bind, $orderBy, $groupBy) { if (!is_array($from)) { $from = array($from); } - if (!$segmentExpression->isEmpty()) { + if(!$segmentExpression->isEmpty()) { $segmentExpression->parseSubExpressionsIntoSqlExpressions($from); - - $joins = $this->generateJoins($from); - $from = $joins['sql']; - $joinWithSubSelect = $joins['joinWithSubSelect']; - $segmentSql = $segmentExpression->getSql(); - $segmentWhere = $segmentSql['where']; - if (!empty($segmentWhere)) { - if (!empty($where)) { - $where = "( $where ) - AND - ($segmentWhere)"; - } else { - $where = $segmentWhere; - } - } - + $where = $this->getWhereMatchBoth($where, $segmentSql['where']); $bind = array_merge($bind, $segmentSql['bind']); - } else { - $joins = $this->generateJoins($from); - $from = $joins['sql']; - $joinWithSubSelect = $joins['joinWithSubSelect']; } + $joins = $this->generateJoins($from); + $joinWithSubSelect = $joins['joinWithSubSelect']; + $from = $joins['sql']; + if ($joinWithSubSelect) { $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy); } else { @@ -71,6 +43,8 @@ class LogQueryBuilder 'bind' => $bind ); } + + /** * Generate the join sql based on the needed tables * @param array $tables tables to join @@ -178,8 +152,53 @@ class LogQueryBuilder } + + /** + * Build a select query where actions have to be joined on visits (or conversions) + * In this case, the query gets wrapped in another query so that grouping by visit is possible + * @param string $select + * @param string $from + * @param string $where + * @param string $orderBy + * @param string $groupBy + * @throws Exception + * @return string + */ + private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy) + { + $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)"; + 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."); + } + + $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); + $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy); + $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy); + + $from = "( + SELECT + " . implode(", + ", $neededFields) . " + FROM + $from + WHERE + $where + GROUP BY log_visit.idvisit + ) AS log_inner"; + + $where = false; + $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); + return $query; + } + + /** * Build select query the normal way + * * @param string $select fieldlist to be selected * @param string $from tablelist to select from * @param string $where where clause @@ -217,45 +236,24 @@ class LogQueryBuilder } /** - * Build a select query where actions have to be joined on visits (or conversions) - * In this case, the query gets wrapped in another query so that grouping by visit is possible - * @param string $select - * @param string $from - * @param string $where - * @param string $orderBy - * @param string $groupBy - * @throws Exception + * @param $where + * @param $segmentWhere * @return string */ - private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy) + protected function getWhereMatchBoth($where, $segmentWhere) { - $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)"; - 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."); + if (empty($segmentWhere) && empty($where)) { + throw new \Exception("Segment where clause should be non empty."); } - - $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); - $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy); - $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy); - - $from = "( - SELECT - " . implode(", - ", $neededFields) . " - FROM - $from - WHERE - $where - GROUP BY log_visit.idvisit - ) AS log_inner"; - - $where = false; - $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); - return $query; + if (empty($segmentWhere)) { + return $where; + } + if (empty($where)) { + return $segmentWhere; + } + return "( $where ) + AND + ($segmentWhere)"; } } \ No newline at end of file diff --git a/core/Segment.php b/core/Segment.php index 5229caf3c5..7c48df3c07 100644 --- a/core/Segment.php +++ b/core/Segment.php @@ -59,7 +59,7 @@ class Segment /** * @var SegmentExpression */ - protected $segment = null; + protected $segmentExpression = null; /** * @var string @@ -114,7 +114,7 @@ class Segment $this->string = $string; $this->idSites = $idSites; $segment = new SegmentExpression($string); - $this->segment = $segment; + $this->segmentExpression = $segment; // parse segments $expressions = $segment->parseSubExpressions(); @@ -138,7 +138,7 @@ class Segment */ public function isEmpty() { - return $this->segment->isEmpty(); + return $this->segmentExpression->isEmpty(); } protected $availableSegments = array(); @@ -237,7 +237,7 @@ class Segment */ public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false) { - $segmentExpression = $this->segment; + $segmentExpression = $this->segmentExpression; $segmentQuery = new LogQueryBuilder(); return $segmentQuery->getSelectQueryString($segmentExpression, $select, $from, $where, $bind, $orderBy, $groupBy); } diff --git a/core/Segment/SegmentExpression.php b/core/Segment/SegmentExpression.php index 4078321ce0..10cf0294e3 100644 --- a/core/Segment/SegmentExpression.php +++ b/core/Segment/SegmentExpression.php @@ -48,7 +48,7 @@ class SegmentExpression public function isEmpty() { - return empty($this->string); + return count($this->tree) == 0; } protected $joins = array(); @@ -342,7 +342,7 @@ class SegmentExpression */ public function getSql() { - if (count($this->tree) == 0) { + if ($this->isEmpty()) { throw new Exception("Invalid segment, please specify a valid segment."); } $sql = ''; -- cgit v1.2.3 From 46629d99f6942e46abaeeb835ebb67a5b3969c60 Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 20:54:17 +1300 Subject: Building the inner query reusing buildSelectQuery --- core/DataAccess/LogQueryBuilder.php | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php index 2be8052d4c..fb1f1c1c47 100644 --- a/core/DataAccess/LogQueryBuilder.php +++ b/core/DataAccess/LogQueryBuilder.php @@ -175,22 +175,21 @@ class LogQueryBuilder . "Please use a table prefix."); } - $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); - $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy); - $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy); + $innerSelect = implode(", \n", $neededFields); + $innerFrom = $from; + $innerWhere = $where; + $innerGroupBy = "log_visit.idvisit"; + $innerOrderBy = "NULL"; - $from = "( - SELECT - " . implode(", - ", $neededFields) . " - FROM - $from - WHERE - $where - GROUP BY log_visit.idvisit - ) AS log_inner"; + $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerOrderBy, $innerGroupBy); + $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); $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); return $query; } -- cgit v1.2.3 From 64bc69c93eac5b7b3af514e625678fb7b3aff76d Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 20:57:07 +1300 Subject: Added ORDER BY NULL optimisation, fixing unit tests --- tests/PHPUnit/Integration/SegmentTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/PHPUnit/Integration/SegmentTest.php b/tests/PHPUnit/Integration/SegmentTest.php index 889f4a8e04..41fe39a392 100644 --- a/tests/PHPUnit/Integration/SegmentTest.php +++ b/tests/PHPUnit/Integration/SegmentTest.php @@ -210,6 +210,7 @@ class SegmentTest extends IntegrationTestCase AND ( log_link_visit_action.custom_var_k1 = ? AND log_visit.visitor_returning = ? ) GROUP BY log_visit.idvisit + ORDER BY NULL ) AS log_inner", "bind" => array(1, 'Test', 0)); @@ -300,6 +301,7 @@ class SegmentTest extends IntegrationTestCase AND ( log_conversion.idgoal = ? ) GROUP BY log_visit.idvisit + ORDER BY NULL ) AS log_inner", "bind" => array(1, 1)); @@ -423,6 +425,7 @@ class SegmentTest extends IntegrationTestCase WHERE log_conversion.idgoal = ? AND HOUR(log_visit.visit_last_action_time) = ? AND log_link_visit_action.custom_var_k1 = ? GROUP BY log_visit.idvisit + ORDER BY NULL ) AS log_inner", "bind" => array(1, 12, 'Test')); -- cgit v1.2.3 From 7f509b059ec54109cd2d7200dc466c4944c33d53 Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 22:20:06 +1300 Subject: Refactor Live SQL into Model --- core/DataAccess/LogQueryBuilder.php | 20 ++- core/Segment.php | 4 +- plugins/Live/API.php | 205 +++------------------------- plugins/Live/Model.php | 262 ++++++++++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+), 199 deletions(-) create mode 100644 plugins/Live/Model.php diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php index fb1f1c1c47..6e5b77561b 100644 --- a/core/DataAccess/LogQueryBuilder.php +++ b/core/DataAccess/LogQueryBuilder.php @@ -16,20 +16,25 @@ use Piwik\Segment\SegmentExpression; class LogQueryBuilder { - public function getSelectQueryString(SegmentExpression $segmentExpression, $select, $from, $where, $bind, $orderBy, $groupBy) + public function __construct(SegmentExpression $segmentExpression) + { + $this->segmentExpression = $segmentExpression; + } + + public function getSelectQueryString($select, $from, $where, $bind, $orderBy, $groupBy) { if (!is_array($from)) { $from = array($from); } - if(!$segmentExpression->isEmpty()) { - $segmentExpression->parseSubExpressionsIntoSqlExpressions($from); - $segmentSql = $segmentExpression->getSql(); + if(!$this->segmentExpression->isEmpty()) { + $this->segmentExpression->parseSubExpressionsIntoSqlExpressions($from); + $segmentSql = $this->segmentExpression->getSql(); $where = $this->getWhereMatchBoth($where, $segmentSql['where']); $bind = array_merge($bind, $segmentSql['bind']); } - $joins = $this->generateJoins($from); + $joins = $this->generateJoinsString($from); $joinWithSubSelect = $joins['joinWithSubSelect']; $from = $joins['sql']; @@ -51,7 +56,7 @@ class LogQueryBuilder * @throws Exception if tables can't be joined * @return array */ - private function generateJoins($tables) + private function generateJoinsString($tables) { $knownTables = array("log_visit", "log_link_visit_action", "log_conversion", "log_conversion_item"); $visitsAvailable = $actionsAvailable = $conversionsAvailable = $conversionItemAvailable = false; @@ -184,7 +189,8 @@ class LogQueryBuilder $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerOrderBy, $innerGroupBy); $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); - $from = "( + $from = " + ( $innerQuery ) AS log_inner"; $where = false; diff --git a/core/Segment.php b/core/Segment.php index 7c48df3c07..91318a0197 100644 --- a/core/Segment.php +++ b/core/Segment.php @@ -238,8 +238,8 @@ class Segment public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false) { $segmentExpression = $this->segmentExpression; - $segmentQuery = new LogQueryBuilder(); - return $segmentQuery->getSelectQueryString($segmentExpression, $select, $from, $where, $bind, $orderBy, $groupBy); + $segmentQuery = new LogQueryBuilder($segmentExpression); + return $segmentQuery->getSelectQueryString($select, $from, $where, $bind, $orderBy, $groupBy); } /** * Returns the segment string. diff --git a/plugins/Live/API.php b/plugins/Live/API.php index 7a36822534..0b66e75f64 100644 --- a/plugins/Live/API.php +++ b/plugins/Live/API.php @@ -69,49 +69,8 @@ class API extends \Piwik\Plugin\API public function getCounters($idSite, $lastMinutes, $segment = false) { Piwik::checkUserHasViewAccess($idSite); - $lastMinutes = (int) $lastMinutes; - - $counters = array( - 'visits' => 0, - 'actions' => 0, - 'visitors' => 0, - 'visitsConverted' => 0, - ); - - if (empty($lastMinutes)) { - return array($counters); - } - - list($whereIdSites, $idSites) = $this->getIdSitesWhereClause($idSite); - - $select = "count(*) as visits, COUNT(DISTINCT log_visit.idvisitor) as visitors"; - $where = $whereIdSites . "AND log_visit.visit_last_action_time >= ?"; - $bind = $idSites; - $bind[] = Date::factory(time() - $lastMinutes * 60)->toString('Y-m-d H:i:s'); - - $segment = new Segment($segment, $idSite); - $query = $segment->getSelectQuery($select, 'log_visit', $where, $bind); - - $data = Db::fetchAll($query['sql'], $query['bind']); - - $counters['visits'] = $data[0]['visits']; - $counters['visitors'] = $data[0]['visitors']; - - $select = "count(*)"; - $from = 'log_link_visit_action'; - list($whereIdSites) = $this->getIdSitesWhereClause($idSite, $from); - $where = $whereIdSites . "AND log_link_visit_action.server_time >= ?"; - $query = $segment->getSelectQuery($select, $from, $where, $bind); - $counters['actions'] = Db::fetchOne($query['sql'], $query['bind']); - - $select = "count(*)"; - $from = 'log_conversion'; - list($whereIdSites) = $this->getIdSitesWhereClause($idSite, $from); - $where = $whereIdSites . "AND log_conversion.server_time >= ?"; - $query = $segment->getSelectQuery($select, $from, $where, $bind); - $counters['visitsConverted'] = Db::fetchOne($query['sql'], $query['bind']); - - return array($counters); + $model = new Model(); + return $model->queryCounters($idSite, $lastMinutes, $segment); } /** @@ -455,38 +414,8 @@ class API extends \Piwik\Plugin\API */ private function getAdjacentVisitorId($idSite, $visitorId, $visitLastActionTime, $segment, $getNext) { - if ($getNext) { - $visitLastActionTimeCondition = "sub.visit_last_action_time <= ?"; - $orderByDir = "DESC"; - } else { - $visitLastActionTimeCondition = "sub.visit_last_action_time >= ?"; - $orderByDir = "ASC"; - } - - $visitLastActionDate = Date::factory($visitLastActionTime); - $dateOneDayAgo = $visitLastActionDate->subDay(1); - $dateOneDayInFuture = $visitLastActionDate->addDay(1); - - $select = "log_visit.idvisitor, MAX(log_visit.visit_last_action_time) as visit_last_action_time"; - $from = "log_visit"; - $where = "log_visit.idsite = ? AND log_visit.idvisitor <> ? AND visit_last_action_time >= ? and visit_last_action_time <= ?"; - $whereBind = array($idSite, @Common::hex2bin($visitorId), $dateOneDayAgo->toString('Y-m-d H:i:s'), $dateOneDayInFuture->toString('Y-m-d H:i:s')); - $orderBy = "MAX(log_visit.visit_last_action_time) $orderByDir"; - $groupBy = "log_visit.idvisitor"; - - $segment = new Segment($segment, $idSite); - $queryInfo = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy, $groupBy); - - $sql = "SELECT sub.idvisitor, sub.visit_last_action_time FROM ({$queryInfo['sql']}) as sub - WHERE $visitLastActionTimeCondition - LIMIT 1"; - $bind = array_merge($queryInfo['bind'], array($visitLastActionTime)); - - $visitorId = Db::fetchOne($sql, $bind); - if (!empty($visitorId)) { - $visitorId = bin2hex($visitorId); - } - return $visitorId; + $model = new Model(); + return $model->queryAdjacentVisitorId($idSite, $visitorId, $visitLastActionTime, $segment, $getNext); } /** @@ -614,112 +543,21 @@ class API extends \Piwik\Plugin\API private function loadLastVisitorDetailsFromDatabase($idSite, $period, $date, $segment = false, $countVisitorsToFetch = 100, $visitorId = false, $minTimestamp = false, $filterSortOrder = false) { - $where = $whereBind = array(); - - list($whereClause, $idSites) = $this->getIdSitesWhereClause($idSite); + $model = new Model(); + $data = $model->queryLogVisits($idSite, $period, $date, $segment, $countVisitorsToFetch, $visitorId, $minTimestamp, $filterSortOrder); - $where[] = $whereClause; - $whereBind = $idSites; - - if (strtolower($filterSortOrder) !== 'asc') { - $filterSortOrder = 'DESC'; - } - - $orderBy = "idsite, visit_last_action_time " . $filterSortOrder; - $orderByParent = "sub.visit_last_action_time " . $filterSortOrder; - - if (!empty($visitorId)) { - $where[] = "log_visit.idvisitor = ? "; - $whereBind[] = @Common::hex2bin($visitorId); - } - - if (!empty($minTimestamp)) { - $where[] = "log_visit.visit_last_action_time > ? "; - $whereBind[] = date("Y-m-d H:i:s", $minTimestamp); - } - - // If no other filter, only look at the last 24 hours of stats - if (empty($visitorId) - && empty($countVisitorsToFetch) - && empty($period) - && empty($date) - ) { - $period = 'day'; - $date = 'yesterdaySameTime'; - } - - // SQL Filter with provided period - if (!empty($period) && !empty($date)) { - $currentSite = new Site($idSite); - $currentTimezone = $currentSite->getTimezone(); - - $dateString = $date; - if ($period == 'range') { - $processedPeriod = new Range('range', $date); - if ($parsedDate = Range::parseDateRange($date)) { - $dateString = $parsedDate[2]; - } - } else { - $processedDate = Date::factory($date); - if ($date == 'today' - || $date == 'now' - || $processedDate->toString() == Date::factory('now', $currentTimezone)->toString() - ) { - $processedDate = $processedDate->subDay(1); - } - $processedPeriod = Period\Factory::build($period, $processedDate); - } - $dateStart = $processedPeriod->getDateStart()->setTimezone($currentTimezone); - $where[] = "log_visit.visit_last_action_time >= ?"; - $whereBind[] = $dateStart->toString('Y-m-d H:i:s'); - - if (!in_array($date, array('now', 'today', 'yesterdaySameTime')) - && strpos($date, 'last') === false - && strpos($date, 'previous') === false - && Date::factory($dateString)->toString('Y-m-d') != Date::factory('now', $currentTimezone)->toString() - ) { - $dateEnd = $processedPeriod->getDateEnd()->setTimezone($currentTimezone); - $where[] = " log_visit.visit_last_action_time <= ?"; - $dateEndString = $dateEnd->addDay(1)->toString('Y-m-d H:i:s'); - $whereBind[] = $dateEndString; - } - } - - if (count($where) > 0) { - $where = join(" - AND ", $where); - } else { - $where = false; - } - - $segment = new Segment($segment, $idSite); - - // Subquery to use the indexes for ORDER BY - $select = "log_visit.*"; - $from = "log_visit"; - $subQuery = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy); - - $sqlLimit = $countVisitorsToFetch >= 1 ? " LIMIT 0, " . (int)$countVisitorsToFetch : ""; - - // Group by idvisit so that a visitor converting 2 goals only appears once - $sql = " - SELECT sub.* FROM ( - " . $subQuery['sql'] . " - $sqlLimit - ) AS sub - GROUP BY sub.idvisit - ORDER BY $orderByParent - "; - try { - $data = Db::fetchAll($sql, $subQuery['bind']); - } catch (Exception $e) { - echo $e->getMessage(); - exit; - } + return $this->makeVisitorTableFromArray($data); + } + /** + * @param $data + * @return DataTable + * @throws Exception + */ + private function makeVisitorTableFromArray($data) + { $dataTable = new DataTable(); $dataTable->addRowsFromSimpleArray($data); - // $dataTable->disableFilter('Truncate'); if (!empty($data[0])) { $columnsToNotAggregate = array_map(function () { @@ -732,18 +570,5 @@ class API extends \Piwik\Plugin\API return $dataTable; } - /** - * @param $idSite - * @param string $table - * @return array - */ - private function getIdSitesWhereClause($idSite, $table = 'log_visit') - { - $idSites = array($idSite); - Piwik::postEvent('Live.API.getIdSitesString', array(&$idSites)); - $idSitesBind = Common::getSqlStringFieldsArray($idSites); - $whereClause = $table . ".idsite in ($idSitesBind) "; - return array($whereClause, $idSites); - } } diff --git a/plugins/Live/Model.php b/plugins/Live/Model.php new file mode 100644 index 0000000000..7bf5485c5a --- /dev/null +++ b/plugins/Live/Model.php @@ -0,0 +1,262 @@ +getIdSitesWhereClause($idSite); + + $where[] = $whereClause; + $whereBind = $idSites; + + if (strtolower($filterSortOrder) !== 'asc') { + $filterSortOrder = 'DESC'; + } + + $orderBy = "idsite, visit_last_action_time " . $filterSortOrder; + $orderByParent = "sub.visit_last_action_time " . $filterSortOrder; + + if (!empty($visitorId)) { + $where[] = "log_visit.idvisitor = ? "; + $whereBind[] = @Common::hex2bin($visitorId); + } + + if (!empty($minTimestamp)) { + $where[] = "log_visit.visit_last_action_time > ? "; + $whereBind[] = date("Y-m-d H:i:s", $minTimestamp); + } + + // If no other filter, only look at the last 24 hours of stats + if (empty($visitorId) + && empty($countVisitorsToFetch) + && empty($period) + && empty($date) + ) { + $period = 'day'; + $date = 'yesterdaySameTime'; + } + + // SQL Filter with provided period + if (!empty($period) && !empty($date)) { + $currentSite = new Site($idSite); + $currentTimezone = $currentSite->getTimezone(); + + $dateString = $date; + if ($period == 'range') { + $processedPeriod = new Range('range', $date); + if ($parsedDate = Range::parseDateRange($date)) { + $dateString = $parsedDate[2]; + } + } else { + $processedDate = Date::factory($date); + if ($date == 'today' + || $date == 'now' + || $processedDate->toString() == Date::factory('now', $currentTimezone)->toString() + ) { + $processedDate = $processedDate->subDay(1); + } + $processedPeriod = Period\Factory::build($period, $processedDate); + } + $dateStart = $processedPeriod->getDateStart()->setTimezone($currentTimezone); + $where[] = "log_visit.visit_last_action_time >= ?"; + $whereBind[] = $dateStart->toString('Y-m-d H:i:s'); + + if (!in_array($date, array('now', 'today', 'yesterdaySameTime')) + && strpos($date, 'last') === false + && strpos($date, 'previous') === false + && Date::factory($dateString)->toString('Y-m-d') != Date::factory('now', $currentTimezone)->toString() + ) { + $dateEnd = $processedPeriod->getDateEnd()->setTimezone($currentTimezone); + $where[] = " log_visit.visit_last_action_time <= ?"; + $dateEndString = $dateEnd->addDay(1)->toString('Y-m-d H:i:s'); + $whereBind[] = $dateEndString; + } + } + + if (count($where) > 0) { + $where = join(" + AND ", $where); + } else { + $where = false; + } + + $segment = new Segment($segment, $idSite); + + // Subquery to use the indexes for ORDER BY + $select = "log_visit.*"; + $from = "log_visit"; + $groupBy = false; + $subQuery = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy, $groupBy); + + $sqlLimit = $countVisitorsToFetch >= 1 ? " LIMIT 0, " . (int)$countVisitorsToFetch : ""; + + // Group by idvisit so that a visitor converting 2 goals only appears once + $sql = " + SELECT sub.* FROM ( + " . $subQuery['sql'] . " + $sqlLimit + ) AS sub + GROUP BY sub.idvisit + ORDER BY $orderByParent + "; + try { + $data = Db::fetchAll($sql, $subQuery['bind']); + return $data; + } catch (Exception $e) { + echo $e->getMessage(); + exit; + } + return $data; + } + + /** + * @param $idSite + * @param $lastMinutes + * @param $segment + * @return array + * @throws Exception + */ + public function queryCounters($idSite, $lastMinutes, $segment) + { + $lastMinutes = (int)$lastMinutes; + + $counters = array( + 'visits' => 0, + 'actions' => 0, + 'visitors' => 0, + 'visitsConverted' => 0, + ); + + if (empty($lastMinutes)) { + return array($counters); + } + + list($whereIdSites, $idSites) = $this->getIdSitesWhereClause($idSite); + + $select = "count(*) as visits, COUNT(DISTINCT log_visit.idvisitor) as visitors"; + $where = $whereIdSites . "AND log_visit.visit_last_action_time >= ?"; + $bind = $idSites; + $bind[] = Date::factory(time() - $lastMinutes * 60)->toString('Y-m-d H:i:s'); + + $segment = new Segment($segment, $idSite); + $query = $segment->getSelectQuery($select, 'log_visit', $where, $bind); + + $data = Db::fetchAll($query['sql'], $query['bind']); + + $counters['visits'] = $data[0]['visits']; + $counters['visitors'] = $data[0]['visitors']; + + $select = "count(*)"; + $from = 'log_link_visit_action'; + list($whereIdSites) = $this->getIdSitesWhereClause($idSite, $from); + $where = $whereIdSites . "AND log_link_visit_action.server_time >= ?"; + $query = $segment->getSelectQuery($select, $from, $where, $bind); + $counters['actions'] = Db::fetchOne($query['sql'], $query['bind']); + + $select = "count(*)"; + $from = 'log_conversion'; + list($whereIdSites) = $this->getIdSitesWhereClause($idSite, $from); + $where = $whereIdSites . "AND log_conversion.server_time >= ?"; + $query = $segment->getSelectQuery($select, $from, $where, $bind); + $counters['visitsConverted'] = Db::fetchOne($query['sql'], $query['bind']); + + return array($counters); + } + + + + /** + * @param $idSite + * @param string $table + * @return array + */ + private function getIdSitesWhereClause($idSite, $table = 'log_visit') + { + $idSites = array($idSite); + Piwik::postEvent('Live.API.getIdSitesString', array(&$idSites)); + + $idSitesBind = Common::getSqlStringFieldsArray($idSites); + $whereClause = $table . ".idsite in ($idSitesBind) "; + return array($whereClause, $idSites); + } + + + /** + * @param $idSite + * @param $visitorId + * @param $visitLastActionTime + * @param $segment + * @param $getNext + * @return string + * @throws Exception + */ + public function queryAdjacentVisitorId($idSite, $visitorId, $visitLastActionTime, $segment, $getNext) + { + if ($getNext) { + $visitLastActionTimeCondition = "sub.visit_last_action_time <= ?"; + $orderByDir = "DESC"; + } else { + $visitLastActionTimeCondition = "sub.visit_last_action_time >= ?"; + $orderByDir = "ASC"; + } + + $visitLastActionDate = Date::factory($visitLastActionTime); + $dateOneDayAgo = $visitLastActionDate->subDay(1); + $dateOneDayInFuture = $visitLastActionDate->addDay(1); + + $select = "log_visit.idvisitor, MAX(log_visit.visit_last_action_time) as visit_last_action_time"; + $from = "log_visit"; + $where = "log_visit.idsite = ? AND log_visit.idvisitor <> ? AND visit_last_action_time >= ? and visit_last_action_time <= ?"; + $whereBind = array($idSite, @Common::hex2bin($visitorId), $dateOneDayAgo->toString('Y-m-d H:i:s'), $dateOneDayInFuture->toString('Y-m-d H:i:s')); + $orderBy = "MAX(log_visit.visit_last_action_time) $orderByDir"; + $groupBy = "log_visit.idvisitor"; + + $segment = new Segment($segment, $idSite); + $queryInfo = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy, $groupBy); + + $sql = "SELECT sub.idvisitor, sub.visit_last_action_time FROM ({$queryInfo['sql']}) as sub + WHERE $visitLastActionTimeCondition + LIMIT 1"; + $bind = array_merge($queryInfo['bind'], array($visitLastActionTime)); + + $visitorId = Db::fetchOne($sql, $bind); + if (!empty($visitorId)) { + $visitorId = bin2hex($visitorId); + } + return $visitorId; + } +} \ No newline at end of file -- cgit v1.2.3 From 8ba4ba154aab7ac286b66ff16b59bb798ac3fb45 Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 22:42:43 +1300 Subject: Refactor Live API Visitor Profile logic in own class --- plugins/Live/API.php | 262 +------------------------------------ plugins/Live/Controller.php | 2 +- plugins/Live/Model.php | 20 ++- plugins/Live/VisitorProfile.php | 279 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 295 insertions(+), 268 deletions(-) create mode 100644 plugins/Live/VisitorProfile.php diff --git a/plugins/Live/API.php b/plugins/Live/API.php index 0b66e75f64..fc08e510db 100644 --- a/plugins/Live/API.php +++ b/plugins/Live/API.php @@ -16,10 +16,8 @@ use Piwik\DataTable\Row; use Piwik\Date; use Piwik\Db; use Piwik\Metrics\Formatter; -use Piwik\Period\Range; use Piwik\Period; use Piwik\Piwik; -use Piwik\Plugins\Referrers\API as APIReferrers; use Piwik\Plugins\SitesManager\API as APISitesManager; use Piwik\Segment; use Piwik\Site; @@ -55,8 +53,6 @@ require_once PIWIK_INCLUDE_PATH . '/plugins/UserCountry/functions.php'; class API extends \Piwik\Plugin\API { const VISITOR_PROFILE_MAX_VISITS_TO_AGGREGATE = 100; - const VISITOR_PROFILE_MAX_VISITS_TO_SHOW = 10; - const VISITOR_PROFILE_DATE_FORMAT = '%day% %shortMonth% %longYear%'; /** * This will return simple counters, for a given website ID, for visits over the last N minutes @@ -166,190 +162,8 @@ class API extends \Piwik\Plugin\API return array(); } - $isEcommerceEnabled = Site::isEcommerceEnabledFor($idSite); - - $result = array(); - $result['totalVisits'] = 0; - $result['totalVisitDuration'] = 0; - $result['totalActions'] = 0; - $result['totalSearches'] = 0; - $result['totalPageViews'] = 0; - $result['totalGoalConversions'] = 0; - $result['totalConversionsByGoal'] = array(); - - if ($isEcommerceEnabled) { - $result['totalEcommerceConversions'] = 0; - $result['totalEcommerceRevenue'] = 0; - $result['totalEcommerceItems'] = 0; - $result['totalAbandonedCarts'] = 0; - $result['totalAbandonedCartsRevenue'] = 0; - $result['totalAbandonedCartsItems'] = 0; - } - - $countries = array(); - $continents = array(); - $cities = array(); - $siteSearchKeywords = array(); - - $pageGenerationTimeTotal = 0; - - // aggregate all requested visits info for total_* info - foreach ($visits->getRows() as $visit) { - ++$result['totalVisits']; - - $result['totalVisitDuration'] += $visit->getColumn('visitDuration'); - $result['totalActions'] += $visit->getColumn('actions'); - $result['totalGoalConversions'] += $visit->getColumn('goalConversions'); - - // individual goal conversions are stored in action details - foreach ($visit->getColumn('actionDetails') as $action) { - if ($action['type'] == 'goal') { - // handle goal conversion - $idGoal = $action['goalId']; - $idGoalKey = 'idgoal=' . $idGoal; - - if (!isset($result['totalConversionsByGoal'][$idGoalKey])) { - $result['totalConversionsByGoal'][$idGoalKey] = 0; - } - ++$result['totalConversionsByGoal'][$idGoalKey]; - - if (!empty($action['revenue'])) { - if (!isset($result['totalRevenueByGoal'][$idGoalKey])) { - $result['totalRevenueByGoal'][$idGoalKey] = 0; - } - $result['totalRevenueByGoal'][$idGoalKey] += $action['revenue']; - } - } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER // handle ecommerce order - && $isEcommerceEnabled - ) { - ++$result['totalEcommerceConversions']; - $result['totalEcommerceRevenue'] += $action['revenue']; - $result['totalEcommerceItems'] += $action['items']; - } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART // handler abandoned cart - && $isEcommerceEnabled - ) { - ++$result['totalAbandonedCarts']; - $result['totalAbandonedCartsRevenue'] += $action['revenue']; - $result['totalAbandonedCartsItems'] += $action['items']; - } - - if (isset($action['siteSearchKeyword'])) { - $keyword = $action['siteSearchKeyword']; - - if (!isset($siteSearchKeywords[$keyword])) { - $siteSearchKeywords[$keyword] = 0; - ++$result['totalSearches']; - } - ++$siteSearchKeywords[$keyword]; - } - - if (isset($action['generationTime'])) { - $pageGenerationTimeTotal += $action['generationTime']; - ++$result['totalPageViews']; - } - } - - $countryCode = $visit->getColumn('countryCode'); - if (!isset($countries[$countryCode])) { - $countries[$countryCode] = 0; - } - ++$countries[$countryCode]; - - $continentCode = $visit->getColumn('continentCode'); - if (!isset($continents[$continentCode])) { - $continents[$continentCode] = 0; - } - ++$continents[$continentCode]; - - if ($countryCode && !array_key_exists($countryCode, $cities)) { - $cities[$countryCode] = array(); - } - $city = $visit->getColumn('city'); - if (!empty($city)) { - $cities[$countryCode][] = $city; - } - } - - // sort countries/continents/search keywords by visit/action - asort($countries); - asort($continents); - arsort($siteSearchKeywords); - - // transform country/continents/search keywords into something that will look good in XML - $result['countries'] = $result['continents'] = $result['searches'] = array(); - - foreach ($countries as $countryCode => $nbVisits) { - - $countryInfo = array('country' => $countryCode, - 'nb_visits' => $nbVisits, - 'flag' => \Piwik\Plugins\UserCountry\getFlagFromCode($countryCode), - 'prettyName' => \Piwik\Plugins\UserCountry\countryTranslate($countryCode)); - if (!empty($cities[$countryCode])) { - $countryInfo['cities'] = array_unique($cities[$countryCode]); - } - $result['countries'][] = $countryInfo; - } - foreach ($continents as $continentCode => $nbVisits) { - $result['continents'][] = array('continent' => $continentCode, - 'nb_visits' => $nbVisits, - 'prettyName' => \Piwik\Plugins\UserCountry\continentTranslate($continentCode)); - } - foreach ($siteSearchKeywords as $keyword => $searchCount) { - $result['searches'][] = array('keyword' => $keyword, - 'searches' => $searchCount); - } - - if ($result['totalPageViews']) { - $result['averagePageGenerationTime'] = - round($pageGenerationTimeTotal / $result['totalPageViews'], $precision = 2); - } - - $formatter = new Formatter(); - $result['totalVisitDurationPretty'] = $formatter->getPrettyTimeFromSeconds($result['totalVisitDuration'], true); - - // use requested visits for first/last visit info - $rows = $visits->getRows(); - $result['firstVisit'] = $this->getVisitorProfileVisitSummary(end($rows)); - $result['lastVisit'] = $this->getVisitorProfileVisitSummary(reset($rows)); - - // check if requested visits have lat/long - if ($checkForLatLong) { - $result['hasLatLong'] = false; - foreach ($rows as $visit) { - if ($visit->getColumn('latitude') !== false) { // realtime map only checks for latitude - $result['hasLatLong'] = true; - break; - } - } - } - - // save count of visits we queries - $result['visitsAggregated'] = count($rows); - - // use N most recent visits for last_visits - $visits->deleteRowsOffset(self::VISITOR_PROFILE_MAX_VISITS_TO_SHOW); - $result['lastVisits'] = $visits; - - // use the right date format for the pretty server date - $timezone = Site::getTimezoneFor($idSite); - foreach ($result['lastVisits']->getRows() as $visit) { - $dateTimeVisitFirstAction = Date::factory($visit->getColumn('firstActionTimestamp'), $timezone); - - $datePretty = $dateTimeVisitFirstAction->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT); - $visit->setColumn('serverDatePrettyFirstAction', $datePretty); - - $dateTimePretty = $datePretty . ' ' . $visit->getColumn('serverTimePrettyFirstAction'); - $visit->setColumn('serverDateTimePrettyFirstAction', $dateTimePretty); - } - - $result['userId'] = $visit->getColumn('userId'); - - // get visitor IDs that are adjacent to this one in log_visit - // TODO: make sure order of visitor ids is not changed if a returning visitor visits while the user is - // looking at the popup. - $latestVisitTime = reset($rows)->getColumn('lastActionDateTime'); - $result['nextVisitorId'] = $this->getAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = true); - $result['previousVisitorId'] = $this->getAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = false); + $profile = new VisitorProfile(); + $result = $profile->makeVisitorProfile($visits, $idSite, $visitorId, $segment, $checkForLatLong); /** * Triggered in the Live.getVisitorProfile API method. Plugins can use this event @@ -398,77 +212,6 @@ class API extends \Piwik\Plugin\API return $visitor->getVisitorId(); } - /** - * Returns the ID of a visitor that is adjacent to another visitor (by time of last action) - * in the log_visit table. - * - * @param int $idSite The ID of the site whose visits should be looked at. - * @param string $visitorId The ID of the visitor to get an adjacent visitor for. - * @param string $visitLastActionTime The last action time of the latest visit for $visitorId. - * @param string $segment - * @param bool $getNext Whether to retrieve the next visitor or the previous visitor. The next - * visitor will be the visitor that appears chronologically later in the - * log_visit table. The previous visitor will be the visitor that appears - * earlier. - * @return string The hex visitor ID. - */ - private function getAdjacentVisitorId($idSite, $visitorId, $visitLastActionTime, $segment, $getNext) - { - $model = new Model(); - return $model->queryAdjacentVisitorId($idSite, $visitorId, $visitLastActionTime, $segment, $getNext); - } - - /** - * Returns a summary for an important visit. Used to describe the first & last visits of a visitor. - * - * @param Row $visit - * @return array - */ - private function getVisitorProfileVisitSummary($visit) - { - $today = Date::today(); - - $serverDate = $visit->getColumn('firstActionTimestamp'); - return array( - 'date' => $serverDate, - 'prettyDate' => Date::factory($serverDate)->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT), - 'daysAgo' => (int)Date::secondsToDays($today->getTimestamp() - Date::factory($serverDate)->getTimestamp()), - 'referrerType' => $visit->getColumn('referrerType'), - 'referralSummary' => self::getReferrerSummaryForVisit($visit), - ); - } - - /** - * Returns a summary for a visit's referral. - * - * @param Row $visit - * @return bool|mixed|string - * @ignore - */ - public static function getReferrerSummaryForVisit($visit) - { - $referrerType = $visit->getColumn('referrerType'); - if ($referrerType === false - || $referrerType == 'direct' - ) { - $result = Piwik::translate('Referrers_DirectEntry'); - } else if ($referrerType == 'search') { - $result = $visit->getColumn('referrerName'); - - $keyword = $visit->getColumn('referrerKeyword'); - if ($keyword !== false - && $keyword != APIReferrers::getKeywordNotDefinedString() - ) { - $result .= ' (' . $keyword . ')'; - } - } else if ($referrerType == 'campaign') { - $result = Piwik::translate('Referrers_ColumnCampaign') . ' (' . $visit->getColumn('referrerName') . ')'; - } else { - $result = $visit->getColumn('referrerName'); - } - - return $result; - } /** * @deprecated @@ -545,7 +288,6 @@ class API extends \Piwik\Plugin\API { $model = new Model(); $data = $model->queryLogVisits($idSite, $period, $date, $segment, $countVisitorsToFetch, $visitorId, $minTimestamp, $filterSortOrder); - return $this->makeVisitorTableFromArray($data); } diff --git a/plugins/Live/Controller.php b/plugins/Live/Controller.php index 9df3e26ff8..da434412dd 100644 --- a/plugins/Live/Controller.php +++ b/plugins/Live/Controller.php @@ -133,7 +133,7 @@ class Controller extends \Piwik\Plugin\Controller 'date' => false )); $view->visitData = $visits->getFirstRow()->getColumns(); - $view->visitReferralSummary = API::getReferrerSummaryForVisit($visits->getFirstRow()); + $view->visitReferralSummary = VisitorProfile::getReferrerSummaryForVisit($visits->getFirstRow()); $view->showLocation = true; $this->setWidgetizedVisitorProfileUrl($view); $view->exportLink = $this->getVisitorProfileExportLink(); diff --git a/plugins/Live/Model.php b/plugins/Live/Model.php index 7bf5485c5a..2353450572 100644 --- a/plugins/Live/Model.php +++ b/plugins/Live/Model.php @@ -7,7 +7,7 @@ * */ -namespace Piwik\plugins\Live; +namespace Piwik\Plugins\Live; use Exception; use Piwik\Common; @@ -216,12 +216,18 @@ class Model /** - * @param $idSite - * @param $visitorId - * @param $visitLastActionTime - * @param $segment - * @param $getNext - * @return string + * Returns the ID of a visitor that is adjacent to another visitor (by time of last action) + * in the log_visit table. + * + * @param int $idSite The ID of the site whose visits should be looked at. + * @param string $visitorId The ID of the visitor to get an adjacent visitor for. + * @param string $visitLastActionTime The last action time of the latest visit for $visitorId. + * @param string $segment + * @param bool $getNext Whether to retrieve the next visitor or the previous visitor. The next + * visitor will be the visitor that appears chronologically later in the + * log_visit table. The previous visitor will be the visitor that appears + * earlier. + * @return string The hex visitor ID. * @throws Exception */ public function queryAdjacentVisitorId($idSite, $visitorId, $visitLastActionTime, $segment, $getNext) diff --git a/plugins/Live/VisitorProfile.php b/plugins/Live/VisitorProfile.php new file mode 100644 index 0000000000..807028d6ad --- /dev/null +++ b/plugins/Live/VisitorProfile.php @@ -0,0 +1,279 @@ +getRows() as $visit) { + ++$result['totalVisits']; + + $result['totalVisitDuration'] += $visit->getColumn('visitDuration'); + $result['totalActions'] += $visit->getColumn('actions'); + $result['totalGoalConversions'] += $visit->getColumn('goalConversions'); + + // individual goal conversions are stored in action details + foreach ($visit->getColumn('actionDetails') as $action) { + if ($action['type'] == 'goal') { + // handle goal conversion + $idGoal = $action['goalId']; + $idGoalKey = 'idgoal=' . $idGoal; + + if (!isset($result['totalConversionsByGoal'][$idGoalKey])) { + $result['totalConversionsByGoal'][$idGoalKey] = 0; + } + ++$result['totalConversionsByGoal'][$idGoalKey]; + + if (!empty($action['revenue'])) { + if (!isset($result['totalRevenueByGoal'][$idGoalKey])) { + $result['totalRevenueByGoal'][$idGoalKey] = 0; + } + $result['totalRevenueByGoal'][$idGoalKey] += $action['revenue']; + } + } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER // handle ecommerce order + && $isEcommerceEnabled + ) { + ++$result['totalEcommerceConversions']; + $result['totalEcommerceRevenue'] += $action['revenue']; + $result['totalEcommerceItems'] += $action['items']; + } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART // handler abandoned cart + && $isEcommerceEnabled + ) { + ++$result['totalAbandonedCarts']; + $result['totalAbandonedCartsRevenue'] += $action['revenue']; + $result['totalAbandonedCartsItems'] += $action['items']; + } + + if (isset($action['siteSearchKeyword'])) { + $keyword = $action['siteSearchKeyword']; + + if (!isset($siteSearchKeywords[$keyword])) { + $siteSearchKeywords[$keyword] = 0; + ++$result['totalSearches']; + } + ++$siteSearchKeywords[$keyword]; + } + + if (isset($action['generationTime'])) { + $pageGenerationTimeTotal += $action['generationTime']; + ++$result['totalPageViews']; + } + } + + $countryCode = $visit->getColumn('countryCode'); + if (!isset($countries[$countryCode])) { + $countries[$countryCode] = 0; + } + ++$countries[$countryCode]; + + $continentCode = $visit->getColumn('continentCode'); + if (!isset($continents[$continentCode])) { + $continents[$continentCode] = 0; + } + ++$continents[$continentCode]; + + if ($countryCode && !array_key_exists($countryCode, $cities)) { + $cities[$countryCode] = array(); + } + $city = $visit->getColumn('city'); + if (!empty($city)) { + $cities[$countryCode][] = $city; + } + } + + // sort countries/continents/search keywords by visit/action + asort($countries); + asort($continents); + arsort($siteSearchKeywords); + + // transform country/continents/search keywords into something that will look good in XML + $result['countries'] = $result['continents'] = $result['searches'] = array(); + + foreach ($countries as $countryCode => $nbVisits) { + + $countryInfo = array('country' => $countryCode, + 'nb_visits' => $nbVisits, + 'flag' => \Piwik\Plugins\UserCountry\getFlagFromCode($countryCode), + 'prettyName' => \Piwik\Plugins\UserCountry\countryTranslate($countryCode)); + if (!empty($cities[$countryCode])) { + $countryInfo['cities'] = array_unique($cities[$countryCode]); + } + $result['countries'][] = $countryInfo; + } + foreach ($continents as $continentCode => $nbVisits) { + $result['continents'][] = array('continent' => $continentCode, + 'nb_visits' => $nbVisits, + 'prettyName' => \Piwik\Plugins\UserCountry\continentTranslate($continentCode)); + } + foreach ($siteSearchKeywords as $keyword => $searchCount) { + $result['searches'][] = array('keyword' => $keyword, + 'searches' => $searchCount); + } + + if ($result['totalPageViews']) { + $result['averagePageGenerationTime'] = + round($pageGenerationTimeTotal / $result['totalPageViews'], $precision = 2); + } + + $formatter = new Formatter(); + $result['totalVisitDurationPretty'] = $formatter->getPrettyTimeFromSeconds($result['totalVisitDuration'], true); + + // use requested visits for first/last visit info + $rows = $visits->getRows(); + $result['firstVisit'] = $this->getVisitorProfileVisitSummary(end($rows)); + $result['lastVisit'] = $this->getVisitorProfileVisitSummary(reset($rows)); + + // check if requested visits have lat/long + if ($checkForLatLong) { + $result['hasLatLong'] = false; + foreach ($rows as $visit) { + if ($visit->getColumn('latitude') !== false) { // realtime map only checks for latitude + $result['hasLatLong'] = true; + break; + } + } + } + + // save count of visits we queries + $result['visitsAggregated'] = count($rows); + + // use N most recent visits for last_visits + $visits->deleteRowsOffset(self::VISITOR_PROFILE_MAX_VISITS_TO_SHOW); + $result['lastVisits'] = $visits; + + // use the right date format for the pretty server date + $timezone = Site::getTimezoneFor($idSite); + foreach ($result['lastVisits']->getRows() as $visit) { + $dateTimeVisitFirstAction = Date::factory($visit->getColumn('firstActionTimestamp'), $timezone); + + $datePretty = $dateTimeVisitFirstAction->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT); + $visit->setColumn('serverDatePrettyFirstAction', $datePretty); + + $dateTimePretty = $datePretty . ' ' . $visit->getColumn('serverTimePrettyFirstAction'); + $visit->setColumn('serverDateTimePrettyFirstAction', $dateTimePretty); + } + + $result['userId'] = $visit->getColumn('userId'); + + // get visitor IDs that are adjacent to this one in log_visit + // TODO: make sure order of visitor ids is not changed if a returning visitor visits while the user is + // looking at the popup. + $latestVisitTime = reset($rows)->getColumn('lastActionDateTime'); + + + $model = new Model(); + $result['nextVisitorId'] = $model->queryAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = true); + $result['previousVisitorId'] = $model->queryAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = false); + return $result; + } + + /** + * Returns a summary for an important visit. Used to describe the first & last visits of a visitor. + * + * @param DataTable\Row $visit + * @return array + */ + private function getVisitorProfileVisitSummary($visit) + { + $today = Date::today(); + + $serverDate = $visit->getColumn('firstActionTimestamp'); + return array( + 'date' => $serverDate, + 'prettyDate' => Date::factory($serverDate)->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT), + 'daysAgo' => (int)Date::secondsToDays($today->getTimestamp() - Date::factory($serverDate)->getTimestamp()), + 'referrerType' => $visit->getColumn('referrerType'), + 'referralSummary' => self::getReferrerSummaryForVisit($visit), + ); + } + + + /** + * Returns a summary for a visit's referral. + * + * @param DataTable\Row $visit + * @return bool|mixed|string + */ + public static function getReferrerSummaryForVisit($visit) + { + $referrerType = $visit->getColumn('referrerType'); + if ($referrerType === false + || $referrerType == 'direct' + ) { + $result = Piwik::translate('Referrers_DirectEntry'); + } else if ($referrerType == 'search') { + $result = $visit->getColumn('referrerName'); + + $keyword = $visit->getColumn('referrerKeyword'); + if ($keyword !== false + && $keyword != APIReferrers::getKeywordNotDefinedString() + ) { + $result .= ' (' . $keyword . ')'; + } + } else if ($referrerType == 'campaign') { + $result = Piwik::translate('Referrers_ColumnCampaign') . ' (' . $visit->getColumn('referrerName') . ')'; + } else { + $result = $visit->getColumn('referrerName'); + } + + return $result; + } + +} \ No newline at end of file -- cgit v1.2.3 From af73717d45280335b33707295fc4a72f3937d74e Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 22:58:46 +1300 Subject: Removed checkLatLong API parameter and always set the info --- plugins/Live/API.php | 6 ++---- plugins/Live/Controller.php | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/Live/API.php b/plugins/Live/API.php index fc08e510db..970056001b 100644 --- a/plugins/Live/API.php +++ b/plugins/Live/API.php @@ -138,11 +138,9 @@ class API extends \Piwik\Plugin\API * @param int $idSite Site ID * @param bool|false|string $visitorId The ID of the visitor whose profile to retrieve. * @param bool|false|string $segment - * @param bool $checkForLatLong If true, hasLatLong will appear in the output and be true if - * one of the first 100 visits has a latitude/longitude. * @return array */ - public function getVisitorProfile($idSite, $visitorId = false, $segment = false, $checkForLatLong = false) + public function getVisitorProfile($idSite, $visitorId = false, $segment = false) { Piwik::checkUserHasViewAccess($idSite); @@ -163,7 +161,7 @@ class API extends \Piwik\Plugin\API } $profile = new VisitorProfile(); - $result = $profile->makeVisitorProfile($visits, $idSite, $visitorId, $segment, $checkForLatLong); + $result = $profile->makeVisitorProfile($visits, $idSite, $visitorId, $segment); /** * Triggered in the Live.getVisitorProfile API method. Plugins can use this event diff --git a/plugins/Live/Controller.php b/plugins/Live/Controller.php index da434412dd..82dccb92bc 100644 --- a/plugins/Live/Controller.php +++ b/plugins/Live/Controller.php @@ -109,7 +109,7 @@ class Controller extends \Piwik\Plugin\Controller $view = new View('@Live/getVisitorProfilePopup.twig'); $view->idSite = $idSite; $view->goals = APIGoals::getInstance()->getGoals($idSite); - $view->visitorData = Request::processRequest('Live.getVisitorProfile', array('checkForLatLong' => true)); + $view->visitorData = Request::processRequest('Live.getVisitorProfile'); $view->exportLink = $this->getVisitorProfileExportLink(); if (Common::getRequestVar('showMap', 1) == 1 -- cgit v1.2.3 From 13bb9a7ecdd65ea61caf70a449af13edf9cb2913 Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 23:33:37 +1300 Subject: Visitor Profile clean code --- plugins/Live/API.php | 4 +- plugins/Live/VisitorProfile.php | 444 ++++++++++++--------- plugins/Live/templates/getVisitorProfilePopup.twig | 2 +- 3 files changed, 266 insertions(+), 184 deletions(-) diff --git a/plugins/Live/API.php b/plugins/Live/API.php index 970056001b..ed11c6c277 100644 --- a/plugins/Live/API.php +++ b/plugins/Live/API.php @@ -160,8 +160,8 @@ class API extends \Piwik\Plugin\API return array(); } - $profile = new VisitorProfile(); - $result = $profile->makeVisitorProfile($visits, $idSite, $visitorId, $segment); + $profile = new VisitorProfile($idSite); + $result = $profile->makeVisitorProfile($visits, $visitorId, $segment); /** * Triggered in the Live.getVisitorProfile API method. Plugins can use this event diff --git a/plugins/Live/VisitorProfile.php b/plugins/Live/VisitorProfile.php index 807028d6ad..d52adbf076 100644 --- a/plugins/Live/VisitorProfile.php +++ b/plugins/Live/VisitorProfile.php @@ -22,206 +22,69 @@ class VisitorProfile const VISITOR_PROFILE_MAX_VISITS_TO_SHOW = 10; const VISITOR_PROFILE_DATE_FORMAT = '%day% %shortMonth% %longYear%'; + protected $profile = array(); + private $siteSearchKeywords = array(); + private $continents = array(); + private $countries = array(); + private $cities = array(); + private $pageGenerationTimeTotal = 0; + + public function __construct($idSite) + { + $this->idSite = $idSite; + $this->isEcommerceEnabled = Site::isEcommerceEnabledFor($this->idSite); + } + /** * @param $visits - * @param $idSite * @param $visitorId * @param $segment - * @param $checkForLatLong * @return array * @throws Exception */ - public function makeVisitorProfile(DataTable $visits, $idSite, $visitorId, $segment, $checkForLatLong) + public function makeVisitorProfile(DataTable $visits, $visitorId, $segment) { - $isEcommerceEnabled = Site::isEcommerceEnabledFor($idSite); - - $result = array(); - $result['totalVisits'] = 0; - $result['totalVisitDuration'] = 0; - $result['totalActions'] = 0; - $result['totalSearches'] = 0; - $result['totalPageViews'] = 0; - $result['totalGoalConversions'] = 0; - $result['totalConversionsByGoal'] = array(); - - if ($isEcommerceEnabled) { - $result['totalEcommerceConversions'] = 0; - $result['totalEcommerceRevenue'] = 0; - $result['totalEcommerceItems'] = 0; - $result['totalAbandonedCarts'] = 0; - $result['totalAbandonedCartsRevenue'] = 0; - $result['totalAbandonedCartsItems'] = 0; - } - - $countries = array(); - $continents = array(); - $cities = array(); - $siteSearchKeywords = array(); + $this->initVisitorProfile(); - $pageGenerationTimeTotal = 0; - - // aggregate all requested visits info for total_* info /** @var DataTable\Row $visit */ foreach ($visits->getRows() as $visit) { - ++$result['totalVisits']; + ++$this->profile['totalVisits']; - $result['totalVisitDuration'] += $visit->getColumn('visitDuration'); - $result['totalActions'] += $visit->getColumn('actions'); - $result['totalGoalConversions'] += $visit->getColumn('goalConversions'); + $this->profile['totalVisitDuration'] += $visit->getColumn('visitDuration'); + $this->profile['totalActions'] += $visit->getColumn('actions'); + $this->profile['totalGoalConversions'] += $visit->getColumn('goalConversions'); // individual goal conversions are stored in action details foreach ($visit->getColumn('actionDetails') as $action) { - if ($action['type'] == 'goal') { - // handle goal conversion - $idGoal = $action['goalId']; - $idGoalKey = 'idgoal=' . $idGoal; - - if (!isset($result['totalConversionsByGoal'][$idGoalKey])) { - $result['totalConversionsByGoal'][$idGoalKey] = 0; - } - ++$result['totalConversionsByGoal'][$idGoalKey]; - - if (!empty($action['revenue'])) { - if (!isset($result['totalRevenueByGoal'][$idGoalKey])) { - $result['totalRevenueByGoal'][$idGoalKey] = 0; - } - $result['totalRevenueByGoal'][$idGoalKey] += $action['revenue']; - } - } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER // handle ecommerce order - && $isEcommerceEnabled - ) { - ++$result['totalEcommerceConversions']; - $result['totalEcommerceRevenue'] += $action['revenue']; - $result['totalEcommerceItems'] += $action['items']; - } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART // handler abandoned cart - && $isEcommerceEnabled - ) { - ++$result['totalAbandonedCarts']; - $result['totalAbandonedCartsRevenue'] += $action['revenue']; - $result['totalAbandonedCartsItems'] += $action['items']; - } - - if (isset($action['siteSearchKeyword'])) { - $keyword = $action['siteSearchKeyword']; - - if (!isset($siteSearchKeywords[$keyword])) { - $siteSearchKeywords[$keyword] = 0; - ++$result['totalSearches']; - } - ++$siteSearchKeywords[$keyword]; - } - - if (isset($action['generationTime'])) { - $pageGenerationTimeTotal += $action['generationTime']; - ++$result['totalPageViews']; - } - } - - $countryCode = $visit->getColumn('countryCode'); - if (!isset($countries[$countryCode])) { - $countries[$countryCode] = 0; - } - ++$countries[$countryCode]; - - $continentCode = $visit->getColumn('continentCode'); - if (!isset($continents[$continentCode])) { - $continents[$continentCode] = 0; - } - ++$continents[$continentCode]; - - if ($countryCode && !array_key_exists($countryCode, $cities)) { - $cities[$countryCode] = array(); - } - $city = $visit->getColumn('city'); - if (!empty($city)) { - $cities[$countryCode][] = $city; - } - } - - // sort countries/continents/search keywords by visit/action - asort($countries); - asort($continents); - arsort($siteSearchKeywords); - - // transform country/continents/search keywords into something that will look good in XML - $result['countries'] = $result['continents'] = $result['searches'] = array(); - - foreach ($countries as $countryCode => $nbVisits) { - - $countryInfo = array('country' => $countryCode, - 'nb_visits' => $nbVisits, - 'flag' => \Piwik\Plugins\UserCountry\getFlagFromCode($countryCode), - 'prettyName' => \Piwik\Plugins\UserCountry\countryTranslate($countryCode)); - if (!empty($cities[$countryCode])) { - $countryInfo['cities'] = array_unique($cities[$countryCode]); + $this->handleIfGoalAction($action); + $this->handleIfEcommerceAction($action); + $this->handleIfSiteSearchAction($action); + $this->handleIfPageGenerationTime($action); } - $result['countries'][] = $countryInfo; - } - foreach ($continents as $continentCode => $nbVisits) { - $result['continents'][] = array('continent' => $continentCode, - 'nb_visits' => $nbVisits, - 'prettyName' => \Piwik\Plugins\UserCountry\continentTranslate($continentCode)); - } - foreach ($siteSearchKeywords as $keyword => $searchCount) { - $result['searches'][] = array('keyword' => $keyword, - 'searches' => $searchCount); + $this->handleGeoLocation($visit); } - if ($result['totalPageViews']) { - $result['averagePageGenerationTime'] = - round($pageGenerationTimeTotal / $result['totalPageViews'], $precision = 2); - } + $this->handleGeoLocationCountries(); + $this->handleGeoLocationContinents(); + $this->handleSiteSearches(); + $this->handleAveragePageGenerationTime(); $formatter = new Formatter(); - $result['totalVisitDurationPretty'] = $formatter->getPrettyTimeFromSeconds($result['totalVisitDuration'], true); + $this->profile['totalVisitDurationPretty'] = $formatter->getPrettyTimeFromSeconds($this->profile['totalVisitDuration'], true); - // use requested visits for first/last visit info - $rows = $visits->getRows(); - $result['firstVisit'] = $this->getVisitorProfileVisitSummary(end($rows)); - $result['lastVisit'] = $this->getVisitorProfileVisitSummary(reset($rows)); - - // check if requested visits have lat/long - if ($checkForLatLong) { - $result['hasLatLong'] = false; - foreach ($rows as $visit) { - if ($visit->getColumn('latitude') !== false) { // realtime map only checks for latitude - $result['hasLatLong'] = true; - break; - } - } - } - - // save count of visits we queries - $result['visitsAggregated'] = count($rows); + $this->handleVisitsSummary($visits); + $this->handleAdjacentVisitorIds($visits, $visitorId, $segment); // use N most recent visits for last_visits $visits->deleteRowsOffset(self::VISITOR_PROFILE_MAX_VISITS_TO_SHOW); - $result['lastVisits'] = $visits; - - // use the right date format for the pretty server date - $timezone = Site::getTimezoneFor($idSite); - foreach ($result['lastVisits']->getRows() as $visit) { - $dateTimeVisitFirstAction = Date::factory($visit->getColumn('firstActionTimestamp'), $timezone); - - $datePretty = $dateTimeVisitFirstAction->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT); - $visit->setColumn('serverDatePrettyFirstAction', $datePretty); - - $dateTimePretty = $datePretty . ' ' . $visit->getColumn('serverTimePrettyFirstAction'); - $visit->setColumn('serverDateTimePrettyFirstAction', $dateTimePretty); - } - $result['userId'] = $visit->getColumn('userId'); + $this->enrichVisitsWithFirstActionDatetime($visits); - // get visitor IDs that are adjacent to this one in log_visit - // TODO: make sure order of visitor ids is not changed if a returning visitor visits while the user is - // looking at the popup. - $latestVisitTime = reset($rows)->getColumn('lastActionDateTime'); + $this->profile['lastVisits'] = $visits; + $this->profile['userId'] = $visit->getColumn('userId'); - $model = new Model(); - $result['nextVisitorId'] = $model->queryAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = true); - $result['previousVisitorId'] = $model->queryAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = false); - return $result; + return $this->profile; } /** @@ -257,23 +120,242 @@ class VisitorProfile if ($referrerType === false || $referrerType == 'direct' ) { - $result = Piwik::translate('Referrers_DirectEntry'); - } else if ($referrerType == 'search') { - $result = $visit->getColumn('referrerName'); + return Piwik::translate('Referrers_DirectEntry'); + } + + if ($referrerType == 'search') { + $referrerName = $visit->getColumn('referrerName'); $keyword = $visit->getColumn('referrerKeyword'); if ($keyword !== false && $keyword != APIReferrers::getKeywordNotDefinedString() ) { - $result .= ' (' . $keyword . ')'; + $referrerName .= ' (' . $keyword . ')'; + } + return $referrerName; + } + + if ($referrerType == 'campaign') { + return Piwik::translate('Referrers_ColumnCampaign') . ' (' . $visit->getColumn('referrerName') . ')'; + } + + return $visit->getColumn('referrerName'); + } + + private function isEcommerceEnabled() + { + return $this->isEcommerceEnabled; + } + + /** + * @param $action + */ + private function handleIfEcommerceAction($action) + { + if (!$this->isEcommerceEnabled()) { + return; + } + if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER) { + ++$this->profile['totalEcommerceConversions']; + $this->profile['totalEcommerceRevenue'] += $action['revenue']; + $this->profile['totalEcommerceItems'] += $action['items']; + } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) { + ++$this->profile['totalAbandonedCarts']; + $this->profile['totalAbandonedCartsRevenue'] += $action['revenue']; + $this->profile['totalAbandonedCartsItems'] += $action['items']; + } + } + + private function handleIfGoalAction($action) + { + if ($action['type'] != 'goal') { + return; + } + $idGoal = $action['goalId']; + $idGoalKey = 'idgoal=' . $idGoal; + + if (!isset($this->profile['totalConversionsByGoal'][$idGoalKey])) { + $this->profile['totalConversionsByGoal'][$idGoalKey] = 0; + } + ++$this->profile['totalConversionsByGoal'][$idGoalKey]; + + if (!empty($action['revenue'])) { + if (!isset($this->profile['totalRevenueByGoal'][$idGoalKey])) { + $this->profile['totalRevenueByGoal'][$idGoalKey] = 0; + } + $this->profile['totalRevenueByGoal'][$idGoalKey] += $action['revenue']; + } + } + + private function handleIfSiteSearchAction($action) + { + if (!isset($action['siteSearchKeyword'])) { + return; + } + $keyword = $action['siteSearchKeyword']; + + if (!isset($this->siteSearchKeywords[$keyword])) { + $this->siteSearchKeywords[$keyword] = 0; + ++$this->profile['totalSearches']; + } + ++$this->siteSearchKeywords[$keyword]; + } + + private function handleGeoLocation(DataTable\Row $visit) + { + // realtime map only checks for latitude + $hasLatitude = $visit->getColumn('latitude') !== false; + if ($hasLatitude) { + $this->profile['hasLatLong'] = true; + } + + $countryCode = $visit->getColumn('countryCode'); + if (!isset($this->countries[$countryCode])) { + $this->countries[$countryCode] = 0; + } + ++$this->countries[$countryCode]; + + $continentCode = $visit->getColumn('continentCode'); + if (!isset($this->continents[$continentCode])) { + $this->continents[$continentCode] = 0; + } + ++$this->continents[$continentCode]; + + if ($countryCode && !array_key_exists($countryCode, $this->cities)) { + $this->cities[$countryCode] = array(); + } + $city = $visit->getColumn('city'); + if (!empty($city)) { + $this->cities[$countryCode][] = $city; + } + } + + private function handleSiteSearches() + { + // sort by visit/action + arsort($this->siteSearchKeywords); + + foreach ($this->siteSearchKeywords as $keyword => $searchCount) { + $this->profile['searches'][] = array('keyword' => $keyword, + 'searches' => $searchCount); + } + } + + private function handleGeoLocationContinents() + { + // sort by visit/action + asort($this->continents); + foreach ($this->continents as $continentCode => $nbVisits) { + $this->profile['continents'][] = array('continent' => $continentCode, + 'nb_visits' => $nbVisits, + 'prettyName' => \Piwik\Plugins\UserCountry\continentTranslate($continentCode)); + } + } + + private function handleGeoLocationCountries() + { + // sort by visit/action + asort($this->countries); + + // transform country/continents/search keywords into something that will look good in XML + $this->profile['countries'] = $this->profile['continents'] = $this->profile['searches'] = array(); + + foreach ($this->countries as $countryCode => $nbVisits) { + + $countryInfo = array('country' => $countryCode, + 'nb_visits' => $nbVisits, + 'flag' => \Piwik\Plugins\UserCountry\getFlagFromCode($countryCode), + 'prettyName' => \Piwik\Plugins\UserCountry\countryTranslate($countryCode)); + if (!empty($this->cities[$countryCode])) { + $countryInfo['cities'] = array_unique($this->cities[$countryCode]); } - } else if ($referrerType == 'campaign') { - $result = Piwik::translate('Referrers_ColumnCampaign') . ' (' . $visit->getColumn('referrerName') . ')'; - } else { - $result = $visit->getColumn('referrerName'); + $this->profile['countries'][] = $countryInfo; + } + } + + private function initVisitorProfile() + { + $this->profile['totalVisits'] = 0; + $this->profile['totalVisitDuration'] = 0; + $this->profile['totalActions'] = 0; + $this->profile['totalSearches'] = 0; + $this->profile['totalPageViewsWithTiming'] = 0; + $this->profile['totalGoalConversions'] = 0; + $this->profile['totalConversionsByGoal'] = array(); + $this->profile['hasLatLong'] = false; + + if ($this->isEcommerceEnabled()) { + $this->profile['totalEcommerceConversions'] = 0; + $this->profile['totalEcommerceRevenue'] = 0; + $this->profile['totalEcommerceItems'] = 0; + $this->profile['totalAbandonedCarts'] = 0; + $this->profile['totalAbandonedCartsRevenue'] = 0; + $this->profile['totalAbandonedCartsItems'] = 0; + } + } + + private function handleAveragePageGenerationTime() + { + if ($this->profile['totalPageViewsWithTiming']) { + $this->profile['averagePageGenerationTime'] = + round($this->pageGenerationTimeTotal / $this->profile['totalPageViewsWithTiming'], $precision = 2); + } + } + + private function handleIfPageGenerationTime($action) + { + if (isset($action['generationTime'])) { + $this->pageGenerationTimeTotal += $action['generationTime']; + ++$this->profile['totalPageViewsWithTiming']; } + } + + /** + * @param DataTable $visits + * @param $visitorId + * @param $segment + */ + private function handleAdjacentVisitorIds(DataTable $visits, $visitorId, $segment) + { + // get visitor IDs that are adjacent to this one in log_visit + // TODO: make sure order of visitor ids is not changed if a returning visitor visits while the user is + // looking at the popup. + $rows = $visits->getRows(); + $latestVisitTime = reset($rows)->getColumn('lastActionDateTime'); + + $model = new Model(); + $this->profile['nextVisitorId'] = $model->queryAdjacentVisitorId($this->idSite, $visitorId, $latestVisitTime, $segment, $getNext = true); + $this->profile['previousVisitorId'] = $model->queryAdjacentVisitorId($this->idSite, $visitorId, $latestVisitTime, $segment, $getNext = false); + } + + /** + * @param DataTable $visits + */ + private function handleVisitsSummary(DataTable $visits) + { + $rows = $visits->getRows(); + $this->profile['firstVisit'] = $this->getVisitorProfileVisitSummary(end($rows)); + $this->profile['lastVisit'] = $this->getVisitorProfileVisitSummary(reset($rows)); + $this->profile['visitsAggregated'] = count($rows); + } + + /** + * @param DataTable $visits + * @return DataTable\Row + * @throws Exception + */ + private function enrichVisitsWithFirstActionDatetime(DataTable $visits) + { + $timezone = Site::getTimezoneFor($this->idSite); + foreach ($visits->getRows() as $visit) { + $dateTimeVisitFirstAction = Date::factory($visit->getColumn('firstActionTimestamp'), $timezone); + + $datePretty = $dateTimeVisitFirstAction->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT); + $visit->setColumn('serverDatePrettyFirstAction', $datePretty); - return $result; + $dateTimePretty = $datePretty . ' ' . $visit->getColumn('serverTimePrettyFirstAction'); + $visit->setColumn('serverDateTimePrettyFirstAction', $dateTimePretty); + } } } \ No newline at end of file diff --git a/plugins/Live/templates/getVisitorProfilePopup.twig b/plugins/Live/templates/getVisitorProfilePopup.twig index 0a25de6a39..afb9551321 100644 --- a/plugins/Live/templates/getVisitorProfilePopup.twig +++ b/plugins/Live/templates/getVisitorProfilePopup.twig @@ -61,7 +61,7 @@

{% endif %} {% if visitorData.averagePageGenerationTime is defined %} -

+

{{ 'Live_AveragePageGenerationTime'|translate('' ~ visitorData.averagePageGenerationTime ~ 's')|raw }}

{% endif %} -- cgit v1.2.3 From 3006097403d7499849adffc5ec3d7d39d3c07cc0 Mon Sep 17 00:00:00 2001 From: mattab Date: Fri, 5 Dec 2014 23:58:38 +1300 Subject: Fix one test --- ...ge_dateIsLastN_MetadataAndNormalAPI__Live.getVisitorProfile.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/PHPUnit/System/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getVisitorProfile.xml b/tests/PHPUnit/System/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getVisitorProfile.xml index 60e389c7ab..845a1a23a9 100644 --- a/tests/PHPUnit/System/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getVisitorProfile.xml +++ b/tests/PHPUnit/System/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getVisitorProfile.xml @@ -4,11 +4,12 @@ 361 2 0 - 0 + 0 1 1 + 0 @@ -42,6 +43,8 @@ Direct Entry 2 + + 1 @@ -263,6 +266,4 @@ 0 - - \ No newline at end of file -- cgit v1.2.3 From d84fbf324a8a4522ad96c4edc2f1328a9218b37a Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 00:00:45 +1300 Subject: Introduce new $limit parameter to the query builder, which will LIMIT all queries including inner ones - do not use when aggregating or it will sample --- core/DataAccess/LogQueryBuilder.php | 24 +++++++++++++++++------- core/Segment.php | 8 ++++++-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php index 6e5b77561b..2bb71d4335 100644 --- a/core/DataAccess/LogQueryBuilder.php +++ b/core/DataAccess/LogQueryBuilder.php @@ -21,7 +21,7 @@ class LogQueryBuilder $this->segmentExpression = $segmentExpression; } - public function getSelectQueryString($select, $from, $where, $bind, $orderBy, $groupBy) + public function getSelectQueryString($select, $from, $where, $bind, $orderBy, $groupBy, $limit) { if (!is_array($from)) { $from = array($from); @@ -39,9 +39,9 @@ class LogQueryBuilder $from = $joins['sql']; if ($joinWithSubSelect) { - $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy); + $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit); } else { - $sql = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); + $sql = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit); } return array( 'sql' => $sql, @@ -166,10 +166,11 @@ class LogQueryBuilder * @param string $where * @param string $orderBy * @param string $groupBy + * @param string $limit * @throws Exception * @return string */ - private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy) + private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit) { $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)"; preg_match_all("/". $matchTables ."\.[a-z0-9_\*]+/", $select, $matches); @@ -185,8 +186,9 @@ class LogQueryBuilder $innerWhere = $where; $innerGroupBy = "log_visit.idvisit"; $innerOrderBy = "NULL"; + $innerLimit = $limit; - $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerOrderBy, $innerGroupBy); + $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerOrderBy, $innerGroupBy, $innerLimit); $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); $from = " @@ -196,7 +198,7 @@ class LogQueryBuilder $where = false; $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy); $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy); - $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); + $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit); return $query; } @@ -209,9 +211,10 @@ class LogQueryBuilder * @param string $where where clause * @param string $orderBy order by clause * @param string $groupBy group by clause + * @param string $limit limit by clause * @return string */ - private function buildSelectQuery($select, $from, $where, $orderBy, $groupBy) + private function buildSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit) { $sql = " SELECT @@ -237,6 +240,13 @@ class LogQueryBuilder $orderBy"; } + $limit = (int)$limit; + if ($limit >= 1) { + $sql .= " + LIMIT + $limit"; + } + return $sql; } diff --git a/core/Segment.php b/core/Segment.php index 91318a0197..e350e03afb 100644 --- a/core/Segment.php +++ b/core/Segment.php @@ -233,14 +233,18 @@ class Segment * @param array|string $bind (optional) Bind parameters, eg, `array($col1Value, $col2Value)`. * @param false|string $orderBy (optional) Order by clause, eg, `"t1.col1 ASC"`. * @param false|string $groupBy (optional) Group by clause, eg, `"t2.col2"`. + * @param int $limit Limit by clause + * @param int If set to value >= 1 then the Select query (and All inner queries) will be LIMIT'ed by this value. + * Use only when you're not aggregating or it will sample the data. * @return string The entire select query. */ - public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false) + public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0) { $segmentExpression = $this->segmentExpression; $segmentQuery = new LogQueryBuilder($segmentExpression); - return $segmentQuery->getSelectQueryString($select, $from, $where, $bind, $orderBy, $groupBy); + return $segmentQuery->getSelectQueryString($select, $from, $where, $bind, $orderBy, $groupBy, $limit); } + /** * Returns the segment string. * -- cgit v1.2.3 From 09196f61430b50039adcbd7ad69938c12b171f62 Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 00:14:54 +1300 Subject: Added unit test for $limit parameter --- tests/PHPUnit/Integration/SegmentTest.php | 66 +++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/tests/PHPUnit/Integration/SegmentTest.php b/tests/PHPUnit/Integration/SegmentTest.php index 41fe39a392..e1439cddda 100644 --- a/tests/PHPUnit/Integration/SegmentTest.php +++ b/tests/PHPUnit/Integration/SegmentTest.php @@ -126,7 +126,7 @@ class SegmentTest extends IntegrationTestCase $this->assertEquals(32, strlen($segment->getHash())); } - public function testGetSelectQueryNoJoin() + public function test_getSelectQuery_whenNoJoin() { $select = '*'; $from = 'log_visit'; @@ -153,7 +153,7 @@ class SegmentTest extends IntegrationTestCase $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); } - public function testGetSelectQueryJoinVisitOnAction() + public function test_getSelectQuery_whenJoinVisitOnAction() { $select = '*'; $from = 'log_link_visit_action'; @@ -181,7 +181,7 @@ class SegmentTest extends IntegrationTestCase $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); } - public function testGetSelectQueryJoinActionOnVisit() + public function test_getSelectQuery_whenJoinActionOnVisit() { $select = 'sum(log_visit.visit_total_actions) as nb_actions, max(log_visit.visit_total_actions) as max_actions, sum(log_visit.visit_total_time) as sum_visit_length'; $from = 'log_visit'; @@ -217,7 +217,7 @@ class SegmentTest extends IntegrationTestCase $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); } - public function testGetSelectQueryJoinConversionOnAction() + public function test_getSelectQuery_whenJoinConversionOnAction() { $select = '*'; $from = 'log_link_visit_action'; @@ -245,7 +245,7 @@ class SegmentTest extends IntegrationTestCase $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); } - public function testGetSelectQueryJoinActionOnConversion() + public function test_getSelectQuery_whenJoinActionOnConversion() { $select = '*'; $from = 'log_conversion'; @@ -273,7 +273,7 @@ class SegmentTest extends IntegrationTestCase $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); } - public function testGetSelectQueryJoinConversionOnVisit() + public function test_getSelectQuery_whenJoinConversionOnVisit() { $select = 'log_visit.*'; $from = 'log_visit'; @@ -308,7 +308,7 @@ class SegmentTest extends IntegrationTestCase $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); } - public function testGetSelectQueryConversionOnly() + public function test_getSelectQuery_whenJoinConversionOnly() { $select = 'log_conversion.*'; $from = 'log_conversion'; @@ -335,7 +335,7 @@ class SegmentTest extends IntegrationTestCase $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); } - public function testGetSelectQueryJoinVisitOnConversion() + public function test_getSelectQuery_whenJoinVisitOnConversion() { $select = '*'; $from = 'log_conversion'; @@ -367,7 +367,7 @@ class SegmentTest extends IntegrationTestCase * visit is joined on action, then conversion is joined * make sure that conversion is joined on action not visit */ - public function testGetSelectQueryJoinVisitAndConversionOnAction() + public function test_getSelectQuery_whenJoinVisitAndConversionOnAction() { $select = '*'; $from = 'log_link_visit_action'; @@ -398,7 +398,7 @@ class SegmentTest extends IntegrationTestCase * join conversion on visit, then actions * make sure actions are joined before conversions */ - public function testGetSelectQueryJoinConversionAndActionOnVisit() + public function test_getSelectQuery_whenJoinConversionAndActionOnVisit() { $select = 'log_visit.*'; $from = 'log_visit'; @@ -433,7 +433,7 @@ class SegmentTest extends IntegrationTestCase } /** - * Dataprovider for testBogusSegmentThrowsException + * Dataprovider for test_bogusSegment_shouldThrowException */ public function getBogusSegments() { @@ -447,7 +447,7 @@ class SegmentTest extends IntegrationTestCase /** * @dataProvider getBogusSegments */ - public function testBogusSegmentThrowsException($segment) + public function test_bogusSegment_shouldThrowException($segment) { try { new Segment($segment, $idSites = array()); @@ -456,4 +456,46 @@ class SegmentTest extends IntegrationTestCase } $this->fail('Expected exception not raised'); } + + + public function test_getSelectQuery_whenLimit_innerQueryShouldHaveLimit() + { + $select = 'sum(log_visit.visit_total_time) as sum_visit_length'; + $from = 'log_visit'; + $where = 'log_visit.idvisit = ?'; + $bind = array(1); + + $segment = 'customVariablePageName1==Test'; + $segment = new Segment($segment, $idSites = array()); + + $orderBy = false; + $groupBy = false; + $limit = 33; + + $query = $segment->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy, $limit); + + $expected = array( + "sql" => " + SELECT + sum(log_visit.visit_total_time) as sum_visit_length + FROM + ( + SELECT + log_visit.visit_total_time + FROM + " . Common::prefixTable('log_visit') . " AS log_visit + LEFT JOIN " . Common::prefixTable('log_link_visit_action') . " AS log_link_visit_action ON log_link_visit_action.idvisit = log_visit.idvisit + WHERE + ( log_visit.idvisit = ? ) + AND + ( log_link_visit_action.custom_var_k1 = ? ) + GROUP BY log_visit.idvisit + ORDER BY NULL + LIMIT 33 + ) AS log_inner + LIMIT 33", + "bind" => array(1, 'Test')); + + $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + } } -- cgit v1.2.3 From 04e3b93fea8ed2ccfe911ce56507e8c5c9091491 Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 00:17:25 +1300 Subject: Re-ordering $orderBy, $groupBy --> $groupBy, $orderBy as it's proper order in SQL logic --- core/DataAccess/LogQueryBuilder.php | 19 ++++++++++--------- core/Segment.php | 2 +- plugins/Actions/Archiver.php | 18 +++++++++--------- plugins/Contents/Archiver.php | 6 +++--- plugins/Events/Archiver.php | 4 ++-- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php index 2bb71d4335..132480514b 100644 --- a/core/DataAccess/LogQueryBuilder.php +++ b/core/DataAccess/LogQueryBuilder.php @@ -21,7 +21,7 @@ class LogQueryBuilder $this->segmentExpression = $segmentExpression; } - public function getSelectQueryString($select, $from, $where, $bind, $orderBy, $groupBy, $limit) + public function getSelectQueryString($select, $from, $where, $bind, $groupBy, $orderBy, $limit) { if (!is_array($from)) { $from = array($from); @@ -39,9 +39,9 @@ class LogQueryBuilder $from = $joins['sql']; if ($joinWithSubSelect) { - $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit); + $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit); } else { - $sql = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit); + $sql = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit); } return array( 'sql' => $sql, @@ -164,13 +164,13 @@ class LogQueryBuilder * @param string $select * @param string $from * @param string $where - * @param string $orderBy * @param string $groupBy + * @param string $orderBy * @param string $limit * @throws Exception * @return string */ - private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit) + private function buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit) { $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)"; preg_match_all("/". $matchTables ."\.[a-z0-9_\*]+/", $select, $matches); @@ -188,7 +188,7 @@ class LogQueryBuilder $innerOrderBy = "NULL"; $innerLimit = $limit; - $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerOrderBy, $innerGroupBy, $innerLimit); + $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerGroupBy, $innerOrderBy, $innerLimit); $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); $from = " @@ -198,7 +198,7 @@ class LogQueryBuilder $where = false; $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy); $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy); - $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit); + $query = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit); return $query; } @@ -209,12 +209,12 @@ class LogQueryBuilder * @param string $select fieldlist to be selected * @param string $from tablelist to select from * @param string $where where clause - * @param string $orderBy order by clause * @param string $groupBy group by clause + * @param string $orderBy order by clause * @param string $limit limit by clause * @return string */ - private function buildSelectQuery($select, $from, $where, $orderBy, $groupBy, $limit) + private function buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit) { $sql = " SELECT @@ -254,6 +254,7 @@ class LogQueryBuilder * @param $where * @param $segmentWhere * @return string + * @throws */ protected function getWhereMatchBoth($where, $segmentWhere) { diff --git a/core/Segment.php b/core/Segment.php index e350e03afb..7fe4c061fd 100644 --- a/core/Segment.php +++ b/core/Segment.php @@ -242,7 +242,7 @@ class Segment { $segmentExpression = $this->segmentExpression; $segmentQuery = new LogQueryBuilder($segmentExpression); - return $segmentQuery->getSelectQueryString($select, $from, $where, $bind, $orderBy, $groupBy, $limit); + return $segmentQuery->getSelectQueryString($select, $from, $where, $bind, $groupBy, $orderBy, $limit); } /** diff --git a/plugins/Actions/Archiver.php b/plugins/Actions/Archiver.php index ab5fea8739..199438445e 100644 --- a/plugins/Actions/Archiver.php +++ b/plugins/Actions/Archiver.php @@ -244,9 +244,9 @@ class Archiver extends \Piwik\Plugin\Archiver $this->updateQuerySelectFromForSiteSearch($select, $from); } - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "idaction_name", $rankingQuery); + $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "idaction_name", $rankingQuery); - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "idaction_url", $rankingQuery); + $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "idaction_url", $rankingQuery); } protected function isSiteSearchEnabled() @@ -254,7 +254,7 @@ class Archiver extends \Piwik\Plugin\Archiver return $this->isSiteSearchEnabled; } - protected function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, $sprintfField, RankingQuery $rankingQuery = null) + protected function archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, $sprintfField, RankingQuery $rankingQuery = null) { $select = sprintf($select, $sprintfField); @@ -321,9 +321,9 @@ class Archiver extends \Piwik\Plugin\Archiver $groupBy = "log_visit.%s, idaction"; - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "visit_entry_idaction_url", $rankingQuery); + $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "visit_entry_idaction_url", $rankingQuery); - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "visit_entry_idaction_name", $rankingQuery); + $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "visit_entry_idaction_name", $rankingQuery); } /** @@ -366,9 +366,9 @@ class Archiver extends \Piwik\Plugin\Archiver $groupBy = "log_visit.%s, idaction"; - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "visit_exit_idaction_url", $rankingQuery); + $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "visit_exit_idaction_url", $rankingQuery); - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "visit_exit_idaction_name", $rankingQuery); + $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "visit_exit_idaction_name", $rankingQuery); return array($rankingQuery, $extraSelects, $from, $orderBy, $select, $where, $groupBy); } @@ -412,9 +412,9 @@ class Archiver extends \Piwik\Plugin\Archiver $groupBy = "log_link_visit_action.%s, idaction"; - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "idaction_url_ref", $rankingQuery); + $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "idaction_url_ref", $rankingQuery); - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "idaction_name_ref", $rankingQuery); + $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "idaction_name_ref", $rankingQuery); } /** diff --git a/plugins/Contents/Archiver.php b/plugins/Contents/Archiver.php index 45607bd7b7..6d3a2e05ce 100644 --- a/plugins/Contents/Archiver.php +++ b/plugins/Contents/Archiver.php @@ -117,7 +117,7 @@ class Archiver extends \Piwik\Plugin\Archiver $rankingQuery->addColumn(array(Metrics::INDEX_CONTENT_NB_IMPRESSIONS, Metrics::INDEX_NB_VISITS), 'sum'); } - $resultSet = $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, $rankingQuery); + $resultSet = $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, $rankingQuery); while ($row = $resultSet->fetch()) { $this->aggregateImpressionRow($row); @@ -174,14 +174,14 @@ class Archiver extends \Piwik\Plugin\Archiver $rankingQuery->addColumn(array(Metrics::INDEX_CONTENT_NB_INTERACTIONS), 'sum'); } - $resultSet = $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, $rankingQuery); + $resultSet = $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, $rankingQuery); while ($row = $resultSet->fetch()) { $this->aggregateInteractionRow($row); } } - private function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, RankingQuery $rankingQuery) + private function archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, RankingQuery $rankingQuery) { // get query with segmentation $query = $this->getLogAggregator()->generateQuery($select, $from, $where, $groupBy, $orderBy); diff --git a/plugins/Events/Archiver.php b/plugins/Events/Archiver.php index b396401de0..5a5160bdb2 100644 --- a/plugins/Events/Archiver.php +++ b/plugins/Events/Archiver.php @@ -179,10 +179,10 @@ class Archiver extends \Piwik\Plugin\Archiver $rankingQuery->addColumn(Metrics::INDEX_EVENT_MAX_EVENT_VALUE, 'max'); } - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, $rankingQuery); + $this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, $rankingQuery); } - protected function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, RankingQuery $rankingQuery) + protected function archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, RankingQuery $rankingQuery) { // get query with segmentation $query = $this->getLogAggregator()->generateQuery($select, $from, $where, $groupBy, $orderBy); -- cgit v1.2.3 From 966fb421bcf0d8efe792797e138d6a7afce31ebe Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 00:23:20 +1300 Subject: Fix test --- tests/PHPUnit/Integration/SegmentTest.php | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/PHPUnit/Integration/SegmentTest.php b/tests/PHPUnit/Integration/SegmentTest.php index e1439cddda..e0fd9cd3c4 100644 --- a/tests/PHPUnit/Integration/SegmentTest.php +++ b/tests/PHPUnit/Integration/SegmentTest.php @@ -36,11 +36,11 @@ class SegmentTest extends IntegrationTestCase parent::tearDown(); } - protected function _filterWhitsSpaces($valueToFilter) + protected function _filterWhiteSpaces($valueToFilter) { if (is_array($valueToFilter)) { foreach ($valueToFilter as $key => $value) { - $valueToFilter[$key] = $this->_filterWhitsSpaces($value); + $valueToFilter[$key] = $this->_filterWhiteSpaces($value); } return $valueToFilter; } else { @@ -117,11 +117,11 @@ class SegmentTest extends IntegrationTestCase $segment = new Segment($segment, $idSites = array()); $sql = $segment->getSelectQuery($select, $from, false); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($sql)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($sql)); // calling twice should give same results $sql = $segment->getSelectQuery($select, array($from)); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($sql)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($sql)); $this->assertEquals(32, strlen($segment->getHash())); } @@ -150,7 +150,7 @@ class SegmentTest extends IntegrationTestCase ( log_visit.custom_var_k1 = ? AND log_visit.visitor_returning = ? )", "bind" => array(1, 'Test', 0)); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinVisitOnAction() @@ -178,7 +178,7 @@ class SegmentTest extends IntegrationTestCase ( log_link_visit_action.custom_var_k1 = ? AND log_visit.visitor_returning = ? )", "bind" => array(1, 'Test', 0)); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinActionOnVisit() @@ -214,7 +214,7 @@ class SegmentTest extends IntegrationTestCase ) AS log_inner", "bind" => array(1, 'Test', 0)); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinConversionOnAction() @@ -242,7 +242,7 @@ class SegmentTest extends IntegrationTestCase ( log_link_visit_action.custom_var_k1 = ? AND log_conversion.idgoal = ? AND log_link_visit_action.custom_var_k2 = ? )", "bind" => array(1, 'Test', 1, 'Test2')); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinActionOnConversion() @@ -270,7 +270,7 @@ class SegmentTest extends IntegrationTestCase ( ( log_conversion.idgoal IS NULL OR log_conversion.idgoal <> ? ) AND log_link_visit_action.custom_var_k1 = ? AND log_conversion.idgoal = ? )", "bind" => array(1, 2, 'Test', 1)); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinConversionOnVisit() @@ -305,7 +305,7 @@ class SegmentTest extends IntegrationTestCase ) AS log_inner", "bind" => array(1, 1)); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinConversionOnly() @@ -332,7 +332,7 @@ class SegmentTest extends IntegrationTestCase ( log_conversion.idgoal = ? )", "bind" => array(1, 1)); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinVisitOnConversion() @@ -360,7 +360,7 @@ class SegmentTest extends IntegrationTestCase ( (log_conversion.idgoal = ? OR HOUR(log_visit.visit_last_action_time) = ? ))", "bind" => array(1, 1, 12)); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } /** @@ -391,7 +391,7 @@ class SegmentTest extends IntegrationTestCase HOUR(log_visit.visit_last_action_time) = ? AND log_conversion.idgoal = ? ", "bind" => array(12, 1)); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } /** @@ -429,7 +429,7 @@ class SegmentTest extends IntegrationTestCase ) AS log_inner", "bind" => array(1, 12, 'Test')); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } /** @@ -477,7 +477,7 @@ class SegmentTest extends IntegrationTestCase $expected = array( "sql" => " SELECT - sum(log_visit.visit_total_time) as sum_visit_length + sum(log_inner.visit_total_time) as sum_visit_length FROM ( SELECT @@ -496,6 +496,6 @@ class SegmentTest extends IntegrationTestCase LIMIT 33", "bind" => array(1, 'Test')); - $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query)); + $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); } } -- cgit v1.2.3 From 9610d1b785cb05e06b94eb2c749e7a7f80dcc90b Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 01:09:12 +1300 Subject: Added integration test for the model, to test the SQL query generated --- plugins/Live/Model.php | 220 ++++++++++++++++------------- plugins/Live/tests/Integration/APITest.php | 152 -------------------- plugins/Live/tests/System/APITest.php | 152 ++++++++++++++++++++ plugins/Live/tests/System/ModelTest.php | 123 ++++++++++++++++ tests/PHPUnit/Integration/SegmentTest.php | 30 ++-- 5 files changed, 413 insertions(+), 264 deletions(-) delete mode 100644 plugins/Live/tests/Integration/APITest.php create mode 100644 plugins/Live/tests/System/APITest.php create mode 100644 plugins/Live/tests/System/ModelTest.php diff --git a/plugins/Live/Model.php b/plugins/Live/Model.php index 2353450572..2d438bff9b 100644 --- a/plugins/Live/Model.php +++ b/plugins/Live/Model.php @@ -36,105 +36,10 @@ class Model */ public function queryLogVisits($idSite, $period, $date, $segment, $countVisitorsToFetch, $visitorId, $minTimestamp, $filterSortOrder) { - $where = $whereBind = array(); - - list($whereClause, $idSites) = $this->getIdSitesWhereClause($idSite); - - $where[] = $whereClause; - $whereBind = $idSites; - - if (strtolower($filterSortOrder) !== 'asc') { - $filterSortOrder = 'DESC'; - } - - $orderBy = "idsite, visit_last_action_time " . $filterSortOrder; - $orderByParent = "sub.visit_last_action_time " . $filterSortOrder; - - if (!empty($visitorId)) { - $where[] = "log_visit.idvisitor = ? "; - $whereBind[] = @Common::hex2bin($visitorId); - } - - if (!empty($minTimestamp)) { - $where[] = "log_visit.visit_last_action_time > ? "; - $whereBind[] = date("Y-m-d H:i:s", $minTimestamp); - } - - // If no other filter, only look at the last 24 hours of stats - if (empty($visitorId) - && empty($countVisitorsToFetch) - && empty($period) - && empty($date) - ) { - $period = 'day'; - $date = 'yesterdaySameTime'; - } - - // SQL Filter with provided period - if (!empty($period) && !empty($date)) { - $currentSite = new Site($idSite); - $currentTimezone = $currentSite->getTimezone(); - - $dateString = $date; - if ($period == 'range') { - $processedPeriod = new Range('range', $date); - if ($parsedDate = Range::parseDateRange($date)) { - $dateString = $parsedDate[2]; - } - } else { - $processedDate = Date::factory($date); - if ($date == 'today' - || $date == 'now' - || $processedDate->toString() == Date::factory('now', $currentTimezone)->toString() - ) { - $processedDate = $processedDate->subDay(1); - } - $processedPeriod = Period\Factory::build($period, $processedDate); - } - $dateStart = $processedPeriod->getDateStart()->setTimezone($currentTimezone); - $where[] = "log_visit.visit_last_action_time >= ?"; - $whereBind[] = $dateStart->toString('Y-m-d H:i:s'); - - if (!in_array($date, array('now', 'today', 'yesterdaySameTime')) - && strpos($date, 'last') === false - && strpos($date, 'previous') === false - && Date::factory($dateString)->toString('Y-m-d') != Date::factory('now', $currentTimezone)->toString() - ) { - $dateEnd = $processedPeriod->getDateEnd()->setTimezone($currentTimezone); - $where[] = " log_visit.visit_last_action_time <= ?"; - $dateEndString = $dateEnd->addDay(1)->toString('Y-m-d H:i:s'); - $whereBind[] = $dateEndString; - } - } + list($sql, $bind) = $this->makeLogVisitsQueryString($idSite, $period, $date, $segment, $countVisitorsToFetch, $visitorId, $minTimestamp, $filterSortOrder); - if (count($where) > 0) { - $where = join(" - AND ", $where); - } else { - $where = false; - } - - $segment = new Segment($segment, $idSite); - - // Subquery to use the indexes for ORDER BY - $select = "log_visit.*"; - $from = "log_visit"; - $groupBy = false; - $subQuery = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy, $groupBy); - - $sqlLimit = $countVisitorsToFetch >= 1 ? " LIMIT 0, " . (int)$countVisitorsToFetch : ""; - - // Group by idvisit so that a visitor converting 2 goals only appears once - $sql = " - SELECT sub.* FROM ( - " . $subQuery['sql'] . " - $sqlLimit - ) AS sub - GROUP BY sub.idvisit - ORDER BY $orderByParent - "; try { - $data = Db::fetchAll($sql, $subQuery['bind']); + $data = Db::fetchAll($sql, $bind); return $data; } catch (Exception $e) { echo $e->getMessage(); @@ -265,4 +170,125 @@ class Model } return $visitorId; } + + /** + * @param $idSite + * @param $period + * @param $date + * @param $segment + * @param $countVisitorsToFetch + * @param $visitorId + * @param $minTimestamp + * @param $filterSortOrder + * @return array + * @throws Exception + */ + public function makeLogVisitsQueryString($idSite, $period, $date, $segment, $countVisitorsToFetch, $visitorId, $minTimestamp, $filterSortOrder) + { + $where = $whereBind = array(); + + list($whereClause, $idSites) = $this->getIdSitesWhereClause($idSite); + + $where[] = $whereClause; + $whereBind = $idSites; + + if (!empty($visitorId)) { + $where[] = "log_visit.idvisitor = ? "; + $whereBind[] = @Common::hex2bin($visitorId); + } + + if (!empty($minTimestamp)) { + $where[] = "log_visit.visit_last_action_time > ? "; + $whereBind[] = date("Y-m-d H:i:s", $minTimestamp); + } + + // If no other filter, only look at the last 24 hours of stats + if (empty($visitorId) + && empty($countVisitorsToFetch) + && empty($period) + && empty($date) + ) { + $period = 'day'; + $date = 'yesterdaySameTime'; + } + + // SQL Filter with provided period + if (!empty($period) && !empty($date)) { + $currentSite = $this->makeSite($idSite); + $currentTimezone = $currentSite->getTimezone(); + + $dateString = $date; + if ($period == 'range') { + $processedPeriod = new Range('range', $date); + if ($parsedDate = Range::parseDateRange($date)) { + $dateString = $parsedDate[2]; + } + } else { + $processedDate = Date::factory($date); + if ($date == 'today' + || $date == 'now' + || $processedDate->toString() == Date::factory('now', $currentTimezone)->toString() + ) { + $processedDate = $processedDate->subDay(1); + } + $processedPeriod = Period\Factory::build($period, $processedDate); + } + $dateStart = $processedPeriod->getDateStart()->setTimezone($currentTimezone); + $where[] = "log_visit.visit_last_action_time >= ?"; + $whereBind[] = $dateStart->toString('Y-m-d H:i:s'); + + if (!in_array($date, array('now', 'today', 'yesterdaySameTime')) + && strpos($date, 'last') === false + && strpos($date, 'previous') === false + && Date::factory($dateString)->toString('Y-m-d') != Date::factory('now', $currentTimezone)->toString() + ) { + $dateEnd = $processedPeriod->getDateEnd()->setTimezone($currentTimezone); + $where[] = " log_visit.visit_last_action_time <= ?"; + $dateEndString = $dateEnd->addDay(1)->toString('Y-m-d H:i:s'); + $whereBind[] = $dateEndString; + } + } + + if (count($where) > 0) { + $where = join(" + AND ", $where); + } else { + $where = false; + } + + if (strtolower($filterSortOrder) !== 'asc') { + $filterSortOrder = 'DESC'; + } + $segment = new Segment($segment, $idSite); + + // Subquery to use the indexes for ORDER BY + $select = "log_visit.*"; + $from = "log_visit"; + $groupBy = false; + $limit = $countVisitorsToFetch >= 1 ? (int)$countVisitorsToFetch : 0; + $orderBy = "idsite, visit_last_action_time " . $filterSortOrder; + $orderByParent = "sub.visit_last_action_time " . $filterSortOrder; + + $subQuery = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy, $groupBy, $limit); + + $bind = $subQuery['bind']; + // Group by idvisit so that a visitor converting 2 goals only appears once + $sql = " + SELECT sub.* FROM ( + " . $subQuery['sql'] . " + ) AS sub + GROUP BY sub.idvisit + ORDER BY $orderByParent + "; + return array($sql, $bind); + } + + /** + * @param $idSite + * @return Site + */ + protected function makeSite($idSite) + { + return new Site($idSite); + } } \ No newline at end of file diff --git a/plugins/Live/tests/Integration/APITest.php b/plugins/Live/tests/Integration/APITest.php deleted file mode 100644 index 2c85088a19..0000000000 --- a/plugins/Live/tests/Integration/APITest.php +++ /dev/null @@ -1,152 +0,0 @@ -api = API::getInstance(); - $this->setSuperUser(); - $this->createSite(); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage checkUserHasViewAccess Fake exception - */ - public function test_GetCounters_ShouldFail_IfUserHasNoPermission() - { - $this->setAnonymous(); - $this->api->getCounters($this->idSite, 5); - } - - public function test_GetCounters_ShouldReturnZeroForAllCounters_IfThereAreNoVisitsEtc() - { - $counters = $this->api->getCounters($this->idSite, 5); - - $this->assertEquals($this->buildCounter(0, 0, 0, 0), $counters); - } - - public function test_GetCounters_ShouldOnlyReturnResultsOfLastMinutes() - { - $this->trackSomeVisits(); - - $counters = $this->api->getCounters($this->idSite, 5); - $this->assertEquals($this->buildCounter(19, 32, 16, 16), $counters); - - $counters = $this->api->getCounters($this->idSite, 20); - $this->assertEquals($this->buildCounter(24, 60, 20, 40), $counters); - - $counters = $this->api->getCounters($this->idSite, 0); - $this->assertEquals($this->buildCounter(0, 0, 0, 0), $counters); - } - - private function trackSomeVisits() - { - $nowTimestamp = time(); - - // use local tracker so mock location provider can be used - $t = Fixture::getTracker($this->idSite, $nowTimestamp, $defaultInit = true, $useLocal = false); - $t->enableBulkTracking(); - - for ($i = 0; $i != 20; ++$i) { - $t->setForceNewVisit(); - $t->setVisitorId( substr(md5($i * 1000), 0, $t::LENGTH_VISITOR_ID)); - - $factor = 10; - if ($i > 15) { - $factor = 30; // make sure first 15 visits are always within 5 minutes to prevent any random fails - } - $time = $nowTimestamp - ($i * $factor); - - // first visit -> this one is > 5 minutes and should be ignored in one test - $date = Date::factory($time - 600); - $t->setForceVisitDateTime($date->getDatetime()); - $t->setUrl("http://piwik.net/space/quest/iv"); - $t->doTrackPageView("Space Quest XV"); - - $t->doTrackGoal(1); // this one is > 5 minutes and should be ignored in one test - - // second visit - $date = Date::factory($time - 1); - $t->setForceVisitDateTime($date->getDatetime()); - $t->setUrl("http://piwik.net/space/quest/iv"); - $t->doTrackPageView("Space Quest XII"); - - if ($i % 6 == 0) { - $t->setForceNewVisit(); // to test visitors vs visits - } - - // third visit - $date = Date::factory($time); - $t->setForceVisitDateTime($date->getDatetime()); - $t->setUrl("http://piwik.net/grue/$i"); - $t->doTrackPageView('It is pitch black...'); - - $t->doTrackGoal(2); - } - - $t->doBulkTrack(); - } - - private function buildCounter($visits, $actions, $visitors, $visitsConverted) - { - return array(array( - 'visits' => $visits, - 'actions' => $actions, - 'visitors' => $visitors, - 'visitsConverted' => $visitsConverted, - )); - } - - private function createSite() - { - Fixture::createWebsite('2013-01-23 01:23:45'); - GoalsApi::getInstance()->addGoal(1, 'MyName', 'manually', '', 'contains'); - GoalsApi::getInstance()->addGoal(1, 'MyGoal', 'manually', '', 'contains'); - } - - private function setSuperUser() - { - $pseudoMockAccess = new FakeAccess(); - FakeAccess::$superUser = true; - Access::setSingletonInstance($pseudoMockAccess); - } - - private function setAnonymous() - { - $pseudoMockAccess = new FakeAccess(); - FakeAccess::$superUser = false; - Access::setSingletonInstance($pseudoMockAccess); - } - -} diff --git a/plugins/Live/tests/System/APITest.php b/plugins/Live/tests/System/APITest.php new file mode 100644 index 0000000000..2c85088a19 --- /dev/null +++ b/plugins/Live/tests/System/APITest.php @@ -0,0 +1,152 @@ +api = API::getInstance(); + $this->setSuperUser(); + $this->createSite(); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage checkUserHasViewAccess Fake exception + */ + public function test_GetCounters_ShouldFail_IfUserHasNoPermission() + { + $this->setAnonymous(); + $this->api->getCounters($this->idSite, 5); + } + + public function test_GetCounters_ShouldReturnZeroForAllCounters_IfThereAreNoVisitsEtc() + { + $counters = $this->api->getCounters($this->idSite, 5); + + $this->assertEquals($this->buildCounter(0, 0, 0, 0), $counters); + } + + public function test_GetCounters_ShouldOnlyReturnResultsOfLastMinutes() + { + $this->trackSomeVisits(); + + $counters = $this->api->getCounters($this->idSite, 5); + $this->assertEquals($this->buildCounter(19, 32, 16, 16), $counters); + + $counters = $this->api->getCounters($this->idSite, 20); + $this->assertEquals($this->buildCounter(24, 60, 20, 40), $counters); + + $counters = $this->api->getCounters($this->idSite, 0); + $this->assertEquals($this->buildCounter(0, 0, 0, 0), $counters); + } + + private function trackSomeVisits() + { + $nowTimestamp = time(); + + // use local tracker so mock location provider can be used + $t = Fixture::getTracker($this->idSite, $nowTimestamp, $defaultInit = true, $useLocal = false); + $t->enableBulkTracking(); + + for ($i = 0; $i != 20; ++$i) { + $t->setForceNewVisit(); + $t->setVisitorId( substr(md5($i * 1000), 0, $t::LENGTH_VISITOR_ID)); + + $factor = 10; + if ($i > 15) { + $factor = 30; // make sure first 15 visits are always within 5 minutes to prevent any random fails + } + $time = $nowTimestamp - ($i * $factor); + + // first visit -> this one is > 5 minutes and should be ignored in one test + $date = Date::factory($time - 600); + $t->setForceVisitDateTime($date->getDatetime()); + $t->setUrl("http://piwik.net/space/quest/iv"); + $t->doTrackPageView("Space Quest XV"); + + $t->doTrackGoal(1); // this one is > 5 minutes and should be ignored in one test + + // second visit + $date = Date::factory($time - 1); + $t->setForceVisitDateTime($date->getDatetime()); + $t->setUrl("http://piwik.net/space/quest/iv"); + $t->doTrackPageView("Space Quest XII"); + + if ($i % 6 == 0) { + $t->setForceNewVisit(); // to test visitors vs visits + } + + // third visit + $date = Date::factory($time); + $t->setForceVisitDateTime($date->getDatetime()); + $t->setUrl("http://piwik.net/grue/$i"); + $t->doTrackPageView('It is pitch black...'); + + $t->doTrackGoal(2); + } + + $t->doBulkTrack(); + } + + private function buildCounter($visits, $actions, $visitors, $visitsConverted) + { + return array(array( + 'visits' => $visits, + 'actions' => $actions, + 'visitors' => $visitors, + 'visitsConverted' => $visitsConverted, + )); + } + + private function createSite() + { + Fixture::createWebsite('2013-01-23 01:23:45'); + GoalsApi::getInstance()->addGoal(1, 'MyName', 'manually', '', 'contains'); + GoalsApi::getInstance()->addGoal(1, 'MyGoal', 'manually', '', 'contains'); + } + + private function setSuperUser() + { + $pseudoMockAccess = new FakeAccess(); + FakeAccess::$superUser = true; + Access::setSingletonInstance($pseudoMockAccess); + } + + private function setAnonymous() + { + $pseudoMockAccess = new FakeAccess(); + FakeAccess::$superUser = false; + Access::setSingletonInstance($pseudoMockAccess); + } + +} diff --git a/plugins/Live/tests/System/ModelTest.php b/plugins/Live/tests/System/ModelTest.php new file mode 100644 index 0000000000..d5dea34767 --- /dev/null +++ b/plugins/Live/tests/System/ModelTest.php @@ -0,0 +1,123 @@ +setSuperUser(); + Fixture::createWebsite('2010-01-01'); + } + + public function test_makeLogVisitsQueryString() + { + $model = new Model(); + list($sql, $bind) = $model->makeLogVisitsQueryString( + $idSite = 1, + $period = 'month', + $date = '2010-01-01', + $segment = false, + $countVisitorsToFetch = 100, + $visitorId = false, + $minTimestamp = false, + $filterSortOrder = false + ); + $expectedSql = ' SELECT sub.* FROM + ( + SELECT log_visit.* + FROM piwiktests_log_visit AS log_visit + WHERE log_visit.idsite in (?) + AND log_visit.visit_last_action_time >= ? + AND log_visit.visit_last_action_time <= ? + ORDER BY idsite, visit_last_action_time DESC + LIMIT 100 + ) AS sub + GROUP BY sub.idvisit + ORDER BY sub.visit_last_action_time DESC + '; + $expectedBind = array( + '1', + '2010-01-01 00:00:00', + '2010-02-01 00:00:00', + ); + $this->assertEquals(SegmentTest::removeExtraWhiteSpaces($expectedSql), SegmentTest::removeExtraWhiteSpaces($sql)); + $this->assertEquals(SegmentTest::removeExtraWhiteSpaces($expectedBind), SegmentTest::removeExtraWhiteSpaces($bind)); + } + + + public function test_makeLogVisitsQueryString_whenSegment() + { + $model = new Model(); + list($sql, $bind) = $model->makeLogVisitsQueryString( + $idSite = 1, + $period = 'month', + $date = '2010-01-01', + $segment = 'customVariablePageName1==Test', + $countVisitorsToFetch = 100, + $visitorId = 'abc', + $minTimestamp = false, + $filterSortOrder = false + ); + $expectedSql = ' SELECT sub.* FROM + ( + + SELECT log_inner.* + FROM ( + SELECT log_visit.* + FROM piwiktests_log_visit AS log_visit + LEFT JOIN piwiktests_log_link_visit_action AS log_link_visit_action + ON log_link_visit_action.idvisit = log_visit.idvisit + WHERE ( log_visit.idsite in (?) + AND log_visit.idvisitor = ? + AND log_visit.visit_last_action_time >= ? + AND log_visit.visit_last_action_time <= ? ) + AND ( log_link_visit_action.custom_var_k1 = ? ) + GROUP BY log_visit.idvisit + ORDER BY NULL + LIMIT 100 + ) AS log_inner + ORDER BY idsite, visit_last_action_time DESC + LIMIT 100 + ) AS sub + GROUP BY sub.idvisit + ORDER BY sub.visit_last_action_time DESC + '; + $expectedBind = array( + '1', + Common::hex2bin('abc'), + '2010-01-01 00:00:00', + '2010-02-01 00:00:00', + 'Test', + ); + $this->assertEquals(SegmentTest::removeExtraWhiteSpaces($expectedSql), SegmentTest::removeExtraWhiteSpaces($sql)); + $this->assertEquals(SegmentTest::removeExtraWhiteSpaces($expectedBind), SegmentTest::removeExtraWhiteSpaces($bind)); + } + + + protected function setSuperUser() + { + $pseudoMockAccess = new FakeAccess(); + FakeAccess::$superUser = true; + Access::setSingletonInstance($pseudoMockAccess); + } + +} \ No newline at end of file diff --git a/tests/PHPUnit/Integration/SegmentTest.php b/tests/PHPUnit/Integration/SegmentTest.php index e0fd9cd3c4..864420a161 100644 --- a/tests/PHPUnit/Integration/SegmentTest.php +++ b/tests/PHPUnit/Integration/SegmentTest.php @@ -36,11 +36,11 @@ class SegmentTest extends IntegrationTestCase parent::tearDown(); } - protected function _filterWhiteSpaces($valueToFilter) + static public function removeExtraWhiteSpaces($valueToFilter) { if (is_array($valueToFilter)) { foreach ($valueToFilter as $key => $value) { - $valueToFilter[$key] = $this->_filterWhiteSpaces($value); + $valueToFilter[$key] = self::removeExtraWhiteSpaces($value); } return $valueToFilter; } else { @@ -117,11 +117,11 @@ class SegmentTest extends IntegrationTestCase $segment = new Segment($segment, $idSites = array()); $sql = $segment->getSelectQuery($select, $from, false); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($sql)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($sql)); // calling twice should give same results $sql = $segment->getSelectQuery($select, array($from)); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($sql)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($sql)); $this->assertEquals(32, strlen($segment->getHash())); } @@ -150,7 +150,7 @@ class SegmentTest extends IntegrationTestCase ( log_visit.custom_var_k1 = ? AND log_visit.visitor_returning = ? )", "bind" => array(1, 'Test', 0)); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinVisitOnAction() @@ -178,7 +178,7 @@ class SegmentTest extends IntegrationTestCase ( log_link_visit_action.custom_var_k1 = ? AND log_visit.visitor_returning = ? )", "bind" => array(1, 'Test', 0)); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinActionOnVisit() @@ -214,7 +214,7 @@ class SegmentTest extends IntegrationTestCase ) AS log_inner", "bind" => array(1, 'Test', 0)); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinConversionOnAction() @@ -242,7 +242,7 @@ class SegmentTest extends IntegrationTestCase ( log_link_visit_action.custom_var_k1 = ? AND log_conversion.idgoal = ? AND log_link_visit_action.custom_var_k2 = ? )", "bind" => array(1, 'Test', 1, 'Test2')); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinActionOnConversion() @@ -270,7 +270,7 @@ class SegmentTest extends IntegrationTestCase ( ( log_conversion.idgoal IS NULL OR log_conversion.idgoal <> ? ) AND log_link_visit_action.custom_var_k1 = ? AND log_conversion.idgoal = ? )", "bind" => array(1, 2, 'Test', 1)); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinConversionOnVisit() @@ -305,7 +305,7 @@ class SegmentTest extends IntegrationTestCase ) AS log_inner", "bind" => array(1, 1)); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinConversionOnly() @@ -332,7 +332,7 @@ class SegmentTest extends IntegrationTestCase ( log_conversion.idgoal = ? )", "bind" => array(1, 1)); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } public function test_getSelectQuery_whenJoinVisitOnConversion() @@ -360,7 +360,7 @@ class SegmentTest extends IntegrationTestCase ( (log_conversion.idgoal = ? OR HOUR(log_visit.visit_last_action_time) = ? ))", "bind" => array(1, 1, 12)); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } /** @@ -391,7 +391,7 @@ class SegmentTest extends IntegrationTestCase HOUR(log_visit.visit_last_action_time) = ? AND log_conversion.idgoal = ? ", "bind" => array(12, 1)); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } /** @@ -429,7 +429,7 @@ class SegmentTest extends IntegrationTestCase ) AS log_inner", "bind" => array(1, 12, 'Test')); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } /** @@ -496,6 +496,6 @@ class SegmentTest extends IntegrationTestCase LIMIT 33", "bind" => array(1, 'Test')); - $this->assertEquals($this->_filterWhiteSpaces($expected), $this->_filterWhiteSpaces($query)); + $this->assertEquals($this->removeExtraWhiteSpaces($expected), $this->removeExtraWhiteSpaces($query)); } } -- cgit v1.2.3 From 269ffeb4b71926fa4973aa0075943dcc2426bd08 Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 01:12:57 +1300 Subject: ORDER BY the inner query so the order is deterministic + It uses the INDEX --- core/DataAccess/LogQueryBuilder.php | 7 ++++++- plugins/Live/tests/System/ModelTest.php | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php index 132480514b..b062b91b3f 100644 --- a/core/DataAccess/LogQueryBuilder.php +++ b/core/DataAccess/LogQueryBuilder.php @@ -185,9 +185,14 @@ class LogQueryBuilder $innerFrom = $from; $innerWhere = $where; $innerGroupBy = "log_visit.idvisit"; - $innerOrderBy = "NULL"; $innerLimit = $limit; + $innerOrderBy = "NULL"; + // Only when there is a LIMIT then we can apply to the inner query the same ORDER BY as the parent query + if($innerLimit) { + $innerOrderBy = $orderBy; + } + $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerGroupBy, $innerOrderBy, $innerLimit); $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); diff --git a/plugins/Live/tests/System/ModelTest.php b/plugins/Live/tests/System/ModelTest.php index d5dea34767..90196e7309 100644 --- a/plugins/Live/tests/System/ModelTest.php +++ b/plugins/Live/tests/System/ModelTest.php @@ -92,7 +92,7 @@ class ModelTest extends SystemTestCase AND log_visit.visit_last_action_time <= ? ) AND ( log_link_visit_action.custom_var_k1 = ? ) GROUP BY log_visit.idvisit - ORDER BY NULL + ORDER BY idsite, visit_last_action_time DESC LIMIT 100 ) AS log_inner ORDER BY idsite, visit_last_action_time DESC -- cgit v1.2.3 From dac71ca1347dbde11618e99b2dfe589d3d1812b8 Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 01:19:33 +1300 Subject: Only set order by if it's set, otherwise we ORDER BY NULL --- core/DataAccess/LogQueryBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php index b062b91b3f..ac55ccf454 100644 --- a/core/DataAccess/LogQueryBuilder.php +++ b/core/DataAccess/LogQueryBuilder.php @@ -189,7 +189,7 @@ class LogQueryBuilder $innerOrderBy = "NULL"; // Only when there is a LIMIT then we can apply to the inner query the same ORDER BY as the parent query - if($innerLimit) { + if($innerLimit && $orderBy) { $innerOrderBy = $orderBy; } -- cgit v1.2.3 From a37254a976502a342a863ece8b285b96faff9494 Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 01:53:27 +1300 Subject: The ultimate prize and final step to close the issue: do not group by the whole set of matched rows, way too slow on huge datasets! --- core/DataAccess/LogQueryBuilder.php | 10 +++++++--- plugins/Live/tests/System/ModelTest.php | 1 - tests/PHPUnit/Integration/SegmentTest.php | 3 +-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php index ac55ccf454..a5b2fcf986 100644 --- a/core/DataAccess/LogQueryBuilder.php +++ b/core/DataAccess/LogQueryBuilder.php @@ -184,14 +184,18 @@ class LogQueryBuilder $innerSelect = implode(", \n", $neededFields); $innerFrom = $from; $innerWhere = $where; - $innerGroupBy = "log_visit.idvisit"; - $innerLimit = $limit; + $innerLimit = $limit; + $innerGroupBy = "log_visit.idvisit"; $innerOrderBy = "NULL"; - // Only when there is a LIMIT then we can apply to the inner query the same ORDER BY as the parent query if($innerLimit && $orderBy) { + // only When LIMITing we can apply to the inner query the same ORDER BY as the parent query $innerOrderBy = $orderBy; } + if($innerLimit) { + // When LIMITing, no need to GROUP BY (GROUPing by is done before the LIMIT which is super slow when large amount of rows is matched) + $innerGroupBy = false; + } $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerGroupBy, $innerOrderBy, $innerLimit); diff --git a/plugins/Live/tests/System/ModelTest.php b/plugins/Live/tests/System/ModelTest.php index 90196e7309..268a7861d4 100644 --- a/plugins/Live/tests/System/ModelTest.php +++ b/plugins/Live/tests/System/ModelTest.php @@ -91,7 +91,6 @@ class ModelTest extends SystemTestCase AND log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? ) AND ( log_link_visit_action.custom_var_k1 = ? ) - GROUP BY log_visit.idvisit ORDER BY idsite, visit_last_action_time DESC LIMIT 100 ) AS log_inner diff --git a/tests/PHPUnit/Integration/SegmentTest.php b/tests/PHPUnit/Integration/SegmentTest.php index 864420a161..19108b1312 100644 --- a/tests/PHPUnit/Integration/SegmentTest.php +++ b/tests/PHPUnit/Integration/SegmentTest.php @@ -458,7 +458,7 @@ class SegmentTest extends IntegrationTestCase } - public function test_getSelectQuery_whenLimit_innerQueryShouldHaveLimit() + public function test_getSelectQuery_whenLimit_innerQueryShouldHaveLimitAndNoGroupBy() { $select = 'sum(log_visit.visit_total_time) as sum_visit_length'; $from = 'log_visit'; @@ -489,7 +489,6 @@ class SegmentTest extends IntegrationTestCase ( log_visit.idvisit = ? ) AND ( log_link_visit_action.custom_var_k1 = ? ) - GROUP BY log_visit.idvisit ORDER BY NULL LIMIT 33 ) AS log_inner -- cgit v1.2.3 From 020dd94590ca601dd5db7de2e8785284b27546fc Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 02:08:26 +1300 Subject: Adding little test case for the culprit pageUrl!= --- tests/PHPUnit/Integration/SegmentTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/PHPUnit/Integration/SegmentTest.php b/tests/PHPUnit/Integration/SegmentTest.php index 19108b1312..91be56af12 100644 --- a/tests/PHPUnit/Integration/SegmentTest.php +++ b/tests/PHPUnit/Integration/SegmentTest.php @@ -398,14 +398,14 @@ class SegmentTest extends IntegrationTestCase * join conversion on visit, then actions * make sure actions are joined before conversions */ - public function test_getSelectQuery_whenJoinConversionAndActionOnVisit() + public function test_getSelectQuery_whenJoinConversionAndActionOnVisit_andPageUrlSet() { $select = 'log_visit.*'; $from = 'log_visit'; $where = false; $bind = array(); - $segment = 'visitConvertedGoalId==1;visitServerHour==12;customVariablePageName1==Test'; + $segment = 'visitConvertedGoalId==1;visitServerHour==12;customVariablePageName1==Test;pageUrl!='; $segment = new Segment($segment, $idSites = array()); $query = $segment->getSelectQuery($select, $from, $where, $bind); @@ -424,6 +424,11 @@ class SegmentTest extends IntegrationTestCase LEFT JOIN " . Common::prefixTable('log_conversion') . " AS log_conversion ON log_conversion.idlink_va = log_link_visit_action.idlink_va AND log_conversion.idsite = log_link_visit_action.idsite WHERE log_conversion.idgoal = ? AND HOUR(log_visit.visit_last_action_time) = ? AND log_link_visit_action.custom_var_k1 = ? + AND ( + log_link_visit_action.idaction_url IS NOT NULL + AND (log_link_visit_action.idaction_url <> '' + OR log_link_visit_action.idaction_url = 0) + ) GROUP BY log_visit.idvisit ORDER BY NULL ) AS log_inner", -- cgit v1.2.3 From 814f3790741fce0843d3074e77bf785aaccb88a6 Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 13:04:52 +1300 Subject: Refactoring SQL into the Model --- plugins/Live/Model.php | 256 ++++++++++++++++++++++++++++++++++++++--------- plugins/Live/Visitor.php | 107 ++------------------ 2 files changed, 216 insertions(+), 147 deletions(-) diff --git a/plugins/Live/Model.php b/plugins/Live/Model.php index 2d438bff9b..81215f864c 100644 --- a/plugins/Live/Model.php +++ b/plugins/Live/Model.php @@ -11,17 +11,163 @@ namespace Piwik\Plugins\Live; use Exception; use Piwik\Common; +use Piwik\DataAccess\LogAggregator; use Piwik\Date; use Piwik\Db; use Piwik\Period; use Piwik\Period\Range; use Piwik\Piwik; +use Piwik\Plugins\CustomVariables\CustomVariables; use Piwik\Segment; use Piwik\Site; +use Piwik\Tracker\GoalManager; class Model { + /** + * @param $idVisit + * @param $actionsLimit + * @return array + * @throws \Exception + */ + public function queryActionsForVisit($idVisit, $actionsLimit) + { + $maxCustomVariables = CustomVariables::getMaxCustomVariables(); + + $sqlCustomVariables = ''; + for ($i = 1; $i <= $maxCustomVariables; $i++) { + $sqlCustomVariables .= ', custom_var_k' . $i . ', custom_var_v' . $i; + } + // The second join is a LEFT join to allow returning records that don't have a matching page title + // eg. Downloads, Outlinks. For these, idaction_name is set to 0 + $sql = " + SELECT + COALESCE(log_action_event_category.type, log_action.type, log_action_title.type) AS type, + log_action.name AS url, + log_action.url_prefix, + log_action_title.name AS pageTitle, + log_action.idaction AS pageIdAction, + log_link_visit_action.server_time as serverTimePretty, + log_link_visit_action.time_spent_ref_action as timeSpentRef, + log_link_visit_action.idlink_va AS pageId, + log_link_visit_action.custom_float + " . $sqlCustomVariables . ", + log_action_event_category.name AS eventCategory, + log_action_event_action.name as eventAction + FROM " . Common::prefixTable('log_link_visit_action') . " AS log_link_visit_action + LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action + ON log_link_visit_action.idaction_url = log_action.idaction + LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_title + ON log_link_visit_action.idaction_name = log_action_title.idaction + LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_event_category + ON log_link_visit_action.idaction_event_category = log_action_event_category.idaction + LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_event_action + ON log_link_visit_action.idaction_event_action = log_action_event_action.idaction + WHERE log_link_visit_action.idvisit = ? + ORDER BY server_time ASC + LIMIT 0, $actionsLimit + "; + $actionDetails = Db::fetchAll($sql, array($idVisit)); + return $actionDetails; + } + + /** + * @param $idVisit + * @param $limit + * @return array + * @throws \Exception + */ + public function queryGoalConversionsForVisit($idVisit, $limit) + { + $sql = " + SELECT + 'goal' as type, + goal.name as goalName, + goal.idgoal as goalId, + goal.revenue as revenue, + log_conversion.idlink_va as goalPageId, + log_conversion.server_time as serverTimePretty, + log_conversion.url as url + FROM " . Common::prefixTable('log_conversion') . " AS log_conversion + LEFT JOIN " . Common::prefixTable('goal') . " AS goal + ON (goal.idsite = log_conversion.idsite + AND + goal.idgoal = log_conversion.idgoal) + AND goal.deleted = 0 + WHERE log_conversion.idvisit = ? + AND log_conversion.idgoal > 0 + ORDER BY server_time ASC + LIMIT 0, $limit + "; + $goalDetails = Db::fetchAll($sql, array($idVisit)); + return $goalDetails; + } + + /** + * @param $idVisit + * @param $limit + * @return array + * @throws \Exception + */ + public function queryEcommerceConversionsForVisit($idVisit, $limit) + { + $sql = "SELECT + case idgoal when " . GoalManager::IDGOAL_CART + . " then '" . Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART + . "' else '" . Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER . "' end as type, + idorder as orderId, + " . LogAggregator::getSqlRevenue('revenue') . " as revenue, + " . LogAggregator::getSqlRevenue('revenue_subtotal') . " as revenueSubTotal, + " . LogAggregator::getSqlRevenue('revenue_tax') . " as revenueTax, + " . LogAggregator::getSqlRevenue('revenue_shipping') . " as revenueShipping, + " . LogAggregator::getSqlRevenue('revenue_discount') . " as revenueDiscount, + items as items, + log_conversion.server_time as serverTimePretty + FROM " . Common::prefixTable('log_conversion') . " AS log_conversion + WHERE idvisit = ? + AND idgoal <= " . GoalManager::IDGOAL_ORDER . " + ORDER BY server_time ASC + LIMIT 0, $limit"; + $ecommerceDetails = Db::fetchAll($sql, array($idVisit)); + return $ecommerceDetails; + } + + + /** + * @param $idVisit + * @param $idOrder + * @param $actionsLimit + * @return array + * @throws \Exception + */ + public function queryEcommerceItemsForOrder($idVisit, $idOrder, $actionsLimit) + { + $sql = "SELECT + log_action_sku.name as itemSKU, + log_action_name.name as itemName, + log_action_category.name as itemCategory, + " . LogAggregator::getSqlRevenue('price') . " as price, + quantity as quantity + FROM " . Common::prefixTable('log_conversion_item') . " + INNER JOIN " . Common::prefixTable('log_action') . " AS log_action_sku + ON idaction_sku = log_action_sku.idaction + LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_name + ON idaction_name = log_action_name.idaction + LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_category + ON idaction_category = log_action_category.idaction + WHERE idvisit = ? + AND idorder = ? + AND deleted = 0 + LIMIT 0, $actionsLimit + "; + + $bind = array($idVisit, $idOrder); + + $itemsDetails = Db::fetchAll($sql, $bind); + return $itemsDetails; + } + /** * @param $idSite * @param $period @@ -185,12 +331,70 @@ class Model */ public function makeLogVisitsQueryString($idSite, $period, $date, $segment, $countVisitorsToFetch, $visitorId, $minTimestamp, $filterSortOrder) { - $where = $whereBind = array(); + // If no other filter, only look at the last 24 hours of stats + if (empty($visitorId) + && empty($countVisitorsToFetch) + && empty($period) + && empty($date) + ) { + $period = 'day'; + $date = 'yesterdaySameTime'; + } + + list($whereBind, $where) = $this->getWhereClauseAndBind($idSite, $period, $date, $visitorId, $minTimestamp); + + if (strtolower($filterSortOrder) !== 'asc') { + $filterSortOrder = 'DESC'; + } + $segment = new Segment($segment, $idSite); + + // Subquery to use the indexes for ORDER BY + $select = "log_visit.*"; + $from = "log_visit"; + $groupBy = false; + $limit = $countVisitorsToFetch >= 1 ? (int)$countVisitorsToFetch : 0; + $orderBy = "idsite, visit_last_action_time " . $filterSortOrder; + $orderByParent = "sub.visit_last_action_time " . $filterSortOrder; + + $subQuery = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy, $groupBy, $limit); + + $bind = $subQuery['bind']; + // Group by idvisit so that a visitor converting 2 goals only appears once + $sql = " + SELECT sub.* FROM ( + " . $subQuery['sql'] . " + ) AS sub + GROUP BY sub.idvisit + ORDER BY $orderByParent + "; + return array($sql, $bind); + } + + /** + * @param $idSite + * @return Site + */ + protected function makeSite($idSite) + { + return new Site($idSite); + } - list($whereClause, $idSites) = $this->getIdSitesWhereClause($idSite); + /** + * @param $idSite + * @param $period + * @param $date + * @param $visitorId + * @param $minTimestamp + * @return array + * @throws Exception + */ + private function getWhereClauseAndBind($idSite, $period, $date, $visitorId, $minTimestamp) + { + list($whereClause, $bindIdSites) = $this->getIdSitesWhereClause($idSite); + $where = array(); $where[] = $whereClause; - $whereBind = $idSites; + $whereBind = $bindIdSites; if (!empty($visitorId)) { $where[] = "log_visit.idvisitor = ? "; @@ -202,16 +406,6 @@ class Model $whereBind[] = date("Y-m-d H:i:s", $minTimestamp); } - // If no other filter, only look at the last 24 hours of stats - if (empty($visitorId) - && empty($countVisitorsToFetch) - && empty($period) - && empty($date) - ) { - $period = 'day'; - $date = 'yesterdaySameTime'; - } - // SQL Filter with provided period if (!empty($period) && !empty($date)) { $currentSite = $this->makeSite($idSite); @@ -255,40 +449,6 @@ class Model } else { $where = false; } - - if (strtolower($filterSortOrder) !== 'asc') { - $filterSortOrder = 'DESC'; - } - $segment = new Segment($segment, $idSite); - - // Subquery to use the indexes for ORDER BY - $select = "log_visit.*"; - $from = "log_visit"; - $groupBy = false; - $limit = $countVisitorsToFetch >= 1 ? (int)$countVisitorsToFetch : 0; - $orderBy = "idsite, visit_last_action_time " . $filterSortOrder; - $orderByParent = "sub.visit_last_action_time " . $filterSortOrder; - - $subQuery = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy, $groupBy, $limit); - - $bind = $subQuery['bind']; - // Group by idvisit so that a visitor converting 2 goals only appears once - $sql = " - SELECT sub.* FROM ( - " . $subQuery['sql'] . " - ) AS sub - GROUP BY sub.idvisit - ORDER BY $orderByParent - "; - return array($sql, $bind); - } - - /** - * @param $idSite - * @return Site - */ - protected function makeSite($idSite) - { - return new Site($idSite); + return array($whereBind, $where); } } \ No newline at end of file diff --git a/plugins/Live/Visitor.php b/plugins/Live/Visitor.php index 5cbe823cd9..eec1ecb6ce 100644 --- a/plugins/Live/Visitor.php +++ b/plugins/Live/Visitor.php @@ -246,44 +246,11 @@ class Visitor implements VisitorInterface { $idVisit = $visitorDetailsArray['idVisit']; - $maxCustomVariables = CustomVariables::getMaxCustomVariables(); - - $sqlCustomVariables = ''; - for ($i = 1; $i <= $maxCustomVariables; $i++) { - $sqlCustomVariables .= ', custom_var_k' . $i . ', custom_var_v' . $i; - } - // The second join is a LEFT join to allow returning records that don't have a matching page title - // eg. Downloads, Outlinks. For these, idaction_name is set to 0 - $sql = " - SELECT - COALESCE(log_action_event_category.type, log_action.type, log_action_title.type) AS type, - log_action.name AS url, - log_action.url_prefix, - log_action_title.name AS pageTitle, - log_action.idaction AS pageIdAction, - log_link_visit_action.server_time as serverTimePretty, - log_link_visit_action.time_spent_ref_action as timeSpentRef, - log_link_visit_action.idlink_va AS pageId, - log_link_visit_action.custom_float - ". $sqlCustomVariables . ", - log_action_event_category.name AS eventCategory, - log_action_event_action.name as eventAction - FROM " . Common::prefixTable('log_link_visit_action') . " AS log_link_visit_action - LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action - ON log_link_visit_action.idaction_url = log_action.idaction - LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_title - ON log_link_visit_action.idaction_name = log_action_title.idaction - LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_event_category - ON log_link_visit_action.idaction_event_category = log_action_event_category.idaction - LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_event_action - ON log_link_visit_action.idaction_event_action = log_action_event_action.idaction - WHERE log_link_visit_action.idvisit = ? - ORDER BY server_time ASC - LIMIT 0, $actionsLimit - "; - $actionDetails = Db::fetchAll($sql, array($idVisit)); + $model = new Model(); + $actionDetails = $model->queryActionsForVisit($idVisit, $actionsLimit); $formatter = new Formatter(); + $maxCustomVariables = CustomVariables::getMaxCustomVariables(); foreach ($actionDetails as $actionIdx => &$actionDetail) { $actionDetail =& $actionDetails[$actionIdx]; @@ -353,46 +320,9 @@ class Visitor implements VisitorInterface } // If the visitor converted a goal, we shall select all Goals - $sql = " - SELECT - 'goal' as type, - goal.name as goalName, - goal.idgoal as goalId, - goal.revenue as revenue, - log_conversion.idlink_va as goalPageId, - log_conversion.server_time as serverTimePretty, - log_conversion.url as url - FROM " . Common::prefixTable('log_conversion') . " AS log_conversion - LEFT JOIN " . Common::prefixTable('goal') . " AS goal - ON (goal.idsite = log_conversion.idsite - AND - goal.idgoal = log_conversion.idgoal) - AND goal.deleted = 0 - WHERE log_conversion.idvisit = ? - AND log_conversion.idgoal > 0 - ORDER BY server_time ASC - LIMIT 0, $actionsLimit - "; - $goalDetails = Db::fetchAll($sql, array($idVisit)); - - $sql = "SELECT - case idgoal when " . GoalManager::IDGOAL_CART . " then '" . Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART . "' else '" . Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER . "' end as type, - idorder as orderId, - " . LogAggregator::getSqlRevenue('revenue') . " as revenue, - " . LogAggregator::getSqlRevenue('revenue_subtotal') . " as revenueSubTotal, - " . LogAggregator::getSqlRevenue('revenue_tax') . " as revenueTax, - " . LogAggregator::getSqlRevenue('revenue_shipping') . " as revenueShipping, - " . LogAggregator::getSqlRevenue('revenue_discount') . " as revenueDiscount, - items as items, - - log_conversion.server_time as serverTimePretty - FROM " . Common::prefixTable('log_conversion') . " AS log_conversion - WHERE idvisit = ? - AND idgoal <= " . GoalManager::IDGOAL_ORDER . " - ORDER BY server_time ASC - LIMIT 0, $actionsLimit"; - $ecommerceDetails = Db::fetchAll($sql, array($idVisit)); + $goalDetails = $model->queryGoalConversionsForVisit($idVisit, $actionsLimit); + $ecommerceDetails = $model->queryEcommerceConversionsForVisit($idVisit, $actionsLimit); foreach ($ecommerceDetails as &$ecommerceDetail) { if ($ecommerceDetail['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) { unset($ecommerceDetail['orderId']); @@ -415,30 +345,9 @@ class Visitor implements VisitorInterface // Enrich ecommerce carts/orders with the list of products usort($ecommerceDetails, array('static', 'sortByServerTime')); foreach ($ecommerceDetails as &$ecommerceConversion) { - $sql = "SELECT - log_action_sku.name as itemSKU, - log_action_name.name as itemName, - log_action_category.name as itemCategory, - " . LogAggregator::getSqlRevenue('price') . " as price, - quantity as quantity - FROM " . Common::prefixTable('log_conversion_item') . " - INNER JOIN " . Common::prefixTable('log_action') . " AS log_action_sku - ON idaction_sku = log_action_sku.idaction - LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_name - ON idaction_name = log_action_name.idaction - LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_category - ON idaction_category = log_action_category.idaction - WHERE idvisit = ? - AND idorder = ? - AND deleted = 0 - LIMIT 0, $actionsLimit - "; - $bind = array($idVisit, isset($ecommerceConversion['orderId']) - ? $ecommerceConversion['orderId'] - : GoalManager::ITEM_IDORDER_ABANDONED_CART - ); - - $itemsDetails = Db::fetchAll($sql, $bind); + $idOrder = isset($ecommerceConversion['orderId']) ? $ecommerceConversion['orderId'] : GoalManager::ITEM_IDORDER_ABANDONED_CART; + + $itemsDetails = $model->queryEcommerceItemsForOrder($idVisit, $idOrder, $actionsLimit); foreach ($itemsDetails as &$detail) { if ($detail['price'] == round($detail['price'])) { $detail['price'] = round($detail['price']); -- cgit v1.2.3 From 8f079b21b1f635a6bb9bbbdfd8396001fc2bdfa3 Mon Sep 17 00:00:00 2001 From: mattab Date: Sat, 6 Dec 2014 13:23:28 +1300 Subject: Prefix tables in expected strings (travis runs without prefix) --- plugins/Live/tests/System/ModelTest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/Live/tests/System/ModelTest.php b/plugins/Live/tests/System/ModelTest.php index 268a7861d4..80f1c8d86f 100644 --- a/plugins/Live/tests/System/ModelTest.php +++ b/plugins/Live/tests/System/ModelTest.php @@ -17,7 +17,8 @@ use Piwik\Tests\Framework\TestCase\SystemTestCase; use Piwik\Tests\Integration\SegmentTest; /** - * @group LiveModelTest + * @group Live + * @group ModelTest * @group Plugins */ class ModelTest extends SystemTestCase @@ -44,7 +45,7 @@ class ModelTest extends SystemTestCase $expectedSql = ' SELECT sub.* FROM ( SELECT log_visit.* - FROM piwiktests_log_visit AS log_visit + FROM ' . Common::prefixTable('log_visit') . ' AS log_visit WHERE log_visit.idsite in (?) AND log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? @@ -83,8 +84,8 @@ class ModelTest extends SystemTestCase SELECT log_inner.* FROM ( SELECT log_visit.* - FROM piwiktests_log_visit AS log_visit - LEFT JOIN piwiktests_log_link_visit_action AS log_link_visit_action + FROM ' . Common::prefixTable('log_visit') . ' AS log_visit + LEFT JOIN ' . Common::prefixTable('log_link_visit_action') . ' AS log_link_visit_action ON log_link_visit_action.idvisit = log_visit.idvisit WHERE ( log_visit.idsite in (?) AND log_visit.idvisitor = ? -- cgit v1.2.3