PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.3.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.3.1
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 6 years ago JoinTables.php 6 years ago
JoinGenerator.php
334 lines
1 <?php
2 /**
3 * Piwik - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
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
17 class JoinGenerator
18 {
19 /**
20 * @var JoinTables
21 */
22 protected $tables;
23
24 /**
25 * @var bool
26 */
27 private $joinWithSubSelect = false;
28
29 /**
30 * @var string
31 */
32 private $joinString = '';
33
34 /**
35 * @var array
36 */
37 private $nonVisitJoins = array();
38
39 public function __construct(JoinTables $tables)
40 {
41 $this->tables = $tables;
42 $this->addMissingTablesNeededForJoins();
43 }
44
45 private function addMissingTablesNeededForJoins()
46 {
47 foreach ($this->tables as $index => $table) {
48 if (is_array($table)) {
49 continue;
50 }
51
52 $logTable = $this->tables->getLogTable($table);
53
54 if (!$logTable->getColumnToJoinOnIdVisit()) {
55 $tableNameToJoin = $logTable->getLinkTableToBeAbleToJoinOnVisit();
56
57 if (empty($tableNameToJoin) && $logTable->getWaysToJoinToOtherLogTables()) {
58 foreach ($logTable->getWaysToJoinToOtherLogTables() as $otherLogTable => $column) {
59 if ($this->tables->hasJoinedTable($otherLogTable)) {
60 $this->tables->addTableDependency($table, $otherLogTable);
61 continue;
62 }
63 if ($this->tables->isTableJoinableOnVisit($otherLogTable) || $this->tables->isTableJoinableOnAction($otherLogTable)) {
64 $this->addMissingTablesForOtherTableJoin($otherLogTable, $table);
65 }
66 }
67 continue;
68 }
69
70 if ($index > 0 && !$this->tables->hasJoinedTable($tableNameToJoin)) {
71 $this->tables->addTableToJoin($tableNameToJoin);
72 }
73
74 if ($this->tables->hasJoinedTable($tableNameToJoin)) {
75 $this->generateNonVisitJoins($table, $tableNameToJoin, $index);
76 }
77 }
78 }
79
80 foreach ($this->tables as $index => $table) {
81 if (is_array($table)) {
82 if (!isset($table['tableAlias'])) {
83 $tableName = $table['table'];
84 $numTables = count($this->tables);
85 for ($j = $index + 1; $j < $numTables; $j++) {
86 if (!isset($this->tables[$j])) {
87 continue;
88 }
89
90 $tableOther = $this->tables[$j];
91 if (is_string($tableOther) && $tableOther === $tableName) {
92 unset($this->tables[$j]);
93 }
94 }
95 }
96 } elseif (is_string($table)) {
97 $numTables = count($this->tables);
98
99 for ($j = $index + 1; $j < $numTables; $j++) {
100 if (isset($this->tables[$j]) && is_array($this->tables[$j]) && !isset($this->tables[$j]['tableAlias'])) {
101 $tableOther = $this->tables[$j];
102 if ($table === $tableOther['table']) {
103 $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)));
104 throw new Exception($message);
105 }
106 }
107
108 }
109 }
110 }
111 }
112
113 private function addMissingTablesForOtherTableJoin($tableName, $dependentTable)
114 {
115 $this->tables->addTableDependency($dependentTable, $tableName);
116
117 if ($this->tables->hasJoinedTable($tableName)) {
118 return;
119 }
120
121 $table = $this->tables->getLogTable($tableName);
122
123 if ($table->getColumnToJoinOnIdAction() || $table->getColumnToJoinOnIdVisit() || $table->getLinkTableToBeAbleToJoinOnVisit()) {
124 $this->tables->addTableToJoin($tableName);
125 return;
126 }
127
128 $otherTableJoins = $table->getWaysToJoinToOtherLogTables();
129
130 foreach ($otherTableJoins as $logTable => $column) {
131 $this->addMissingTablesForOtherTableJoin($logTable, $tableName);
132 }
133
134 $this->tables->addTableToJoin($tableName);
135 }
136
137 /**
138 * Generate the join sql based on the needed tables
139 * @throws Exception if tables can't be joined
140 * @return array
141 */
142 public function generate()
143 {
144 /** @var LogTable[] $availableLogTables */
145 $availableLogTables = array();
146
147 $this->tables->sort();
148
149 foreach ($this->tables as $i => $table) {
150 if (is_array($table)) {
151
152 // join condition provided
153 $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table'];
154
155 if (isset($table['join'])) {
156 $this->joinString .= ' ' . $table['join'];
157 } else {
158 $this->joinString .= ' LEFT JOIN';
159 }
160
161 if (!isset($table['joinOn']) && $this->tables->getLogTable($table['table'])) {
162 $logTable = $this->tables->getLogTable($table['table']);
163 if (!empty($availableLogTables)) {
164 $table['joinOn'] = $this->findJoinCriteriasForTables($logTable, $availableLogTables);
165 }
166 if (!isset($table['tableAlias'])) {
167 // eg array('table' => 'log_link_visit_action', 'join' => 'RIGHT JOIN')
168 // we treat this like a regular string table which we can join automatically
169 $availableLogTables[$table['table']] = $logTable;
170 }
171 }
172
173 $this->joinString .= ' ' . Common::prefixTable($table['table']) . " AS " . $alias
174 . " ON " . $table['joinOn'];
175 continue;
176 }
177
178 $tableSql = Common::prefixTable($table) . " AS $table";
179
180 $logTable = $this->tables->getLogTable($table);
181
182 if ($i == 0) {
183 // first table
184 $this->joinString .= $tableSql;
185 } else {
186
187 $join = $this->findJoinCriteriasForTables($logTable, $availableLogTables);
188
189 if ($join === null) {
190 $availableLogTables[$table] = $logTable;
191 continue;
192 }
193
194 $joinName = 'LEFT JOIN';
195 if ($i > 0
196 && $this->tables[$i - 1]
197 && is_string($this->tables[$i - 1])
198 && strpos($this->tables[$i - 1], LogAggregator::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX) === 0) {
199 $joinName = 'INNER JOIN';
200 // when we archive a segment there will be eg `logtmpsegment$HASH` as first table.
201 // then we join log_conversion for example... if we didn't use INNER JOIN we would as a result
202 // get rows for visits even when they didn't have a conversion. Instead we only want to find rows
203 // that have an entry in both tables when doing eg
204 // logtmpsegment57cd546b7203d68a41027547c4abe1a2.idvisit = log_conversion.idvisit
205 }
206 // the join sql the default way
207 $this->joinString .= " $joinName $tableSql ON " . $join;
208 }
209
210 $availableLogTables[$table] = $logTable;
211 }
212 }
213
214 public function getJoinString()
215 {
216 return $this->joinString;
217 }
218
219 public function shouldJoinWithSelect()
220 {
221 return $this->joinWithSubSelect;
222 }
223
224 /**
225 * @param LogTable $logTable
226 * @param LogTable[] $availableLogTables
227 * @return string|null returns null in case the table is already joined, or the join string if the table needs
228 * to be joined
229 * @throws Exception if table cannot be joined for segmentation
230 */
231 public function findJoinCriteriasForTables(LogTable $logTable, $availableLogTables)
232 {
233 $join = null;
234 $alternativeJoin = null;
235 $table = $logTable->getName();
236
237 foreach ($availableLogTables as $availableLogTable) {
238 if ($logTable->getColumnToJoinOnIdVisit() && $availableLogTable->getColumnToJoinOnIdVisit()) {
239
240 $join = sprintf("%s.%s = %s.%s", $table, $logTable->getColumnToJoinOnIdVisit(),
241 $availableLogTable->getName(), $availableLogTable->getColumnToJoinOnIdVisit());
242 $alternativeJoin = sprintf("%s.%s = %s.%s", $availableLogTable->getName(), $availableLogTable->getColumnToJoinOnIdVisit(),
243 $table, $logTable->getColumnToJoinOnIdVisit());
244
245 if ($availableLogTable->shouldJoinWithSubSelect()) {
246 $this->joinWithSubSelect = true;
247 }
248
249 break;
250 }
251
252 if ($logTable->getColumnToJoinOnIdAction() && $availableLogTable->getColumnToJoinOnIdAction()) {
253 if (isset($this->nonVisitJoins[$logTable->getName()][$availableLogTable->getName()])) {
254 $join = $this->nonVisitJoins[$logTable->getName()][$availableLogTable->getName()];
255 }
256
257 break;
258 }
259
260 $otherJoins = $logTable->getWaysToJoinToOtherLogTables();
261 foreach ($otherJoins as $joinTable => $column) {
262 if($availableLogTable->getName() == $joinTable) {
263 $join = sprintf("`%s`.`%s` = `%s`.`%s`", $table, $column, $availableLogTable->getName(), $column);
264 break;
265 }
266 }
267
268 }
269
270 if (!isset($join)) {
271 throw new Exception("Table '$table' can't be joined for segmentation");
272 }
273
274 if ($this->tables->hasJoinedTableManually($table, $join)
275 || $this->tables->hasJoinedTableManually($table, $alternativeJoin)) {
276 // already joined, no need to join it again
277 return null;
278 }
279
280 if ($table == 'log_conversion_item') { // by default we don't want to consider deleted columns
281 $join .= sprintf(' AND `%s`.deleted = 0', $table);
282 }
283
284 return $join;
285 }
286
287 /**
288 * This code is a bit tricky. We have to execute this right at the beginning before actually iterating over all the
289 * tables and generating the join string as we may have to delete a table from the tables. If we did not delete
290 * this table upfront, we would have maybe already added a joinString for that table, even though it will be later
291 * removed by another table. This means if we wouldn't delete/unset that table upfront, we would need to alter
292 * an already generated join string which would not be really nice code as well.
293 *
294 * Next problem is, because we are deleting a table, we have to remember the "joinOn" string for that table in a
295 * property "nonVisitJoins". Otherwise we would not be able to generate the correct "joinOn" string when actually
296 * iterating over all the tables to generate that string.
297 *
298 * @param $tableName
299 * @param $tableNameToJoin
300 * @param $index
301 */
302 protected function generateNonVisitJoins($tableName, $tableNameToJoin, $index)
303 {
304 $logTable = $this->tables->getLogTable($tableName);
305 $logTableToJoin = $this->tables->getLogTable($tableNameToJoin);
306
307 $nonVisitJoin = sprintf("%s.%s = %s.%s", $logTableToJoin->getName(), $logTableToJoin->getColumnToJoinOnIdAction(),
308 $tableName, $logTable->getColumnToJoinOnIdAction());
309
310 $altNonVisitJoin = sprintf("%s.%s = %s.%s", $tableName, $logTable->getColumnToJoinOnIdAction(),
311 $logTableToJoin->getName(), $logTableToJoin->getColumnToJoinOnIdAction());
312
313 if ($index > 0
314 && $this->tables->hasAddedTableManually($tableName)
315 && !$this->tables->hasJoinedTableManually($tableName, $nonVisitJoin)
316 && !$this->tables->hasJoinedTableManually($tableName, $altNonVisitJoin)) {
317 $tableIndex = $this->tables->findIndexOfManuallyAddedTable($tableName);
318 $nonVisitJoin = '(' . $this->tables[$tableIndex]['joinOn'] . ' AND ' . $nonVisitJoin . ')';
319 unset($this->tables[$tableIndex]);
320 }
321
322 if (!isset($this->nonVisitJoins[$tableName])) {
323 $this->nonVisitJoins[$tableName] = array();
324 }
325
326 if (!isset($this->nonVisitJoins[$tableNameToJoin])) {
327 $this->nonVisitJoins[$tableNameToJoin] = array();
328 }
329
330 $this->nonVisitJoins[$tableName][$tableNameToJoin] = $nonVisitJoin;
331 $this->nonVisitJoins[$tableNameToJoin][$tableName] = $nonVisitJoin;
332 }
333 }
334