1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
|
<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik;
use Exception;
use Piwik\Db\Adapter;
use Piwik\Db\Schema;
use Piwik\Db\TransactionalDatabaseInterface;
use Piwik\Db\TransactionalDatabaseStaticTrait;
/**
* Contains SQL related helper functions for Piwik's MySQL database.
*
* Plugins should always use this class to execute SQL against the database.
*
* ### Examples
*
* $rows = Db::fetchAll("SELECT col1, col2 FROM mytable WHERE thing = ?", array('thingvalue'));
* foreach ($rows as $row) {
* doSomething($row['col1'], $row['col2']);
* }
*
* $value = Db::fetchOne("SELECT MAX(col1) FROM mytable");
* doSomethingElse($value);
*
* Db::query("DELETE FROM mytable WHERE id < ?", array(23));
*
* @api
*/
class Db implements TransactionalDatabaseInterface
{
use TransactionalDatabaseStaticTrait;
public const SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';
private static $connection = null;
private static $readerConnection = null;
private static $logQueries = true;
/**
* Returns the database connection and creates it if it hasn't been already.
*
* @return \Piwik\Tracker\Db|\Piwik\Db\AdapterInterface|\Piwik\Db
*/
public static function get()
{
if (SettingsServer::isTrackerApiRequest()) {
return Tracker::getDatabase();
}
if (!self::hasDatabaseObject()) {
self::createDatabaseObject();
}
return self::$connection;
}
/**
* @internal
* @ignore
* @return bool
*/
public static function hasReaderConfigured()
{
$readerConfig = Config::getInstance()->database_reader;
return !empty($readerConfig['host']);
}
/**
* Returns the database connection and creates it if it hasn't been already. Make sure to not write any data on
* the reader and only use the connection to read data.
*
* @since Matomo 3.12
*
* @return \Piwik\Tracker\Db|\Piwik\Db\AdapterInterface|\Piwik\Db
*/
public static function getReader()
{
if (!self::hasReaderConfigured()) {
return self::get();
}
if (!self::hasReaderDatabaseObject()) {
self::createReaderDatabaseObject();
}
return self::$readerConnection;
}
/**
* Returns an array with the Database connection information.
*
* @param array|null $dbConfig
* @return array
*/
public static function getDatabaseConfig($dbConfig = null)
{
$config = Config::getInstance();
if (is_null($dbConfig)) {
$dbConfig = $config->database;
}
/**
* Triggered before a database connection is established.
*
* This event can be used to change the settings used to establish a connection.
*
* @param array *$dbInfos Reference to an array containing database connection info,
* including:
*
* - **host**: The host name or IP address to the MySQL database.
* - **username**: The username to use when connecting to the
* database.
* - **password**: The password to use when connecting to the
* database.
* - **dbname**: The name of the Piwik MySQL database.
* - **port**: The MySQL database port to use.
* - **adapter**: either `'PDO\MYSQL'` or `'MYSQLI'`
* - **type**: The MySQL engine to use, for instance 'InnoDB'
*/
Piwik::postEvent('Db.getDatabaseConfig', array(&$dbConfig));
$dbConfig['profiler'] = @$config->Debug['enable_sql_profiler'];
return $dbConfig;
}
/**
* For tests only.
* @param $connection
* @ignore
* @internal
*/
public static function setDatabaseObject($connection)
{
self::$connection = $connection;
}
/**
* Connects to the database.
*
* Shouldn't be called directly, use {@link get()} instead.
*
* @param array|null $dbConfig Connection parameters in an array. Defaults to the `[database]`
* INI config section.
*/
public static function createDatabaseObject($dbConfig = null)
{
$dbConfig = self::getDatabaseConfig($dbConfig);
$db = @Adapter::factory($dbConfig['adapter'], $dbConfig);
self::$connection = $db;
}
/**
* Connects to the reader database.
*
* Shouldn't be called directly, use {@link get()} instead.
*
* @param array|null $dbConfig Connection parameters in an array. Defaults to the `[database]`
* INI config section.
*
* @since Matomo 3.12
*/
public static function createReaderDatabaseObject($dbConfig = null)
{
if (!isset($dbConfig)) {
$dbConfig = Config::getInstance()->database_reader;
}
$masterDbConfig = self::getDatabaseConfig();
$dbConfig = self::getDatabaseConfig($dbConfig);
$dbConfig['adapter'] = $masterDbConfig['adapter'];
$dbConfig['schema'] = $masterDbConfig['schema'];
$dbConfig['type'] = $masterDbConfig['type'];
$dbConfig['tables_prefix'] = $masterDbConfig['tables_prefix'];
$dbConfig['charset'] = $masterDbConfig['charset'];
$dbConfig['collation'] = $masterDbConfig['collation'] ?? null;
$db = @Adapter::factory($dbConfig['adapter'], $dbConfig);
if (!empty($dbConfig['aurora_readonly_read_committed'])) {
$db->exec('set session aurora_read_replica_read_committed = ON;set session transaction isolation level read committed;');
}
self::$readerConnection = $db;
}
/**
* Detect whether a database object is initialized / created or not.
*
* @internal
*/
public static function hasDatabaseObject()
{
return isset(self::$connection);
}
/**
* Detect whether a database object is initialized / created or not.
*
* @internal
*/
public static function hasReaderDatabaseObject()
{
return isset(self::$readerConnection);
}
/**
* Disconnects and destroys the database connection.
*
* For tests.
*/
public static function destroyDatabaseObject()
{
if (self::hasDatabaseObject()) {
DbHelper::disconnectDatabase();
}
self::$connection = null;
if (self::hasReaderDatabaseObject()) {
self::$readerConnection->closeConnection();
}
self::$readerConnection = null;
}
/**
* Executes an unprepared SQL query. Recommended for DDL statements like `CREATE`,
* `DROP` and `ALTER`. The return value is DBMS-specific. For MySQLI, it returns the
* number of rows affected. For PDO, it returns a
* [Zend_Db_Statement](https://framework.zend.com/manual/1.12/en/zend.db.statement.html) object.
*
* @param string $sql The SQL query.
* @throws \Exception If there is an error in the SQL.
* @return integer|\Zend_Db_Statement
*/
public static function exec($sql)
{
/** @var \Zend_Db_Adapter_Abstract $db */
$db = self::get();
$profiler = $db->getProfiler();
$q = $profiler->queryStart($sql, \Zend_Db_Profiler::INSERT);
try {
self::logSql(__FUNCTION__, $sql);
$return = self::get()->exec($sql);
} catch (Exception $ex) {
self::logExtraInfoIfDeadlock($ex);
throw $ex;
}
$profiler->queryEnd($q);
return $return;
}
/**
* Executes an SQL query and returns the [Zend_Db_Statement](https://framework.zend.com/manual/1.12/en/zend.db.statement.html)
* for the query.
*
* This method is meant for non-query SQL statements like `INSERT` and `UPDATE. If you want to fetch
* data from the DB you should use one of the fetch... functions.
*
* @param string $sql The SQL query.
* @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
* @throws \Exception If there is a problem with the SQL or bind parameters.
* @return \Zend_Db_Statement
*/
public static function query($sql, $parameters = array())
{
try {
self::logSql(__FUNCTION__, $sql, $parameters);
return self::get()->query($sql, $parameters);
} catch (Exception $ex) {
self::logExtraInfoIfDeadlock($ex);
throw $ex;
}
}
/**
* Executes a callback with potential recovery from a "MySQL server has gone away" error.
*
* If the callback throws a "MySQL server has gone away" exception
* it will be called again after a single reconnection attempt.
*
*
* @return mixed
*
* @throws Exception
*
* @internal
*/
public static function executeWithDatabaseWriterReconnectionAttempt(callable $callback)
{
try {
return $callback();
} catch (Exception $ex) {
// only attempt reconnection in a reader/writer configuration
if (!self::hasReaderConfigured()) {
throw $ex;
}
// only attempt reconnection if we encounter a "server has gone away" error
if (
!self::get()->isErrNo($ex, Updater\Migration\Db::ERROR_CODE_MYSQL_SERVER_HAS_GONE_AWAY)
&& false === stripos($ex->getMessage(), 'server has gone away')
) {
throw $ex;
}
// reconnect and retry query
// after a 100ms wait (to avoid re-hitting a network problem immediately)
self::$connection = null;
usleep(100 * 1000);
self::createDatabaseObject();
return $callback();
}
}
/**
* Executes an SQL `SELECT` statement and returns all fetched rows from the result set.
*
* @param string $sql The SQL query.
* @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
* @throws \Exception If there is a problem with the SQL or bind parameters.
* @return array The fetched rows, each element is an associative array mapping column names
* with column values.
*/
public static function fetchAll($sql, $parameters = array())
{
try {
self::logSql(__FUNCTION__, $sql, $parameters);
return self::get()->fetchAll($sql, $parameters);
} catch (Exception $ex) {
self::logExtraInfoIfDeadlock($ex);
throw $ex;
}
}
/**
* Executes an SQL `SELECT` statement and returns the first row of the result set.
*
* @param string $sql The SQL query.
* @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
* @throws \Exception If there is a problem with the SQL or bind parameters.
* @return array The fetched row, each element is an associative array mapping column names
* with column values.
*/
public static function fetchRow($sql, $parameters = array())
{
try {
self::logSql(__FUNCTION__, $sql, $parameters);
return self::get()->fetchRow($sql, $parameters);
} catch (Exception $ex) {
self::logExtraInfoIfDeadlock($ex);
throw $ex;
}
}
/**
* Executes an SQL `SELECT` statement and returns the first column value of the first
* row in the result set.
*
* @param string $sql The SQL query.
* @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
* @throws \Exception If there is a problem with the SQL or bind parameters.
* @return string
*/
public static function fetchOne($sql, $parameters = array())
{
try {
self::logSql(__FUNCTION__, $sql, $parameters);
return self::get()->fetchOne($sql, $parameters);
} catch (Exception $ex) {
self::logExtraInfoIfDeadlock($ex);
throw $ex;
}
}
/**
* Executes an SQL `SELECT` statement and returns the entire result set indexed by the first
* selected field.
*
* @param string $sql The SQL query.
* @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
* @throws \Exception If there is a problem with the SQL or bind parameters.
* @return array eg,
* ```
* array('col1value1' => array('col2' => '...', 'col3' => ...),
* 'col1value2' => array('col2' => '...', 'col3' => ...))
* ```
*/
public static function fetchAssoc($sql, $parameters = array())
{
try {
self::logSql(__FUNCTION__, $sql, $parameters);
return self::get()->fetchAssoc($sql, $parameters);
} catch (Exception $ex) {
self::logExtraInfoIfDeadlock($ex);
throw $ex;
}
}
/**
* Deletes all desired rows in a table, while using a limit. This function will execute many
* DELETE queries until there are no more rows to delete.
*
* Use this function when you need to delete many thousands of rows from a table without
* locking the table for too long.
*
* **Example**
*
* // delete all visit rows whose ID is less than a certain value, 100000 rows at a time
* $idVisit = // ...
* Db::deleteAllRows(Common::prefixTable('log_visit'), "WHERE idvisit <= ?", "idvisit ASC", 100000, array($idVisit));
*
* @param string $table The name of the table to delete from. Must be prefixed (see {@link Piwik\Common::prefixTable()}).
* @param string $where The where clause of the query. Must include the WHERE keyword.
* @param string $orderBy The column to order by and the order by direction, eg, `idvisit ASC`.
* @param int $maxRowsPerQuery The maximum number of rows to delete per `DELETE` query.
* @param array $parameters Parameters to bind for each query.
* @return int The total number of rows deleted.
*/
public static function deleteAllRows($table, $where, $orderBy, $maxRowsPerQuery = 100000, $parameters = array())
{
$orderByClause = $orderBy ? "ORDER BY $orderBy" : "";
$sql = "DELETE FROM $table $where $orderByClause
LIMIT " . (int)$maxRowsPerQuery;
// delete rows w/ a limit
$totalRowsDeleted = 0;
do {
$rowsDeleted = self::query($sql, $parameters)->rowCount();
$totalRowsDeleted += $rowsDeleted;
} while ($rowsDeleted >= $maxRowsPerQuery);
return $totalRowsDeleted;
}
/**
* Runs an `OPTIMIZE TABLE` query on the supplied table or tables.
*
* Tables will only be optimized if the `[General] enable_sql_optimize_queries` INI config option is
* set to **1**.
*
* @param string|array $tables The name of the table to optimize or an array of tables to optimize.
* Table names must be prefixed (see {@link Piwik\Common::prefixTable()}).
* @param bool $force If true, the `OPTIMIZE TABLE` query will be run even if InnoDB tables are being used.
* @return bool
* @deprecated will be removed in Matomo 6
* use Schema::getInstance()->optimizeTables() instead
*/
public static function optimizeTables($tables, $force = false)
{
if (empty($tables)) {
return false;
}
$tables = !is_array($tables) ? [$tables] : $tables;
return Schema::getInstance()->optimizeTables($tables, (bool) $force);
}
/**
* Drops the supplied table or tables.
*
* @param string|array $tables The name of the table to drop or an array of table names to drop.
* Table names must be prefixed (see {@link Piwik\Common::prefixTable()}).
* @return \Zend_Db_Statement
*/
public static function dropTables($tables)
{
if (!is_array($tables)) {
$tables = array($tables);
}
return self::query("DROP TABLE `" . implode('`,`', $tables) . "`");
}
/**
* Drops all tables
*/
public static function dropAllTables()
{
$tablesAlreadyInstalled = DbHelper::getTablesInstalled();
self::dropTables($tablesAlreadyInstalled);
}
/**
* Locks the supplied table or tables.
*
* **NOTE:** Piwik does not require the `LOCK TABLES` privilege to be available. Piwik
* should still work if it has not been granted.
*
* @param string|array $tablesToRead The table or tables to obtain 'read' locks on. Table names must
* be prefixed (see {@link Piwik\Common::prefixTable()}).
* @param string|array $tablesToWrite The table or tables to obtain 'write' locks on. Table names must
* be prefixed (see {@link Piwik\Common::prefixTable()}).
* @return \Zend_Db_Statement
*/
public static function lockTables($tablesToRead, $tablesToWrite = array())
{
if (!is_array($tablesToRead)) {
$tablesToRead = array($tablesToRead);
}
if (!is_array($tablesToWrite)) {
$tablesToWrite = array($tablesToWrite);
}
$lockExprs = array();
foreach ($tablesToWrite as $table) {
$lockExprs[] = $table . " WRITE";
}
foreach ($tablesToRead as $table) {
$lockExprs[] = $table . " READ";
}
return self::exec("LOCK TABLES " . implode(', ', $lockExprs));
}
/**
* Releases all table locks.
*
* **NOTE:** Piwik does not require the `LOCK TABLES` privilege to be available. Piwik
* should still work if it has not been granted.
*
* @return \Zend_Db_Statement
*/
public static function unlockAllTables()
{
return self::exec("UNLOCK TABLES");
}
/**
* Performs a `SELECT` statement on a table one chunk at a time and returns the first
* successfully fetched value.
*
* This function will execute a query on one set of rows in a table. If nothing
* is fetched, it will execute the query on the next set of rows and so on until
* the query returns a value.
*
* This function will break up a `SELECT into several smaller `SELECT`s and
* should be used when performing a `SELECT` that can take a long time to finish.
* Using several smaller `SELECT`s will ensure that the table will not be locked
* for too long.
*
* **Example**
*
* // find the most recent visit that is older than a certain date
* $dateStart = // ...
* $sql = "SELECT idvisit
* FROM $logVisit
* WHERE '$dateStart' > visit_last_action_time
* AND idvisit <= ?
* AND idvisit > ?
* ORDER BY idvisit DESC
* LIMIT 1";
*
* // since visits
* return Db::segmentedFetchFirst($sql, $maxIdVisit, 0, -self::$selectSegmentSize);
*
* @param string $sql The SQL to perform. The last two conditions of the `WHERE`
* expression must be as follows: `'id >= ? AND id < ?'` where
* **id** is the int id of the table.
* @param int $first The minimum ID to loop from.
* @param int $last The maximum ID to loop to.
* @param int $step The maximum number of rows to scan in one query.
* @param array $params Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`
*
* @return string
*/
public static function segmentedFetchFirst($sql, $first, $last, $step, $params = array())
{
$result = false;
if ($step > 0) {
for ($i = $first; $result === false && $i <= $last; $i += $step) {
$result = self::fetchOne($sql, array_merge($params, array($i, $i + $step)));
}
} else {
for ($i = $first; $result === false && $i >= $last; $i += $step) {
$result = self::fetchOne($sql, array_merge($params, array($i, $i + $step)));
}
}
return $result;
}
/**
* Performs a `SELECT` on a table one chunk at a time and returns an array
* of every fetched value.
*
* This function will break up a `SELECT` query into several smaller queries by
* using only a limited number of rows at a time. It will accumulate the results
* of each smaller query and return the result.
*
* This function should be used when performing a `SELECT` that can
* take a long time to finish. Using several smaller queries will ensure that
* the table will not be locked for too long.
*
* @param string $sql The SQL to perform. The last two conditions of the `WHERE`
* expression must be as follows: `'id >= ? AND id < ?'` where
* **id** is the int id of the table.
* @param int $first The minimum ID to loop from.
* @param int $last The maximum ID to loop to.
* @param int $step The maximum number of rows to scan in one query.
* @param array $params Parameters to bind in the query, `array(param1 => value1, param2 => value2)`
* @return array An array of primitive values.
*/
public static function segmentedFetchOne($sql, $first, $last, $step, $params = array())
{
$result = array();
if ($step > 0) {
for ($i = $first; $i <= $last; $i += $step) {
$result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step)));
}
} else {
for ($i = $first; $i >= $last; $i += $step) {
$result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step)));
}
}
return $result;
}
/**
* Performs a SELECT on a table one chunk at a time and returns an array
* of every fetched row.
*
* This function will break up a `SELECT` query into several smaller queries by
* using only a limited number of rows at a time. It will accumulate the results
* of each smaller query and return the result.
*
* This function should be used when performing a `SELECT` that can
* take a long time to finish. Using several smaller queries will ensure that
* the table will not be locked for too long.
*
* @param string $sql The SQL to perform. The last two conditions of the `WHERE`
* expression must be as follows: `'id >= ? AND id < ?'` where
* **id** is the int id of the table.
* @param int $first The minimum ID to loop from.
* @param int $last The maximum ID to loop to.
* @param int $step The maximum number of rows to scan in one query.
* @param array $params Parameters to bind in the query, array( param1 => value1, param2 => value2)
* @return array An array of rows that includes the result set of every smaller
* query.
*/
public static function segmentedFetchAll($sql, $first, $last, $step, $params = array())
{
$result = array();
if ($step > 0) {
for ($i = $first; $i <= $last; $i += $step) {
$currentParams = array_merge($params, array($i, $i + $step));
$result = array_merge($result, self::fetchAll($sql, $currentParams));
}
} else {
for ($i = $first; $i >= $last; $i += $step) {
$currentParams = array_merge($params, array($i, $i + $step));
$result = array_merge($result, self::fetchAll($sql, $currentParams));
}
}
return $result;
}
/**
* Performs a `UPDATE` or `DELETE` statement on a table one chunk at a time.
*
* This function will break up a query into several smaller queries by
* using only a limited number of rows at a time.
*
* This function should be used when executing a non-query statement will
* take a long time to finish. Using several smaller queries will ensure that
* the table will not be locked for too long.
*
* @param string $sql The SQL to perform. The last two conditions of the `WHERE`
* expression must be as follows: `'id >= ? AND id < ?'` where
* **id** is the int id of the table.
* @param int $first The minimum ID to loop from.
* @param int $last The maximum ID to loop to.
* @param int $step The maximum number of rows to scan in one query.
* @param array $params Parameters to bind in the query, `array(param1 => value1, param2 => value2)`
*/
public static function segmentedQuery($sql, $first, $last, $step, $params = array())
{
if ($step > 0) {
for ($i = $first; $i <= $last; $i += $step) {
$currentParams = array_merge($params, array($i, $i + $step));
self::query($sql, $currentParams);
}
} else {
for ($i = $first; $i >= $last; $i += $step) {
$currentParams = array_merge($params, array($i, $i + $step));
self::query($sql, $currentParams);
}
}
}
/**
* Attempts to get a named lock. This function uses a timeout of 1s, but will
* retry a set number of times.
*
* @param string $lockName The lock name.
* @param int $maxRetries The max number of times to retry.
* @return bool `true` if the lock was obtained, `false` if otherwise.
* @throws \Exception if Lock name is too long
*/
public static function getDbLock($lockName, $maxRetries = 30)
{
if (strlen($lockName) > 64) {
throw new \Exception('DB lock name has to be 64 characters or less for MySQL 5.7 compatibility.');
}
/*
* the server (e.g., shared hosting) may have a low wait timeout
* so instead of a single GET_LOCK() with a 30 second timeout,
* we use a 1 second timeout and loop, to avoid losing our MySQL
* connection
*/
$sql = 'SELECT GET_LOCK(?, 1)';
$db = self::get();
while ($maxRetries > 0) {
$result = $db->fetchOne($sql, array($lockName));
if ($result == '1') {
return true;
}
$maxRetries--;
}
return false;
}
/**
* Releases a named lock.
*
* @param string $lockName The lock name.
* @return bool `true` if the lock was released, `false` if otherwise.
*/
public static function releaseDbLock($lockName)
{
$sql = 'SELECT RELEASE_LOCK(?)';
$db = self::get();
return $db->fetchOne($sql, array($lockName)) == '1';
}
/**
* Cached result of isLockprivilegeGranted function.
*
* Public so tests can simulate the situation where the lock tables privilege isn't granted.
*
* @var bool
* @ignore
*/
public static $lockPrivilegeGranted = null;
/**
* Checks whether the database user is allowed to lock tables.
*
* @return bool
*/
public static function isLockPrivilegeGranted()
{
if (is_null(self::$lockPrivilegeGranted)) {
try {
Db::lockTables(Common::prefixTable('site_url'));
Db::unlockAllTables();
self::$lockPrivilegeGranted = true;
} catch (Exception $ex) {
self::$lockPrivilegeGranted = false;
}
}
return self::$lockPrivilegeGranted;
}
private static function logExtraInfoIfDeadlock($ex)
{
if (
!self::get()->isErrNo($ex, 1213)
&& !self::get()->isErrNo($ex, 1205)
) {
return;
}
try {
$deadlockInfo = self::fetchAll("SHOW ENGINE INNODB STATUS");
// log using exception so backtrace appears in log output
Log::debug(new Exception("Encountered deadlock: " . print_r($deadlockInfo, true)));
} catch (\Exception $e) {
// 1227 Access denied; you need (at least one of) the PROCESS privilege(s) for this operation
}
}
private static function logSql($functionName, $sql, $parameters = array())
{
self::checkBoundParametersIfInDevMode($sql, $parameters);
if (
self::$logQueries === false
|| @Config::getInstance()->Debug['log_sql_queries'] != 1
) {
return;
}
// NOTE: at the moment we don't log parameters in order to avoid sensitive information leaks
Log::debug("Db::%s() executing SQL: %s", $functionName, $sql);
}
private static function checkBoundParametersIfInDevMode($sql, $parameters)
{
if (!Development::isEnabled()) {
return;
}
if (!is_array($parameters)) {
$parameters = [$parameters];
}
foreach ($parameters as $index => $parameter) {
if ($parameter instanceof Date) {
throw new \Exception("Found bound parameter (index = $index) is Date instance which will not work correctly in following SQL: $sql");
}
}
}
/**
* @param bool $enable
*/
public static function enableQueryLog($enable)
{
self::$logQueries = $enable;
}
/**
* @return boolean
*/
public static function isQueryLogEnabled()
{
return self::$logQueries;
}
/**
* @deprecated will be removed with Matomo 6
* use Schema::getInstance()->isOptimizeInnoDBSupported() instead
*/
public static function isOptimizeInnoDBSupported($version = null)
{
return Db\Schema::getInstance()->isOptimizeInnoDBSupported();
}
}
|