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