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

Memcached_DataObject.php 28.7 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
    static function listGetClass($cls, $keyCol, array $keyVals)
Evan Prodromou's avatar
Evan Prodromou committed
268
    {
269 270 271
        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
272
        $pkeyMap = array_fill_keys($keyVals, array());
Evan Prodromou's avatar
Evan Prodromou committed
273
        $result = array_fill_keys($keyVals, array());
274 275

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

Siebrand Mazeland's avatar
Siebrand Mazeland committed
277
        $toFetch = array();
278 279 280 281
        $allPkeys = array();

        // We only cache keys -- not objects!

Siebrand Mazeland's avatar
Siebrand Mazeland committed
282 283 284 285
        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
286 287 288
                foreach ($l as $pkey) {
                    $allPkeys[] = $pkey;
                }
Siebrand Mazeland's avatar
Siebrand Mazeland committed
289 290 291 292
            } else {
                $toFetch[] = $keyVal;
            }
        }
293

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

Evan Prodromou's avatar
Evan Prodromou committed
297 298 299 300 301 302
            foreach ($pkeyMap as $keyVal => $pkeyList) {
                foreach ($pkeyList as $pkeyVal) {
                    $i = $keyResults[implode(',',$pkeyVal)];
                    if (!empty($i)) {
                        $result[$keyVal][] = $i;
                    }
303 304 305 306
                }
            }
        }

307
        if (count($toFetch) > 0) {
308
            $i = new $cls;
309 310
            $i->whereAddIn($keyCol, $toFetch, $i->columnType($keyCol));
            if ($i->find()) {
311
                sprintf(__CLASS__ . "() got {$i->N} results for class $cls key $keyCol");
312 313 314 315 316 317 318 319 320 321
                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;
                }
Siebrand Mazeland's avatar
Siebrand Mazeland committed
322 323
            }
            foreach ($toFetch as $keyVal) {
324
                self::cacheSet(sprintf("%s:list-ids:%s:%s", strtolower($cls), $keyCol, $keyVal),
325
                               $pkeyMap[$keyVal]);
Siebrand Mazeland's avatar
Siebrand Mazeland committed
326
            }
Evan Prodromou's avatar
Evan Prodromou committed
327
        }
Evan Prodromou's avatar
Evan Prodromou committed
328

Siebrand Mazeland's avatar
Siebrand Mazeland committed
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
        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
346
    }
347

348
    /**
349
     * @todo FIXME: Should this return false on lookup fail to match getKV?
350
     */
351
    static function pkeyGetClass($cls, array $kv)
352
    {
353 354 355
        if (!is_a($cls, __CLASS__, true)) {
            throw new Exception('Trying to fetch ' . __CLASS__ . ' into a non-related class');
        }
356
        $i = Memcached_DataObject::multicache($cls, $kv);
357
        if ($i !== false) { // false == cache miss
358 359
            return $i;
        } else {
360
            $i = new $cls;
361
            foreach ($kv as $k => $v) {
362 363 364 365 366 367 368
                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;
                }
369 370 371 372
            }
            if ($i->find(true)) {
                $i->encache();
            } else {
Evan Prodromou's avatar
Evan Prodromou committed
373
                $i = null;
374 375 376 377 378
                $c = self::memcache();
                if (!empty($c)) {
                    $ck = self::multicacheKey($cls, $kv);
                    $c->set($ck, null);
                }
379
            }
380
            return $i;
381 382
        }
    }
383

384 385
    function insert()
    {
386
        $result = parent::insert();
387
        if ($result) {
388
            $this->fixupTimestamps();
389 390
            $this->encache(); // in case of cached negative lookups
        }
391 392
        return $result;
    }
393

394 395
    function update($orig=null)
    {
396 397 398 399 400
        if (is_object($orig) && $orig instanceof Memcached_DataObject) {
            $orig->decache(); # might be different keys
        }
        $result = parent::update($orig);
        if ($result) {
401
            $this->fixupTimestamps();
402 403 404 405
            $this->encache();
        }
        return $result;
    }
406

407 408
    function delete()
    {
409 410 411
        $this->decache(); # while we still have the values!
        return parent::delete();
    }
412

413
    static function memcache() {
414
        return Cache::instance();
415
    }
416

417
    static function cacheKey($cls, $k, $v) {
418
        if (is_object($cls) || is_object($k) || (is_object($v) && !($v instanceof DB_DataObject_Cast))) {
419 420 421 422
            $e = new Exception();
            common_log(LOG_ERR, __METHOD__ . ' object in param: ' .
                str_replace("\n", " ", $e->getTraceAsString()));
        }
423
        $vstr = self::valueString($v);
424
        return Cache::key(strtolower($cls).':'.$k.':'.$vstr);
425
    }
