We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

Memcached_DataObject.php 29.1 KB
Newer Older
1 2
<?php
/*
3
 * StatusNet - the distributed open-source microblogging tool
4
 * Copyright (C) 2008, 2009, StatusNet, Inc.
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

20
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
21

22
class Memcached_DataObject extends Safe_DataObject
23
{
24 25 26 27 28 29 30 31 32
    /**
     * Wrapper for DB_DataObject's static lookup using memcached
     * as backing instead of an in-process cache array.
     *
     * @param string $cls classname of object type to load
     * @param mixed $k key field name, or value for primary key
     * @param mixed $v key field value, or leave out for primary key lookup
     * @return mixed Memcached_DataObject subtype or false
     */
33
    static function getClassKV($cls, $k, $v=null)
34
    {
35 36 37
        if (!is_a($cls, __CLASS__, true)) {
            throw new Exception('Trying to fetch ' . __CLASS__ . ' into a non-related class');
        }
38 39
        if (is_null($v)) {
            $v = $k;
40 41
            $keys = self::pkeyCols($cls);
            if (count($keys) > 1) {
42 43
                // FIXME: maybe call pkeyGetClass() ourselves?
                throw new Exception('Use pkeyGetClass() for compound primary keys');
44
            }
45 46
            $k = $keys[0];
        }
47
        $i = self::getcached($cls, $k, $v);
48
        if ($i === false) { // false == cache miss
49
            $i = new $cls;
50 51
            $result = $i->get($k, $v);
            if ($result) {
52
                // Hit!
53
                $i->encache();
54
            } else {
55 56 57 58 59 60
                // save the fact that no such row exists
                $c = self::memcache();
                if (!empty($c)) {
                    $ck = self::cachekey($cls, $k, $v);
                    $c->set($ck, null);
                }
61
                $i = false;
62 63
            }
        }
64
        return $i;
65
    }
66

67 68
    /**
     * Get multiple items from the database by key
69
     *
70 71 72 73
     * @param string  $cls       Class to fetch
     * @param string  $keyCol    name of column for key
     * @param array   $keyVals   key values to fetch
     * @param boolean $skipNulls return only non-null results?
74
     *
75 76
     * @return array Array of objects, in order
     */
mattl's avatar
mattl committed
77
    static function multiGetClass($cls, $keyCol, array $keyVals, $skipNulls=true)
78
    {
mattl's avatar
mattl committed
79
        $result = self::pivotGetClass($cls, $keyCol, $keyVals);
80 81 82 83 84 85 86 87 88 89 90 91 92 93

        $values = array_values($result);

        if ($skipNulls) {
            $tmp = array();
            foreach ($values as $value) {
                if (!empty($value)) {
                    $tmp[] = $value;
                }
            }
            $values = $tmp;
        }

        return new ArrayWrapper($values);
94
    }
95

96 97
    /**
     * Get multiple items from the database by key
98
     *
99 100 101 102
     * @param string  $cls       Class to fetch
     * @param string  $keyCol    name of column for key
     * @param array   $keyVals   key values to fetch
     * @param boolean $otherCols Other columns to hold fixed
103
     *
104 105
     * @return array Array mapping $keyVals to objects, or null if not found
     */
mattl's avatar
mattl committed
106
    static function pivotGetClass($cls, $keyCol, array $keyVals, array $otherCols = array())
