2 rcs_id('$Id: dbaBase.php,v 1.26 2006-12-22 00:27:37 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();
76 $this->_linkdb->rebuild();
81 return $this->_linkdb->check();
84 function get_pagedata($pagename) {
85 $result = $this->_pagedb->get($pagename);
88 list(,,$packed) = explode(':', $result, 3);
89 $data = unserialize($packed);
93 function update_pagedata($pagename, $newdata) {
94 $result = $this->_pagedb->get($pagename);
96 list($latestversion,$flags,$data) = explode(':', $result, 3);
97 $data = unserialize($data);
100 $latestversion = $flags = 0;
104 foreach ($newdata as $key => $val) {
110 $this->_pagedb->set($pagename,
111 (int)$latestversion . ':'
116 function get_latest_version($pagename) {
117 return (int) $this->_pagedb->get($pagename);
120 function get_previous_version($pagename, $version) {
121 $versdb = &$this->_versiondb;
123 while (--$version > 0) {
124 if ($versdb->exists($version . ":$pagename"))
130 //check $want_content
131 function get_versiondata($pagename, $version, $want_content=false) {
132 $data = $this->_versiondb->get((int)$version . ":$pagename");
133 if (empty($data)) return false;
135 $data = unserialize($data);
137 $data['%content'] = !empty($data['%content']);
143 * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
146 //function delete_page($pagename) { $this->purge_page($pagename); }
149 * Completely delete page from the database.
151 function purge_page($pagename) {
152 $pagedb = &$this->_pagedb;
153 $versdb = &$this->_versiondb;
155 $version = $this->get_latest_version($pagename);
156 while ($version > 0) {
157 $versdb->set($version-- . ":$pagename", false);
159 $pagedb->set($pagename, false);
161 $this->set_links($pagename, false);
164 function rename_page($pagename, $to) {
165 $result = $this->_pagedb->get($pagename);
167 list($version,$flags,$data) = explode(':', $result, 3);
168 $data = unserialize($data);
173 $this->_pagedb->delete($pagename);
174 $data['pagename'] = $to;
175 $this->_pagedb->set($to,
179 // move over the latest version only
180 $pvdata = $this->get_versiondata($pagename, $version, true);
181 $this->set_versiondata($to, $version, $pvdata);
186 * Delete an old revision of a page.
188 function delete_versiondata($pagename, $version) {
189 $versdb = &$this->_versiondb;
191 $latest = $this->get_latest_version($pagename);
193 assert($version > 0);
194 assert($version <= $latest);
196 $versdb->set((int)$version . ":$pagename", false);
198 if ($version == $latest) {
199 $previous = $this->get_previous_version($version);
201 $pvdata = $this->get_versiondata($pagename, $previous);
202 $is_empty = empty($pvdata['%content']);
206 $this->_update_latest_version($pagename, $previous, $is_empty);
211 * Create a new revision of a page.
213 function set_versiondata($pagename, $version, $data) {
214 $versdb = &$this->_versiondb;
216 $versdb->set((int)$version . ":$pagename", serialize($data));
217 if ($version > $this->get_latest_version($pagename))
218 $this->_update_latest_version($pagename, $version, empty($data['%content']));
221 function _update_latest_version($pagename, $latest, $flags) {
222 $pagedb = &$this->_pagedb;
224 $pdata = $pagedb->get($pagename);
226 list(,,$pagedata) = explode(':',$pdata,3);
228 $pagedata = serialize(array());
230 $pagedb->set($pagename, (int)$latest . ':' . (int)$flags . ":$pagedata");
233 function numPages($include_empty=false, $exclude=false) {
234 $pagedb = &$this->_pagedb;
236 for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
238 assert(!empty($page));
241 if ($exclude and in_array($page, $exclude)) continue;
242 if (!$include_empty) {
243 if (!($data = $pagedb->get($page))) continue;
244 list($latestversion,$flags,) = explode(':', $data, 3);
246 if ($latestversion == 0 || $flags != 0)
247 continue; // current content is empty
254 function get_all_pages($include_empty=false, $sortby=false, $limit=false, $exclude=false) {
255 $pagedb = &$this->_pagedb;
257 for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
259 assert(!empty($page));
262 if ($exclude and in_array($page, $exclude)) continue;
263 if ($limit and count($pages) > $limit) break;
264 if (!$include_empty) {
265 if (!($data = $pagedb->get($page))) continue;
266 list($latestversion,$flags,) = explode(':', $data, 3);
268 if ($latestversion == 0 || $flags != 0)
269 continue; // current content is empty
273 return new WikiDB_backend_dbaBase_pageiter($this, $pages,
274 array('sortby'=>$sortby,
278 function set_links($pagename, $links) {
279 $this->_linkdb->set_links($pagename, $links);
282 function get_links($pagename, $reversed=true, $include_empty=false,
283 $sortby=false, $limit=false, $exclude=false,
284 $want_relations=false)
286 $links = $this->_linkdb->get_links($pagename, $reversed, $want_relations);
287 return new WikiDB_backend_dbaBase_pageiter($this, $links,
288 array('sortby'=>$sortby,
291 'want_relations'=>$want_relations,
295 function list_relations($pagename) {
296 $links = $this->_linkdb->get_links($pagename, false, true);
297 return new WikiDB_backend_pageiter($this, $links,
298 array('want_relations'=>true));
302 function WikiDB_backend_dbaBase_sortby_pagename_ASC ($a, $b) {
303 return strcasecmp($a, $b);
305 function WikiDB_backend_dbaBase_sortby_pagename_DESC ($a, $b) {
306 return strcasecmp($b, $a);
308 function WikiDB_backend_dbaBase_sortby_mtime_ASC ($a, $b) {
309 return WikiDB_backend_dbaBase_sortby_num($a, $b, 'mtime');
311 function WikiDB_backend_dbaBase_sortby_mtime_DESC ($a, $b) {
312 return WikiDB_backend_dbaBase_sortby_num($b, $a, 'mtime');
315 function WikiDB_backend_dbaBase_sortby_hits_ASC ($a, $b) {
316 return WikiDB_backend_dbaBase_sortby_num($a, $b, 'hits');
318 function WikiDB_backend_dbaBase_sortby_hits_DESC ($a, $b) {
319 return WikiDB_backend_dbaBase_sortby_num($b, $a, 'hits');
322 function WikiDB_backend_dbaBase_sortby_num($aname, $bname, $field) {
324 $dbi = $request->getDbh();
325 // fields are stored in versiondata
326 $av = $dbi->_backend->get_latest_version($aname);
327 $bv = $dbi->_backend->get_latest_version($bname);
328 $a = $dbi->_backend->get_versiondata($aname, $av, false);
330 $b = $dbi->_backend->get_versiondata($bname, $bv, false);
332 if ((!isset($a[$field]) && !isset($b[$field])) || ($a[$field] === $b[$field])) {
335 return (!isset($a[$field]) || ($a[$field] < $b[$field])) ? -1 : 1;
339 class WikiDB_backend_dbaBase_pageiter
340 extends WikiDB_backend_iterator
342 // fixed for linkrelations
343 function WikiDB_backend_dbaBase_pageiter(&$backend, &$pages, $options=false) {
344 $this->_backend = $backend;
345 $this->_options = $options;
347 if (!empty($options['sortby'])) {
348 $sortby = WikiDB_backend::sortby($options['sortby'], 'db', array('pagename','mtime'));
349 if ($sortby and !strstr($sortby, "hits ")) { // check for which column to sortby
350 usort($pages, 'WikiDB_backend_dbaBase_sortby_'.str_replace(' ','_',$sortby));
353 if (!empty($options['limit'])) {
354 list($offset,$limit) = WikiDB_backend::limit($options['limit']);
355 $pages = array_slice($pages, $offset, $limit);
357 $this->_pages = $pages;
359 $this->_pages = array();
362 // fixed for relations
364 if ( ! ($pagename = array_shift($this->_pages)) )
366 if (!empty($options['want_relations'])) {
367 $linkrelation = $pagename['linkrelation'];
368 $pagename = $pagename['pagename'];
369 if (!empty($options['exclude']) and in_array($pagename, $options['exclude']))
370 return $this->next();
371 return array('pagename' => $pagename,
372 'linkrelation' => $linkrelation);
374 if (!empty($options['exclude']) and in_array($pagename, $options['exclude']))
375 return $this->next();
376 return array('pagename' => $pagename);
380 $this->_pages = array();
384 class WikiDB_backend_dbaBase_linktable
386 function WikiDB_backend_dbaBase_linktable(&$dba) {
390 //FIXME: try storing link lists as hashes rather than arrays.
391 // (backlink deletion would be faster.)
392 function get_links($page, $reversed=true, $want_relations=false) {
393 if ($want_relations) {
394 $links = $this->_get_links($reversed ? 'i' : 'o', $page);
395 $linksonly = array();
396 foreach ($links as $link) { // linkto => page, linkrelation => page
397 if (is_array($link) and $link['relation'])
398 $linksonly[] = array('pagename' => $link['linkto'],
399 'linkrelation' => $link['relation']);
400 else { // empty relations are stripped
401 $linksonly[] = array('pagename' => $link['linkto']);
406 $links = $this->_get_links($reversed ? 'i' : 'o', $page);
407 $linksonly = array();
408 foreach ($links as $link) {
410 $linksonly[] = $link['linkto'];
412 $linksonly[] = $link;
418 // fixed: relations ready
419 function set_links($page, $links) {
421 $oldlinks = $this->get_links($page, false, false);
423 if (!is_array($links)) {
424 assert(empty($links));
427 $this->_set_links('o', $page, $links);
429 /* Now for the backlink update we squash the linkto hashes into a simple array */
431 foreach ($links as $hash) {
432 $newlinks[] = $hash['linkto'];
434 $newlinks = array_unique($newlinks);
440 $new = current($newlinks);
441 $old = current($oldlinks);
442 while ($new !== false || $old !== false) {
443 if ($old === false || ($new !== false && $new < $old)) {
444 // $new is a new link (not in $oldlinks).
445 $this->_add_backlink($new, $page);
446 $new = next($newlinks);
448 elseif ($new === false || $old < $new) {
449 // $old is a obsolete link (not in $newlinks).
450 $this->_delete_backlink($old, $page);
451 $old = next($oldlinks);
454 // Unchanged link (in both $newlist and $oldlinks).
455 assert($new == $old);
456 $new = next($newlinks);
457 $old = next($oldlinks);
463 * Rebuild the back-link index.
465 * This should never be needed, but if the database gets hosed for some reason,
466 * this should put it back into a consistent state.
468 * We assume the forward links in the our table are correct, and recalculate
469 * all the backlinks appropriately.
471 function rebuild () {
474 // Delete the backlink tables, make a list of lo.page keys.
476 for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
479 elseif ($key[0] == 'o')
482 trigger_error("Bad key in linktable: '$key'", E_USER_WARNING);
486 foreach ($okeys as $key) {
487 $page = substr($key,1);
488 $links = $this->_get_links('o', $page);
490 $this->set_links($page, $links);
497 // FIXME: check for sortedness and uniqueness in links lists.
499 for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
500 if (strlen($key) < 1 || ($key[0] != 'i' && $key[0] != 'o')) {
501 $errs[] = "Bad key '$key' in table";
504 $page = substr($key, 1);
505 if ($key[0] == 'o') {
507 foreach($this->_get_links('o', $page) as $link) {
508 $link = $link['linkto'];
509 if (!$this->_has_link('i', $link, $page))
510 $errs[] = "backlink entry missing for link '$page'->'$link'";
514 assert($key[0] == 'i');
516 foreach($this->_get_links('i', $page) as $link) {
517 if (!$this->_has_link('o', $link, $page))
518 $errs[] = "link entry missing for backlink '$page'<-'$link'";
522 //if ($errs) $this->rebuild();
523 return isset($errs) ? $errs : false;
526 /* TODO: Add another lrRelationName key for relations.
527 * lrRelationName: frompage => topage
530 function _add_relation($page, $linkedfrom) {
531 $relations = $this->_get_links('r', $page);
532 $backlinks[] = $linkedfrom;
534 $this->_set_links('i', $page, $backlinks);
537 function _add_backlink($page, $linkedfrom) {
538 $backlinks = $this->_get_links('i', $page);
539 $backlinks[] = $linkedfrom;
541 $this->_set_links('i', $page, $backlinks);
544 function _delete_backlink($page, $linkedfrom) {
545 $backlinks = $this->_get_links('i', $page);
546 foreach ($backlinks as $key => $backlink) {
547 if ($backlink == $linkedfrom)
548 unset($backlinks[$key]);
550 $this->_set_links('i', $page, $backlinks);
553 function _has_link($which, $page, $link) {
554 $links = $this->_get_links($which, $page);
555 //TODO: since links are always sorted do a binary search or at least break if >
556 foreach($links as $l) {
557 if ($l['linkto'] == $link)
563 function _get_links($which, $page) {
564 $data = $this->_db->get($which . $page);
565 return $data ? unserialize($data) : array();
568 function _set_links($which, $page, &$links) {
569 $key = $which . $page;
571 $this->_db->set($key, serialize($links));
573 $this->_db->set($key, false);
577 // $Log: not supported by cvs2svn $
579 // (c-file-style: "gnu")
584 // c-hanging-comment-ender-p: nil
585 // indent-tabs-mode: nil