PageRenderTime 26ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/common/includes/class.dbcachedquery.php

https://code.google.com/
PHP | 468 lines | 298 code | 46 blank | 124 comment | 71 complexity | c95c73e057edb1bc01e55cdc983b54d2 MD5 | raw file
Possible License(s): AGPL-1.0, LGPL-2.1
  1. <?php
  2. /**
  3. * mysqli file-cached query class. Manages SQL queries to a MySQL DB using mysqli.
  4. * @package EDK
  5. */
  6. class DBCachedQuery extends DBBaseQuery
  7. {
  8. /**
  9. * @var float
  10. * this is the minimum runtime a query has to run to be
  11. * eligible for caching in seconds
  12. */
  13. protected static $minruntime = 0.1;
  14. /** @var float */
  15. protected static $maxmem = null;
  16. /**
  17. * @var integer
  18. * maximum size of a cached result set (512kB)
  19. */
  20. protected static $maxcachesize = 524288;
  21. /** @var string */
  22. protected static $location = "SQL";
  23. /** @var integer */
  24. protected static $maxage = 10800;
  25. /** @var array */
  26. protected $cache = array();
  27. /** @var array */
  28. protected $usedtables = array();
  29. /** @var boolean */
  30. protected $cached = false;
  31. /** @var boolean */
  32. protected $nocache = false;
  33. /** @var string */
  34. protected $sql = '';
  35. /** @var integer */
  36. protected $mtime = 0;
  37. /**
  38. * @var integer
  39. * The current row of the cached result.
  40. */
  41. protected $currrow = 0;
  42. /** @var MySQLi_Result|boolean */
  43. protected $resid = null;
  44. /** @var CacheHandlerHashed */
  45. protected static $cachehandler = null;
  46. /** @var array */
  47. protected static $baseTables = null;
  48. /**
  49. * Set up a mysqli cached query object with default values.
  50. *
  51. * @param boolean $nocache true to retrieve results directly from the db.
  52. */
  53. function DBCachedQuery($nocache = false)
  54. {
  55. $this->nocache = $nocache;
  56. self::$cachehandler = new CacheHandlerHashed();
  57. if(is_null(self::$maxmem))
  58. {
  59. $tmp = @ini_get('memory_limit');
  60. $tmp = @str_replace('M', '000000', $tmp) * 0.8;
  61. self::$maxmem = @intval(str_replace('G', '000000000', $tmp) * 0.8);
  62. if(!self::$maxmem) self::$maxmem = 128000000;
  63. }
  64. }
  65. /**
  66. * Check if this query has been cached and the cache valid.
  67. *
  68. * @return boolean true if this query has been cached and the cache is valid.
  69. */
  70. protected function checkCache()
  71. {
  72. // only cache selects
  73. // we don't use select ... into so there is no problem
  74. $this->sql = str_replace(array("\r\n", "\n"), ' ', $this->sql);
  75. if (strtolower(substr($this->sql, 0, 6)) != 'select' && strtolower(substr($this->sql, 0, 4)) != 'show')
  76. {
  77. // this is no select, update the table
  78. self::markAffectedTables($this->sql);
  79. return false;
  80. }
  81. if($this->nocache) return false;
  82. if (self::$cachehandler->exists($this->sql, self::$location))
  83. {
  84. $this->mtime = self::$cachehandler->age($this->sql, self::$location);
  85. if ($this->mtime > self::$maxage ) return false;
  86. if ($this->isCacheValid()) return true;
  87. }
  88. return false;
  89. }
  90. /**
  91. * Extract all tables affected by a database modification.
  92. */
  93. protected function parseSQL($sql)
  94. {
  95. // Check list of tables daily.
  96. $daily = 86400;
  97. if(is_null(self::$baseTables)
  98. && ( self::$cachehandler->age('SHOW TABLES', self::$location) > $daily
  99. || !self::$cachehandler->exists('SHOW TABLES', self::$location)))
  100. {
  101. // we have no valid cache so open the connection and run the query
  102. if(is_null(self::$dbconn))self::$dbconn = new DBConnection();
  103. $t1 = microtime(true);
  104. $resid = mysqli_query(self::$dbconn->id(), 'SHOW TABLES');
  105. if (!$resid || self::$dbconn->id()->errno)
  106. {
  107. // Clear the cache to prevent errors spreading.
  108. DBDebug::killCache();
  109. if(defined('KB_PROFILE'))
  110. {
  111. DBDebug::recordError("Database error: ".self::$dbconn->id()->error);
  112. DBDebug::recordError("SQL: ".$this->sql);
  113. }
  114. throw new Exception( "SQL error (".self::$dbconn->id()->error );
  115. }
  116. $this->exectime = microtime(true) - $t1;
  117. self::$totalexectime += $this->exectime;
  118. if(defined('KB_PROFILE')) DBDebug::profile($sql, $this->exectime);
  119. $this->queryCount(true);
  120. $bsize = 0;
  121. while ($row = $resid->fetch_assoc())
  122. {
  123. $table = strtolower(array_shift($row));
  124. self::$baseTables[$table] = $table;
  125. }
  126. // write data to cache
  127. self::$cachehandler->put('SHOW TABLES', self::$baseTables, self::$location, $daily);
  128. }
  129. else if(is_null(self::$baseTables)) self::$baseTables = self::$cachehandler->get('SHOW TABLES', self::$location);
  130. // gets all involved tables for a select statement
  131. $sql = strtolower($sql).' ';
  132. $regex = '/'.implode('|', self::$baseTables).'/';
  133. $matches = array();
  134. if(!preg_match_all($regex, $sql, $matches)) $this->usedtables = array();
  135. else $this->usedtables = $matches[0];
  136. return '';
  137. }
  138. /**
  139. * Check if the cached query is valid.
  140. *
  141. * Determines whether the tables used by a query have been modified
  142. * since the query was cached.
  143. *
  144. * @return boolean
  145. */
  146. protected function isCacheValid()
  147. {
  148. // check if cachefiles are still valid
  149. $this->usedtables = array();
  150. // first, we need to get all involved tables
  151. $this->parseSQL($this->sql);
  152. foreach ($this->usedtables as $table)
  153. {
  154. $file = 'qcache_tbl_'.trim($table);
  155. if (self::$cachehandler->exists($file, self::$location))
  156. {
  157. // if one of the tables is outdated, the query is outdated
  158. $age = self::$cachehandler->age($file, self::$location);
  159. if ($this->mtime >= $age) return false;
  160. }
  161. }
  162. return true;
  163. }
  164. /**
  165. * Marks all tables affected by a database modification
  166. */
  167. public static function markAffectedTables($sql = null)
  168. {
  169. if(is_null($sql)) return true;
  170. // this function invalidates cache files for touched tables
  171. $text = trim(strtolower($sql));
  172. $text = str_replace(array('ignore','`', "\r\n", "\n"), '', $text);
  173. $text = str_replace('(', ' (', $text);
  174. $ta = preg_split('/\s/', $text, 0, PREG_SPLIT_NO_EMPTY);
  175. // check for sql keywords and get the table from the appropriate position
  176. $tables = array();
  177. if ($ta[0] == 'update')
  178. {
  179. $tables[] = $ta[1];
  180. }
  181. elseif ($ta[0] == 'insert')
  182. {
  183. $tables[] = $ta[2];
  184. }
  185. elseif ($ta[0] == 'replace')
  186. {
  187. $tables[] = $ta[2];
  188. }
  189. elseif ($ta[0] == 'delete')
  190. {
  191. $tables[] = $ta[2];
  192. }elseif ($ta[0] == 'drop')
  193. {
  194. $tables[] = $ta[2];
  195. }
  196. elseif ($ta[0] == 'alter')
  197. {
  198. if ($ta[1] == 'ignore') $tables[] = $ta[3];
  199. else $tables[] = $ta[2];
  200. }
  201. elseif ($ta[0] == 'create')
  202. {
  203. return false;
  204. }
  205. elseif ($ta[0] == 'truncate')
  206. {
  207. if($ta[1] == 'table') $tables[] = $ta[2];
  208. else $tables[] = $ta[1];
  209. }
  210. elseif ($ta[0] == 'lock')
  211. {
  212. return false;
  213. }
  214. elseif ($ta[0] == 'unlock')
  215. {
  216. return false;
  217. }
  218. elseif ($ta[0] == 'set')
  219. {
  220. return false;
  221. }
  222. else
  223. {
  224. trigger_error('No suitable handler for query found. "'.$ta[0].'"',E_USER_NOTICE);
  225. return false;
  226. }
  227. foreach ($tables as $table)
  228. {
  229. self::$cachehandler->put('qcache_tbl_'.$table, time(), self::$location, 0);
  230. }
  231. }
  232. /**
  233. * Generate the query cache.
  234. *
  235. * Serialise a query and write to file.
  236. *
  237. * @return boolean
  238. */
  239. protected function genCache()
  240. {
  241. // this function fetches all rows and writes the data into a textfile
  242. // don't attemp to cache updates!
  243. if (strtolower(substr($this->sql, 0, 6)) != 'select'
  244. && strtolower(substr($this->sql, 0, 4)) != 'show')
  245. {
  246. return false;
  247. }
  248. $bsize = 0;
  249. while ($row = $this->getRow())
  250. {
  251. $this->cache[] = $row;
  252. // if the bytesize of the table exceeds the limit we'll abort
  253. // the cache generation and leave this query unbuffered
  254. // If we're running out of memory then run uncached.
  255. $bsize += strlen(join('', $row));
  256. if ($bsize > self::$maxcachesize || self::$maxmem < memory_get_usage())
  257. {
  258. unset($this->cache);
  259. $this->cache = array();
  260. $this->cached = false;
  261. $this->rewind();
  262. return false;
  263. }
  264. }
  265. // write data to cache
  266. self::$cachehandler->put($this->sql, $this->cache, self::$location, self::$maxage);
  267. $this->cached = true;
  268. $this->currrow = 0;
  269. $this->executed = true;
  270. }
  271. /**
  272. * Read a cached query from file.
  273. */
  274. protected function loadCache()
  275. {
  276. // loads the cachefile into the memory
  277. $this->cache = self::$cachehandler->get($this->sql, self::$location);
  278. $this->cached = true;
  279. $this->currrow = 0;
  280. $this->executed = true;
  281. }
  282. /**
  283. * Execute an SQL string.
  284. *
  285. * @param string $sql
  286. * @return boolean false on error or true if successful.
  287. */
  288. function execute($sql)
  289. {
  290. $t1 = microtime(true);
  291. $this->sql = $sql;
  292. unset($this->cache);
  293. $this->cache = array();
  294. $this->cached = false;
  295. if ($this->checkCache())
  296. {
  297. $this->loadCache();
  298. $this->queryCachedCount(true);
  299. $this->exectime = microtime(true) - $t1;
  300. self::$totalexectime += $this->exectime;
  301. return true;
  302. }
  303. // we have no valid cache so open the connection and run the query
  304. if(is_null(self::$dbconn))self::$dbconn = new DBConnection();
  305. $this->resid = mysqli_query(self::$dbconn->id(), $this->sql);
  306. if (!$this->resid || self::$dbconn->id()->errno)
  307. {
  308. // Clear the cache to prevent errors spreading.
  309. DBDebug::killCache();
  310. if(defined('KB_PROFILE'))
  311. {
  312. DBDebug::recordError("Database error: ".self::$dbconn->id()->error);
  313. DBDebug::recordError("SQL: ".$this->sql);
  314. }
  315. throw new Exception( "SQL Execution error: " . self::$dbconn->id()->error . " Query: " . $this->sql );
  316. }
  317. $this->exectime = microtime(true) - $t1;
  318. self::$totalexectime += $this->exectime;
  319. $this->executed = true;
  320. if(defined('KB_PROFILE')) DBDebug::profile($sql, $this->exectime);
  321. // if the query was too slow we'll fetch all rows and run it cached
  322. if ($this->exectime > self::$minruntime)
  323. {
  324. $this->genCache();
  325. }
  326. $this->queryCount(true);
  327. return true;
  328. }
  329. /**
  330. * Return the number of rows returned by the last query.
  331. *
  332. * @return integer|boolean
  333. */
  334. function recordCount()
  335. {
  336. if ($this->cached)
  337. {
  338. return count($this->cache);
  339. }
  340. elseif ($this->resid)
  341. {
  342. return $this->resid->num_rows;
  343. }
  344. return false;
  345. }
  346. /**
  347. * Return the next row of results from the last query.
  348. *
  349. * @return array
  350. */
  351. function getRow()
  352. {
  353. if ($this->cached)
  354. {
  355. if (!isset($this->cache[$this->currrow]))
  356. {
  357. return false;
  358. }
  359. // return the current row and increase the pointer by one
  360. return $this->cache[$this->currrow++];
  361. }
  362. if ($this->resid)
  363. {
  364. return $this->resid->fetch_assoc();
  365. }
  366. return false;
  367. }
  368. /**
  369. * Reset list of results to return the first row from the last query.
  370. */
  371. function rewind()
  372. {
  373. if ($this->cached)
  374. {
  375. $this->currrow = 0;
  376. }
  377. if(!is_null($this->resid)) {
  378. @mysqli_data_seek($this->resid, 0);
  379. }
  380. }
  381. /**
  382. * Return the most recent error message for the DB connection.
  383. */
  384. function getErrorMsg()
  385. {
  386. $msg = $this->sql."<br>";
  387. $msg .= "Query failed. ".mysqli_error(self::$dbconn->id());
  388. return $msg;
  389. }
  390. /**
  391. * Set the autocommit status.
  392. *
  393. * The default of true commits after every query.
  394. * If set to false the queries will not be commited until autocommit is set
  395. * to true.
  396. * @param boolean$commit The new autocommit status.
  397. * @return boolean true on success and false on failure.
  398. */
  399. function autocommit($commit = true)
  400. {
  401. if(defined('KB_PROFILE') && KB_PROFILE == 3)
  402. {
  403. if(!$commit) DBDebug::recordError("Transaction started.");
  404. else DBDebug::recordError("Transaction ended.");
  405. }
  406. if(!self::$dbconn) self::$dbconn = new DBConnection();
  407. return self::$dbconn->id()->autocommit($commit);
  408. }
  409. /**
  410. * Rollback all queries in the current transaction.
  411. *
  412. * @return boolean true on success.
  413. */
  414. function rollback()
  415. {
  416. // if there's no connection to the db then there's nothing to roll back
  417. if(!self::$dbconn) {
  418. return true;
  419. }
  420. return self::$dbconn->id()->rollback();
  421. }
  422. }