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