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