PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.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
280 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 * @return array
117 */
118 public function generate()
119 {
120 /** @var LogTable[] $availableLogTables */
121 $availableLogTables = array();
122 $this->tables->sort();
123 foreach ($this->tables as $i => $table) {
124 $useIndex = '';
125 if ($i === 0 && is_array($table)) {
126 $useIndex = $table['useIndex'] ?? '';
127 $table = $table['table'];
128 }
129 if (is_array($table)) {
130 // join condition provided
131 $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table'];
132 if (isset($table['join'])) {
133 $this->joinString .= ' ' . $table['join'];
134 } else {
135 $this->joinString .= ' LEFT JOIN';
136 }
137 if (!isset($table['joinOn']) && $this->tables->getLogTable($table['table'])) {
138 $logTable = $this->tables->getLogTable($table['table']);
139 if (!empty($availableLogTables)) {
140 $table['joinOn'] = $this->findJoinCriteriasForTables($logTable, $availableLogTables);
141 }
142 if (!isset($table['tableAlias'])) {
143 // eg array('table' => 'log_link_visit_action', 'join' => 'RIGHT JOIN')
144 // we treat this like a regular string table which we can join automatically
145 $availableLogTables[$table['table']] = $logTable;
146 }
147 }
148 $this->joinString .= ' ' . Common::prefixTable($table['table']) . " AS " . $alias . " ON " . $table['joinOn'];
149 continue;
150 }
151 $tableSql = Common::prefixTable($table) . " AS {$table}";
152 $logTable = $this->tables->getLogTable($table);
153 if ($i == 0) {
154 // first table
155 $this->joinString .= $tableSql;
156 // Force the use of the index if an index was provided
157 if (!empty($useIndex)) {
158 $this->joinString .= " USE INDEX ({$useIndex})";
159 }
160 } else {
161 $join = $this->findJoinCriteriasForTables($logTable, $availableLogTables);
162 if ($join === null) {
163 $availableLogTables[$table] = $logTable;
164 continue;
165 }
166 $joinName = 'LEFT JOIN';
167 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) {
168 $joinName = 'INNER JOIN';
169 // when we archive a segment there will be eg `logtmpsegment$HASH` as first table.
170 // then we join log_conversion for example... if we didn't use INNER JOIN we would as a result
171 // get rows for visits even when they didn't have a conversion. Instead we only want to find rows
172 // that have an entry in both tables when doing eg
173 // logtmpsegment57cd546b7203d68a41027547c4abe1a2.idvisit = log_conversion.idvisit
174 }
175 // the join sql the default way
176 $this->joinString .= " {$joinName} {$tableSql} ON " . $join;
177 }
178 $availableLogTables[$table] = $logTable;
179 }
180 }
181 public function getJoinString()
182 {
183 return $this->joinString;
184 }
185 public function shouldJoinWithSelect()
186 {
187 return $this->joinWithSubSelect;
188 }
189 /**
190 * @param LogTable $logTable
191 * @param LogTable[] $availableLogTables
192 * @return string|null returns null in case the table is already joined, or the join string if the table needs
193 * to be joined
194 * @throws Exception if table cannot be joined for segmentation
195 */
196 public function findJoinCriteriasForTables(LogTable $logTable, $availableLogTables)
197 {
198 $join = null;
199 $alternativeJoin = null;
200 $table = $logTable->getName();
201 foreach ($availableLogTables as $availableLogTable) {
202 if ($logTable->getColumnToJoinOnIdVisit() && $availableLogTable->getColumnToJoinOnIdVisit()) {
203 $join = sprintf("%s.%s = %s.%s", $table, $logTable->getColumnToJoinOnIdVisit(), $availableLogTable->getName(), $availableLogTable->getColumnToJoinOnIdVisit());
204 $alternativeJoin = sprintf("%s.%s = %s.%s", $availableLogTable->getName(), $availableLogTable->getColumnToJoinOnIdVisit(), $table, $logTable->getColumnToJoinOnIdVisit());
205 if ($availableLogTable->shouldJoinWithSubSelect()) {
206 $this->joinWithSubSelect = \true;
207 }
208 break;
209 }
210 if ($logTable->getColumnToJoinOnIdAction() && $availableLogTable->getColumnToJoinOnIdAction()) {
211 if (isset($this->nonVisitJoins[$logTable->getName()][$availableLogTable->getName()])) {
212 $join = $this->nonVisitJoins[$logTable->getName()][$availableLogTable->getName()];
213 }
214 break;
215 }
216 $otherJoins = $logTable->getWaysToJoinToOtherLogTables();
217 foreach ($otherJoins as $joinTable => $column) {
218 if ($availableLogTable->getName() == $joinTable) {
219 $join = sprintf("`%s`.`%s` = `%s`.`%s`", $table, $column, $availableLogTable->getName(), $column);
220 break;
221 }
222 }
223 $otherJoins = $availableLogTable->getWaysToJoinToOtherLogTables();
224 foreach ($otherJoins as $joinTable => $column) {
225 if ($table == $joinTable) {
226 $join = sprintf("`%s`.`%s` = `%s`.`%s`", $table, $column, $availableLogTable->getName(), $column);
227 break;
228 }
229 }
230 }
231 if (!isset($join)) {
232 throw new Exception("Table '{$table}' can't be joined for segmentation");
233 }
234 if ($this->tables->hasJoinedTableManually($table, $join) || $this->tables->hasJoinedTableManually($table, $alternativeJoin)) {
235 // already joined, no need to join it again
236 return null;
237 }
238 if ($table == 'log_conversion_item') {
239 // by default we don't want to consider deleted columns
240 $join .= sprintf(' AND `%s`.deleted = 0', $table);
241 }
242 return $join;
243 }
244 /**
245 * This code is a bit tricky. We have to execute this right at the beginning before actually iterating over all the
246 * tables and generating the join string as we may have to delete a table from the tables. If we did not delete
247 * this table upfront, we would have maybe already added a joinString for that table, even though it will be later
248 * removed by another table. This means if we wouldn't delete/unset that table upfront, we would need to alter
249 * an already generated join string which would not be really nice code as well.
250 *
251 * Next problem is, because we are deleting a table, we have to remember the "joinOn" string for that table in a
252 * property "nonVisitJoins". Otherwise we would not be able to generate the correct "joinOn" string when actually
253 * iterating over all the tables to generate that string.
254 *
255 * @param $tableName
256 * @param $tableNameToJoin
257 * @param $index
258 */
259 protected function generateNonVisitJoins($tableName, $tableNameToJoin, $index)
260 {
261 $logTable = $this->tables->getLogTable($tableName);
262 $logTableToJoin = $this->tables->getLogTable($tableNameToJoin);
263 $nonVisitJoin = sprintf("%s.%s = %s.%s", $logTableToJoin->getName(), $logTableToJoin->getColumnToJoinOnIdAction(), $tableName, $logTable->getColumnToJoinOnIdAction());
264 $altNonVisitJoin = sprintf("%s.%s = %s.%s", $tableName, $logTable->getColumnToJoinOnIdAction(), $logTableToJoin->getName(), $logTableToJoin->getColumnToJoinOnIdAction());
265 if ($index > 0 && $this->tables->hasAddedTableManually($tableName) && !$this->tables->hasJoinedTableManually($tableName, $nonVisitJoin) && !$this->tables->hasJoinedTableManually($tableName, $altNonVisitJoin)) {
266 $tableIndex = $this->tables->findIndexOfManuallyAddedTable($tableName);
267 $nonVisitJoin = '(' . $this->tables[$tableIndex]['joinOn'] . ' AND ' . $nonVisitJoin . ')';
268 unset($this->tables[$tableIndex]);
269 }
270 if (!isset($this->nonVisitJoins[$tableName])) {
271 $this->nonVisitJoins[$tableName] = array();
272 }
273 if (!isset($this->nonVisitJoins[$tableNameToJoin])) {
274 $this->nonVisitJoins[$tableNameToJoin] = array();
275 }
276 $this->nonVisitJoins[$tableName][$tableNameToJoin] = $nonVisitJoin;
277 $this->nonVisitJoins[$tableNameToJoin][$tableName] = $nonVisitJoin;
278 }
279 }
280