PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.1.6
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.1.6
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 / JoinTables.php
matomo / app / core / DataAccess / LogQueryBuilder Last commit date
JoinGenerator.php 2 years ago JoinTables.php 2 years ago
JoinTables.php
269 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\DataAccess\LogAggregator;
13 use Piwik\Plugin\LogTablesProvider;
14 class JoinTables extends \ArrayObject
15 {
16 /**
17 * @var LogTablesProvider
18 */
19 private $logTableProvider;
20 // NOTE: joins can be specified explicitly as arrays w/ 'joinOn' keys or implicitly as table names. when
21 // table names are used, the joins dependencies are assumed based on how we want to order those joins.
22 // the below table list the possible dependencies of each table, and is specifically designed to enforce
23 // the following order:
24 // log_link_visit_action, log_action, log_visit, log_conversion, log_conversion_item
25 // which means if an array is supplied where log_visit comes before log_link_visitAction, it will
26 // be moved to after it.
27 private $implicitTableDependencies = ['log_link_visit_action' => [], 'log_action' => ['log_link_visit_action', 'log_conversion', 'log_conversion_item', 'log_visit'], 'log_visit' => ['log_link_visit_action', 'log_action'], 'log_conversion' => ['log_link_visit_action', 'log_action', 'log_visit'], 'log_conversion_item' => ['log_link_visit_action', 'log_action', 'log_visit', 'log_conversion']];
28 /**
29 * Tables constructor.
30 * @param LogTablesProvider $logTablesProvider
31 * @param array $tables
32 */
33 public function __construct(LogTablesProvider $logTablesProvider, $tables)
34 {
35 $this->logTableProvider = $logTablesProvider;
36 foreach ($tables as $table) {
37 $this->checkTableCanBeUsedForSegmentation($table);
38 }
39 $this->exchangeArray(array_values($tables));
40 }
41 public function getTables()
42 {
43 return $this->getArrayCopy();
44 }
45 public function addTableToJoin($tableName)
46 {
47 $this->checkTableCanBeUsedForSegmentation($tableName);
48 $this->append($tableName);
49 }
50 public function hasJoinedTable($tableName)
51 {
52 $tables = in_array($tableName, $this->getTables());
53 if ($tables) {
54 return true;
55 }
56 foreach ($this as $table) {
57 if (is_array($table)) {
58 if (!isset($table['tableAlias']) && $table['table'] === $table) {
59 return true;
60 } elseif (isset($table['tableAlias']) && $table['tableAlias'] === $table) {
61 return true;
62 }
63 }
64 }
65 return false;
66 }
67 public function hasJoinedTableManually($tableToFind, $joinToFind)
68 {
69 foreach ($this as $table) {
70 if (is_array($table) && !empty($table['table']) && $table['table'] === $tableToFind && (!isset($table['tableAlias']) || $table['tableAlias'] === $tableToFind) && (!isset($table['join']) || strtolower($table['join']) === 'left join') && isset($table['joinOn']) && $table['joinOn'] === $joinToFind) {
71 return true;
72 }
73 }
74 return false;
75 }
76 public function getLogTable($tableName)
77 {
78 return $this->logTableProvider->getLogTable($tableName);
79 }
80 public function findIndexOfManuallyAddedTable($tableNameToFind)
81 {
82 foreach ($this as $index => $table) {
83 if (is_array($table) && !empty($table['table']) && $table['table'] === $tableNameToFind && (!isset($table['join']) || strtolower($table['join']) === 'left join') && (!isset($table['tableAlias']) || $table['tableAlias'] === $tableNameToFind)) {
84 return $index;
85 }
86 }
87 }
88 public function hasAddedTableManually($tableToFind)
89 {
90 $table = $this->findIndexOfManuallyAddedTable($tableToFind);
91 return isset($table);
92 }
93 public function sort()
94 {
95 // we do not use $this->uasort as we do not want to maintain keys
96 $tables = $this->getTables();
97 // the first entry is always the FROM table
98 $firstTable = array_shift($tables);
99 $sorted = [$firstTable];
100 if (strpos($firstTable, LogAggregator::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX) === 0) {
101 // the first table might be a temporary segment table in which case we need to keep the next one as well
102 $sorted[] = array_shift($tables);
103 }
104 $dependencies = $this->parseDependencies($tables);
105 $this->visitTableListDfs($tables, $dependencies, function ($tableInfo) use(&$sorted) {
106 $sorted[] = $tableInfo;
107 });
108 $this->exchangeArray($sorted);
109 }
110 public function isTableJoinableOnVisit($tableToCheck)
111 {
112 $table = $this->getLogTable($tableToCheck);
113 if (empty($table)) {
114 return false;
115 }
116 if ($table->getColumnToJoinOnIdVisit()) {
117 return true;
118 }
119 if ($table->getLinkTableToBeAbleToJoinOnVisit()) {
120 return true;
121 }
122 $otherWays = $table->getWaysToJoinToOtherLogTables();
123 if (empty($otherWays)) {
124 return false;
125 }
126 foreach ($otherWays as $logTable => $column) {
127 if ($logTable == 'log_visit' || $this->isTableJoinableOnVisit($logTable)) {
128 return true;
129 }
130 }
131 return false;
132 }
133 public function isTableJoinableOnAction($tableToCheck)
134 {
135 $table = $this->getLogTable($tableToCheck);
136 if (empty($table)) {
137 return false;
138 }
139 if ($table->getColumnToJoinOnIdAction()) {
140 return true;
141 }
142 $otherWays = $table->getWaysToJoinToOtherLogTables();
143 if (empty($otherWays)) {
144 return false;
145 }
146 foreach ($otherWays as $logTable => $column) {
147 if ($logTable == 'log_action' || $this->isTableJoinableOnAction($logTable)) {
148 return true;
149 }
150 }
151 return false;
152 }
153 public function addTableDependency($table, $dependentTable)
154 {
155 if (!empty($this->implicitTableDependencies[$table])) {
156 return;
157 }
158 $this->implicitTableDependencies[$table] = [$dependentTable];
159 }
160 private function checkTableCanBeUsedForSegmentation($tableName)
161 {
162 if (!is_array($tableName) && !$this->getLogTable($tableName)) {
163 throw new Exception("Table '{$tableName}' can't be used for segmentation");
164 }
165 }
166 private function parseDependencies(array $tables)
167 {
168 $dependencies = [];
169 foreach ($tables as $key => &$fromInfo) {
170 if (is_string($fromInfo)) {
171 $dependencies[$key] = $this->assumeImplicitJoinDependencies($tables, $fromInfo);
172 continue;
173 }
174 if (empty($fromInfo['joinOn'])) {
175 continue;
176 }
177 $table = isset($fromInfo['tableAlias']) ? $fromInfo['tableAlias'] : $fromInfo['table'];
178 $tablesInExpr = $this->parseSqlTables($fromInfo['joinOn'], $table);
179 $dependencies[$key] = $tablesInExpr;
180 }
181 return $dependencies;
182 }
183 private function assumeImplicitJoinDependencies($allTablesToQuery, $table)
184 {
185 $implicitTableDependencies = $this->implicitTableDependencies;
186 $result = [];
187 if (isset($implicitTableDependencies[$table])) {
188 $result = $implicitTableDependencies[$table];
189 // only include dependencies that are in the list of requested tables (ie, if we want to
190 // query from log_conversion joining on log_link_visit_action, we don't want to add log_visit
191 // to the sql statement)
192 $result = array_filter($result, function ($table) use($allTablesToQuery) {
193 return $this->isInTableArray($allTablesToQuery, $table);
194 });
195 }
196 return $result;
197 }
198 private function isInTableArray($tables, $table)
199 {
200 foreach ($tables as $entry) {
201 if (is_string($entry) && $entry == $table) {
202 return true;
203 }
204 if (is_array($entry) && $entry['table'] == $table) {
205 return true;
206 }
207 }
208 return false;
209 }
210 private function parseSqlTables($joinOn, $self)
211 {
212 preg_match_all('/\\b([a-zA-Z0-9_`]+)\\.[a-zA-Z0-9_`]+\\b/', $joinOn, $matches);
213 $tables = [];
214 foreach ($matches[1] as $table) {
215 if ($table === $self) {
216 continue;
217 }
218 $tables[] = $table;
219 }
220 return $tables;
221 }
222 private function visitTableListDfs($tables, $dependencies, $visitor)
223 {
224 $visited = [];
225 foreach ($tables as $index => $tableInfo) {
226 if (empty($visited[$index])) {
227 $this->visitTableListDfsSingle($tables, $dependencies, $visitor, $index, $visited);
228 }
229 }
230 }
231 private function visitTableListDfsSingle($tables, $dependencies, $visitor, $tableToVisitIndex, &$visited)
232 {
233 $visited[$tableToVisitIndex] = true;
234 $tableToVisit = $tables[$tableToVisitIndex];
235 if (!empty($dependencies[$tableToVisitIndex])) {
236 foreach ($dependencies[$tableToVisitIndex] as $dependencyTableName) {
237 $dependentTableToVisit = $this->findTableIndex($tables, $dependencyTableName);
238 if ($dependentTableToVisit === null) {
239 // sanity check, in case the dependent table is not in the list of tables to query
240 continue;
241 }
242 if (!empty($visited[$dependentTableToVisit])) {
243 // skip if already visited
244 continue;
245 }
246 // visit dependent table...
247 $this->visitTableListDfsSingle($tables, $dependencies, $visitor, $dependentTableToVisit, $visited);
248 }
249 }
250 // ...then visit current table
251 $visitor($tableToVisit);
252 }
253 private function findTableIndex($tables, $tableToSearchFor)
254 {
255 foreach ($tables as $key => $info) {
256 $tableName = null;
257 if (is_string($info)) {
258 $tableName = $info;
259 } elseif (is_array($info)) {
260 $tableName = isset($info['tableAlias']) ? $info['tableAlias'] : $info['table'];
261 }
262 if ($tableName == $tableToSearchFor) {
263 return $key;
264 }
265 }
266 return null;
267 }
268 }
269