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