1 <?php rcs_id('$Id: dbaBase.php,v 1.19 2004-12-06 19:50:04 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.
144 function delete_page($pagename) {
145 $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 $data = get_pagedata($pagename);
166 if (isset($data['pagename']))
167 $data['pagename'] = $to;
168 //$vdata = get_versiondata($pagename, $version, 1);
169 //$this->delete_page($pagename);
170 $this->update_pagedata($to, $data);
175 * Delete an old revision of a page.
177 function delete_versiondata($pagename, $version) {
178 $versdb = &$this->_versiondb;
180 $latest = $this->get_latest_version($pagename);
182 assert($version > 0);
183 assert($version <= $latest);
185 $versdb->set((int)$version . ":$pagename", false);
187 if ($version == $latest) {
188 $previous = $this->get_previous_version($version);
190 $pvdata = $this->get_versiondata($pagename, $previous);
191 $is_empty = empty($pvdata['%content']);
195 $this->_update_latest_version($pagename, $previous, $is_empty);
200 * Create a new revision of a page.
202 function set_versiondata($pagename, $version, $data) {
203 $versdb = &$this->_versiondb;
205 $versdb->set((int)$version . ":$pagename", serialize($data));
206 if ($version > $this->get_latest_version($pagename))
207 $this->_update_latest_version($pagename, $version, empty($data['%content']));
210 function _update_latest_version($pagename, $latest, $flags) {
211 $pagedb = &$this->_pagedb;
213 $pdata = $pagedb->get($pagename);
215 list(,,$pagedata) = explode(':',$pdata,3);
217 $pagedata = serialize(array());
219 $pagedb->set($pagename, (int)$latest . ':' . (int)$flags . ":$pagedata");
222 function numPages($include_empty=false, $exclude=false) {
223 $pagedb = &$this->_pagedb;
225 for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
227 assert(!empty($page));
230 if ($exclude and in_array($page, $exclude)) continue;
231 if (!$include_empty) {
232 if (!($data = $pagedb->get($page))) continue;
233 list($latestversion,$flags,) = explode(':', $data, 3);
235 if ($latestversion == 0 || $flags != 0)
236 continue; // current content is empty
243 function get_all_pages($include_empty=false, $sortby=false, $limit=false, $exclude=false) {
244 $pagedb = &$this->_pagedb;
246 for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
248 assert(!empty($page));
251 if ($exclude and in_array($page, $exclude)) continue;
252 if ($limit and count($pages) > $limit) break;
253 if (!$include_empty) {
254 if (!($data = $pagedb->get($page))) continue;
255 list($latestversion,$flags,) = explode(':', $data, 3);
257 if ($latestversion == 0 || $flags != 0)
258 continue; // current content is empty
262 return new WikiDB_backend_dbaBase_pageiter($this, $pages,
263 array('sortby'=>$sortby,
267 function set_links($pagename, $links) {
268 $this->_linkdb->set_links($pagename, $links);
271 function get_links($pagename, $reversed=true, $include_empty=false,
272 $sortby=false, $limit=false, $exclude=false) {
273 $links = $this->_linkdb->get_links($pagename, $reversed);
274 return new WikiDB_backend_dbaBase_pageiter($this, $links,
275 array('sortby'=>$sortby,
282 function WikiDB_backend_dbaBase_sortby_pagename_ASC ($a, $b) {
283 return strcasecmp($a, $b);
285 function WikiDB_backend_dbaBase_sortby_pagename_DESC ($a, $b) {
286 return strcasecmp($b, $a);
288 function WikiDB_backend_dbaBase_sortby_mtime_ASC ($a, $b) {
289 return WikiDB_backend_dbaBase_sortby_num($a, $b, 'mtime');
291 function WikiDB_backend_dbaBase_sortby_mtime_DESC ($a, $b) {
292 return WikiDB_backend_dbaBase_sortby_num($b, $a, 'mtime');
295 function WikiDB_backend_dbaBase_sortby_hits_ASC ($a, $b) {
296 return WikiDB_backend_dbaBase_sortby_num($a, $b, 'hits');
298 function WikiDB_backend_dbaBase_sortby_hits_DESC ($a, $b) {
299 return WikiDB_backend_dbaBase_sortby_num($b, $a, 'hits');
302 function WikiDB_backend_dbaBase_sortby_num($aname, $bname, $field) {
304 $dbi = $request->getDbh();
305 // fields are stored in versiondata
306 $av = $dbi->_backend->get_latest_version($aname);
307 $bv = $dbi->_backend->get_latest_version($bname);
308 $a = $dbi->_backend->get_versiondata($aname, $av, false);
310 $b = $dbi->_backend->get_versiondata($bname, $bv, false);
312 if ((!isset($a[$field]) && !isset($b[$field])) || ($a[$field] === $b[$field])) {
315 return (!isset($a[$field]) || ($a[$field] < $b[$field])) ? -1 : 1;
319 class WikiDB_backend_dbaBase_pageiter
320 extends WikiDB_backend_iterator
322 function WikiDB_backend_dbaBase_pageiter(&$backend, &$pages, $options=false) {
323 $this->_backend = $backend;
324 $this->_options = $options;
326 if (!empty($options['sortby'])) {
327 $sortby = WikiDB_backend::sortby($options['sortby'], 'db', array('pagename','mtime'));
328 if ($sortby and !strstr($sortby, "hits ")) { // check for which column to sortby
329 usort($pages, 'WikiDB_backend_dbaBase_sortby_'.str_replace(' ','_',$sortby));
332 if (!empty($options['limit'])) {
333 list($offset,$limit) = WikiDB_backend::limit($options['limit']);
334 $pages = array_slice($pages, $offset, $limit);
336 $this->_pages = $pages;
338 $this->_pages = array();
342 if ( ! ($next = array_shift($this->_pages)) )
344 if (!empty($options['exclude']) and in_array($next, $options['exclude']))
345 return $this->next();
346 return array('pagename' => $next);
350 return count($this->_pages);
354 reset($this->_pages);
355 return $this->_pages;
359 $this->_pages = array();
363 class WikiDB_backend_dbaBase_linktable
365 function WikiDB_backend_dbaBase_linktable(&$dba) {
369 //FIXME: try storing link lists as hashes rather than arrays.
370 // (backlink deletion would be faster.)
371 function get_links($page, $reversed=true) {
372 return $this->_get_links($reversed ? 'i' : 'o', $page);
375 function set_links($page, $newlinks) {
377 $oldlinks = $this->_get_links('o', $page);
379 if (!is_array($newlinks)) {
380 assert(empty($newlinks));
384 $newlinks = array_unique($newlinks);
387 $this->_set_links('o', $page, $newlinks);
391 $new = current($newlinks);
392 $old = current($oldlinks);
393 while ($new !== false || $old !== false) {
394 if ($old === false || ($new !== false && $new < $old)) {
395 // $new is a new link (not in $oldlinks).
396 $this->_add_backlink($new, $page);
397 $new = next($newlinks);
399 elseif ($new === false || $old < $new) {
400 // $old is a obsolete link (not in $newlinks).
401 $this->_delete_backlink($old, $page);
402 $old = next($oldlinks);
405 // Unchanged link (in both $newlist and $oldlinks).
406 assert($new == $old);
407 $new = next($newlinks);
408 $old = next($oldlinks);
414 * Rebuild the back-link index.
416 * This should never be needed, but if the database gets hosed for some reason,
417 * this should put it back into a consistent state.
419 * We assume the forward links in the our table are correct, and recalculate
420 * all the backlinks appropriately.
422 function rebuild () {
425 // Delete the backlink tables, make a list of page names.
428 for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
431 elseif ($key[0] == 'o')
434 trigger_error("Bad key in linktable: '$key'", E_USER_WARNING);
438 foreach ($ikeys as $key) {
441 foreach ($okeys as $key) {
442 $page = substr($key,1);
443 $links = $this->_get_links('o', $page);
445 $this->set_links($page, $links);
452 // FIXME: check for sortedness and uniqueness in links lists.
454 for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
455 if (strlen($key) < 1 || ($key[0] != 'i' && $key[0] != 'o')) {
456 $errs[] = "Bad key '$key' in table";
459 $page = substr($key, 1);
460 if ($key[0] == 'o') {
462 foreach($this->_get_links('o', $page) as $link) {
463 if (!$this->_has_link('i', $link, $page))
464 $errs[] = "backlink entry missing for link '$page'->'$link'";
468 assert($key[0] == 'i');
470 foreach($this->_get_links('i', $page) as $link) {
471 if (!$this->_has_link('o', $link, $page))
472 $errs[] = "link entry missing for backlink '$page'<-'$link'";
477 return isset($errs) ? $errs : false;
481 function _add_backlink($page, $linkedfrom) {
482 $backlinks = $this->_get_links('i', $page);
483 $backlinks[] = $linkedfrom;
485 $this->_set_links('i', $page, $backlinks);
488 function _delete_backlink($page, $linkedfrom) {
489 $backlinks = $this->_get_links('i', $page);
490 foreach ($backlinks as $key => $backlink) {
491 if ($backlink == $linkedfrom)
492 unset($backlinks[$key]);
494 $this->_set_links('i', $page, $backlinks);
497 function _has_link($which, $page, $link) {
498 $links = $this->_get_links($which, $page);
499 foreach($links as $l) {
506 function _get_links($which, $page) {
507 $data = $this->_db->get($which . $page);
508 return $data ? unserialize($data) : array();
511 function _set_links($which, $page, &$links) {
512 $key = $which . $page;
514 $this->_db->set($key, serialize($links));
516 $this->_db->set($key, false);
520 // (c-file-style: "gnu")
525 // c-hanging-comment-ender-p: nil
526 // indent-tabs-mode: nil