107
    {
108 109 110
        if (!is_a($cls, __CLASS__, true)) {
            throw new Exception('Trying to fetch ' . __CLASS__ . ' into a non-related class');
        }
111 112 113 114 115 116 117
        if (is_array($keyCol)) {
            foreach ($keyVals as $keyVal) {
                $result[implode(',', $keyVal)] = null;
            }
        } else {
            $result = array_fill_keys($keyVals, null);
        }
Siebrand Mazeland's avatar
Siebrand Mazeland committed
118 119 120 121

        $toFetch = array();

        foreach ($keyVals as $keyVal) {
122 123 124 125 126 127 128

            if (is_array($keyCol)) {
                $kv = array_combine($keyCol, $keyVal);
            } else {
                $kv = array($keyCol => $keyVal);
            }

Siebrand Mazeland's avatar
Siebrand Mazeland committed
129 130 131 132 133
            $kv = array_merge($otherCols, $kv);

            $i = self::multicache($cls, $kv);

            if ($i !== false) {
134 135 136 137 138
                if (is_array($keyCol)) {
                    $result[implode(',', $keyVal)] = $i;
                } else {
                    $result[$keyVal] = $i;
                }
Siebrand Mazeland's avatar
Siebrand Mazeland committed
139 140 141 142 143 144
            } else if (!empty($keyVal)) {
                $toFetch[] = $keyVal;
            }
        }

        if (count($toFetch) > 0) {
145
            $i = new $cls;
146 147
            foreach ($otherCols as $otherKeyCol => $otherKeyVal) {
                $i->$otherKeyCol = $otherKeyVal;
148
            }
149 150 151 152 153
            if (is_array($keyCol)) {
                $i->whereAdd(self::_inMultiKey($i, $keyCol, $toFetch));
            } else {
                $i->whereAddIn($keyCol, $toFetch, $i->columnType($keyCol));
            }
Siebrand Mazeland's avatar
Siebrand Mazeland committed
154 155 156 157
            if ($i->find()) {
                while ($i->fetch()) {
                    $copy = clone($i);
                    $copy->encache();
158 159 160 161 162 163 164 165 166
                    if (is_array($keyCol)) {
                        $vals = array();
                        foreach ($keyCol as $k) {
                            $vals[] = $i->$k;
                        }
                        $result[implode(',', $vals)] = $copy;
                    } else {
                        $result[$i->$keyCol] = $copy;
                    }
Siebrand Mazeland's avatar
Siebrand Mazeland committed
167 168 169 170 171 172
                }
            }

            // Save state of DB misses

            foreach ($toFetch as $keyVal) {
173 174 175 176 177 178
                $r = null;
                if (is_array($keyCol)) {
                    $r = $result[implode(',', $keyVal)];
                } else {
                    $r = $result[$keyVal];
                }
Siebrand Mazeland's avatar
Siebrand Mazeland committed
179
                if (empty($r)) {
180 181 182 183 184 185
                    if (is_array($keyCol)) {
                        $kv = array_combine($keyCol, $keyVal);
                    } else {
                        $kv = array($keyCol => $keyVal);
                    }
                    $kv = array_merge($otherCols, $kv);
Siebrand Mazeland's avatar
Siebrand Mazeland committed
186 187 188 189 190 191 192 193 194 195 196
                    // save the fact that no such row exists
                    $c = self::memcache();
                    if (!empty($c)) {
                        $ck = self::multicacheKey($cls, $kv);
                        $c->set($ck, null);
                    }
                }
            }
        }

        return $result;
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

    static function _inMultiKey($i, $cols, $values)
    {
        $types = array();

        foreach ($cols as $col) {
            $types[$col] = $i->columnType($col);
        }

        $first = true;

        $query = '';

        foreach ($values as $value) {
            if ($first) {
                $query .= '( ';
                $first = false;
            } else {
                $query .= ' OR ';
            }
            $query .= '( ';
            $i = 0;
            $firstc = true;
            foreach ($cols as $col) {
                if (!$firstc) {
                    $query .= ' AND ';
                } else {
                    $firstc = false;
                }
                switch ($types[$col]) {
                case 'string':
                case 'datetime':
                    $query .= sprintf("%s = %s", $col, $i->_quote($value[$i]));
                    break;
                default:
                    $query .= sprintf("%s = %s", $col, $value[$i]);
                    break;
                }
            }
            $query .= ') ';
        }

        if (!$first) {
            $query .= ' )';
        }

        return $query;
    }

    static function pkeyCols($cls)
    {
249 250
        if (!is_a($cls, __CLASS__, true)) {
            throw new Exception('Trying to fetch ' . __CLASS__ . ' into a non-related class');
251
        }
252
        $i = new $cls;
253 254 255 256 257 258 259 260 261 262 263 264 265 266
        $types = $i->keyTypes();
        ksort($types);

        $pkey = array();

        foreach ($types as $key => $type) {
            if ($type == 'K' || $type == 'N') {
                $pkey[] = $key;
            }
        }

        return $pkey;
    }

267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
    static function listFindClass($cls, $keyCol, array $keyVals)
    {
        if (!is_a($cls, __CLASS__, true)) {
            throw new Exception('Trying to fetch ' . __CLASS__ . ' into a non-related class');
        }

        $i = new $cls;
        $i->whereAddIn($keyCol, $keyVals, $i->columnType($keyCol));
        if (!$i->find()) {
            throw new NoResultException($i);
        }

        sprintf(__CLASS__ . "() got {$i->N} results for class $cls key $keyCol");
        return $i;
    }

283
    static function listGetClass($cls, $keyCol, array $keyVals)
Evan Prodromou's avatar
Evan Prodromou committed
284
    {
285 286 287
        if (!is_a($cls, __CLASS__, true)) {
            throw new Exception('Trying to fetch ' . __CLASS__ . ' into a non-related class');
        }
Siebrand Mazeland's avatar
Siebrand Mazeland committed
288
        $pkeyMap = array_fill_keys($keyVals, array());
Evan Prodromou's avatar
Evan Prodromou committed
289
        $result = array_fill_keys($keyVals, array());
290 291

        $pkeyCols = self::pkeyCols($cls);
Evan Prodromou's avatar
Evan Prodromou committed
292

Siebrand Mazeland's avatar
Siebrand Mazeland committed
293
        $toFetch = array();
294 295 296 297
        $allPkeys = array();

        // We only cache keys -- not objects!

Siebrand Mazeland's avatar
Siebrand Mazeland committed
298 299 300 301
        foreach ($keyVals as $keyVal) {
            $l = self::cacheGet(sprintf("%s:list-ids:%s:%s", strtolower($cls), $keyCol, $keyVal));
            if ($l !== false) {
                $pkeyMap[$keyVal] = $l;
Evan Prodromou's avatar
Evan Prodromou committed
302 303 304
                foreach ($l as $pkey) {
                    $allPkeys[] = $pkey;
                }
Siebrand Mazeland's avatar
Siebrand Mazeland committed
305 306 307 308
            } else {
                $toFetch[] = $keyVal;
            }
        }
309

Evan Prodromou's avatar
Evan Prodromou committed
310
        if (count($allPkeys) > 0) {
mattl's avatar
mattl committed
311
            $keyResults = self::pivotGetClass($cls, $pkeyCols, $allPkeys);
312

Evan Prodromou's avatar
Evan Prodromou committed
313 314 315 316 317 318
            foreach ($pkeyMap as $keyVal => $pkeyList) {
                foreach ($pkeyList as $pkeyVal) {
                    $i = $keyResults[implode(',',$pkeyVal)];
                    if (!empty($i)) {
                        $result[$keyVal][] = $i;
                    }
319 320 321 322
                }
            }
        }

323
        if (count($toFetch) > 0) {
324 325 326
            try {
                $i = self::listFindClass($cls, $keyCol, $toFetch);

327 328 329 330 331 332 333 334 335 336
                while ($i->fetch()) {
                    $copy = clone($i);
                    $copy->encache();
                    $result[$i->$keyCol][] = $copy;
                    $pkeyVal = array();
                    foreach ($pkeyCols as $pkeyCol) {
                        $pkeyVal[] = $i->$pkeyCol;
                    }
                    $pkeyMap[$i->$keyCol][] = $pkeyVal;
                }
337 338
            } catch (NoResultException $e) {
                // no results foudn for our keyVals, so we leave them as empty arrays
Siebrand Mazeland's avatar
Siebrand Mazeland committed
339 340
            }
            foreach ($toFetch as $keyVal) {
341
                self::cacheSet(sprintf("%s:list-ids:%s:%s", strtolower($cls), $keyCol, $keyVal),
342
                               $pkeyMap[$keyVal]);
Siebrand Mazeland's avatar
Siebrand Mazeland committed
343
            }
Evan Prodromou's avatar
Evan Prodromou committed
344
        }
Evan Prodromou's avatar
Evan Prodromou committed
345

Siebrand Mazeland's avatar
Siebrand Mazeland committed
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
        return $result;
    }

    function columnType($columnName)
    {
        $keys = $this->table();
        if (!array_key_exists($columnName, $keys)) {
            throw new Exception('Unknown key column ' . $columnName . ' in ' . join(',', array_keys($keys)));
        }

        $def = $keys[$columnName];

        if ($def & DB_DATAOBJECT_INT) {
            return 'integer';
        } else {
            return 'string';
        }
Evan Prodromou's avatar
Evan Prodromou committed
363
    }
364

365
    /**
366
     * @todo FIXME: Should this return false on lookup fail to match getKV?
367
     */
368
    static function pkeyGetClass($cls, array $kv)
369
    {
370 371 372
        if (!is_a($cls, __CLASS__, true)) {
            throw new Exception('Trying to fetch ' . __CLASS__ . ' into a non-related class');
        }
373
        $i = Memcached_DataObject::multicache($cls, $kv);
374
        if ($i !== false) { // false == cache miss
375 376
            return $i;
        } else {
377
            $i = new $cls;
378
            foreach ($kv as $k => $v) {
379 380 381 382 383 384 385
                if (is_null($v)) {
                    // XXX: possible SQL injection...? Don't
                    // pass keys from the browser, eh.
                    $i->whereAdd("$k is null");
                } else {
                    $i->$k = $v;
                }
386 387 388 389
            }
            if ($i->find(true)) {
                $i->encache();
            } else {
Evan Prodromou's avatar
Evan Prodromou committed
390
                $i = null;
391 392 393 394 395
                $c = self::memcache();
                if (!empty($c)) {
                    $ck = self::multicacheKey($cls, $kv);
                    $c->set($ck, null);
                }
396
            }
397
            return $i;
398 399
        }
    }
400

401 402
    function insert()
    {
403
        $result = parent::insert();
404
        if ($result) {
405
            $this->fixupTimestamps();
406 407
            $this->encache(); // in case of cached negative lookups
        }
408 409
        return $result;
    }
410

411 412
    function update($orig=null)
    {
413 414 415 416 417
        if (is_object($orig) && $orig instanceof Memcached_DataObject) {
            $orig->decache(); # might be different keys
        }
        $result = parent::update($orig);
        if ($result) {
418
            $this->fixupTimestamps();
419 420 421 422
            $this->encache();
        }
        return $result;
    }
423

424 425
    function delete()
    {
426 427 428
        $this->decache(); # while we still have the values!
        return parent::delete();
    }
429

430
    static function memcache() {
431
        return Cache::instance();
432
    }
433

434
    static function cacheKey($cls, $k, $v) {
435
        if (is_object($cls) || is_object($k) || (is_object($v) && !($v instanceof DB_DataObject_Cast))) {
436 437 438 439
            $e = new Exception();
            common_log(LOG_ERR, __METHOD__ . ' object in param: ' .
                str_replace("\n", " ", $e->getTraceAsString()));
        }
440
        $vstr = self::valueString($v);
441
        return Cache::key(strtolower($cls).':'.$k.':'.$vstr);
442
    }
443

444
    static function getcached($cls, $k, $v) {
445
        $c = Memcached_DataObject::memcache();
446 447 448
        if (!$c) {
            return false;
        } else {
449 450 451
            $obj = $c->get(Memcached_DataObject::cacheKey($cls, $k, $v));
            if (0 == strcasecmp($cls, 'User')) {
                // Special case for User
452
                if (is_object($obj) && is_object($obj->id)) {
453 454 455 456 457 458
                    common_log(LOG_ERR, "User " . $obj->nickname . " was cached with User as ID; deleting");
                    $c->delete(Memcached_DataObject::cacheKey($cls, $k, $v));
                    return false;
                }
            }
            return $obj;
459 460
        }
    }
461

462 463
    function keyTypes()
    {
464 465 466 467 468 469 470 471 472 473 474
        // ini-based classes return number-indexed arrays. handbuilt
        // classes return column => keytype. Make this uniform.

        $keys = $this->keys();

        $keyskeys = array_keys($keys);

        if (is_string($keyskeys[0])) {
            return $keys;
        }

475
        global $_DB_DATAOBJECT;
476
        if (!isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"])) {
477
            $this->databaseStructure();
478 479

        }
480 481
        return $_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"];
    }
482

483 484
    function encache()
    {
485
        $c = $this->memcache();
486

487 488
        if (!$c) {
            return false;
489 490 491 492 493 494
        } else if ($this->tableName() == 'user' && is_object($this->id)) {
            // Special case for User bug
            $e = new Exception();
            common_log(LOG_ERR, __METHOD__ . ' caching user with User object as ID ' .
                       str_replace("\n", " ", $e->getTraceAsString()));
            return false;
495
        } else {
496
            $keys = $this->_allCacheKeys();
497

498 499 500
            foreach ($keys as $key) {
                $c->set($key, $this);
            }
501 502
        }
    }
503

504 505
    function decache()
    {
506
        $c = $this->memcache();
507

508 509
        if (!$c) {
            return false;
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
        }

        $keys = $this->_allCacheKeys();

        foreach ($keys as $key) {
            $c->delete($key, $this);
        }
    }

    function _allCacheKeys()
    {
        $ckeys = array();

        $types = $this->keyTypes();
        ksort($types);

        $pkey = array();
        $pval = array();

        foreach ($types as $key => $type) {

            assert(!empty($key));

            if ($type == 'U') {
                if (empty($this->$key)) {
                    continue;
536
                }
537
                $ckeys[] = $this->cacheKey($this->tableName(), $key, self::valueString($this->$key));
538 539
            } else if ($type == 'K' || $type == 'N') {
                $pkey[] = $key;
540
                $pval[] = self::valueString($this->$key);
541
            } else {
542
                // Low level exception. No need for i18n as discussed with Brion.
543
                throw new Exception("Unknown key type $key => $type for " . $this->tableName());
544 545
            }
        }
546 547 548 549 550 551 552 553 554 555

        assert(count($pkey) > 0);

        // XXX: should work for both compound and scalar pkeys
        $pvals = implode(',', $pval);
        $pkeys = implode(',', $pkey);

        $ckeys[] = $this->cacheKey($this->tableName(), $pkeys, $pvals);

        return $ckeys;
556
    }
557

558
    static function multicache($cls, $kv)
559
    {
560
        ksort($kv);
561
        $c = self::memcache();
562 563 564
        if (!$c) {
            return false;
        } else {
565
            return $c->get(self::multicacheKey($cls, $kv));
566 567
        }
    }
millette's avatar
millette committed
568

569 570 571 572 573 574 575 576
    static function multicacheKey($cls, $kv)
    {
        ksort($kv);
        $pkeys = implode(',', array_keys($kv));
        $pvals = implode(',', array_values($kv));
        return self::cacheKey($cls, $pkeys, $pvals);
    }

577 578
    function getSearchEngine($table)
    {
millette's avatar
millette committed
579
        require_once INSTALLDIR.'/lib/search_engines.php';
580 581 582 583 584 585 586 587

        if (Event::handle('GetSearchEngine', array($this, $table, &$search_engine))) {
            if ('mysql' === common_config('db', 'type')) {
                $type = common_config('search', 'type');
                if ($type == 'like') {
                    $search_engine = new MySQLLikeSearch($this, $table);
                } else if ($type == 'fulltext') {
                    $search_engine = new MySQLSearch($this, $table);
588
                } else {
589 590
                    // Low level exception. No need for i18n as discussed with Brion.
                    throw new ServerException('Unknown search type: ' . $type);
millette's avatar
millette committed
591
                }
592 593
            } else {
                $search_engine = new PGSearch($this, $table);
594
            }
millette's avatar
millette committed
595
        }
596

millette's avatar
millette committed
597 598
        return $search_engine;
    }
599 600 601

    static function cachedQuery($cls, $qry, $expiry=3600)
    {
602
        $c = Memcached_DataObject::memcache();
603 604 605
        if (!$c) {
            $inst = new $cls();
            $inst->query($qry);
606
            return $inst;
607
        }
608
        $key_part = Cache::keyize($cls).':'.md5($qry);
609
        $ckey = Cache::key($key_part);
610
        $stored = $c->get($ckey);
611 612

        if ($stored !== false) {
613 614 615 616
            return new ArrayWrapper($stored);
        }

        $inst = new $cls();
617
        $inst->query($qry);
618 619 620 621 622
        $cached = array();
        while ($inst->fetch()) {
            $cached[] = clone($inst);
        }
        $inst->free();
623
        $c->set($ckey, $cached, Cache::COMPRESSED, $expiry);
624
        return new ArrayWrapper($cached);
625
    }
626

627
    /**
628
     * sends query to database - this is the private one that must work
629 630 631 632 633 634 635 636 637 638
     *   - internal functions use this rather than $this->query()
     *
     * Overridden to do logging.
     *
     * @param  string  $string
     * @access private
     * @return mixed none or PEAR_Error
     */
    function _query($string)
    {
639 640 641 642
        if (common_config('db', 'annotate_queries')) {
            $string = $this->annotateQuery($string);
        }

643
        $start = microtime(true);
644
        $fail = false;
Brion Vibber's avatar
Brion Vibber committed
645 646
        $result = null;
        if (Event::handle('StartDBQuery', array($this, $string, &$result))) {
647
            common_perf_counter('query', $string);
Brion Vibber's avatar
Brion Vibber committed
648 649 650 651 652
            try {
                $result = parent::_query($string);
            } catch (Exception $e) {
                $fail = $e;
            }
Brion Vibber's avatar
Brion Vibber committed
653
            Event::handle('EndDBQuery', array($this, $string, &$result));
654
        }
655 656 657 658 659
        $delta = microtime(true) - $start;

        $limit = common_config('db', 'log_slow_queries');
        if (($limit > 0 && $delta >= $limit) || common_config('db', 'log_queries')) {
            $clean = $this->sanitizeQuery($string);
660 661 662 663 664 665 666 667 668 669
            if ($fail) {
                $msg = sprintf("FAILED DB query (%0.3fs): %s - %s", $delta, $fail->getMessage(), $clean);
            } else {
                $msg = sprintf("DB query (%0.3fs): %s", $delta, $clean);
            }
            common_log(LOG_DEBUG, $msg);
        }

        if ($fail) {
            throw $fail;
670 671 672 673
        }
        return $result;
    }

674 675 676 677 678
    /**
     * Find the first caller in the stack trace that's not a
     * low-level database function and add a comment to the
     * query string. This should then be visible in process lists
     * and slow query logs, to help identify problem areas.
679
     *
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695
     * Also marks whether this was a web GET/POST or which daemon
     * was running it.
     *
     * @param string $string SQL query string
     * @return string SQL query string, with a comment in it
     */
    function annotateQuery($string)
    {
        $ignore = array('annotateQuery',
                        '_query',
                        'query',
                        'get',
                        'insert',
                        'delete',
                        'update',
                        'find');
696
        $ignoreStatic = array('getKV',
697
                              'getClassKV',
698
                              'pkeyGet',
699
                              'pkeyGetClass',
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717
                              'cachedQuery');
        $here = get_class($this); // if we get confused
        $bt = debug_backtrace();

        // Find the first caller that's not us?
        foreach ($bt as $frame) {
            $func = $frame['function'];
            if (isset($frame['type']) && $frame['type'] == '::') {
                if (in_array($func, $ignoreStatic)) {
                    continue;
                }
                $here = $frame['class'] . '::' . $func;
                break;
            } else if (isset($frame['type']) && $frame['type'] == '->') {
                if ($frame['object'] === $this && in_array($func, $ignore)) {
                    continue;
                }
                if (in_array($func, $ignoreStatic)) {
718
                    continue; // @todo FIXME: This shouldn't be needed?
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
                }
                $here = get_class($frame['object']) . '->' . $func;
                break;
            }
            $here = $func;
            break;
        }

        if (php_sapi_name() == 'cli') {
            $context = basename($_SERVER['PHP_SELF']);
        } else {
            $context = $_SERVER['REQUEST_METHOD'];
        }

        // Slip the comment in after the first command,
        // or DB_DataObject gets confused about handling inserts and such.
        $parts = explode(' ', $string, 2);
        $parts[0] .= " /* $context $here */";
        return implode(' ', $parts);
    }

740 741 742 743 744 745 746 747 748
    // Sanitize a query for logging
    // @fixme don't trim spaces in string literals
    function sanitizeQuery($string)
    {
        $string = preg_replace('/\s+/', ' ', $string);
        $string = trim($string);
        return $string;
    }

749 750 751 752 753
    // We overload so that 'SET NAMES "utf8"' is called for
    // each connection

    function _connect()
    {
754
        global $_DB_DATAOBJECT, $_PEAR;
755 756 757 758

        $sum = $this->_getDbDsnMD5();

        if (!empty($_DB_DATAOBJECT['CONNECTIONS'][$sum]) &&
759
            !$_PEAR->isError($_DB_DATAOBJECT['CONNECTIONS'][$sum])) {
760 761 762 763 764
            $exists = true;
        } else {
            $exists = false;
       }

765 766 767 768 769 770 771 772 773 774 775 776 777 778
        // @fixme horrible evil hack!
        //
        // In multisite configuration we don't want to keep around a separate
        // connection for every database; we could end up with thousands of
        // connections open per thread. In an ideal world we might keep
        // a connection per server and select different databases, but that'd
        // be reliant on having the same db username/pass as well.
        //
        // MySQL connections are cheap enough we're going to try just
        // closing out the old connection and reopening when we encounter
        // a new DSN.
        //
        // WARNING WARNING if we end up actually using multiple DBs at a time
        // we'll need some fancier logic here.
779
        if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS']) && php_sapi_name() == 'cli') {
780 781 782 783 784 785 786
            foreach ($_DB_DATAOBJECT['CONNECTIONS'] as $index => $conn) {
                if (!empty($conn)) {
                    $conn->disconnect();
                }
                unset($_DB_DATAOBJECT['CONNECTIONS'][$index]);
            }
        }
787

788
        $result = parent::_connect();
789 790

        if ($result && !$exists) {
791
            $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
792 793 794
            if (common_config('db', 'type') == 'mysql' &&
                common_config('db', 'utf8')) {
                $conn = $DB->connection;
795 796 797 798 799 800
                if (!empty($conn)) {
                    if ($DB instanceof DB_mysqli) {
                        mysqli_set_charset($conn, 'utf8');
                    } else if ($DB instanceof DB_mysql) {
                        mysql_set_charset('utf8', $conn);
                    }
801
                }
802
            }
803 804 805 806
            // Needed to make timestamp values usefully comparable.
            if (common_config('db', 'type') == 'mysql') {
                parent::_query("set time_zone='+0:00'");
            }
807
        }
808

809 810
        return $result;
    }
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
    // XXX: largely cadged from DB_DataObject

    function _getDbDsnMD5()
    {
        if ($this->_database_dsn_md5) {
            return $this->_database_dsn_md5;
        }

        $dsn = $this->_getDbDsn();

        if (is_string($dsn)) {
            $sum = md5($dsn);
        } else {
            /// support array based dsn's
            $sum = md5(serialize($dsn));
        }

        return $sum;
    }

    function _getDbDsn()
    {
        global $_DB_DATAOBJECT;

        if (empty($_DB_DATAOBJECT['CONFIG'])) {
            DB_DataObject::_loadConfig();
        }

        $options = &$_DB_DATAOBJECT['CONFIG'];

        // if the databse dsn dis defined in the object..

        $dsn = isset($this->_database_dsn) ? $this->_database_dsn : null;

        if (!$dsn) {

            if (!$this->_database) {
                $this->_database = isset($options["table_{$this->__table}"]) ? $options["table_{$this->__table}"] : null;
            }

            if ($this->_database && !empty($options["database_{$this->_database}"]))  {
                $dsn = $options["database_{$this->_database}"];
            } else if (!empty($options['database'])) {
                $dsn = $options['database'];
            }
        }

        if (!$dsn) {
860
            // TRANS: Exception thrown when database name or Data Source Name could not be found.
861
            throw new Exception(_('No database name or DSN found anywhere.'));
862 863 864 865
        }

        return $dsn;
    }
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880

    static function blow()
    {
        $c = self::memcache();

        if (empty($c)) {
            return false;
        }

        $args = func_get_args();

        $format = array_shift($args);

        $keyPart = vsprintf($format, $args);

881
        $cacheKey = Cache::key($keyPart);
882 883 884

        return $c->delete($cacheKey);
    }
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900

    function fixupTimestamps()
    {
        // Fake up timestamp columns
        $columns = $this->table();
        foreach ($columns as $name => $type) {
            if ($type & DB_DATAOBJECT_MYSQLTIMESTAMP) {
                $this->$name = common_sql_now();
            }
        }
    }

    function debugDump()
    {
        common_debug("debugDump: " . common_log_objstring($this));
    }
