Memcached_DataObject.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * @copyright 2008, 2009 StatusNet, Inc.
  18. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  19. */
  20. defined('GNUSOCIAL') || die();
  21. class Memcached_DataObject extends Safe_DataObject
  22. {
  23. /**
  24. * Wrapper for DB_DataObject's static lookup using memcached
  25. * as backing instead of an in-process cache array.
  26. *
  27. * @param string $cls classname of object type to load
  28. * @param mixed $k key field name, or value for primary key
  29. * @param mixed $v key field value, or leave out for primary key lookup
  30. * @return mixed Memcached_DataObject subtype or false
  31. */
  32. public static function getClassKV($cls, $k, $v = null)
  33. {
  34. if (is_null($v)) {
  35. $v = $k;
  36. $keys = static::pkeyCols();
  37. if (count($keys) > 1) {
  38. // FIXME: maybe call pkeyGetClass() ourselves?
  39. throw new Exception('Use pkeyGetClass() for compound primary keys');
  40. }
  41. $k = $keys[0];
  42. }
  43. $i = self::getcached($cls, $k, $v);
  44. if ($i === false) { // false == cache miss
  45. $i = new $cls;
  46. $result = $i->get($k, $v);
  47. if ($result) {
  48. // Hit!
  49. $i->encache();
  50. } else {
  51. // save the fact that no such row exists
  52. $c = self::memcache();
  53. if (!empty($c)) {
  54. $ck = self::cachekey($cls, $k, $v);
  55. $c->set($ck, null);
  56. }
  57. $i = false;
  58. }
  59. }
  60. return $i;
  61. }
  62. /**
  63. * Get multiple items from the database by key
  64. *
  65. * @param string $cls Class to fetch
  66. * @param string $keyCol name of column for key
  67. * @param array $keyVals key values to fetch
  68. * @param bool $skipNulls return only non-null results
  69. * @param bool $preserve return the same tuples as input
  70. * @return object An object with tuples to be fetched, in order
  71. */
  72. public static function multiGetClass(
  73. string $cls,
  74. string $keyCol,
  75. array $keyVals,
  76. bool $skipNulls,
  77. bool $preserve
  78. ): object {
  79. $obj = new $cls();
  80. // Do not select anything extra
  81. $obj->selectAdd();
  82. $obj->selectAdd($obj->escapedTableName() . '.*');
  83. // A PHP-compatible datatype to check against
  84. $col_type = $obj->columnType($keyCol);
  85. // The code below assumes one of the two results
  86. if (!in_array($col_type, ['int', 'string'])) {
  87. throw new ServerException(
  88. 'Cannot do multiGet on anything but integer or string columns'
  89. );
  90. }
  91. // Actually need to know if MariaDB or Oracle MySQL this time
  92. $db_type = common_config('db', 'type');
  93. if ($db_type === 'mysql') {
  94. $tmp_obj = new $cls();
  95. $tmp_obj->query('SELECT 0 /*M! + 1 */ AS is_mariadb;');
  96. if ($tmp_obj->fetch() && $tmp_obj->is_mariadb) {
  97. $db_type = 'mariadb';
  98. }
  99. }
  100. // Since we're inputting straight to a query: format and escape
  101. $vals_escaped = [];
  102. foreach (array_values($keyVals) as $i => $val) {
  103. if (is_null($val)) {
  104. $val_escaped = 'NULL';
  105. } elseif ($col_type === 'int') {
  106. $val_escaped = (string)(int) $val;
  107. } else {
  108. $val_escaped = "'{$obj->escape($val)}'";
  109. }
  110. if ($db_type !== 'mariadb') {
  111. $vals_escaped[] = $val_escaped;
  112. } else {
  113. // A completely different approach for MariaDB (see below)
  114. $vals_escaped[] = "({$val_escaped},{$i})";
  115. }
  116. }
  117. // One way to guarantee that there is no name collision
  118. $join_tablename = common_database_tablename(
  119. $obj->tableName() . '_vals'
  120. );
  121. $join_keyword = ($preserve ? 'RIGHT' : 'LEFT') . ' JOIN';
  122. $vals_cast_type = ($col_type === 'int') ? 'INTEGER' : 'TEXT';
  123. // A lot of magic to ensure we get an ordered reply with the same exact
  124. // values as on input.
  125. switch ($db_type) {
  126. case 'pgsql':
  127. // Explicit casting is done to cast empty arrays
  128. $obj->_join = "\n" . sprintf(
  129. <<<END
  130. {$join_keyword} unnest(
  131. CAST(ARRAY[%s] AS {$vals_cast_type}[])
  132. ) WITH ORDINALITY
  133. AS {$join_tablename} ({$keyCol}, {$keyCol}_pos)
  134. USING ({$keyCol})
  135. END,
  136. implode(',', $vals_escaped)
  137. );
  138. break;
  139. case 'mariadb':
  140. // Delivers an empty set
  141. if (count($vals_escaped) == 0) {
  142. $vals_escaped[] = '(NULL,0) LIMIT 0';
  143. }
  144. // MariaDB doesn't support JSON_TABLE, but Oracle MySQL does,
  145. // which doesn't support VALUES without a ROW keyword though.
  146. $obj->_join = "\n" . sprintf(
  147. <<<END
  148. {$join_keyword} (
  149. WITH t1 ({$keyCol}, {$keyCol}_pos) AS (VALUES %s)
  150. SELECT * FROM t1
  151. ) AS {$join_tablename} USING ({$keyCol})
  152. END,
  153. implode(',', $vals_escaped)
  154. );
  155. break;
  156. case 'mysql':
  157. default:
  158. $obj->_join = "\n" . sprintf(
  159. <<<END
  160. {$join_keyword} JSON_TABLE(
  161. JSON_ARRAY(%s), '$[*]' COLUMNS (
  162. {$keyCol} {$vals_cast_type} PATH '$',
  163. {$keyCol}_pos FOR ORDINALITY
  164. )
  165. ) AS {$join_tablename} USING ({$keyCol})
  166. END,
  167. implode(',', $vals_escaped)
  168. );
  169. }
  170. if (!$preserve) {
  171. // Implements a left semi-join
  172. $obj->whereAdd("{$join_tablename}.{$keyCol}_pos IS NOT NULL");
  173. }
  174. // Filters both NULLs requested and non-matching NULLs
  175. if ($skipNulls) {
  176. $obj->whereAdd("{$obj->escapedTableName()}.{$keyCol} IS NOT NULL");
  177. }
  178. $obj->orderBy("{$join_tablename}.{$keyCol}_pos");
  179. $obj->find();
  180. return $obj;
  181. }
  182. /**
  183. * Get multiple items from the database by key
  184. *
  185. * @param string $cls Class to fetch
  186. * @param string $keyCol name of column for key
  187. * @param array $keyVals key values to fetch
  188. * @param boolean $otherCols Other columns to hold fixed
  189. *
  190. * @return array Array mapping $keyVals to objects, or null if not found
  191. */
  192. public static function pivotGetClass(
  193. $cls,
  194. $keyCol,
  195. array $keyVals,
  196. array $otherCols = []
  197. ) {
  198. if (is_array($keyCol)) {
  199. foreach ($keyVals as $keyVal) {
  200. if (!is_array($keyVal)) {
  201. throw new ServerException(
  202. 'keyVals passed to pivotGet must be an array of arrays '
  203. . 'if keyCol is an array'
  204. );
  205. }
  206. $result[implode(',', $keyVal)] = null;
  207. }
  208. } else {
  209. $result = array_fill_keys($keyVals, null);
  210. }
  211. $toFetch = array();
  212. foreach ($keyVals as $keyVal) {
  213. if (is_array($keyCol)) {
  214. $kv = array_combine($keyCol, $keyVal);
  215. } else {
  216. $kv = array($keyCol => $keyVal);
  217. }
  218. $kv = array_merge($otherCols, $kv);
  219. $i = self::multicache($cls, $kv);
  220. if ($i !== false) {
  221. if (is_array($keyCol)) {
  222. $result[implode(',', $keyVal)] = $i;
  223. } else {
  224. $result[$keyVal] = $i;
  225. }
  226. } elseif (!empty($keyVal)) {
  227. $toFetch[] = $keyVal;
  228. }
  229. }
  230. if (count($toFetch) > 0) {
  231. $i = new $cls;
  232. foreach ($otherCols as $otherKeyCol => $otherKeyVal) {
  233. $i->$otherKeyCol = $otherKeyVal;
  234. }
  235. if (is_array($keyCol)) {
  236. $i->whereAdd(self::_inMultiKey($i, $keyCol, $toFetch));
  237. } else {
  238. $i->whereAddIn($keyCol, $toFetch, $i->columnType($keyCol));
  239. }
  240. if ($i->find()) {
  241. while ($i->fetch()) {
  242. $copy = clone($i);
  243. $copy->encache();
  244. if (is_array($keyCol)) {
  245. $vals = array();
  246. foreach ($keyCol as $k) {
  247. $vals[] = $i->$k;
  248. }
  249. $result[implode(',', $vals)] = $copy;
  250. } else {
  251. $result[$i->$keyCol] = $copy;
  252. }
  253. }
  254. }
  255. // Save state of DB misses
  256. foreach ($toFetch as $keyVal) {
  257. $r = null;
  258. if (is_array($keyCol)) {
  259. $r = $result[implode(',', $keyVal)];
  260. } else {
  261. $r = $result[$keyVal];
  262. }
  263. if (empty($r)) {
  264. if (is_array($keyCol)) {
  265. $kv = array_combine($keyCol, $keyVal);
  266. } else {
  267. $kv = array($keyCol => $keyVal);
  268. }
  269. $kv = array_merge($otherCols, $kv);
  270. // save the fact that no such row exists
  271. $c = self::memcache();
  272. if (!empty($c)) {
  273. $ck = self::multicacheKey($cls, $kv);
  274. $c->set($ck, null);
  275. }
  276. }
  277. }
  278. }
  279. return $result;
  280. }
  281. public static function _inMultiKey($i, $cols, $values)
  282. {
  283. $types = array();
  284. foreach ($cols as $col) {
  285. $types[$col] = $i->columnType($col);
  286. }
  287. $first = true;
  288. $query = '';
  289. foreach ($values as $value) {
  290. if ($first) {
  291. $query .= '( ';
  292. $first = false;
  293. } else {
  294. $query .= ' OR ';
  295. }
  296. $query .= '( ';
  297. $i = 0;
  298. $firstc = true;
  299. foreach ($cols as $col) {
  300. if (!$firstc) {
  301. $query .= ' AND ';
  302. } else {
  303. $firstc = false;
  304. }
  305. switch ($types[$col]) {
  306. case 'string':
  307. case 'datetime':
  308. $query .= sprintf("%s = %s", $col, $i->_quote($value[$i]));
  309. break;
  310. default:
  311. $query .= sprintf("%s = %s", $col, $value[$i]);
  312. break;
  313. }
  314. }
  315. $query .= ') ';
  316. }
  317. if (!$first) {
  318. $query .= ' )';
  319. }
  320. return $query;
  321. }
  322. public static function pkeyColsClass($cls)
  323. {
  324. $i = new $cls;
  325. $types = $i->keyTypes();
  326. ksort($types);
  327. $pkey = array();
  328. foreach ($types as $key => $type) {
  329. if ($type == 'K' || $type == 'N') {
  330. $pkey[] = $key;
  331. }
  332. }
  333. return $pkey;
  334. }
  335. public static function listFindClass($cls, $keyCol, array $keyVals)
  336. {
  337. $i = new $cls;
  338. $i->whereAddIn($keyCol, $keyVals, $i->columnType($keyCol));
  339. if (!$i->find()) {
  340. throw new NoResultException($i);
  341. }
  342. return $i;
  343. }
  344. public static function listGetClass($cls, $keyCol, array $keyVals)
  345. {
  346. $pkeyMap = array_fill_keys($keyVals, array());
  347. $result = array_fill_keys($keyVals, array());
  348. $pkeyCols = static::pkeyCols();
  349. $toFetch = array();
  350. $allPkeys = array();
  351. // We only cache keys -- not objects!
  352. foreach ($keyVals as $keyVal) {
  353. $l = self::cacheGet(sprintf('%s:list-ids:%s:%s', strtolower($cls), $keyCol, $keyVal));
  354. if ($l !== false) {
  355. $pkeyMap[$keyVal] = $l;
  356. foreach ($l as $pkey) {
  357. $allPkeys[] = $pkey;
  358. }
  359. } else {
  360. $toFetch[] = $keyVal;
  361. }
  362. }
  363. if (count($allPkeys) > 0) {
  364. $keyResults = self::pivotGetClass($cls, $pkeyCols, $allPkeys);
  365. foreach ($pkeyMap as $keyVal => $pkeyList) {
  366. foreach ($pkeyList as $pkeyVal) {
  367. $i = $keyResults[implode(',', $pkeyVal)];
  368. if (!empty($i)) {
  369. $result[$keyVal][] = $i;
  370. }
  371. }
  372. }
  373. }
  374. if (count($toFetch) > 0) {
  375. try {
  376. $i = self::listFindClass($cls, $keyCol, $toFetch);
  377. while ($i->fetch()) {
  378. $copy = clone($i);
  379. $copy->encache();
  380. $result[$i->$keyCol][] = $copy;
  381. $pkeyVal = array();
  382. foreach ($pkeyCols as $pkeyCol) {
  383. $pkeyVal[] = $i->$pkeyCol;
  384. }
  385. $pkeyMap[$i->$keyCol][] = $pkeyVal;
  386. }
  387. } catch (NoResultException $e) {
  388. // no results found for our keyVals, so we leave them as empty arrays
  389. }
  390. foreach ($toFetch as $keyVal) {
  391. self::cacheSet(
  392. sprintf("%s:list-ids:%s:%s", strtolower($cls), $keyCol, $keyVal),
  393. $pkeyMap[$keyVal]
  394. );
  395. }
  396. }
  397. return $result;
  398. }
  399. public function escapedTableName()
  400. {
  401. return common_database_tablename($this->tableName());
  402. }
  403. public function columnType($columnName)
  404. {
  405. $keys = $this->table();
  406. if (!array_key_exists($columnName, $keys)) {
  407. throw new Exception('Unknown key column ' . $columnName . ' in ' . join(',', array_keys($keys)));
  408. }
  409. $def = $keys[$columnName];
  410. if ($def & DB_DATAOBJECT_INT) {
  411. return 'int';
  412. } else {
  413. return 'string';
  414. }
  415. }
  416. /**
  417. * @todo FIXME: Should this return false on lookup fail to match getKV?
  418. */
  419. public static function pkeyGetClass($cls, array $kv)
  420. {
  421. $i = self::multicache($cls, $kv);
  422. if ($i !== false) { // false == cache miss
  423. return $i;
  424. } else {
  425. $i = new $cls;
  426. foreach ($kv as $k => $v) {
  427. if (is_null($v)) {
  428. // XXX: possible SQL injection...? Don't
  429. // pass keys from the browser, eh.
  430. $i->whereAdd("$k is null");
  431. } else {
  432. $i->$k = $v;
  433. }
  434. }
  435. if ($i->find(true)) {
  436. $i->encache();
  437. } else {
  438. $i = null;
  439. $c = self::memcache();
  440. if (!empty($c)) {
  441. $ck = self::multicacheKey($cls, $kv);
  442. $c->set($ck, null);
  443. }
  444. }
  445. return $i;
  446. }
  447. }
  448. public function insert()
  449. {
  450. $result = parent::insert();
  451. if ($result) {
  452. $this->encache(); // in case of cached negative lookups
  453. }
  454. return $result;
  455. }
  456. public function update($dataObject = false)
  457. {
  458. if (is_object($dataObject) && $dataObject instanceof Memcached_DataObject) {
  459. $dataObject->decache(); # might be different keys
  460. }
  461. $result = parent::update($dataObject);
  462. if ($result !== false) {
  463. $this->encache();
  464. }
  465. return $result;
  466. }
  467. public function delete($useWhere = false)
  468. {
  469. $this->decache(); # while we still have the values!
  470. return parent::delete($useWhere);
  471. }
  472. public static function memcache()
  473. {
  474. return Cache::instance();
  475. }
  476. public static function cacheKey($cls, $k, $v)
  477. {
  478. if (is_object($cls) || is_object($k) || (is_object($v) && !($v instanceof DB_DataObject_Cast))) {
  479. $e = new Exception();
  480. common_log(LOG_ERR, __METHOD__ . ' object in param: ' .
  481. str_replace("\n", " ", $e->getTraceAsString()));
  482. }
  483. $vstr = self::valueString($v);
  484. return Cache::key(strtolower($cls).':'.$k.':'.$vstr);
  485. }
  486. public static function getcached($cls, $k, $v)
  487. {
  488. $c = self::memcache();
  489. if (!$c) {
  490. return false;
  491. } else {
  492. $obj = $c->get(self::cacheKey($cls, $k, $v));
  493. if (0 == strcasecmp($cls, 'User')) {
  494. // Special case for User
  495. if (is_object($obj) && is_object($obj->id)) {
  496. common_log(LOG_ERR, "User " . $obj->nickname . " was cached with User as ID; deleting");
  497. $c->delete(self::cacheKey($cls, $k, $v));
  498. return false;
  499. }
  500. }
  501. return $obj;
  502. }
  503. }
  504. public function keyTypes()
  505. {
  506. // ini-based classes return number-indexed arrays. handbuilt
  507. // classes return column => keytype. Make this uniform.
  508. $keys = $this->keys();
  509. $keyskeys = array_keys($keys);
  510. if (is_string($keyskeys[0])) {
  511. return $keys;
  512. }
  513. global $_DB_DATAOBJECT;
  514. if (!isset($_DB_DATAOBJECT['INI'][$this->_database][$this->tableName()."__keys"])) {
  515. $this->databaseStructure();
  516. }
  517. return $_DB_DATAOBJECT['INI'][$this->_database][$this->tableName()."__keys"];
  518. }
  519. public function encache()
  520. {
  521. $c = self::memcache();
  522. if (!$c) {
  523. return false;
  524. } elseif ($this->tableName() === 'user' && is_object($this->id)) {
  525. // Special case for User bug
  526. $e = new Exception();
  527. common_log(LOG_ERR, __METHOD__ . ' caching user with User object as ID ' .
  528. str_replace("\n", " ", $e->getTraceAsString()));
  529. return false;
  530. } else {
  531. $keys = $this->_allCacheKeys();
  532. foreach ($keys as $key) {
  533. $c->set($key, $this);
  534. }
  535. }
  536. }
  537. public function decache()
  538. {
  539. $c = self::memcache();
  540. if (!$c) {
  541. return false;
  542. }
  543. $keys = $this->_allCacheKeys();
  544. foreach ($keys as $key) {
  545. $c->delete($key, $this);
  546. }
  547. }
  548. public function _allCacheKeys()
  549. {
  550. $ckeys = array();
  551. $types = $this->keyTypes();
  552. ksort($types);
  553. $pkey = array();
  554. $pval = array();
  555. foreach ($types as $key => $type) {
  556. assert(!empty($key));
  557. if ($type == 'U') {
  558. if (empty($this->$key)) {
  559. continue;
  560. }
  561. $ckeys[] = self::cacheKey($this->tableName(), $key, self::valueString($this->$key));
  562. } elseif (in_array($type, ['K', 'N'])) {
  563. $pkey[] = $key;
  564. $pval[] = self::valueString($this->$key);
  565. } else {
  566. // Low level exception. No need for i18n as discussed with Brion.
  567. throw new Exception("Unknown key type $key => $type for " . $this->tableName());
  568. }
  569. }
  570. assert(count($pkey) > 0);
  571. // XXX: should work for both compound and scalar pkeys
  572. $pvals = implode(',', $pval);
  573. $pkeys = implode(',', $pkey);
  574. $ckeys[] = self::cacheKey($this->tableName(), $pkeys, $pvals);
  575. return $ckeys;
  576. }
  577. public static function multicache($cls, array $kv)
  578. {
  579. ksort($kv);
  580. $c = self::memcache();
  581. if (!$c) {
  582. return false;
  583. } else {
  584. return $c->get(self::multicacheKey($cls, $kv));
  585. }
  586. }
  587. public static function multicacheKey($cls, array $kv)
  588. {
  589. ksort($kv);
  590. $pkeys = implode(',', array_keys($kv));
  591. $pvals = implode(',', array_values($kv));
  592. return self::cacheKey($cls, $pkeys, $pvals);
  593. }
  594. public function getSearchEngine($table)
  595. {
  596. require_once INSTALLDIR . '/lib/search/search_engines.php';
  597. if (Event::handle('GetSearchEngine', [$this, $table, &$search_engine])) {
  598. $type = common_config('search', 'type');
  599. if ($type === 'like') {
  600. $search_engine = new SQLLikeSearch($this, $table);
  601. } elseif ($type === 'fulltext') {
  602. switch (common_config('db', 'type')) {
  603. case 'pgsql':
  604. $search_engine = new PostgreSQLSearch($this, $table);
  605. break;
  606. case 'mysql':
  607. $search_engine = new MySQLSearch($this, $table);
  608. break;
  609. default:
  610. throw new ServerException('Unknown DB type selected.');
  611. }
  612. } else {
  613. // Low level exception. No need for i18n as discussed with Brion.
  614. throw new ServerException('Unknown search type: ' . $type);
  615. }
  616. }
  617. return $search_engine;
  618. }
  619. public static function cachedQuery($cls, $qry, $expiry = 3600)
  620. {
  621. $c = self::memcache();
  622. if (!$c) {
  623. $inst = new $cls();
  624. $inst->query($qry);
  625. return $inst;
  626. }
  627. $key_part = Cache::keyize($cls).':'.md5($qry);
  628. $ckey = Cache::key($key_part);
  629. $stored = $c->get($ckey);
  630. if ($stored !== false) {
  631. return new ArrayWrapper($stored);
  632. }
  633. $inst = new $cls();
  634. $inst->query($qry);
  635. $cached = array();
  636. while ($inst->fetch()) {
  637. $cached[] = clone($inst);
  638. }
  639. $inst->free();
  640. $c->set($ckey, $cached, Cache::COMPRESSED, $expiry);
  641. return new ArrayWrapper($cached);
  642. }
  643. /**
  644. * sends query to database - this is the private one that must work
  645. * - internal functions use this rather than $this->query()
  646. *
  647. * Overridden to do logging.
  648. *
  649. * @param string $string
  650. * @access private
  651. * @return mixed none or PEAR_Error
  652. */
  653. public function _query($string)
  654. {
  655. if (common_config('db', 'annotate_queries')) {
  656. $string = $this->annotateQuery($string);
  657. }
  658. $start = hrtime(true);
  659. $fail = false;
  660. $result = null;
  661. if (Event::handle('StartDBQuery', array($this, $string, &$result))) {
  662. common_perf_counter('query', $string);
  663. try {
  664. $result = parent::_query($string);
  665. } catch (Exception $e) {
  666. $fail = $e;
  667. }
  668. Event::handle('EndDBQuery', array($this, $string, &$result));
  669. }
  670. $delta = (hrtime(true) - $start) / 1000000000;
  671. $limit = common_config('db', 'log_slow_queries');
  672. if (($limit > 0 && $delta >= $limit) || common_config('db', 'log_queries')) {
  673. $clean = $this->sanitizeQuery($string);
  674. if ($fail) {
  675. $msg = sprintf("FAILED DB query (%0.3fs): %s - %s", $delta, $fail->getMessage(), $clean);
  676. } else {
  677. $msg = sprintf("DB query (%0.3fs): %s", $delta, $clean);
  678. }
  679. common_log(LOG_DEBUG, $msg);
  680. }
  681. if ($fail) {
  682. throw $fail;
  683. }
  684. return $result;
  685. }
  686. /**
  687. * Find the first caller in the stack trace that's not a
  688. * low-level database function and add a comment to the
  689. * query string. This should then be visible in process lists
  690. * and slow query logs, to help identify problem areas.
  691. *
  692. * Also marks whether this was a web GET/POST or which daemon
  693. * was running it.
  694. *
  695. * @param string $string SQL query string
  696. * @return string SQL query string, with a comment in it
  697. */
  698. public function annotateQuery($string)
  699. {
  700. $ignore = array('annotateQuery',
  701. '_query',
  702. 'query',
  703. 'get',
  704. 'insert',
  705. 'delete',
  706. 'update',
  707. 'find');
  708. $ignoreStatic = array('getKV',
  709. 'getClassKV',
  710. 'pkeyGet',
  711. 'pkeyGetClass',
  712. 'cachedQuery');
  713. $here = get_class($this); // if we get confused
  714. $bt = debug_backtrace();
  715. // Find the first caller that's not us?
  716. foreach ($bt as $frame) {
  717. $func = $frame['function'];
  718. if (isset($frame['type']) && $frame['type'] == '::') {
  719. if (in_array($func, $ignoreStatic)) {
  720. continue;
  721. }
  722. $here = $frame['class'] . '::' . $func;
  723. break;
  724. } elseif (isset($frame['type']) && $frame['type'] === '->') {
  725. if ($frame['object'] === $this && in_array($func, $ignore)) {
  726. continue;
  727. }
  728. if (in_array($func, $ignoreStatic)) {
  729. continue; // @todo FIXME: This shouldn't be needed?
  730. }
  731. $here = get_class($frame['object']) . '->' . $func;
  732. break;
  733. }
  734. $here = $func;
  735. break;
  736. }
  737. if (php_sapi_name() == 'cli') {
  738. $context = basename($_SERVER['PHP_SELF']);
  739. } else {
  740. $context = $_SERVER['REQUEST_METHOD'];
  741. }
  742. // Slip the comment in after the first command,
  743. // or DB_DataObject gets confused about handling inserts and such.
  744. $parts = explode(' ', $string, 2);
  745. $parts[0] .= " /* $context $here */";
  746. return implode(' ', $parts);
  747. }
  748. // Sanitize a query for logging
  749. // @fixme don't trim spaces in string literals
  750. public function sanitizeQuery($string)
  751. {
  752. $string = preg_replace('/\s+/', ' ', $string);
  753. $string = trim($string);
  754. return $string;
  755. }
  756. // We overload so that 'SET NAMES "utf8mb4"' is called for
  757. // each connection
  758. public function _connect()
  759. {
  760. global $_DB_DATAOBJECT, $_PEAR;
  761. $sum = $this->_getDbDsnMD5();
  762. if (!empty($_DB_DATAOBJECT['CONNECTIONS'][$sum]) &&
  763. !$_PEAR->isError($_DB_DATAOBJECT['CONNECTIONS'][$sum])) {
  764. $exists = true;
  765. } else {
  766. $exists = false;
  767. }
  768. // @fixme horrible evil hack!
  769. //
  770. // In multisite configuration we don't want to keep around a separate
  771. // connection for every database; we could end up with thousands of
  772. // connections open per thread. In an ideal world we might keep
  773. // a connection per server and select different databases, but that'd
  774. // be reliant on having the same db username/pass as well.
  775. //
  776. // MySQL connections are cheap enough we're going to try just
  777. // closing out the old connection and reopening when we encounter
  778. // a new DSN.
  779. //
  780. // WARNING WARNING if we end up actually using multiple DBs at a time
  781. // we'll need some fancier logic here.
  782. if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS']) && php_sapi_name() == 'cli') {
  783. foreach ($_DB_DATAOBJECT['CONNECTIONS'] as $index => $conn) {
  784. if ($_PEAR->isError($conn)) {
  785. common_log(LOG_WARNING, __METHOD__ . " cannot disconnect failed DB connection: '".$conn->getMessage()."'.");
  786. } elseif (!empty($conn)) {
  787. $conn->disconnect();
  788. }
  789. unset($_DB_DATAOBJECT['CONNECTIONS'][$index]);
  790. }
  791. }
  792. $result = parent::_connect();
  793. if ($result && !$exists) {
  794. // Needed to make timestamp values usefully comparable.
  795. if (common_config('db', 'type') !== 'mysql') {
  796. parent::_query("SET TIME ZONE INTERVAL '+00:00' HOUR TO MINUTE");
  797. } else {
  798. parent::_query("SET time_zone = '+0:00'");
  799. }
  800. }
  801. return $result;
  802. }
  803. // XXX: largely cadged from DB_DataObject
  804. public function _getDbDsnMD5()
  805. {
  806. if ($this->_database_dsn_md5) {
  807. return $this->_database_dsn_md5;
  808. }
  809. $dsn = $this->_getDbDsn();
  810. if (is_string($dsn)) {
  811. $sum = md5($dsn);
  812. } else {
  813. /// support array based dsn's
  814. $sum = md5(serialize($dsn));
  815. }
  816. return $sum;
  817. }
  818. public function _getDbDsn()
  819. {
  820. global $_DB_DATAOBJECT;
  821. if (empty($_DB_DATAOBJECT['CONFIG'])) {
  822. self::_loadConfig();
  823. }
  824. $options = &$_DB_DATAOBJECT['CONFIG'];
  825. // if the databse dsn dis defined in the object..
  826. $dsn = isset($this->_database_dsn) ? $this->_database_dsn : null;
  827. if (!$dsn) {
  828. if (!$this->_database) {
  829. $this->_database = isset($options["table_{$this->tableName()}"]) ? $options["table_{$this->tableName()}"] : null;
  830. }
  831. if ($this->_database && !empty($options["database_{$this->_database}"])) {
  832. $dsn = $options["database_{$this->_database}"];
  833. } elseif (!empty($options['database'])) {
  834. $dsn = $options['database'];
  835. }
  836. }
  837. if (!$dsn) {
  838. // TRANS: Exception thrown when database name or Data Source Name could not be found.
  839. throw new Exception(_('No database name or DSN found anywhere.'));
  840. }
  841. return $dsn;
  842. }
  843. public static function blow()
  844. {
  845. $c = self::memcache();
  846. if (empty($c)) {
  847. return false;
  848. }
  849. $args = func_get_args();
  850. $format = array_shift($args);
  851. $keyPart = vsprintf($format, $args);
  852. $cacheKey = Cache::key($keyPart);
  853. return $c->delete($cacheKey);
  854. }
  855. public function raiseError($message, $type = null, $behavior = null)
  856. {
  857. $id = get_class($this);
  858. if (!empty($this->id)) {
  859. $id .= ':' . $this->id;
  860. }
  861. if ($message instanceof PEAR_Error) {
  862. $message = $message->getMessage();
  863. }
  864. // Low level exception. No need for i18n as discussed with Brion.
  865. throw new ServerException("[$id] DB_DataObject error [$type]: $message");
  866. }
  867. public static function cacheGet($keyPart)
  868. {
  869. $c = self::memcache();
  870. if (empty($c)) {
  871. return false;
  872. }
  873. $cacheKey = Cache::key($keyPart);
  874. return $c->get($cacheKey);
  875. }
  876. public static function cacheSet($keyPart, $value, $flag = null, $expiry = null)
  877. {
  878. $c = self::memcache();
  879. if (empty($c)) {
  880. return false;
  881. }
  882. $cacheKey = Cache::key($keyPart);
  883. return $c->set($cacheKey, $value, $flag, $expiry);
  884. }
  885. public static function valueString($v)
  886. {
  887. $vstr = null;
  888. if (is_object($v) && $v instanceof DB_DataObject_Cast) {
  889. switch ($v->type) {
  890. case 'date':
  891. $vstr = "{$v->year} - {$v->month} - {$v->day}";
  892. break;
  893. case 'sql':
  894. if (strcasecmp($v->value, 'NULL') == 0) {
  895. // Very selectively handling NULLs.
  896. $vstr = '';
  897. break;
  898. }
  899. // no break
  900. case 'blob':
  901. case 'string':
  902. case 'datetime':
  903. case 'time':
  904. // Low level exception. No need for i18n as discussed with Brion.
  905. throw new ServerException("Unhandled DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
  906. break;
  907. default:
  908. // Low level exception. No need for i18n as discussed with Brion.
  909. throw new ServerException("Unknown DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
  910. break;
  911. }
  912. } else {
  913. $vstr = strval($v);
  914. }
  915. return $vstr;
  916. }
  917. }