426

427
    static function getcached($cls, $k, $v) {
428
        $c = Memcached_DataObject::memcache();
429 430 431
        if (!$c) {
            return false;
        } else {
432 433 434
            $obj = $c->get(Memcached_DataObject::cacheKey($cls, $k, $v));
            if (0 == strcasecmp($cls, 'User')) {
                // Special case for User
435
                if (is_object($obj) && is_object($obj->id)) {
436 437 438 439 440 441
                    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;
442 443
        }
    }
444

445 446
    function keyTypes()
    {
447 448 449 450 451 452 453 454 455 456 457
        // 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;
        }

458
        global $_DB_DATAOBJECT;
459
        if (!isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"])) {
460
            $this->databaseStructure();
461 462

        }
463 464
        return $_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"];
    }
465

466 467
    function encache()
    {
468
        $c = $this->memcache();
469

470 471
        if (!$c) {
            return false;
472 473 474 475 476 477
        } 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;
478
        } else {
479
            $keys = $this->_allCacheKeys();
480

481 482 483
            foreach ($keys as $key) {
                $c->set($key, $this);
            }
484 485
        }
    }
486

487 488
    function decache()
    {
489
        $c = $this->memcache();
490

491 492
        if (!$c) {
            return false;
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
        }

        $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;
519
                }
520
                $ckeys[] = $this->cacheKey($this->tableName(), $key, self::valueString($this->$key));
521 522
            } else if ($type == 'K' || $type == 'N') {
                $pkey[] = $key;
523
                $pval[] = self::valueString($this->$key);
524
            } else {
525
                // Low level exception. No need for i18n as discussed with Brion.
526
                throw new Exception("Unknown key type $key => $type for " . $this->tableName());
527 528
            }
        }
529 530 531 532 533 534 535 536 537 538

        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;
539
    }
540

541
    static function multicache($cls, $kv)
542
    {
543
        ksort($kv);
544
        $c = self::memcache();
545 546 547
        if (!$c) {
            return false;
        } else {
548
            return $c->get(self::multicacheKey($cls, $kv));
549 550
        }
    }
millette's avatar
millette committed
551

552 553 554 555 556 557 558 559
    static function multicacheKey($cls, $kv)
    {
        ksort($kv);
        $pkeys = implode(',', array_keys($kv));
        $pvals = implode(',', array_values($kv));
        return self::cacheKey($cls, $pkeys, $pvals);
    }

560 561
    function getSearchEngine($table)
    {
millette's avatar
millette committed
562
        require_once INSTALLDIR.'/lib/search_engines.php';
563 564 565 566 567 568 569 570

        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);
571
                } else {
572 573
                    // Low level exception. No need for i18n as discussed with Brion.
                    throw new ServerException('Unknown search type: ' . $type);
millette's avatar
millette committed
574
                }
575 576
            } else {
                $search_engine = new PGSearch($this, $table);
577
            }
millette's avatar
millette committed
578
        }
579

millette's avatar
millette committed
580 581
        return $search_engine;
    }
582 583 584

    static function cachedQuery($cls, $qry, $expiry=3600)
    {
585
        $c = Memcached_DataObject::memcache();
586 587 588
        if (!$c) {
            $inst = new $cls();
            $inst->query($qry);
589
            return $inst;
590
        }
591
        $key_part = Cache::keyize($cls).':'.md5($qry);
592
        $ckey = Cache::key($key_part);
593
        $stored = $c->get($ckey);
594 595

        if ($stored !== false) {
596 597 598 599
            return new ArrayWrapper($stored);
        }

        $inst = new $cls();
600
        $inst->query($qry);
601 602 603 604 605
        $cached = array();
        while ($inst->fetch()) {
            $cached[] = clone($inst);
        }
        $inst->free();
606
        $c->set($ckey, $cached, Cache::COMPRESSED, $expiry);
607
        return new ArrayWrapper($cached);
608
    }
609

