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