PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.3.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.3.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 / RawLogDao.php
matomo / app / core / DataAccess Last commit date
LogQueryBuilder 5 years ago Actions.php 5 years ago ArchiveSelector.php 5 years ago ArchiveTableCreator.php 5 years ago ArchiveTableDao.php 5 years ago ArchiveWriter.php 5 years ago ArchivingDbAdapter.php 5 years ago LogAggregator.php 5 years ago LogQueryBuilder.php 5 years ago LogTableTemporary.php 5 years ago Model.php 5 years ago RawLogDao.php 5 years ago TableMetadata.php 5 years ago
RawLogDao.php
456 lines
1 <?php
2 /**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9 namespace Piwik\DataAccess;
10
11 use Piwik\Common;
12 use Piwik\Config as PiwikConfig;
13 use Piwik\Container\StaticContainer;
14 use Piwik\Db;
15 use Piwik\Plugin\Dimension\DimensionMetadataProvider;
16 use Piwik\Plugin\LogTablesProvider;
17
18 /**
19 * DAO that queries log tables.
20 */
21 class RawLogDao
22 {
23 const DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME = 'tmp_log_actions_to_keep';
24
25 /**
26 * @var DimensionMetadataProvider
27 */
28 private $dimensionMetadataProvider;
29
30 /**
31 * @var LogTablesProvider
32 */
33 private $logTablesProvider;
34
35 public function __construct(DimensionMetadataProvider $provider = null, LogTablesProvider $logTablesProvider = null)
36 {
37 $this->dimensionMetadataProvider = $provider ?: StaticContainer::get('Piwik\Plugin\Dimension\DimensionMetadataProvider');
38 $this->logTablesProvider = $logTablesProvider ?: StaticContainer::get('Piwik\Plugin\LogTablesProvider');
39 }
40
41 /**
42 * @param array $values
43 * @param string $idVisit
44 */
45 public function updateVisits(array $values, $idVisit)
46 {
47 $sql = "UPDATE " . Common::prefixTable('log_visit')
48 . " SET " . $this->getColumnSetExpressions(array_keys($values))
49 . " WHERE idvisit = ?";
50
51 $this->update($sql, $values, $idVisit);
52 }
53
54 /**
55 * @param array $values
56 * @param string $idVisit
57 */
58 public function updateConversions(array $values, $idVisit)
59 {
60 $sql = "UPDATE " . Common::prefixTable('log_conversion')
61 . " SET " . $this->getColumnSetExpressions(array_keys($values))
62 . " WHERE idvisit = ?";
63
64 $this->update($sql, $values, $idVisit);
65 }
66
67 /**
68 * @param string $from
69 * @param string $to
70 * @return int
71 */
72 public function countVisitsWithDatesLimit($from, $to)
73 {
74 $sql = "SELECT COUNT(*) AS num_rows"
75 . " FROM " . Common::prefixTable('log_visit')
76 . " WHERE visit_last_action_time >= ? AND visit_last_action_time < ?";
77
78 $bind = array($from, $to);
79
80 return (int) Db::fetchOne($sql, $bind);
81 }
82
83 /**
84 * Iterates over logs in a log table in chunks. Parameters to this function are as backend agnostic
85 * as possible w/o dramatically increasing code complexity.
86 *
87 * @param string $logTable The log table name. Unprefixed, eg, `log_visit`.
88 * @param array[] $conditions An array describing the conditions logs must match in the query. Translates to
89 * the WHERE part of a SELECT statement. Each element must contain three elements:
90 *
91 * * the column name
92 * * the operator (ie, '=', '<>', '<', etc.)
93 * * the operand (ie, a value)
94 *
95 * The elements are AND-ed together.
96 *
97 * Example:
98 *
99 * ```
100 * array(
101 * array('visit_first_action_time', '>=', ...),
102 * array('visit_first_action_time', '<', ...)
103 * )
104 * ```
105 * @param int $iterationStep The number of rows to query at a time.
106 * @param callable $callback The callback that processes each chunk of rows.
107 * @param string $willDelete Set to true if you will make sure to delete all rows that were fetched. If you are in
108 * doubt and not sure if to set true or false, use "false". Setting it to true will
109 * enable an internal performance improvement but it can result in an endless loop if not
110 * used properly.
111 */
112 public function forAllLogs($logTable, $fields, $conditions, $iterationStep, $callback, $willDelete)
113 {
114 $lastId = 0;
115
116 if ($willDelete) {
117 // we don't want to look at eg idvisit so the query will be mostly index covered as the
118 // "where idvisit > 0 ... ORDER BY idvisit ASC" will be gone... meaning we don't need to look at a huge range
119 // of visits...
120 $idField = null;
121 $bindFunction = function ($bind, $lastId) {
122 return $bind;
123 };
124 } else {
125 // when we are not deleting, we need to ensure to iterate over each visitor step by step... meaning we
126 // need to remember which visit we have already looked at and which one not. Therefore we need to apply
127 // "where idvisit > $lastId" in the query and "order by idvisit ASC"
128 $idField = $this->getIdFieldForLogTable($logTable);
129 $bindFunction = function ($bind, $lastId) {
130 return array_merge(array($lastId), $bind);
131 };
132 }
133
134 list($query, $bind) = $this->createLogIterationQuery($logTable, $idField, $fields, $conditions, $iterationStep);
135
136 do {
137 $rows = Db::fetchAll($query, call_user_func($bindFunction, $bind, $lastId));
138 if (!empty($rows)) {
139 if ($idField) {
140 $lastId = $rows[count($rows) - 1][$idField];
141 }
142 $callback($rows);
143 }
144 } while (count($rows) == $iterationStep);
145 }
146
147 /**
148 * Deletes conversions for the supplied visit IDs from log_conversion. This method does not cascade, so
149 * conversion items will not be deleted.
150 *
151 * @param int[] $visitIds
152 * @return int The number of deleted rows.
153 */
154 public function deleteFromLogTable($tableName, $visitIds)
155 {
156 $sql = "DELETE FROM `" . Common::prefixTable($tableName) . "` WHERE idvisit IN "
157 . $this->getInFieldExpressionWithInts($visitIds);
158
159 $statement = Db::query($sql);
160 return $statement->rowCount();
161 }
162
163 /**
164 * Deletes conversion items for the supplied visit IDs from log_conversion_item.
165 *
166 * @param int[] $visitIds
167 * @return int The number of deleted rows.
168 */
169 public function deleteConversionItems($visitIds)
170 {
171 $sql = "DELETE FROM `" . Common::prefixTable('log_conversion_item') . "` WHERE idvisit IN "
172 . $this->getInFieldExpressionWithInts($visitIds);
173
174 $statement = Db::query($sql);
175 return $statement->rowCount();
176 }
177
178 /**
179 * Deletes all unused entries from the log_action table. This method uses a temporary table to store used
180 * actions, and then deletes rows from log_action that are not in this temporary table.
181 *
182 * Table locking is required to avoid concurrency issues.
183 *
184 * @throws \Exception If table locking permission is not granted to the current MySQL user.
185 */
186 public function deleteUnusedLogActions()
187 {
188 if (!Db::isLockPrivilegeGranted()) {
189 throw new \Exception("RawLogDao.deleteUnusedLogActions() requires table locking permission in order to complete without error.");
190 }
191
192 // get current max ID in log tables w/ idaction references.
193 $maxIds = $this->getMaxIdsInLogTables();
194
195 // get max rows to analyze
196 $max_rows_per_query = PiwikConfig::getInstance()->Deletelogs['delete_logs_unused_actions_max_rows_per_query'];
197
198 $this->createTempTableForStoringUsedActions();
199
200 // do large insert (inserting everything before maxIds) w/o locking tables...
201 $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = true, $max_rows_per_query);
202
203 // ... then do small insert w/ locked tables to minimize the amount of time tables are locked.
204 $this->lockLogTables();
205 $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = false, $max_rows_per_query);
206
207 // delete before unlocking tables so there's no chance a new log row that references an
208 // unused action will be inserted.
209 $this->deleteUnusedActions();
210
211 Db::unlockAllTables();
212
213 $this->dropTempTableForStoringUsedActions();
214 }
215
216 /**
217 * Returns the list of the website IDs that received some visits between the specified timestamp. The
218 * start date and the end date is included in the time frame.
219 *
220 * @param string $fromDateTime
221 * @param string $toDateTime
222 * @return bool true if there are visits for this site between the given timeframe, false if not
223 */
224 public function hasSiteVisitsBetweenTimeframe($fromDateTime, $toDateTime, $idSite)
225 {
226 $sites = Db::fetchOne("SELECT 1
227 FROM " . Common::prefixTable('log_visit') . "
228 WHERE idsite = ?
229 AND visit_last_action_time >= ?
230 AND visit_last_action_time <= ?
231 LIMIT 1", array($idSite, $fromDateTime, $toDateTime));
232
233 return (bool) $sites;
234 }
235
236 /**
237 * @param array $columnsToSet
238 * @return string
239 */
240 protected function getColumnSetExpressions(array $columnsToSet)
241 {
242 $columnsToSet = array_map(
243 function ($column) {
244 return $column . ' = ?';
245 },
246 $columnsToSet
247 );
248
249 return implode(', ', $columnsToSet);
250 }
251
252 /**
253 * @param array $values
254 * @param $idVisit
255 * @param $sql
256 * @return \Zend_Db_Statement
257 * @throws \Exception
258 */
259 protected function update($sql, array $values, $idVisit)
260 {
261 return Db::query($sql, array_merge(array_values($values), array($idVisit)));
262 }
263
264 protected function getIdFieldForLogTable($logTable)
265 {
266 $idColumns = $this->getTableIdColumns();
267
268 if (isset($idColumns[$logTable])) {
269 return $idColumns[$logTable];
270 }
271
272 throw new \InvalidArgumentException("Unknown log table '$logTable'.");
273 }
274
275 // TODO: instead of creating a log query like this, we should re-use segments. to do this, however, there must be a 1-1
276 // mapping for dimensions => segments, and each dimension should automatically have a segment.
277 private function createLogIterationQuery($logTable, $idField, $fields, $conditions, $iterationStep)
278 {
279 $bind = array();
280
281 $sql = "SELECT " . implode(', ', $fields) . " FROM `" . Common::prefixTable($logTable) . "` WHERE ";
282
283 $parts = array();
284
285 if ($idField) {
286 $parts[] = "$idField > ?";
287 }
288
289 foreach ($conditions as $condition) {
290 list($column, $operator, $value) = $condition;
291
292 if (is_array($value)) {
293 $parts[] = "$column IN (" . Common::getSqlStringFieldsArray($value) . ")";
294
295 $bind = array_merge($bind, $value);
296 } else {
297 $parts[]= "$column $operator ?";
298
299 $bind[] = $value;
300 }
301 }
302 $sql .= implode(' AND ', $parts);
303
304 if ($idField) {
305 $sql .= " ORDER BY $idField ASC";
306 }
307
308 $sql .= " LIMIT " . (int)$iterationStep;
309
310 return array($sql, $bind);
311 }
312
313 private function getInFieldExpressionWithInts($idVisits)
314 {
315 $sql = "(";
316
317 $isFirst = true;
318 foreach ($idVisits as $idVisit) {
319 if ($isFirst) {
320 $isFirst = false;
321 } else {
322 $sql .= ', ';
323 }
324
325 $sql .= (int)$idVisit;
326 }
327
328 $sql .= ")";
329
330 return $sql;
331 }
332
333 protected function getMaxIdsInLogTables()
334 {
335 $idColumns = $this->getTableIdColumns();
336 $tables = array_keys($idColumns);
337
338 $result = array();
339 foreach ($tables as $table) {
340 $idCol = $idColumns[$table];
341 $result[$table] = Db::fetchOne("SELECT MAX($idCol) FROM " . Common::prefixTable($table));
342 }
343
344 return $result;
345 }
346
347 private function createTempTableForStoringUsedActions()
348 {
349 $sql = "CREATE TEMPORARY TABLE " . Common::prefixTable(self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME) . " (
350 idaction INT(11),
351 PRIMARY KEY (idaction)
352 )";
353 Db::query($sql);
354 }
355
356 private function dropTempTableForStoringUsedActions()
357 {
358 $sql = "DROP TABLE " . Common::prefixTable(self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME);
359 Db::query($sql);
360 }
361
362 // protected for testing purposes
363 protected function insertActionsToKeep($maxIds, $olderThan = true, $insertIntoTempIterationStep = 100000)
364 {
365 $tempTableName = Common::prefixTable(self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME);
366
367 $idColumns = $this->getTableIdColumns();
368 foreach ($this->dimensionMetadataProvider->getActionReferenceColumnsByTable() as $table => $columns) {
369 $idCol = $idColumns[$table];
370 // Create select query for requesting ALL needed fields at once
371 $sql = "SELECT " . implode(',' ,$columns) . " FROM " . Common::prefixTable($table) . " WHERE $idCol >= ? AND $idCol < ?";
372
373 if ($olderThan) {
374 // Why start on zero? When running for a couple of months, this will generate about 10000+ queries with zero result. Use the lowest value instead.... saves a LOT of waiting time!
375 $start = (int) Db::fetchOne("SELECT MIN($idCol) FROM " . Common::prefixTable($table));;
376 $finish = $maxIds[$table];
377 } else {
378 $start = $maxIds[$table];
379 $finish = (int) Db::fetchOne("SELECT MAX($idCol) FROM " . Common::prefixTable($table));
380 }
381 // Borrowed from Db::segmentedFetchAll
382 // Request records per $insertIntoTempIterationStep amount
383 // Loop over the result set, mapping all numeric fields in a single insert query
384
385 // Insert query would be: INSERT IGNORE INTO [temp_table] VALUES (X),(Y),(Z) depending on the amount of fields requested per row
386 for ($i = $start; $i <= $finish; $i += $insertIntoTempIterationStep) {
387 $currentParams = array($i, $i + $insertIntoTempIterationStep);
388 $result = Db::fetchAll($sql, $currentParams);
389 // Now we loop over the result set of max $insertIntoTempIterationStep rows and create insert queries
390 $keepValues = [];
391 foreach ($result as $row) {
392 $keepValues = array_merge($keepValues, array_filter(array_values($row), "is_numeric"));
393 if (count($keepValues) >= 1000) {
394 $insert = 'INSERT IGNORE INTO ' . $tempTableName .' VALUES (';
395 $insert .= implode('),(', $keepValues);
396 $insert .= ')';
397
398 Db::exec($insert);
399 $keepValues = [];
400 }
401 }
402
403 $insert = 'INSERT IGNORE INTO ' . $tempTableName .' VALUES (';
404 $insert .= implode('),(', $keepValues);
405 $insert .= ')';
406
407 Db::exec($insert);
408 }
409 }
410 }
411
412 private function lockLogTables()
413 {
414 $tables = $this->getTableIdColumns();
415 unset($tables['log_action']); // we write lock it
416 $tableNames = array_keys($tables);
417
418 $readLocks = array();
419 foreach ($tableNames as $tableName) {
420 $readLocks[] = Common::prefixTable($tableName);
421 }
422
423 Db::lockTables(
424 $readLocks,
425 $writeLocks = Common::prefixTables('log_action')
426 );
427 }
428
429 private function deleteUnusedActions()
430 {
431 list($logActionTable, $tempTableName) = Common::prefixTables("log_action", self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME);
432
433 $deleteSql = "DELETE LOW_PRIORITY QUICK IGNORE $logActionTable
434 FROM $logActionTable
435 LEFT JOIN $tempTableName tmp ON tmp.idaction = $logActionTable.idaction
436 WHERE tmp.idaction IS NULL";
437
438 Db::query($deleteSql);
439 }
440
441 protected function getTableIdColumns()
442 {
443 $columns = array();
444
445 foreach ($this->logTablesProvider->getAllLogTables() as $logTable) {
446 $idColumn = $logTable->getIdColumn();
447
448 if (!empty($idColumn)) {
449 $columns[$logTable->getName()] = $idColumn;
450 }
451 }
452
453 return $columns;
454 }
455 }
456