1 <?php rcs_id('$Id: dbaBase.php,v 1.22 2005-08-07 10:11:24 rurban Exp $');
3 require_once('lib/WikiDB/backend.php');
5 // FIXME:padding of data? Is it needed? dba_optimize() seems to do a good
6 // job at packing 'gdbm' (and 'db2') databases.
13 * Values: latestversion . ':' . flags . ':' serialized hash of page meta data
14 * Currently flags = 1 if latest version has empty content.
17 * Index: version:pagename
18 * Value: serialized hash of revision meta data, including:
19 * + quasi-meta-data %content
22 * index: 'o' . pagename
23 * value: serialized list of pages (names) which pagename links to.
24 * index: 'i' . pagename
25 * value: serialized list of pages which link to pagename
28 * Don't keep tables locked the whole time
31 * list of pagenames for get_all_pages
33 * RecentChanges support:
34 * lists of most recent edits (major, minor, either).
37 * Separate hit table, so we don't have to update the whole page entry
38 * each time we get a hit. (Maybe not so important though...).
41 require_once('lib/DbaPartition.php');
43 class WikiDB_backend_dbaBase
44 extends WikiDB_backend
46 function WikiDB_backend_dbaBase (&$dba) {
48 // TODO: page and version tables should be in their own files, probably.
49 // We'll pack them all in one for now (testing).
50 // 2004-07-09 10:07:30 rurban: It's fast enough this way.
51 $this->_pagedb = new DbaPartition($dba, 'p');
52 $this->_versiondb = new DbaPartition($dba, 'v');
53 $linkdbpart = new DbaPartition($dba, 'l');
54 $this->_linkdb = new WikiDB_backend_dbaBase_linktable($linkdbpart);
55 $this->_dbdb = new DbaPartition($dba, 'd');
58 function sortable_columns() {
59 return array('pagename','mtime'/*,'author_id','author'*/);
67 $this->_db->optimize();
75 $this->_linkdb->rebuild();
80 return $this->_linkdb->check();
83 function get_pagedata($pagename) {
84 $result = $this->_pagedb->get($pagename);
87 list(,,$packed) = explode(':', $result, 3);
88 $data = unserialize($packed);
92 function update_pagedata($pagename, $newdata) {
93 $result = $this->_pagedb->get($pagename);
95 list($latestversion,$flags,$data) = explode(':', $result, 3);
96 $data = unserialize($data);
99 $latestversion = $flags = 0;
103 foreach ($newdata as $key => $val) {
109 $this->_pagedb->set($pagename,
110 (int)$latestversion . ':'
115 function get_latest_version($pagename) {
116 return (int) $this->_pagedb->get($pagename);
119 function get_previous_version($pagename, $version) {
120 $versdb = &$this->_versiondb;
122 while (--$version > 0) {
123 if ($versdb->exists($version . ":$pagename"))
129 //check $want_content
130 function get_versiondata($pagename, $version, $want_content=false) {
131 $data = $this->_versiondb->get((int)$version . ":$pagename");
132 if (empty($data)) return false;
134 $data = unserialize($data);
136 $data['%content'] = !empty($data['%content']);
142 * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
145 //function delete_page($pagename) { $this->purge_page($pagename); }
148 * Completely delete page from the database.
150 function purge_page($pagename) {
151 $pagedb = &$this->_pagedb;
152 $versdb = &$this->_versiondb;
154 $version = $this->get_latest_version($pagename);
155 while ($version > 0) {
156 $versdb->set($version-- . ":$pagename", false);
158 $pagedb->set($pagename, false);
160 $this->set_links($pagename, false);
163 function rename_page($pagename, $to) {
164 $result = $this->_pagedb->get($pagename);
166 list($version,$flags,$data) = explode(':', $result, 3);
167 $data = unserialize($data);
172 $this->_pagedb->delete($pagename);
173 $data['pagename'] = $to;
174 $this->_pagedb->set($to,
178 // move over the latest version only
179 $pvdata = $this->get_versiondata($pagename, $version, true);
180 $this->set_versiondata($to, $version, $pvdata);
185 * Delete an old revision of a page.
187 function delete_versiondata($pagename, $version) {
188 $versdb = &$this->_versiondb;
190 $latest = $this->get_latest_version($pagename);
192 assert($version > 0);
193 assert($version <= $latest);
195 $versdb->set((int)$version . ":$pagename", false);
197 if ($version == $latest) {
198 $previous = $this->get_previous_version($version);
200 $pvdata = $this->get_versiondata($pagename, $previous);
201 $is_empty = empty($pvdata['%content']);
205 $this->_update_latest_version($pagename, $previous, $is_empty);
210 * Create a new revision of a page.
212 function set_versiondata($pagename, $version, $data) {
213 $versdb = &$this->_versiondb;
215 $versdb->set((int)$version . ":$pagename", serialize($data));
216 if ($version > $this->get_latest_version($pagename))
217 $this->_update_latest_version($pagename, $version, empty($data['%content']));
220 function _update_latest_version($pagename, $latest, $flags) {
221 $pagedb = &$this->_pagedb;
223 $pdata = $pagedb->get($pagename);
225 list(,,$pagedata) = explode(':',$pdata,3);
227 $pagedata = serialize(array());
229 $pagedb->set($pagename, (int)$latest . ':' . (int)$flags . ":$pagedata");
232 function numPages($include_empty=false, $exclude=false) {
233 $pagedb = &$this->_pagedb;
235 for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
237 assert(!empty($page));
240 if ($exclude and in_array($page, $exclude)) continue;
241 if (!$include_empty) {
242 if (!($data = $pagedb->get($page))) continue;
243 list($latestversion,$flags,) = explode(':', $data, 3);
245 if ($latestversion == 0 || $flags != 0)
246 continue; // current content is empty
253 function get_all_pages($include_empty=false, $sortby=false, $limit=false, $exclude=false) {
254 $pagedb = &$this->_pagedb;
256 for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
258 assert(!empty($page));
261 if ($exclude and in_array($page, $exclude)) continue;
262 if ($limit and count($pages) > $limit) break;
263 if (!$include_empty) {
264 if (!($data = $pagedb->get($page))) continue;
265 list($latestversion,$flags,) = explode(':', $data, 3);
267 if ($latestversion == 0 || $flags != 0)
268 continue; // current content is empty
272 return new WikiDB_backend_dbaBase_pageiter($this, $pages,
273 array('sortby'=>$sortby,
277 function set_links($pagename, $links) {
278 $this->_linkdb->set_links($pagename, $links);
281 function get_links($pagename, $reversed=true, $include_empty=false,
282 $sortby=false, $limit=false, $exclude=false) {
283 $links = $this->_linkdb->get_links($pagename, $reversed);
284 return new WikiDB_backend_dbaBase_pageiter($this, $links,
285 array('sortby'=>$sortby,
292 function WikiDB_backend_dbaBase_sortby_pagename_ASC ($a, $b) {
293 return strcasecmp($a, $b);
295 function WikiDB_backend_dbaBase_sortby_pagename_DESC ($a, $b) {
296 return strcasecmp($b, $a);
298 function WikiDB_backend_dbaBase_sortby_mtime_ASC ($a, $b) {
299 return WikiDB_backend_dbaBase_sortby_num($a, $b, 'mtime');
301 function WikiDB_backend_dbaBase_sortby_mtime_DESC ($a, $b) {
302 return WikiDB_backend_dbaBase_sortby_num($b, $a, 'mtime');
305 function WikiDB_backend_dbaBase_sortby_hits_ASC ($a, $b) {
306 return WikiDB_backend_dbaBase_sortby_num($a, $b, 'hits');
308 function WikiDB_backend_dbaBase_sortby_hits_DESC ($a, $b) {
309 return WikiDB_backend_dbaBase_sortby_num($b, $a, 'hits');
312 function WikiDB_backend_dbaBase_sortby_num($aname, $bname, $field) {
314 $dbi = $request->getDbh();
315 // fields are stored in versiondata
316 $av = $dbi->_backend->get_latest_version($aname);
317 $bv = $dbi->_backend->get_latest_version($bname);
318 $a = $dbi->_backend->get_versiondata($aname, $av, false);
320 $b = $dbi->_backend->get_versiondata($bname, $bv, false);
322 if ((!isset($a[$field]) && !isset($b[$field])) || ($a[$field] === $b[$field])) {
325 return (!isset($a[$field]) || ($a[$field] < $b[$field])) ? -1 : 1;
329 class WikiDB_backend_dbaBase_pageiter
330 extends WikiDB_backend_iterator
332 function WikiDB_backend_dbaBase_pageiter(&$backend, &$pages, $options=false) {
333 $this->_backend = $backend;
334 $this->_options = $options;
336 if (!empty($options['sortby'])) {
337 $sortby = WikiDB_backend::sortby($options['sortby'], 'db', array('pagename','mtime'));
338 if ($sortby and !strstr($sortby, "hits ")) { // check for which column to sortby
339 usort($pages, 'WikiDB_backend_dbaBase_sortby_'.str_replace(' ','_',$sortby));
342 if (!empty($options['limit'])) {
343 list($offset,$limit) = WikiDB_backend::limit($options['limit']);
344 $pages = array_slice($pages, $offset, $limit);
346 $this->_pages = $pages;
348 $this->_pages = array();
352 if ( ! ($next = array_shift($this->_pages)) )
354 if (!empty($options['exclude']) and in_array($next, $options['exclude']))
355 return $this->next();
356 return array('pagename' => $next);
360 return count($this->_pages);
364 reset($this->_pages);
365 return $this->_pages;
369 $this->_pages = array();
373 class WikiDB_backend_dbaBase_linktable
375 function WikiDB_backend_dbaBase_linktable(&$dba) {
379 //FIXME: try storing link lists as hashes rather than arrays.
380 // (backlink deletion would be faster.)
381 function get_links($page, $reversed=true) {
382 return $this->_get_links($reversed ? 'i' : 'o', $page);
385 function set_links($page, $newlinks) {
387 $oldlinks = $this->_get_links('o', $page);
389 if (!is_array($newlinks)) {
390 assert(empty($newlinks));
394 $newlinks = array_unique($newlinks);
397 $this->_set_links('o', $page, $newlinks);
401 $new = current($newlinks);
402 $old = current($oldlinks);
403 while ($new !== false || $old !== false) {
404 if ($old === false || ($new !== false && $new < $old)) {
405 // $new is a new link (not in $oldlinks).
406 $this->_add_backlink($new, $page);
407 $new = next($newlinks);
409 elseif ($new === false || $old < $new) {
410 // $old is a obsolete link (not in $newlinks).
411 $this->_delete_backlink($old, $page);
412 $old = next($oldlinks);
415 // Unchanged link (in both $newlist and $oldlinks).
416 assert($new == $old);
417 $new = next($newlinks);
418 $old = next($oldlinks);
424 * Rebuild the back-link index.
426 * This should never be needed, but if the database gets hosed for some reason,
427 * this should put it back into a consistent state.
429 * We assume the forward links in the our table are correct, and recalculate
430 * all the backlinks appropriately.
432 function rebuild () {
435 // Delete the backlink tables, make a list of page names.
438 for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
441 elseif ($key[0] == 'o')
444 trigger_error("Bad key in linktable: '$key'", E_USER_WARNING);
448 foreach ($ikeys as $key) {
451 foreach ($okeys as $key) {
452 $page = substr($key,1);
453 $links = $this->_get_links('o', $page);
455 $this->set_links($page, $links);
462 // FIXME: check for sortedness and uniqueness in links lists.
464 for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
465 if (strlen($key) < 1 || ($key[0] != 'i' && $key[0] != 'o')) {
466 $errs[] = "Bad key '$key' in table";
469 $page = substr($key, 1);
470 if ($key[0] == 'o') {
472 foreach($this->_get_links('o', $page) as $link) {
473 if (!$this->_has_link('i', $link, $page))
474 $errs[] = "backlink entry missing for link '$page'->'$link'";
478 assert($key[0] == 'i');
480 foreach($this->_get_links('i', $page) as $link) {
481 if (!$this->_has_link('o', $link, $page))
482 $errs[] = "link entry missing for backlink '$page'<-'$link'";
487 return isset($errs) ? $errs : false;
491 function _add_backlink($page, $linkedfrom) {
492 $backlinks = $this->_get_links('i', $page);
493 $backlinks[] = $linkedfrom;
495 $this->_set_links('i', $page, $backlinks);
498 function _delete_backlink($page, $linkedfrom) {
499 $backlinks = $this->_get_links('i', $page);
500 foreach ($backlinks as $key => $backlink) {
501 if ($backlink == $linkedfrom)
502 unset($backlinks[$key]);
504 $this->_set_links('i', $page, $backlinks);
507 function _has_link($which, $page, $link) {
508 $links = $this->_get_links($which, $page);
509 foreach($links as $l) {
516 function _get_links($which, $page) {
517 $data = $this->_db->get($which . $page);
518 return $data ? unserialize($data) : array();
521 function _set_links($which, $page, &$links) {
522 $key = $which . $page;
524 $this->_db->set($key, serialize($links));
526 $this->_db->set($key, false);
530 // (c-file-style: "gnu")
535 // c-hanging-comment-ender-p: nil
536 // indent-tabs-mode: nil