2 rcs_id('$Id: dbaBase.php,v 1.27 2007-01-02 13:19:33 rurban Exp $');
4 require_once('lib/WikiDB/backend.php');
6 // FIXME:padding of data? Is it needed? dba_optimize() seems to do a good
7 // job at packing 'gdbm' (and 'db2') databases.
14 * Values: latestversion . ':' . flags . ':' serialized hash of page meta data
15 * Currently flags = 1 if latest version has empty content.
18 * Index: version:pagename
19 * Value: serialized hash of revision meta data, including:
20 * + quasi-meta-data %content
23 * index: 'o' . pagename
24 * value: serialized list of pages (names) which pagename links to.
25 * index: 'i' . pagename
26 * value: serialized list of pages which link to pagename
29 * Don't keep tables locked the whole time
32 * list of pagenames for get_all_pages
34 * RecentChanges support:
35 * lists of most recent edits (major, minor, either).
38 * Separate hit table, so we don't have to update the whole page entry
39 * each time we get a hit. (Maybe not so important though...).
42 require_once('lib/DbaPartition.php');
44 class WikiDB_backend_dbaBase
45 extends WikiDB_backend
47 function WikiDB_backend_dbaBase (&$dba) {
49 // TODO: page and version tables should be in their own files, probably.
50 // We'll pack them all in one for now (testing).
51 // 2004-07-09 10:07:30 rurban: It's fast enough this way.
52 $this->_pagedb = new DbaPartition($dba, 'p');
53 $this->_versiondb = new DbaPartition($dba, 'v');
54 $linkdbpart = new DbaPartition($dba, 'l');
55 $this->_linkdb = new WikiDB_backend_dbaBase_linktable($linkdbpart);
56 $this->_dbdb = new DbaPartition($dba, 'd');
59 function sortable_columns() {
60 return array('pagename','mtime'/*,'author_id','author'*/);
68 $this->_db->optimize();
77 // rebuild backlink table
78 $this->_linkdb->rebuild();
83 return $this->_linkdb->check();
86 function get_pagedata($pagename) {
87 $result = $this->_pagedb->get($pagename);
90 list(,,$packed) = explode(':', $result, 3);
91 $data = unserialize($packed);
95 function update_pagedata($pagename, $newdata) {
96 $result = $this->_pagedb->get($pagename);
98 list($latestversion,$flags,$data) = explode(':', $result, 3);
99 $data = unserialize($data);
102 $latestversion = $flags = 0;
106 foreach ($newdata as $key => $val) {
112 $this->_pagedb->set($pagename,
113 (int)$latestversion . ':'
118 function get_latest_version($pagename) {
119 return (int) $this->_pagedb->get($pagename);
122 function get_previous_version($pagename, $version) {
123 $versdb = &$this->_versiondb;
125 while (--$version > 0) {
126 if ($versdb->exists($version . ":$pagename"))
132 //check $want_content
133 function get_versiondata($pagename, $version, $want_content=false) {
134 $data = $this->_versiondb->get((int)$version . ":$pagename");
135 if (empty($data)) return false;
137 $data = unserialize($data);
139 $data['%content'] = !empty($data['%content']);
145 * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
148 //function delete_page($pagename) { $this->purge_page($pagename); }
151 * Completely delete page from the database.
153 function purge_page($pagename) {
154 $pagedb = &$this->_pagedb;
155 $versdb = &$this->_versiondb;
157 $version = $this->get_latest_version($pagename);
158 while ($version > 0) {
159 $versdb->set($version-- . ":$pagename", false);
161 $pagedb->set($pagename, false);
163 $this->set_links($pagename, false);
166 function rename_page($pagename, $to) {
167 $result = $this->_pagedb->get($pagename);
169 list($version,$flags,$data) = explode(':', $result, 3);
170 $data = unserialize($data);
175 $this->_pagedb->delete($pagename);
176 $data['pagename'] = $to;
177 $this->_pagedb->set($to,
181 // move over the latest version only
182 $pvdata = $this->get_versiondata($pagename, $version, true);
183 $this->set_versiondata($to, $version, $pvdata);
188 * Delete an old revision of a page.
190 function delete_versiondata($pagename, $version) {
191 $versdb = &$this->_versiondb;
193 $latest = $this->get_latest_version($pagename);
195 assert($version > 0);
196 assert($version <= $latest);
198 $versdb->set((int)$version . ":$pagename", false);
200 if ($version == $latest) {
201 $previous = $this->get_previous_version($version);
203 $pvdata = $this->get_versiondata($pagename, $previous);
204 $is_empty = empty($pvdata['%content']);
208 $this->_update_latest_version($pagename, $previous, $is_empty);
213 * Create a new revision of a page.
215 function set_versiondata($pagename, $version, $data) {
216 $versdb = &$this->_versiondb;
218 $versdb->set((int)$version . ":$pagename", serialize($data));
219 if ($version > $this->get_latest_version($pagename))
220 $this->_update_latest_version($pagename, $version, empty($data['%content']));
223 function _update_latest_version($pagename, $latest, $flags) {
224 $pagedb = &$this->_pagedb;
226 $pdata = $pagedb->get($pagename);
228 list(,,$pagedata) = explode(':',$pdata,3);
230 $pagedata = serialize(array());
232 $pagedb->set($pagename, (int)$latest . ':' . (int)$flags . ":$pagedata");
235 function numPages($include_empty=false, $exclude='') {
236 $pagedb = &$this->_pagedb;
238 for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
240 assert(!empty($page));
243 if ($exclude and in_array($page, $exclude)) continue;
244 if (!$include_empty) {
245 if (!($data = $pagedb->get($page))) continue;
246 list($latestversion,$flags,) = explode(':', $data, 3);
248 if ($latestversion == 0 || $flags != 0)
249 continue; // current content is empty
256 function get_all_pages($include_empty=false, $sortby='', $limit='', $exclude='') {
257 $pagedb = &$this->_pagedb;
259 for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
261 assert(!empty($page));
264 if ($exclude and in_array($page, $exclude)) continue;
265 if ($limit and count($pages) > $limit) break;
266 if (!$include_empty) {
267 if (!($data = $pagedb->get($page))) continue;
268 list($latestversion,$flags,) = explode(':', $data, 3);
270 if ($latestversion == 0 || $flags != 0)
271 continue; // current content is empty
275 return new WikiDB_backend_dbaBase_pageiter($this, $pages,
276 array('sortby'=>$sortby,
280 function set_links($pagename, $links) {
281 $this->_linkdb->set_links($pagename, $links);
284 function get_links($pagename, $reversed=true, $include_empty=false,
285 $sortby='', $limit='', $exclude='',
286 $want_relations=false)
288 // optimization: if no relation at all is found, mark it in the iterator.
289 $links = $this->_linkdb->get_links($pagename, $reversed, $want_relations);
290 return new WikiDB_backend_dbaBase_pageiter
292 array('sortby'=>$sortby,
295 'want_relations'=>$want_relations,
296 'found_relations' => $want_relations ? $this->_linkdb->found_relations : 0
303 * @return array of all linkrelations
304 * Faster than the dumb WikiDB method.
306 function list_relations($also_attributes=false, $only_attributes=false, $sorted=true) {
307 $linkdb = &$this->_linkdb;
308 $relations = array();
309 for ($link = $linkdb->_db->firstkey(); $link!== false; $link = $linkdb->_db->nextkey()) {
310 if ($link[0] != 'o') continue;
311 $links = $linkdb->_get_links('o', substr($link,1));
312 foreach ($links as $link) { // linkto => page, linkrelation => page
314 and $link['relation']
315 and !in_array($link['relation'], $relations))
317 $is_attribute = empty($link['linkto']); // a relation has both
319 if ($only_attributes or $also_attributes)
320 $relations[] = $link['relation'];
321 } elseif (!$only_attributes) {
322 $relations[] = $link['relation'];
335 * WikiDB_backend_dumb_LinkSearchIter searches over all pages and then all its links.
336 * Since there are less links than pages, and we easily get the pagename from the link key,
337 * we iterate here directly over the linkdb and check the pagematch there.
339 * @param $pages object A TextSearchQuery object.
340 * @param $linkvalue object A TextSearchQuery object for the linkvalues
341 * (linkto, relation or backlinks or attribute values).
342 * @param $linktype string One of the 4 linktypes.
343 * @param $relation object A TextSearchQuery object or false.
344 * @param $options array Currently ignored. hash of sortby, limit, exclude.
345 * @return object A WikiDB_backend_iterator.
346 * @see WikiDB::linkSearch
348 function link_search( $pages, $linkvalue, $linktype, $relation=false, $options=array() ) {
349 $linkdb = &$this->_linkdb;
352 $want_relations = false;
353 if ($linktype == 'relation') {
354 $want_relations = true;
355 $field = 'linkrelation';
357 if ($linktype == 'attribute') {
358 $want_relations = true;
359 $field = 'attribute';
361 if ($linktype == 'linkfrom') {
365 for ($link = $linkdb->_db->firstkey(); $link!== false; $link = $linkdb->_db->nextkey()) {
366 $type = $reverse ? 'i' : 'o';
367 if ($link[0] != $type) continue;
368 $pagename = substr($link, 1);
369 if (!$pages->match($pagename)) continue;
370 if ($linktype == 'attribute') {
371 $page = $GLOBALS['request']->_dbi->getPage($pagename);
372 $attribs = $page->get('attributes');
374 foreach ($attribs as $attribute => $value) {
375 if ($relation and !$relation->match($attribute)) continue;
376 if (!$linkvalue->match($value)) continue;
377 $links[] = array('pagename' => $pagename,
378 'linkname' => $attribute,
379 'linkvalue' => $value);
383 if ($want_relations) {
384 // MAP linkrelation : pagename => thispagename : linkname : linkvalue
385 $_links = $linkdb->_get_links('o', $pagename);
386 foreach ($_links as $link) { // linkto => page, linkrelation => page
387 if ($relation and !$relation->match($link['linkrelation'])) continue;
388 if (!$linkvalue->match($link['linkto'])) continue;
389 $links[] = array('pagename' => $pagename,
390 'linkname' => $link['linkrelation'],
391 'linkvalue' => $link['linkto']);
394 $_links = $linkdb->_get_links($reverse ? 'i' : 'o', $pagename);
395 foreach ($_links as $link) { // linkto => page
397 $link = $link['linkto'];
398 if (!$linkvalue->match($link)) continue;
399 $links[] = array('pagename' => $pagename,
401 'linkvalue' => $link);
406 $options['want_relations'] = true; // Iter hack to force return of the whole hash
407 return new WikiDB_backend_dbaBase_pageiter($this, $links, $options);
412 function WikiDB_backend_dbaBase_sortby_pagename_ASC ($a, $b) {
413 return strcasecmp($a, $b);
415 function WikiDB_backend_dbaBase_sortby_pagename_DESC ($a, $b) {
416 return strcasecmp($b, $a);
418 function WikiDB_backend_dbaBase_sortby_mtime_ASC ($a, $b) {
419 return WikiDB_backend_dbaBase_sortby_num($a, $b, 'mtime');
421 function WikiDB_backend_dbaBase_sortby_mtime_DESC ($a, $b) {
422 return WikiDB_backend_dbaBase_sortby_num($b, $a, 'mtime');
425 function WikiDB_backend_dbaBase_sortby_hits_ASC ($a, $b) {
426 return WikiDB_backend_dbaBase_sortby_num($a, $b, 'hits');
428 function WikiDB_backend_dbaBase_sortby_hits_DESC ($a, $b) {
429 return WikiDB_backend_dbaBase_sortby_num($b, $a, 'hits');
432 function WikiDB_backend_dbaBase_sortby_num($aname, $bname, $field) {
434 $dbi = $request->getDbh();
435 // fields are stored in versiondata
436 $av = $dbi->_backend->get_latest_version($aname);
437 $bv = $dbi->_backend->get_latest_version($bname);
438 $a = $dbi->_backend->get_versiondata($aname, $av, false);
440 $b = $dbi->_backend->get_versiondata($bname, $bv, false);
442 if ((!isset($a[$field]) && !isset($b[$field])) || ($a[$field] === $b[$field])) {
445 return (!isset($a[$field]) || ($a[$field] < $b[$field])) ? -1 : 1;
449 class WikiDB_backend_dbaBase_pageiter
450 extends WikiDB_backend_iterator
452 // fixed for linkrelations
453 function WikiDB_backend_dbaBase_pageiter(&$backend, &$pages, $options=false) {
454 $this->_backend = $backend;
455 $this->_options = $options;
457 if (!empty($options['sortby'])) {
458 $sortby = WikiDB_backend::sortby($options['sortby'], 'db', array('pagename','mtime'));
459 if ($sortby and !strstr($sortby, "hits ")) { // check for which column to sortby
460 usort($pages, 'WikiDB_backend_dbaBase_sortby_'.str_replace(' ','_',$sortby));
463 if (!empty($options['limit'])) {
464 list($offset,$limit) = WikiDB_backend::limit($options['limit']);
465 $pages = array_slice($pages, $offset, $limit);
467 $this->_pages = $pages;
469 $this->_pages = array();
472 // fixed for relations
474 if ( ! ($page = array_shift($this->_pages)) )
476 if (!empty($this->_options['want_relations'])) {
477 // $linkrelation = $page['linkrelation'];
478 $pagename = $page['pagename'];
479 if (!empty($this->_options['exclude']) and in_array($pagename, $this->_options['exclude']))
480 return $this->next();
483 if (!empty($this->_options['exclude']) and in_array($page, $this->_options['exclude']))
484 return $this->next();
485 return array('pagename' => $page);
489 $this->_pages = array();
493 class WikiDB_backend_dbaBase_linktable
495 function WikiDB_backend_dbaBase_linktable(&$dba) {
499 //FIXME: try storing link lists as hashes rather than arrays.
500 // (backlink deletion would be faster.)
501 function get_links($page, $reversed=true, $want_relations=false) {
502 if ($want_relations) {
503 $this->found_relations = 0;
504 $links = $this->_get_links($reversed ? 'i' : 'o', $page);
505 $linksonly = array();
506 foreach ($links as $link) { // linkto => page, linkrelation => page
507 if (is_array($link) and isset($link['relation'])) {
508 if ($link['relation'])
509 $this->found_relations++;
510 $linksonly[] = array('pagename' => $link['linkto'],
511 'linkrelation' => $link['relation']);
512 } else { // empty relations are stripped
513 $linksonly[] = array('pagename' => $link['linkto']);
518 $links = $this->_get_links($reversed ? 'i' : 'o', $page);
519 $linksonly = array();
520 foreach ($links as $link) {
521 if (is_array($link)) {
522 $linksonly[] = $link['linkto'];
524 $linksonly[] = $link;
530 // fixed: relations ready
531 function set_links($page, $links) {
533 $oldlinks = $this->get_links($page, false, false);
535 if (!is_array($links)) {
536 assert(empty($links));
539 $this->_set_links('o', $page, $links);
541 /* Now for the backlink update we squash the linkto hashes into a simple array */
543 foreach ($links as $hash) {
544 if (!empty($hash['linkto'])
545 and !in_array($hash['linkto'],$newlinks))
546 // for attributes it's empty
547 $newlinks[] = $hash['linkto'];
549 //$newlinks = array_unique($newlinks);
555 $new = current($newlinks);
556 $old = current($oldlinks);
557 while ($new !== false || $old !== false) {
558 if ($old === false || ($new !== false && $new < $old)) {
559 // $new is a new link (not in $oldlinks).
560 $this->_add_backlink($new, $page);
561 $new = next($newlinks);
563 elseif ($new === false || $old < $new) {
564 // $old is a obsolete link (not in $newlinks).
565 $this->_delete_backlink($old, $page);
566 $old = next($oldlinks);
569 // Unchanged link (in both $newlist and $oldlinks).
570 assert($new == $old);
571 $new = next($newlinks);
572 $old = next($oldlinks);
578 * Rebuild the back-link index.
580 * This should never be needed, but if the database gets hosed for some reason,
581 * this should put it back into a consistent state.
583 * We assume the forward links in the our table are correct, and recalculate
584 * all the backlinks appropriately.
586 function rebuild () {
589 // Delete the backlink tables, make a list of lo.page keys.
591 for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
594 elseif ($key[0] == 'o')
597 trigger_error("Bad key in linktable: '$key'", E_USER_WARNING);
601 foreach ($okeys as $key) {
602 $page = substr($key,1);
603 $links = $this->_get_links('o', $page);
605 $this->set_links($page, $links);
612 // FIXME: check for sortedness and uniqueness in links lists.
614 for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
615 if (strlen($key) < 1 || ($key[0] != 'i' && $key[0] != 'o')) {
616 $errs[] = "Bad key '$key' in table";
619 $page = substr($key, 1);
620 if ($key[0] == 'o') {
622 foreach($this->_get_links('o', $page) as $link) {
623 $link = $link['linkto'];
624 if (!$this->_has_link('i', $link, $page))
625 $errs[] = "backlink entry missing for link '$page'->'$link'";
629 assert($key[0] == 'i');
631 foreach($this->_get_links('i', $page) as $link) {
632 if (!$this->_has_link('o', $link, $page))
633 $errs[] = "link entry missing for backlink '$page'<-'$link'";
637 //if ($errs) $this->rebuild();
638 return isset($errs) ? $errs : false;
641 /* TODO: Add another lrRelationName key for relations.
642 * lrRelationName: frompage => topage
645 function _add_relation($page, $linkedfrom) {
646 $relations = $this->_get_links('r', $page);
647 $backlinks[] = $linkedfrom;
649 $this->_set_links('i', $page, $backlinks);
652 function _add_backlink($page, $linkedfrom) {
653 $backlinks = $this->_get_links('i', $page);
654 $backlinks[] = $linkedfrom;
656 $this->_set_links('i', $page, $backlinks);
659 function _delete_backlink($page, $linkedfrom) {
660 $backlinks = $this->_get_links('i', $page);
661 foreach ($backlinks as $key => $backlink) {
662 if ($backlink == $linkedfrom)
663 unset($backlinks[$key]);
665 $this->_set_links('i', $page, $backlinks);
668 function _has_link($which, $page, $link) {
669 $links = $this->_get_links($which, $page);
670 //TODO: since links are always sorted do a binary search or at least break if >
671 foreach($links as $l) {
672 if ($l['linkto'] == $link)
678 function _get_links($which, $page) {
679 $data = $this->_db->get($which . $page);
680 return $data ? unserialize($data) : array();
683 function _set_links($which, $page, &$links) {
684 $key = $which . $page;
686 $this->_db->set($key, serialize($links));
688 $this->_db->set($key, false);
692 // $Log: not supported by cvs2svn $
693 // Revision 1.26 2006/12/22 00:27:37 rurban
697 // (c-file-style: "gnu")
702 // c-hanging-comment-ender-p: nil
703 // indent-tabs-mode: nil