PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.0.3
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.0.3
5.13.0 5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / DataAccess / LogQueryBuilder / JoinGenerator.php
matomo / app / core / DataAccess / LogQueryBuilder Last commit date
JoinGenerator.php 2 years ago JoinTables.php 2 years ago
JoinGenerator.php
272 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 *
9 */
10 namespace Piwik\DataAccess\LogQueryBuilder;
11
12 use Exception;
13 use Piwik\Common;
14 use Piwik\DataAccess\LogAggregator;
15 use Piwik\Tracker\LogTable;
16 class JoinGenerator
17 {
18 /**
19 * @var JoinTables
20 */
21 protected $tables;
22 /**
23 * @var bool
24 */
25 private $joinWithSubSelect = false;
26 /**
27 * @var string
28 */
29 private $joinString = '';
30 /**
31 * @var array
32 */
33 private $nonVisitJoins = array();
34 public function __construct(\Piwik\DataAccess\LogQueryBuilder\JoinTables $tables)
35 {
36 $this->tables = $tables;
37 $this->addMissingTablesNeededForJoins();
38 }
39 private function addMissingTablesNeededForJoins()
40 {
41 foreach ($this->tables as $index => $table) {
42 if (is_array($table)) {
43 continue;
44 }
45 $logTable = $this->tables->getLogTable($table);
46 if (!$logTable->getColumnToJoinOnIdVisit()) {
47 $tableNameToJoin = $logTable->getLinkTableToBeAbleToJoinOnVisit();
48 if (empty($tableNameToJoin) && $logTable->getWaysToJoinToOtherLogTables()) {
49 foreach ($logTable->getWaysToJoinToOtherLogTables() as $otherLogTable => $column) {
50 if ($this->tables->hasJoinedTable($otherLogTable)) {
51 $this->tables->addTableDependency($table, $otherLogTable);
52 continue;
53 }
54 if ($this->tables->isTableJoinableOnVisit($otherLogTable) || $this->tables->isTableJoinableOnAction($otherLogTable)) {
55 $this->addMissingTablesForOtherTableJoin($otherLogTable, $table);
56 }
57 }
58 continue;
59 }
60 if ($index > 0 && !$this->tables->hasJoinedTable($tableNameToJoin)) {
61 $this->tables->addTableToJoin($tableNameToJoin);
62 }
63 if ($this->tables->hasJoinedTable($tableNameToJoin)) {
64 $this->generateNonVisitJoins($table, $tableNameToJoin, $index);
65 }
66 }
67 }
68 foreach ($this->tables as $index => $table) {
69 if (is_array($table)) {
70 if (!isset($table['tableAlias'])) {
71 $tableName = $table['table'];
72 $numTables = count($this->tables);
73 for ($j = $index + 1; $j < $numTables; $j++) {
74 if (!isset($this->tables[$j])) {
75 continue;
76 }
77 $tableOther = $this->tables[$j];
78 if (is_string($tableOther) && $tableOther === $tableName) {
79 unset($this->tables[$j]);
80 }
81 }
82 }
83 } elseif (is_string($table)) {
84 $numTables = count($this->tables);
85 for ($j = $index + 1; $j < $numTables; $j++) {
86 if (isset($this->tables[$j]) && is_array($this->tables[$j]) && !isset($this->tables[$j]['tableAlias'])) {
87 $tableOther = $this->tables[$j];
88 if ($table === $tableOther['table']) {
89 $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)));
90 throw new Exception($message);
91 }
92 }
93 }
94 }
95 }
96 }
97 private function addMissingTablesForOtherTableJoin($tableName, $dependentTable)
98 {
99 $this->tables->addTableDependency($dependentTable, $tableName);
100 if ($this->tables->hasJoinedTable($tableName)) {
101 return;
102 }
103 $table = $this->tables->getLogTable($tableName);
104 if ($table->getColumnToJoinOnIdAction() || $table->getColumnToJoinOnIdVisit() || $table->getLinkTableToBeAbleToJoinOnVisit()) {
105 $this->tables->addTableToJoin($tableName);
106 return;
107 }
108 $otherTableJoins = $table->getWaysToJoinToOtherLogTables();
109 foreach ($otherTableJoins as $logTable => $column) {
110 $this->addMissingTablesForOtherTableJoin($logTable, $tableName);
111 }
112 $this->tables->addTableToJoin($tableName);
113 }
114 /**
115 * Generate the join sql based on the needed tables
116 * @throws Exception if tables can't be joined
117 * @return array
118 */
119 public function generate()
120 {
121 /** @var LogTable[] $availableLogTables */
122 $availableLogTables = array();
123 $this->tables->sort();
124 foreach ($this->tables as $i => $table) {
125 if (is_array($table)) {
126 // join condition provided
127 $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table'];
128 if (isset($table['join'])) {
129 $this->joinString .= ' ' . $table['join'];
130 } else {
131 $this->joinString .= ' LEFT JOIN';
132 }
133 if (!isset($table['joinOn']) && $this->tables->getLogTable($table['table'])) {
134 $logTable = $this->tables->getLogTable($table['table']);
135 if (!empty($availableLogTables)) {
136 $table['joinOn'] = $this->findJoinCriteriasForTables($logTable, $availableLogTables);
137 }
138 if (!isset($table['tableAlias'])) {
139 // eg array('table' => 'log_link_visit_action', 'join' => 'RIGHT JOIN')
140 // we treat this like a regular string table which we can join automatically
141 $availableLogTables[$table['table']] = $logTable;
142 }
143 }
144 $this->joinString .= ' ' . Common::prefixTable($table['table']) . " AS " . $alias . " ON " . $table['joinOn'];
145 continue;
146 }
147 $tableSql = Common::prefixTable($table) . " AS {$table}";
148 $logTable = $this->tables->getLogTable($table);
149 if ($i == 0) {
150 // first table
151 $this->joinString .= $tableSql;
152 } else {
153 $join = $this->findJoinCriteriasForTables($logTable, $availableLogTables);
154 if ($join === null) {
155 $availableLogTables[$table] = $logTable;
156 continue;
157 }
158 $joinName = 'LEFT JOIN';
159 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) {
160 $joinName = 'INNER JOIN';
161 // when we archive a segment there will be eg `logtmpsegment$HASH` as first table.
162 // then we join log_conversion for example... if we didn't use INNER JOIN we would as a result
163 // get rows for visits even when they didn't have a conversion. Instead we only want to find rows
164 // that have an entry in both tables when doing eg
165 // logtmpsegment57cd546b7203d68a41027547c4abe1a2.idvisit = log_conversion.idvisit
166 }
167 // the join sql the default way
168 $this->joinString .= " {$joinName} {$tableSql} ON " . $join;
169 }
170 $availableLogTables[$table] = $logTable;
171 }
172 }
173 public function getJoinString()
174 {
175 return $this->joinString;
176 }
177 public function shouldJoinWithSelect()
178 {
179 return $this->joinWithSubSelect;
180 }
181 /**
182 * @param LogTable $logTable
183 * @param LogTable[] $availableLogTables
184 * @return string|null returns null in case the table is already joined, or the join string if the table needs
185 * to be joined
186 * @throws Exception if table cannot be joined for segmentation
187 */
188 public function findJoinCriteriasForTables(LogTable $logTable, $availableLogTables)
189 {
190 $join = null;
191 $alternativeJoin = null;
192 $table = $logTable->getName();
193 foreach ($availableLogTables as $availableLogTable) {
194 if ($logTable->getColumnToJoinOnIdVisit() && $availableLogTable->getColumnToJoinOnIdVisit()) {
195 $join = sprintf("%s.%s = %s.%s", $table, $logTable->getColumnToJoinOnIdVisit(), $availableLogTable->getName(), $availableLogTable->getColumnToJoinOnIdVisit());
196 $alternativeJoin = sprintf("%s.%s = %s.%s", $availableLogTable->getName(), $availableLogTable->getColumnToJoinOnIdVisit(), $table, $logTable->getColumnToJoinOnIdVisit());
197 if ($availableLogTable->shouldJoinWithSubSelect()) {
198 $this->joinWithSubSelect = true;
199 }
200 break;
201 }
202 if ($logTable->getColumnToJoinOnIdAction() && $availableLogTable->getColumnToJoinOnIdAction()) {
203 if (isset($this->nonVisitJoins[$logTable->getName()][$availableLogTable->getName()])) {
204 $join = $this->nonVisitJoins[$logTable->getName()][$availableLogTable->getName()];
205 }
206 break;
207 }
208 $otherJoins = $logTable->getWaysToJoinToOtherLogTables();
209 foreach ($otherJoins as $joinTable => $column) {
210 if ($availableLogTable->getName() == $joinTable) {
211 $join = sprintf("`%s`.`%s` = `%s`.`%s`", $table, $column, $availableLogTable->getName(), $column);
212 break;
213 }
214 }
215 $otherJoins = $availableLogTable->getWaysToJoinToOtherLogTables();
216 foreach ($otherJoins as $joinTable => $column) {
217 if ($table == $joinTable) {
218 $join = sprintf("`%s`.`%s` = `%s`.`%s`", $table, $column, $availableLogTable->getName(), $column);
219 break;
220 }
221 }
222 }
223 if (!isset($join)) {
224 throw new Exception("Table '{$table}' can't be joined for segmentation");
225 }
226 if ($this->tables->hasJoinedTableManually($table, $join) || $this->tables->hasJoinedTableManually($table, $alternativeJoin)) {
227 // already joined, no need to join it again
228 return null;
229 }
230 if ($table == 'log_conversion_item') {
231 // by default we don't want to consider deleted columns
232 $join .= sprintf(' AND `%s`.deleted = 0', $table);
233 }
234 return $join;
235 }
236 /**
237 * This code is a bit tricky. We have to execute this right at the beginning before actually iterating over all the
238 * tables and generating the join string as we may have to delete a table from the tables. If we did not delete
239 * this table upfront, we would have maybe already added a joinString for that table, even though it will be later
240 * removed by another table. This means if we wouldn't delete/unset that table upfront, we would need to alter
241 * an already generated join string which would not be really nice code as well.
242 *
243 * Next problem is, because we are deleting a table, we have to remember the "joinOn" string for that table in a
244 * property "nonVisitJoins". Otherwise we would not be able to generate the correct "joinOn" string when actually
245 * iterating over all the tables to generate that string.
246 *
247 * @param $tableName
248 * @param $tableNameToJoin
249 * @param $index
250 */
251 protected function generateNonVisitJoins($tableName, $tableNameToJoin, $index)
252 {
253 $logTable = $this->tables->getLogTable($tableName);
254 $logTableToJoin = $this->tables->getLogTable($tableNameToJoin);
255 $nonVisitJoin = sprintf("%s.%s = %s.%s", $logTableToJoin->getName(), $logTableToJoin->getColumnToJoinOnIdAction(), $tableName, $logTable->getColumnToJoinOnIdAction());
256 $altNonVisitJoin = sprintf("%s.%s = %s.%s", $tableName, $logTable->getColumnToJoinOnIdAction(), $logTableToJoin->getName(), $logTableToJoin->getColumnToJoinOnIdAction());
257 if ($index > 0 && $this->tables->hasAddedTableManually($tableName) && !$this->tables->hasJoinedTableManually($tableName, $nonVisitJoin) && !$this->tables->hasJoinedTableManually($tableName, $altNonVisitJoin)) {
258 $tableIndex = $this->tables->findIndexOfManuallyAddedTable($tableName);
259 $nonVisitJoin = '(' . $this->tables[$tableIndex]['joinOn'] . ' AND ' . $nonVisitJoin . ')';
260 unset($this->tables[$tableIndex]);
261 }
262 if (!isset($this->nonVisitJoins[$tableName])) {
263 $this->nonVisitJoins[$tableName] = array();
264 }
265 if (!isset($this->nonVisitJoins[$tableNameToJoin])) {
266 $this->nonVisitJoins[$tableNameToJoin] = array();
267 }
268 $this->nonVisitJoins[$tableName][$tableNameToJoin] = $nonVisitJoin;
269 $this->nonVisitJoins[$tableNameToJoin][$tableName] = $nonVisitJoin;
270 }
271 }
272