PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Db.php

https://github.com/CodeYellowBV/piwik
PHP | 671 lines | 281 code | 57 blank | 333 comment | 38 complexity | 16351ca6a4f5e1dcbc9c32f318843c92 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Exception;
  11. use Piwik\Db\Adapter;
  12. use Piwik\Tracker;
  13. /**
  14. * Contains SQL related helper functions for Piwik's MySQL database.
  15. *
  16. * Plugins should always use this class to execute SQL against the database.
  17. *
  18. * ### Examples
  19. *
  20. * $rows = Db::fetchAll("SELECT col1, col2 FROM mytable WHERE thing = ?", array('thingvalue'));
  21. * foreach ($rows as $row) {
  22. * doSomething($row['col1'], $row['col2']);
  23. * }
  24. *
  25. * $value = Db::fetchOne("SELECT MAX(col1) FROM mytable");
  26. * doSomethingElse($value);
  27. *
  28. * Db::query("DELETE FROM mytable WHERE id < ?", array(23));
  29. *
  30. * @api
  31. */
  32. class Db
  33. {
  34. private static $connection = null;
  35. /**
  36. * Returns the database connection and creates it if it hasn't been already.
  37. *
  38. * @return \Piwik\Tracker\Db|\Piwik\Db\AdapterInterface|\Piwik\Db
  39. */
  40. public static function get()
  41. {
  42. if (SettingsServer::isTrackerApiRequest()) {
  43. return Tracker::getDatabase();
  44. }
  45. if (self::$connection === null) {
  46. self::createDatabaseObject();
  47. }
  48. return self::$connection;
  49. }
  50. public static function getDatabaseConfig($dbConfig = null)
  51. {
  52. $config = Config::getInstance();
  53. if (is_null($dbConfig)) {
  54. $dbConfig = $config->database;
  55. }
  56. /**
  57. * Triggered before a database connection is established.
  58. *
  59. * This event can be used to change the settings used to establish a connection.
  60. *
  61. * @param array *$dbInfos Reference to an array containing database connection info,
  62. * including:
  63. *
  64. * - **host**: The host name or IP address to the MySQL database.
  65. * - **username**: The username to use when connecting to the
  66. * database.
  67. * - **password**: The password to use when connecting to the
  68. * database.
  69. * - **dbname**: The name of the Piwik MySQL database.
  70. * - **port**: The MySQL database port to use.
  71. * - **adapter**: either `'PDO_MYSQL'` or `'MYSQLI'`
  72. * - **type**: The MySQL engine to use, for instance 'InnoDB'
  73. */
  74. Piwik::postEvent('Db.getDatabaseConfig', array(&$dbConfig));
  75. $dbConfig['profiler'] = $config->Debug['enable_sql_profiler'];
  76. return $dbConfig;
  77. }
  78. /**
  79. * Connects to the database.
  80. *
  81. * Shouldn't be called directly, use {@link get()} instead.
  82. *
  83. * @param array|null $dbConfig Connection parameters in an array. Defaults to the `[database]`
  84. * INI config section.
  85. */
  86. public static function createDatabaseObject($dbConfig = null)
  87. {
  88. $dbConfig = self::getDatabaseConfig($dbConfig);
  89. $db = @Adapter::factory($dbConfig['adapter'], $dbConfig);
  90. self::$connection = $db;
  91. }
  92. /**
  93. * Disconnects and destroys the database connection.
  94. *
  95. * For tests.
  96. */
  97. public static function destroyDatabaseObject()
  98. {
  99. DbHelper::disconnectDatabase();
  100. self::$connection = null;
  101. }
  102. /**
  103. * Executes an unprepared SQL query. Recommended for DDL statements like `CREATE`,
  104. * `DROP` and `ALTER`. The return value is DBMS-specific. For MySQLI, it returns the
  105. * number of rows affected. For PDO, it returns a
  106. * [Zend_Db_Statement](http://framework.zend.com/manual/1.12/en/zend.db.statement.html) object.
  107. *
  108. * @param string $sql The SQL query.
  109. * @throws \Exception If there is an error in the SQL.
  110. * @return integer|\Zend_Db_Statement
  111. */
  112. static public function exec($sql)
  113. {
  114. /** @var \Zend_Db_Adapter_Abstract $db */
  115. $db = self::get();
  116. $profiler = $db->getProfiler();
  117. $q = $profiler->queryStart($sql, \Zend_Db_Profiler::INSERT);
  118. try {
  119. $return = self::get()->exec($sql);
  120. } catch (Exception $ex) {
  121. self::logExtraInfoIfDeadlock($ex);
  122. throw $ex;
  123. }
  124. $profiler->queryEnd($q);
  125. return $return;
  126. }
  127. /**
  128. * Executes an SQL query and returns the [Zend_Db_Statement](http://framework.zend.com/manual/1.12/en/zend.db.statement.html)
  129. * for the query.
  130. *
  131. * This method is meant for non-query SQL statements like `INSERT` and `UPDATE. If you want to fetch
  132. * data from the DB you should use one of the fetch... functions.
  133. *
  134. * @param string $sql The SQL query.
  135. * @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
  136. * @throws \Exception If there is a problem with the SQL or bind parameters.
  137. * @return \Zend_Db_Statement
  138. */
  139. static public function query($sql, $parameters = array())
  140. {
  141. try {
  142. return self::get()->query($sql, $parameters);
  143. } catch (Exception $ex) {
  144. self::logExtraInfoIfDeadlock($ex);
  145. throw $ex;
  146. }
  147. }
  148. /**
  149. * Executes an SQL `SELECT` statement and returns all fetched rows from the result set.
  150. *
  151. * @param string $sql The SQL query.
  152. * @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
  153. * @throws \Exception If there is a problem with the SQL or bind parameters.
  154. * @return array The fetched rows, each element is an associative array mapping column names
  155. * with column values.
  156. */
  157. static public function fetchAll($sql, $parameters = array())
  158. {
  159. try {
  160. return self::get()->fetchAll($sql, $parameters);
  161. } catch (Exception $ex) {
  162. self::logExtraInfoIfDeadlock($ex);
  163. throw $ex;
  164. }
  165. }
  166. /**
  167. * Executes an SQL `SELECT` statement and returns the first row of the result set.
  168. *
  169. * @param string $sql The SQL query.
  170. * @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
  171. * @throws \Exception If there is a problem with the SQL or bind parameters.
  172. * @return array The fetched row, each element is an associative array mapping column names
  173. * with column values.
  174. */
  175. static public function fetchRow($sql, $parameters = array())
  176. {
  177. try {
  178. return self::get()->fetchRow($sql, $parameters);
  179. } catch (Exception $ex) {
  180. self::logExtraInfoIfDeadlock($ex);
  181. throw $ex;
  182. }
  183. }
  184. /**
  185. * Executes an SQL `SELECT` statement and returns the first column value of the first
  186. * row in the result set.
  187. *
  188. * @param string $sql The SQL query.
  189. * @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
  190. * @throws \Exception If there is a problem with the SQL or bind parameters.
  191. * @return string
  192. */
  193. static public function fetchOne($sql, $parameters = array())
  194. {
  195. try {
  196. return self::get()->fetchOne($sql, $parameters);
  197. } catch (Exception $ex) {
  198. self::logExtraInfoIfDeadlock($ex);
  199. throw $ex;
  200. }
  201. }
  202. /**
  203. * Executes an SQL `SELECT` statement and returns the entire result set indexed by the first
  204. * selected field.
  205. *
  206. * @param string $sql The SQL query.
  207. * @param array $parameters Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`.
  208. * @throws \Exception If there is a problem with the SQL or bind parameters.
  209. * @return array eg,
  210. * ```
  211. * array('col1value1' => array('col2' => '...', 'col3' => ...),
  212. * 'col1value2' => array('col2' => '...', 'col3' => ...))
  213. * ```
  214. */
  215. static public function fetchAssoc($sql, $parameters = array())
  216. {
  217. try {
  218. return self::get()->fetchAssoc($sql, $parameters);
  219. } catch (Exception $ex) {
  220. self::logExtraInfoIfDeadlock($ex);
  221. throw $ex;
  222. }
  223. }
  224. /**
  225. * Deletes all desired rows in a table, while using a limit. This function will execute many
  226. * DELETE queries until there are no more rows to delete.
  227. *
  228. * Use this function when you need to delete many thousands of rows from a table without
  229. * locking the table for too long.
  230. *
  231. * **Example**
  232. *
  233. * // delete all visit rows whose ID is less than a certain value, 100000 rows at a time
  234. * $idVisit = // ...
  235. * Db::deleteAllRows(Common::prefixTable('log_visit'), "WHERE idvisit <= ?", "idvisit ASC", 100000, array($idVisit));
  236. *
  237. * @param string $table The name of the table to delete from. Must be prefixed (see {@link Piwik\Common::prefixTable()}).
  238. * @param string $where The where clause of the query. Must include the WHERE keyword.
  239. * @param $orderBy The column to order by and the order by direction, eg, `idvisit ASC`.
  240. * @param int $maxRowsPerQuery The maximum number of rows to delete per `DELETE` query.
  241. * @param array $parameters Parameters to bind for each query.
  242. * @return int The total number of rows deleted.
  243. */
  244. static public function deleteAllRows($table, $where, $orderBy, $maxRowsPerQuery = 100000, $parameters = array())
  245. {
  246. $orderByClause = $orderBy ? "ORDER BY $orderBy" : "";
  247. $sql = "DELETE FROM $table
  248. $where
  249. $orderByClause
  250. LIMIT " . (int)$maxRowsPerQuery;
  251. // delete rows w/ a limit
  252. $totalRowsDeleted = 0;
  253. do {
  254. $rowsDeleted = self::query($sql, $parameters)->rowCount();
  255. $totalRowsDeleted += $rowsDeleted;
  256. } while ($rowsDeleted >= $maxRowsPerQuery);
  257. return $totalRowsDeleted;
  258. }
  259. /**
  260. * Runs an `OPTIMIZE TABLE` query on the supplied table or tables.
  261. *
  262. * Tables will only be optimized if the `[General] enable_sql_optimize_queries` INI config option is
  263. * set to **1**.
  264. *
  265. * @param string|array $tables The name of the table to optimize or an array of tables to optimize.
  266. * Table names must be prefixed (see {@link Piwik\Common::prefixTable()}).
  267. * @return \Zend_Db_Statement
  268. */
  269. static public function optimizeTables($tables)
  270. {
  271. $optimize = Config::getInstance()->General['enable_sql_optimize_queries'];
  272. if (empty($optimize)) {
  273. return;
  274. }
  275. if (empty($tables)) {
  276. return false;
  277. }
  278. if (!is_array($tables)) {
  279. $tables = array($tables);
  280. }
  281. // filter out all InnoDB tables
  282. $myisamDbTables = array();
  283. foreach (Db::fetchAll("SHOW TABLE STATUS") as $row) {
  284. if (strtolower($row['Engine']) == 'myisam'
  285. && in_array($row['Name'], $tables)
  286. ) {
  287. $myisamDbTables[] = $row['Name'];
  288. }
  289. }
  290. if (empty($myisamDbTables)) {
  291. return false;
  292. }
  293. // optimize the tables
  294. return self::query("OPTIMIZE TABLE " . implode(',', $myisamDbTables));
  295. }
  296. /**
  297. * Drops the supplied table or tables.
  298. *
  299. * @param string|array $tables The name of the table to drop or an array of table names to drop.
  300. * Table names must be prefixed (see {@link Piwik\Common::prefixTable()}).
  301. * @return \Zend_Db_Statement
  302. */
  303. static public function dropTables($tables)
  304. {
  305. if (!is_array($tables)) {
  306. $tables = array($tables);
  307. }
  308. return self::query("DROP TABLE " . implode(',', $tables));
  309. }
  310. /**
  311. * Drops all tables
  312. */
  313. static public function dropAllTables()
  314. {
  315. $tablesAlreadyInstalled = DbHelper::getTablesInstalled();
  316. self::dropTables($tablesAlreadyInstalled);
  317. }
  318. /**
  319. * Get columns information from table
  320. *
  321. * @param string|array $table The name of the table you want to get the columns definition for.
  322. * @return \Zend_Db_Statement
  323. */
  324. static public function getColumnNamesFromTable($table)
  325. {
  326. $columns = self::fetchAll("SHOW COLUMNS FROM " . $table);
  327. $columnNames = array();
  328. foreach ($columns as $column) {
  329. $columnNames[] = $column['Field'];
  330. }
  331. return $columnNames;
  332. }
  333. /**
  334. * Locks the supplied table or tables.
  335. *
  336. * **NOTE:** Piwik does not require the `LOCK TABLES` privilege to be available. Piwik
  337. * should still work if it has not been granted.
  338. *
  339. * @param string|array $tablesToRead The table or tables to obtain 'read' locks on. Table names must
  340. * be prefixed (see {@link Piwik\Common::prefixTable()}).
  341. * @param string|array $tablesToWrite The table or tables to obtain 'write' locks on. Table names must
  342. * be prefixed (see {@link Piwik\Common::prefixTable()}).
  343. * @return \Zend_Db_Statement
  344. */
  345. static public function lockTables($tablesToRead, $tablesToWrite = array())
  346. {
  347. if (!is_array($tablesToRead)) {
  348. $tablesToRead = array($tablesToRead);
  349. }
  350. if (!is_array($tablesToWrite)) {
  351. $tablesToWrite = array($tablesToWrite);
  352. }
  353. $lockExprs = array();
  354. foreach ($tablesToWrite as $table) {
  355. $lockExprs[] = $table . " WRITE";
  356. }
  357. foreach ($tablesToRead as $table) {
  358. $lockExprs[] = $table . " READ";
  359. }
  360. return self::exec("LOCK TABLES " . implode(', ', $lockExprs));
  361. }
  362. /**
  363. * Releases all table locks.
  364. *
  365. * **NOTE:** Piwik does not require the `LOCK TABLES` privilege to be available. Piwik
  366. * should still work if it has not been granted.
  367. *
  368. * @return \Zend_Db_Statement
  369. */
  370. static public function unlockAllTables()
  371. {
  372. return self::exec("UNLOCK TABLES");
  373. }
  374. /**
  375. * Performs a `SELECT` statement on a table one chunk at a time and returns the first
  376. * successfully fetched value.
  377. *
  378. * This function will execute a query on one set of rows in a table. If nothing
  379. * is fetched, it will execute the query on the next set of rows and so on until
  380. * the query returns a value.
  381. *
  382. * This function will break up a `SELECT into several smaller `SELECT`s and
  383. * should be used when performing a `SELECT` that can take a long time to finish.
  384. * Using several smaller `SELECT`s will ensure that the table will not be locked
  385. * for too long.
  386. *
  387. * **Example**
  388. *
  389. * // find the most recent visit that is older than a certain date
  390. * $dateStart = // ...
  391. * $sql = "SELECT idvisit
  392. * FROM $logVisit
  393. * WHERE '$dateStart' > visit_last_action_time
  394. * AND idvisit <= ?
  395. * AND idvisit > ?
  396. * ORDER BY idvisit DESC
  397. * LIMIT 1";
  398. *
  399. * // since visits
  400. * return Db::segmentedFetchFirst($sql, $maxIdVisit, 0, -self::$selectSegmentSize);
  401. *
  402. * @param string $sql The SQL to perform. The last two conditions of the `WHERE`
  403. * expression must be as follows: `'id >= ? AND id < ?'` where
  404. * **id** is the int id of the table.
  405. * @param int $first The minimum ID to loop from.
  406. * @param int $last The maximum ID to loop to.
  407. * @param int $step The maximum number of rows to scan in one query.
  408. * @param array $params Parameters to bind in the query, eg, `array(param1 => value1, param2 => value2)`
  409. *
  410. * @return string
  411. */
  412. static public function segmentedFetchFirst($sql, $first, $last, $step, $params = array())
  413. {
  414. $result = false;
  415. if ($step > 0) {
  416. for ($i = $first; $result === false && $i <= $last; $i += $step) {
  417. $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step)));
  418. }
  419. } else {
  420. for ($i = $first; $result === false && $i >= $last; $i += $step) {
  421. $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step)));
  422. }
  423. }
  424. return $result;
  425. }
  426. /**
  427. * Performs a `SELECT` on a table one chunk at a time and returns an array
  428. * of every fetched value.
  429. *
  430. * This function will break up a `SELECT` query into several smaller queries by
  431. * using only a limited number of rows at a time. It will accumulate the results
  432. * of each smaller query and return the result.
  433. *
  434. * This function should be used when performing a `SELECT` that can
  435. * take a long time to finish. Using several smaller queries will ensure that
  436. * the table will not be locked for too long.
  437. *
  438. * @param string $sql The SQL to perform. The last two conditions of the `WHERE`
  439. * expression must be as follows: `'id >= ? AND id < ?'` where
  440. * **id** is the int id of the table.
  441. * @param int $first The minimum ID to loop from.
  442. * @param int $last The maximum ID to loop to.
  443. * @param int $step The maximum number of rows to scan in one query.
  444. * @param array $params Parameters to bind in the query, `array(param1 => value1, param2 => value2)`
  445. * @return array An array of primitive values.
  446. */
  447. static public function segmentedFetchOne($sql, $first, $last, $step, $params = array())
  448. {
  449. $result = array();
  450. if ($step > 0) {
  451. for ($i = $first; $i <= $last; $i += $step) {
  452. $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step)));
  453. }
  454. } else {
  455. for ($i = $first; $i >= $last; $i += $step) {
  456. $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step)));
  457. }
  458. }
  459. return $result;
  460. }
  461. /**
  462. * Performs a SELECT on a table one chunk at a time and returns an array
  463. * of every fetched row.
  464. *
  465. * This function will break up a `SELECT` query into several smaller queries by
  466. * using only a limited number of rows at a time. It will accumulate the results
  467. * of each smaller query and return the result.
  468. *
  469. * This function should be used when performing a `SELECT` that can
  470. * take a long time to finish. Using several smaller queries will ensure that
  471. * the table will not be locked for too long.
  472. *
  473. * @param string $sql The SQL to perform. The last two conditions of the `WHERE`
  474. * expression must be as follows: `'id >= ? AND id < ?'` where
  475. * **id** is the int id of the table.
  476. * @param int $first The minimum ID to loop from.
  477. * @param int $last The maximum ID to loop to.
  478. * @param int $step The maximum number of rows to scan in one query.
  479. * @param array $params Parameters to bind in the query, array( param1 => value1, param2 => value2)
  480. * @return array An array of rows that includes the result set of every smaller
  481. * query.
  482. */
  483. static public function segmentedFetchAll($sql, $first, $last, $step, $params = array())
  484. {
  485. $result = array();
  486. if ($step > 0) {
  487. for ($i = $first; $i <= $last; $i += $step) {
  488. $currentParams = array_merge($params, array($i, $i + $step));
  489. $result = array_merge($result, self::fetchAll($sql, $currentParams));
  490. }
  491. } else {
  492. for ($i = $first; $i >= $last; $i += $step) {
  493. $currentParams = array_merge($params, array($i, $i + $step));
  494. $result = array_merge($result, self::fetchAll($sql, $currentParams));
  495. }
  496. }
  497. return $result;
  498. }
  499. /**
  500. * Performs a `UPDATE` or `DELETE` statement on a table one chunk at a time.
  501. *
  502. * This function will break up a query into several smaller queries by
  503. * using only a limited number of rows at a time.
  504. *
  505. * This function should be used when executing a non-query statement will
  506. * take a long time to finish. Using several smaller queries will ensure that
  507. * the table will not be locked for too long.
  508. *
  509. * @param string $sql The SQL to perform. The last two conditions of the `WHERE`
  510. * expression must be as follows: `'id >= ? AND id < ?'` where
  511. * **id** is the int id of the table.
  512. * @param int $first The minimum ID to loop from.
  513. * @param int $last The maximum ID to loop to.
  514. * @param int $step The maximum number of rows to scan in one query.
  515. * @param array $params Parameters to bind in the query, `array(param1 => value1, param2 => value2)`
  516. */
  517. static public function segmentedQuery($sql, $first, $last, $step, $params = array())
  518. {
  519. if ($step > 0) {
  520. for ($i = $first; $i <= $last; $i += $step) {
  521. $currentParams = array_merge($params, array($i, $i + $step));
  522. self::query($sql, $currentParams);
  523. }
  524. } else {
  525. for ($i = $first; $i >= $last; $i += $step) {
  526. $currentParams = array_merge($params, array($i, $i + $step));
  527. self::query($sql, $currentParams);
  528. }
  529. }
  530. }
  531. /**
  532. * Returns `true` if a table in the database, `false` if otherwise.
  533. *
  534. * @param string $tableName The name of the table to check for. Must be prefixed.
  535. * @return bool
  536. */
  537. static public function tableExists($tableName)
  538. {
  539. return self::query("SHOW TABLES LIKE ?", $tableName)->rowCount() > 0;
  540. }
  541. /**
  542. * Attempts to get a named lock. This function uses a timeout of 1s, but will
  543. * retry a set number of times.
  544. *
  545. * @param string $lockName The lock name.
  546. * @param int $maxRetries The max number of times to retry.
  547. * @return bool `true` if the lock was obtained, `false` if otherwise.
  548. */
  549. static public function getDbLock($lockName, $maxRetries = 30)
  550. {
  551. /*
  552. * the server (e.g., shared hosting) may have a low wait timeout
  553. * so instead of a single GET_LOCK() with a 30 second timeout,
  554. * we use a 1 second timeout and loop, to avoid losing our MySQL
  555. * connection
  556. */
  557. $sql = 'SELECT GET_LOCK(?, 1)';
  558. $db = self::get();
  559. while ($maxRetries > 0) {
  560. if ($db->fetchOne($sql, array($lockName)) == '1') {
  561. return true;
  562. }
  563. $maxRetries--;
  564. }
  565. return false;
  566. }
  567. /**
  568. * Releases a named lock.
  569. *
  570. * @param string $lockName The lock name.
  571. * @return bool `true` if the lock was released, `false` if otherwise.
  572. */
  573. static public function releaseDbLock($lockName)
  574. {
  575. $sql = 'SELECT RELEASE_LOCK(?)';
  576. $db = self::get();
  577. return $db->fetchOne($sql, array($lockName)) == '1';
  578. }
  579. /**
  580. * Cached result of isLockprivilegeGranted function.
  581. *
  582. * Public so tests can simulate the situation where the lock tables privilege isn't granted.
  583. *
  584. * @var bool
  585. * @ignore
  586. */
  587. public static $lockPrivilegeGranted = null;
  588. /**
  589. * Checks whether the database user is allowed to lock tables.
  590. *
  591. * @return bool
  592. */
  593. public static function isLockPrivilegeGranted()
  594. {
  595. if (is_null(self::$lockPrivilegeGranted)) {
  596. try {
  597. Db::lockTables(Common::prefixTable('log_visit'));
  598. Db::unlockAllTables();
  599. self::$lockPrivilegeGranted = true;
  600. } catch (Exception $ex) {
  601. self::$lockPrivilegeGranted = false;
  602. }
  603. }
  604. return self::$lockPrivilegeGranted;
  605. }
  606. private static function logExtraInfoIfDeadlock($ex)
  607. {
  608. if (self::get()->isErrNo($ex, 1213)) {
  609. $deadlockInfo = self::fetchAll("SHOW ENGINE INNODB STATUS");
  610. // log using exception so backtrace appears in log output
  611. Log::debug(new Exception("Encountered deadlock: " . print_r($deadlockInfo, true)));
  612. }
  613. }
  614. }