| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\DataAccess\LogQueryBuilder; |
|
|
| use Exception; |
| use Piwik\Common; |
| use Piwik\DataAccess\LogAggregator; |
| use Piwik\Tracker\LogTable; |
|
|
| class JoinGenerator |
| { |
| |
| |
| |
| protected $tables; |
|
|
| |
| |
| |
| private $joinWithSubSelect = false; |
|
|
| |
| |
| |
| private $joinString = ''; |
|
|
| |
| |
| |
| private $nonVisitJoins = array(); |
|
|
| public function __construct(JoinTables $tables) |
| { |
| $this->tables = $tables; |
| $this->addMissingTablesNeededForJoins(); |
| } |
|
|
| private function addMissingTablesNeededForJoins() |
| { |
| foreach ($this->tables as $index => $table) { |
| if (is_array($table)) { |
| continue; |
| } |
|
|
| $logTable = $this->tables->getLogTable($table); |
|
|
| if (!$logTable->getColumnToJoinOnIdVisit()) { |
| $tableNameToJoin = $logTable->getLinkTableToBeAbleToJoinOnVisit(); |
|
|
| if (empty($tableNameToJoin) && $logTable->getWaysToJoinToOtherLogTables()) { |
| foreach ($logTable->getWaysToJoinToOtherLogTables() as $otherLogTable => $column) { |
| if ($this->tables->hasJoinedTable($otherLogTable)) { |
| $this->tables->addTableDependency($table, $otherLogTable); |
| continue; |
| } |
| if ($this->tables->isTableJoinableOnVisit($otherLogTable) || $this->tables->isTableJoinableOnAction($otherLogTable)) { |
| $this->addMissingTablesForOtherTableJoin($otherLogTable, $table); |
| } |
| } |
| continue; |
| } |
|
|
| if ($index > 0 && !$this->tables->hasJoinedTable($tableNameToJoin)) { |
| $this->tables->addTableToJoin($tableNameToJoin); |
| } |
|
|
| if ($this->tables->hasJoinedTable($tableNameToJoin)) { |
| $this->generateNonVisitJoins($table, $tableNameToJoin, $index); |
| } |
| } |
| } |
|
|
| foreach ($this->tables as $index => $table) { |
| if (is_array($table)) { |
| if (!isset($table['tableAlias'])) { |
| $tableName = $table['table']; |
| $numTables = count($this->tables); |
| for ($j = $index + 1; $j < $numTables; $j++) { |
| if (!isset($this->tables[$j])) { |
| continue; |
| } |
|
|
| $tableOther = $this->tables[$j]; |
| if (is_string($tableOther) && $tableOther === $tableName) { |
| unset($this->tables[$j]); |
| } |
| } |
| } |
| } elseif (is_string($table)) { |
| $numTables = count($this->tables); |
|
|
| for ($j = $index + 1; $j < $numTables; $j++) { |
| if (isset($this->tables[$j]) && is_array($this->tables[$j]) && !isset($this->tables[$j]['tableAlias'])) { |
| $tableOther = $this->tables[$j]; |
| if ($table === $tableOther['table']) { |
| $message = sprintf('Please reorganize the joined tables as the table %s in %s cannot be joined correctly. We recommend to join tables with arrays first. %s', $table, json_encode($this->tables), json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10))); |
| throw new Exception($message); |
| } |
| } |
| } |
| } |
| } |
| } |
|
|
| private function addMissingTablesForOtherTableJoin($tableName, $dependentTable) |
| { |
| $this->tables->addTableDependency($dependentTable, $tableName); |
|
|
| if ($this->tables->hasJoinedTable($tableName)) { |
| return; |
| } |
|
|
| $table = $this->tables->getLogTable($tableName); |
|
|
| if ($table->getColumnToJoinOnIdAction() || $table->getColumnToJoinOnIdVisit() || $table->getLinkTableToBeAbleToJoinOnVisit()) { |
| $this->tables->addTableToJoin($tableName); |
| return; |
| } |
|
|
| $otherTableJoins = $table->getWaysToJoinToOtherLogTables(); |
|
|
| foreach ($otherTableJoins as $logTable => $column) { |
| $this->addMissingTablesForOtherTableJoin($logTable, $tableName); |
| } |
|
|
| $this->tables->addTableToJoin($tableName); |
| } |
|
|
| |
| |
| |
| |
| public function generate(): void |
| { |
| |
| $availableLogTables = array(); |
|
|
| $this->tables->sort(); |
|
|
| foreach ($this->tables as $i => $table) { |
| $useIndex = ''; |
| if ($i === 0 && is_array($table)) { |
| $useIndex = $table['useIndex'] ?? ''; |
| $table = $table['table']; |
| } |
|
|
| if (is_array($table)) { |
| |
| $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table']; |
|
|
| if (isset($table['join'])) { |
| $this->joinString .= ' ' . $table['join']; |
| } else { |
| $this->joinString .= ' LEFT JOIN'; |
| } |
|
|
| if (!isset($table['joinOn']) && $this->tables->getLogTable($table['table'])) { |
| $logTable = $this->tables->getLogTable($table['table']); |
| if (!empty($availableLogTables)) { |
| $table['joinOn'] = $this->findJoinCriteriasForTables($logTable, $availableLogTables); |
| } |
| if (!isset($table['tableAlias'])) { |
| |
| |
| $availableLogTables[$table['table']] = $logTable; |
| } |
| } |
|
|
| $this->joinString .= ' ' . Common::prefixTable($table['table']) . " AS " . $alias |
| . " ON " . $table['joinOn']; |
| continue; |
| } |
|
|
| $tableSql = Common::prefixTable($table) . " AS $table"; |
|
|
| $logTable = $this->tables->getLogTable($table); |
|
|
| if ($i == 0) { |
| |
| $this->joinString .= $tableSql; |
|
|
| |
| if (!empty($useIndex)) { |
| $this->joinString .= " USE INDEX ($useIndex)"; |
| } |
| } else { |
| $join = $this->findJoinCriteriasForTables($logTable, $availableLogTables); |
|
|
| if ($join === null) { |
| $availableLogTables[$table] = $logTable; |
| continue; |
| } |
|
|
| $joinName = 'LEFT JOIN'; |
| if ( |
| $i > 0 |
| && $this->tables[$i - 1] |
| && is_string($this->tables[$i - 1]) |
| && strpos($this->tables[$i - 1], LogAggregator::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX) === 0 |
| ) { |
| $joinName = 'INNER JOIN'; |
| |
| |
| |
| |
| |
| } |
| |
| $this->joinString .= " $joinName $tableSql ON " . $join; |
| } |
|
|
| $availableLogTables[$table] = $logTable; |
| } |
| } |
|
|
| public function getJoinString() |
| { |
| return $this->joinString; |
| } |
|
|
| public function shouldJoinWithSelect() |
| { |
| return $this->joinWithSubSelect; |
| } |
|
|
| |
| |
| |
| |
| |
| public function findJoinCriteriasForTables(LogTable $logTable, $availableLogTables) |
| { |
| $join = null; |
| $alternativeJoin = null; |
| $table = $logTable->getName(); |
|
|
| foreach ($availableLogTables as $availableLogTable) { |
| if ($logTable->getColumnToJoinOnIdVisit() && $availableLogTable->getColumnToJoinOnIdVisit()) { |
| $join = sprintf( |
| "%s.%s = %s.%s", |
| $table, |
| $logTable->getColumnToJoinOnIdVisit(), |
| $availableLogTable->getName(), |
| $availableLogTable->getColumnToJoinOnIdVisit() |
| ); |
| $alternativeJoin = sprintf( |
| "%s.%s = %s.%s", |
| $availableLogTable->getName(), |
| $availableLogTable->getColumnToJoinOnIdVisit(), |
| $table, |
| $logTable->getColumnToJoinOnIdVisit() |
| ); |
|
|
| if ($availableLogTable->shouldJoinWithSubSelect()) { |
| $this->joinWithSubSelect = true; |
| } |
|
|
| break; |
| } |
|
|
| if ($logTable->getColumnToJoinOnIdAction() && $availableLogTable->getColumnToJoinOnIdAction()) { |
| if (isset($this->nonVisitJoins[$logTable->getName()][$availableLogTable->getName()])) { |
| $join = $this->nonVisitJoins[$logTable->getName()][$availableLogTable->getName()]; |
| } |
|
|
| break; |
| } |
|
|
| $otherJoins = $logTable->getWaysToJoinToOtherLogTables(); |
| foreach ($otherJoins as $joinTable => $column) { |
| if ($availableLogTable->getName() == $joinTable) { |
| $join = sprintf("`%s`.`%s` = `%s`.`%s`", $table, $column, $availableLogTable->getName(), $column); |
| break; |
| } |
| } |
|
|
| $otherJoins = $availableLogTable->getWaysToJoinToOtherLogTables(); |
| foreach ($otherJoins as $joinTable => $column) { |
| if ($table == $joinTable) { |
| $join = sprintf("`%s`.`%s` = `%s`.`%s`", $table, $column, $availableLogTable->getName(), $column); |
| break; |
| } |
| } |
| } |
|
|
| if (!isset($join)) { |
| throw new Exception("Table '$table' can't be joined for segmentation"); |
| } |
|
|
| if ( |
| $this->tables->hasJoinedTableManually($table, $join) |
| || $this->tables->hasJoinedTableManually($table, $alternativeJoin) |
| ) { |
| |
| return null; |
| } |
|
|
| if ($table == 'log_conversion_item') { |
| $join .= sprintf(' AND `%s`.deleted = 0', $table); |
| } |
|
|
| return $join; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| protected function generateNonVisitJoins($tableName, $tableNameToJoin, $index) |
| { |
| $logTable = $this->tables->getLogTable($tableName); |
| $logTableToJoin = $this->tables->getLogTable($tableNameToJoin); |
|
|
| $nonVisitJoin = sprintf( |
| "%s.%s = %s.%s", |
| $logTableToJoin->getName(), |
| $logTableToJoin->getColumnToJoinOnIdAction(), |
| $tableName, |
| $logTable->getColumnToJoinOnIdAction() |
| ); |
|
|
| $altNonVisitJoin = sprintf( |
| "%s.%s = %s.%s", |
| $tableName, |
| $logTable->getColumnToJoinOnIdAction(), |
| $logTableToJoin->getName(), |
| $logTableToJoin->getColumnToJoinOnIdAction() |
| ); |
|
|
| if ( |
| $index > 0 |
| && $this->tables->hasAddedTableManually($tableName) |
| && !$this->tables->hasJoinedTableManually($tableName, $nonVisitJoin) |
| && !$this->tables->hasJoinedTableManually($tableName, $altNonVisitJoin) |
| ) { |
| $tableIndex = $this->tables->findIndexOfManuallyAddedTable($tableName); |
| $nonVisitJoin = '(' . $this->tables[$tableIndex]['joinOn'] . ' AND ' . $nonVisitJoin . ')'; |
| unset($this->tables[$tableIndex]); |
| } |
|
|
| if (!isset($this->nonVisitJoins[$tableName])) { |
| $this->nonVisitJoins[$tableName] = array(); |
| } |
|
|
| if (!isset($this->nonVisitJoins[$tableNameToJoin])) { |
| $this->nonVisitJoins[$tableNameToJoin] = array(); |
| } |
|
|
| $this->nonVisitJoins[$tableName][$tableNameToJoin] = $nonVisitJoin; |
| $this->nonVisitJoins[$tableNameToJoin][$tableName] = $nonVisitJoin; |
| } |
| } |
|
|