610
    /**
611
     * sends query to database - this is the private one that must work
612 613 614 615 616 617 618 619 620 621
     *   - 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)
    {
622 623 624 625
        if (common_config('db', 'annotate_queries')) {
            $string = $this->annotateQuery($string);
        }

626
        $start = microtime(true);
627
        $fail = false;
Brion Vibber's avatar
Brion Vibber committed
628 629
        $result = null;
        if (Event::handle('StartDBQuery', array($this, $string, &$result))) {
630
            common_perf_counter('query', $string);
Brion Vibber's avatar
Brion Vibber committed
631 632 633 634 635
            try {
                $result = parent::_query($string);
            } catch (Exception $e) {
                $fail = $e;
            }
Brion Vibber's avatar
Brion Vibber committed
636
            Event::handle('EndDBQuery', array($this, $string, &$result));
637
        }
638 639 640 641 642
        $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);
643 644 645 646 647 648 649 650 651 652
            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;
653 654 655 656
        }
        return $result;
    }

657 658 659 660 661
    /**
     * 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.
662
     *
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
     * 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');
679
        $ignoreStatic = array('getKV',
680
                              'getClassKV',
681
                              'pkeyGet',
682
                              'pkeyGetClass',
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700
                              '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)) {
701
                    continue; // @todo FIXME: This shouldn't be needed?
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722
                }
                $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);
    }

723 724 725 726 727 728 729 730 731
    // 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;
    }

732 733 734 735 736 737
    // We overload so that 'SET NAMES "utf8"' is called for
    // each connection

    function _connect()
    {
        global $_DB_DATAOBJECT;
738 739 740 741 742 743 744 745 746 747

        $sum = $this->_getDbDsnMD5();

        if (!empty($_DB_DATAOBJECT['CONNECTIONS'][$sum]) &&
            !PEAR::isError($_DB_DATAOBJECT['CONNECTIONS'][$sum])) {
            $exists = true;
        } else {
            $exists = false;
       }

748 749 750 751 752 753 754 755 756 757 758 759 760 761
        // @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.
762
        if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS']) && php_sapi_name() == 'cli') {
763 764 765 766 767 768 769
            foreach ($_DB_DATAOBJECT['CONNECTIONS'] as $index => $conn) {
                if (!empty($conn)) {
                    $conn->disconnect();
                }
                unset($_DB_DATAOBJECT['CONNECTIONS'][$index]);
            }
        }
770

771
        $result = parent::_connect();
772 773

        if ($result && !$exists) {
774
            $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
775 776 777
            if (common_config('db', 'type') == 'mysql' &&
                common_config('db', 'utf8')) {
                $conn = $DB->connection;
778 779 780 781 782 783
                if (!empty($conn)) {
                    if ($DB instanceof DB_mysqli) {
                        mysqli_set_charset($conn, 'utf8');
                    } else if ($DB instanceof DB_mysql) {
                        mysql_set_charset('utf8', $conn);
                    }
784
                }
785
            }
786 787 788 789
            // Needed to make timestamp values usefully comparable.
            if (common_config('db', 'type') == 'mysql') {
                parent::_query("set time_zone='+0:00'");
            }
790
        }
791

792 793
        return $result;
    }
794

795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842
    // 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) {
843
            // TRANS: Exception thrown when database name or Data Source Name could not be found.
844
            throw new Exception(_('No database name or DSN found anywhere.'));
845 846 847 848
        }

        return $dsn;
    }
849 850 851 852 853 854 855 856 857 858 859 860 861 862 863

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

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

        $args = func_get_args();

        $format = array_shift($args);

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

864
        $cacheKey = Cache::key($keyPart);
865 866 867

        return $c->delete($cacheKey);
    }
868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883

    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));
    }
884 885 886

    function raiseError($message, $type = null, $behaviour = null)
    {
887
        $id = get_class($this);
888
        if (!empty($this->id)) {
889 890
            $id .= ':' . $this->id;
        }
891 892 893
        if ($message instanceof PEAR_Error) {
            $message = $message->getMessage();
        }
894
        // Low level exception. No need for i18n as discussed with Brion.
895
        throw new ServerException("[$id] DB_DataObject error [$type]: $message");
896
    }
897 898 899 900 901 902 903 904 905

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

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

906
        $cacheKey = Cache::key($keyPart);
907 908 909 910

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

911
    static function cacheSet($keyPart, $value, $flag=null, $expiry=null)
912 913 914 915 916 917 918
    {
        $c = self::memcache();

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

919
        $cacheKey = Cache::key($keyPart);
920

921
        return $c->set($cacheKey, $value, $flag, $expiry);
922
    }
923 924 925 926 927 928 929 930 931 932 933 934 935 936

    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':
937
                // Low level exception. No need for i18n as discussed with Brion.
938 939 940
                throw new ServerException("Unhandled DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
                break;
            default:
941
                // Low level exception. No need for i18n as discussed with Brion.
942 943 944 945 946 947 948 949
                throw new ServerException("Unknown DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
                break;
            }
        } else {
            $vstr = strval($v);
        }
        return $vstr;
    }
950
}