901 902 903

    function raiseError($message, $type = null, $behaviour = null)
    {
904
        $id = get_class($this);
905
        if (!empty($this->id)) {
906 907
            $id .= ':' . $this->id;
        }
908 909 910
        if ($message instanceof PEAR_Error) {
            $message = $message->getMessage();
        }
911
        // Low level exception. No need for i18n as discussed with Brion.
912
        throw new ServerException("[$id] DB_DataObject error [$type]: $message");
913
    }
914 915 916 917 918 919 920 921 922

    static function cacheGet($keyPart)
    {
        $c = self::memcache();

        if (empty($c)) {
            return false;
        }

923
        $cacheKey = Cache::key($keyPart);
924 925 926 927

        return $c->get($cacheKey);
    }

928
    static function cacheSet($keyPart, $value, $flag=null, $expiry=null)
929 930 931 932 933 934 935
    {
        $c = self::memcache();

        if (empty($c)) {
            return false;
        }

936
        $cacheKey = Cache::key($keyPart);
937

938
        return $c->set($cacheKey, $value, $flag, $expiry);
939
    }
940 941 942 943 944 945 946 947 948 949 950 951 952 953

    static function valueString($v)
    {
        $vstr = null;
        if (is_object($v) && $v instanceof DB_DataObject_Cast) {
            switch ($v->type) {
            case 'date':
                $vstr = $v->year . '-' . $v->month . '-' . $v->day;
                break;
            case 'blob':
            case 'string':
            case 'sql':
            case 'datetime':
            case 'time':
954
                // Low level exception. No need for i18n as discussed with Brion.
955 956 957
                throw new ServerException("Unhandled DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
                break;
            default:
958
                // Low level exception. No need for i18n as discussed with Brion.
959 960 961 962 963 964 965 966
                throw new ServerException("Unknown DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
                break;
            }
        } else {
            $vstr = strval($v);
        }
        return $vstr;
    }
967
}