]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/dbaBase.php
faster list_relations method. new native link_search method. additions to rebuild...
[SourceForge/phpwiki.git] / lib / WikiDB / backend / dbaBase.php
1 <?php // -*-php-*-
2 rcs_id('$Id: dbaBase.php,v 1.27 2007-01-02 13:19:33 rurban Exp $');
3
4 require_once('lib/WikiDB/backend.php');
5
6 // FIXME:padding of data?  Is it needed?  dba_optimize() seems to do a good
7 // job at packing 'gdbm' (and 'db2') databases.
8
9 /*
10  * Tables:
11  *
12  *  page:
13  *   Index: pagename
14  *  Values: latestversion . ':' . flags . ':' serialized hash of page meta data
15  *           Currently flags = 1 if latest version has empty content.
16  *
17  *  version
18  *   Index: version:pagename
19  *   Value: serialized hash of revision meta data, including:
20  *          + quasi-meta-data %content
21  *
22  *  links
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
27  *
28  *  TODO:
29  *  Don't keep tables locked the whole time
30  *
31  *  index table with:
32  *   list of pagenames for get_all_pages
33  *   mostpopular list?
34  *   RecentChanges support: 
35  *     lists of most recent edits (major, minor, either).
36  *   
37  *
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...).
40  */     
41
42 require_once('lib/DbaPartition.php');
43
44 class WikiDB_backend_dbaBase
45 extends WikiDB_backend
46 {
47     function WikiDB_backend_dbaBase (&$dba) {
48         $this->_db = &$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');
57     }
58
59     function sortable_columns() {
60         return array('pagename','mtime'/*,'author_id','author'*/);
61     }
62     
63     function close() {
64         $this->_db->close();
65     }
66
67     function optimize() {
68         $this->_db->optimize();
69     }
70
71     function sync() {
72         $this->_db->sync();
73     }
74
75     function rebuild() {
76         parent::rebuild();
77         // rebuild backlink table
78         $this->_linkdb->rebuild();
79         $this->optimize();
80     }
81     
82     function check() {
83         return $this->_linkdb->check();
84     }
85
86     function get_pagedata($pagename) {
87         $result = $this->_pagedb->get($pagename);
88         if (!$result)
89             return false;
90         list(,,$packed) = explode(':', $result, 3);
91         $data = unserialize($packed);
92         return $data;
93     }
94             
95     function update_pagedata($pagename, $newdata) {
96         $result = $this->_pagedb->get($pagename);
97         if ($result) {
98             list($latestversion,$flags,$data) = explode(':', $result, 3);
99             $data = unserialize($data);
100         }
101         else {
102             $latestversion = $flags = 0;
103             $data = array();
104         }
105         
106         foreach ($newdata as $key => $val) {
107             if (empty($val))
108                 unset($data[$key]);
109             else
110                 $data[$key] = $val;
111         }
112         $this->_pagedb->set($pagename,
113                             (int)$latestversion . ':'
114                             . (int)$flags . ':'
115                             . serialize($data));
116     }
117
118     function get_latest_version($pagename) {
119         return (int) $this->_pagedb->get($pagename);
120     }
121
122     function get_previous_version($pagename, $version) {
123         $versdb = &$this->_versiondb;
124
125         while (--$version > 0) {
126             if ($versdb->exists($version . ":$pagename"))
127                 return $version;
128         }
129         return false;
130     }
131
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;
136         else {
137             $data = unserialize($data);
138             if (!$want_content)
139                 $data['%content'] = !empty($data['%content']);
140             return $data;
141         }
142     }
143         
144     /**
145      * See ADODB for a better delete_page(), which can be undone and is seen in RecentChanges.
146      * See backend.php
147      */
148     //function delete_page($pagename) { $this->purge_page($pagename);  }
149
150     /**
151      * Completely delete page from the database.
152      */
153     function purge_page($pagename) {
154         $pagedb = &$this->_pagedb;
155         $versdb = &$this->_versiondb;
156
157         $version = $this->get_latest_version($pagename);
158         while ($version > 0) {
159             $versdb->set($version-- . ":$pagename", false);
160         }
161         $pagedb->set($pagename, false);
162
163         $this->set_links($pagename, false);
164     }
165
166     function rename_page($pagename, $to) {
167         $result = $this->_pagedb->get($pagename);
168         if ($result) {
169             list($version,$flags,$data) = explode(':', $result, 3);
170             $data = unserialize($data);
171         }
172         else
173             return false;
174
175         $this->_pagedb->delete($pagename);
176         $data['pagename'] = $to;
177         $this->_pagedb->set($to,
178                             (int)$version . ':'
179                             . (int)$flags . ':'
180                             . serialize($data));
181         // move over the latest version only
182         $pvdata = $this->get_versiondata($pagename, $version, true);
183         $this->set_versiondata($to, $version, $pvdata);
184         return true;
185     }
186             
187     /**
188      * Delete an old revision of a page.
189      */
190     function delete_versiondata($pagename, $version) {
191         $versdb = &$this->_versiondb;
192
193         $latest = $this->get_latest_version($pagename);
194
195         assert($version > 0);
196         assert($version <= $latest);
197         
198         $versdb->set((int)$version . ":$pagename", false);
199
200         if ($version == $latest) {
201             $previous = $this->get_previous_version($version);
202             if ($previous > 0) {
203                 $pvdata = $this->get_versiondata($pagename, $previous);
204                 $is_empty = empty($pvdata['%content']);
205             }
206             else
207                 $is_empty = true;
208             $this->_update_latest_version($pagename, $previous, $is_empty);
209         }
210     }
211
212     /**
213      * Create a new revision of a page.
214      */
215     function set_versiondata($pagename, $version, $data) {
216         $versdb = &$this->_versiondb;
217
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']));
221     }
222
223     function _update_latest_version($pagename, $latest, $flags) {
224         $pagedb = &$this->_pagedb;
225
226         $pdata = $pagedb->get($pagename);
227         if ($pdata)
228             list(,,$pagedata) = explode(':',$pdata,3);
229         else
230             $pagedata = serialize(array());
231         
232         $pagedb->set($pagename, (int)$latest . ':' . (int)$flags . ":$pagedata");
233     }
234
235     function numPages($include_empty=false, $exclude='') {
236         $pagedb = &$this->_pagedb;
237         $count = 0;
238         for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
239             if (!$page) {
240                 assert(!empty($page));
241                 continue;
242             }
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);
247                 unset($data);
248                 if ($latestversion == 0 || $flags != 0)
249                     continue;   // current content is empty 
250             }
251             $count++;
252         }
253         return $count;
254     }
255
256     function get_all_pages($include_empty=false, $sortby='', $limit='', $exclude='') {
257         $pagedb = &$this->_pagedb;
258         $pages = array();
259         for ($page = $pagedb->firstkey(); $page!== false; $page = $pagedb->nextkey()) {
260             if (!$page) {
261                 assert(!empty($page));
262                 continue;
263             }
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);
269                 unset($data);
270                 if ($latestversion == 0 || $flags != 0)
271                     continue;   // current content is empty 
272             }
273             $pages[] = $page;
274         }
275         return new WikiDB_backend_dbaBase_pageiter($this, $pages, 
276                                                    array('sortby'=>$sortby,
277                                                          'limit' =>$limit));
278     }
279
280     function set_links($pagename, $links) {
281         $this->_linkdb->set_links($pagename, $links);
282     }
283
284     function get_links($pagename, $reversed=true, $include_empty=false,
285                        $sortby='', $limit='', $exclude='',
286                        $want_relations=false) 
287     {
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
291             ($this, $links, 
292              array('sortby'=>$sortby,
293                    'limit' =>$limit,
294                    'exclude'=>$exclude,
295                    'want_relations'=>$want_relations,
296                    'found_relations' => $want_relations ? $this->_linkdb->found_relations : 0
297                    ));
298     }
299     
300     /**
301      * @access public
302      *
303      * @return array of all linkrelations
304      * Faster than the dumb WikiDB method.
305      */
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
313                 if (is_array($link)
314                     and $link['relation'] 
315                     and !in_array($link['relation'], $relations)) 
316                 {
317                     $is_attribute = empty($link['linkto']); // a relation has both
318                     if ($is_attribute) {
319                         if ($only_attributes or $also_attributes)
320                             $relations[] = $link['relation'];
321                     } elseif (!$only_attributes) {
322                           $relations[] = $link['relation'];
323                     }
324                 }
325             }
326         }
327         if ($sorted) {
328             sort($relations);
329             reset($relations);
330         }
331         return $relations;
332     }
333
334     /**
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.
338      *
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
347      */
348     function link_search( $pages, $linkvalue, $linktype, $relation=false, $options=array() ) {
349         $linkdb = &$this->_linkdb;
350         $links = array();
351         $reverse = false;
352         $want_relations = false;
353         if ($linktype == 'relation') {
354             $want_relations = true;
355             $field = 'linkrelation';
356         }
357         if ($linktype == 'attribute') {
358             $want_relations = true;
359             $field = 'attribute';
360         }
361         if ($linktype == 'linkfrom') {
362             $reverse = true;
363         }
364
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');
373                 if ($attribs) {
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);
380                     }
381                 }
382             } else {
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']);
392                     }
393                 } else {
394                     $_links = $linkdb->_get_links($reverse ? 'i' : 'o', $pagename);
395                     foreach ($_links as $link) { // linkto => page
396                         if (is_array($link))
397                             $link = $link['linkto'];
398                         if (!$linkvalue->match($link)) continue; 
399                         $links[] = array('pagename'  => $pagename,
400                                          'linkname'  => '',
401                                          'linkvalue' => $link);
402                     }
403                 }
404             }
405         }
406         $options['want_relations'] = true; // Iter hack to force return of the whole hash
407         return new WikiDB_backend_dbaBase_pageiter($this, $links, $options);
408     }
409
410 };
411
412 function WikiDB_backend_dbaBase_sortby_pagename_ASC ($a, $b) {
413     return strcasecmp($a, $b);
414 }
415 function WikiDB_backend_dbaBase_sortby_pagename_DESC ($a, $b) {
416     return strcasecmp($b, $a);
417 }
418 function WikiDB_backend_dbaBase_sortby_mtime_ASC ($a, $b) {
419     return WikiDB_backend_dbaBase_sortby_num($a, $b, 'mtime');
420 }
421 function WikiDB_backend_dbaBase_sortby_mtime_DESC ($a, $b) {
422     return WikiDB_backend_dbaBase_sortby_num($b, $a, 'mtime');
423 }
424 /*
425 function WikiDB_backend_dbaBase_sortby_hits_ASC ($a, $b) {
426     return WikiDB_backend_dbaBase_sortby_num($a, $b, 'hits');
427 }
428 function WikiDB_backend_dbaBase_sortby_hits_DESC ($a, $b) {
429     return WikiDB_backend_dbaBase_sortby_num($b, $a, 'hits');
430 }
431 */
432 function WikiDB_backend_dbaBase_sortby_num($aname, $bname, $field) {
433     global $request;
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);
439     if (!$a) return 0;
440     $b = $dbi->_backend->get_versiondata($bname, $bv, false);
441     if (!$b) return 0;
442     if ((!isset($a[$field]) && !isset($b[$field])) || ($a[$field] === $b[$field])) {
443         return 0; 
444     } else {
445         return (!isset($a[$field]) || ($a[$field] < $b[$field])) ? -1 : 1;
446     }
447 }
448
449 class WikiDB_backend_dbaBase_pageiter
450 extends WikiDB_backend_iterator
451 {
452     // fixed for linkrelations
453     function WikiDB_backend_dbaBase_pageiter(&$backend, &$pages, $options=false) {
454         $this->_backend = $backend;
455         $this->_options = $options;
456         if ($pages) { 
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));
461                 }
462             }
463             if (!empty($options['limit'])) {
464                 list($offset,$limit) = WikiDB_backend::limit($options['limit']);
465                 $pages = array_slice($pages, $offset, $limit);
466             }
467             $this->_pages = $pages;
468         } else 
469             $this->_pages = array();
470     }
471
472     // fixed for relations
473     function next() {
474         if ( ! ($page = array_shift($this->_pages)) )
475             return false;
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();
481             return $page;
482         }
483         if (!empty($this->_options['exclude']) and in_array($page, $this->_options['exclude']))
484             return $this->next();
485         return array('pagename' => $page);
486     }
487
488     function free() {
489         $this->_pages = array();
490     }
491 };
492
493 class WikiDB_backend_dbaBase_linktable 
494 {
495     function WikiDB_backend_dbaBase_linktable(&$dba) {
496         $this->_db = &$dba;
497     }
498
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']);
514                 }
515             }
516             return $linksonly;
517         } else {
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'];
523                 } else
524                     $linksonly[] = $link;
525             }
526             return $linksonly;
527         }
528     }
529     
530     // fixed: relations ready
531     function set_links($page, $links) {
532
533         $oldlinks = $this->get_links($page, false, false);
534
535         if (!is_array($links)) {
536             assert(empty($links));
537             $links = array();
538         }
539         $this->_set_links('o', $page, $links);
540         
541         /* Now for the backlink update we squash the linkto hashes into a simple array */
542         $newlinks = 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'];          
548         }
549         //$newlinks = array_unique($newlinks);
550         sort($oldlinks);
551         sort($newlinks);
552
553         reset($newlinks);
554         reset($oldlinks);
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);
562             }
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);
567             }
568             else {
569                 // Unchanged link (in both $newlist and $oldlinks).
570                 assert($new == $old);
571                 $new = next($newlinks);
572                 $old = next($oldlinks);
573             }
574         }
575     }
576
577     /**
578      * Rebuild the back-link index.
579      *
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.
582      *
583      * We assume the forward links in the our table are correct, and recalculate
584      * all the backlinks appropriately.
585      */
586     function rebuild () {
587         $db = &$this->_db;
588
589         // Delete the backlink tables, make a list of lo.page keys.
590         $okeys = array();
591         for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
592             if ($key[0] == 'i')
593                 $db->delete($key);
594             elseif ($key[0] == 'o')
595                 $okeys[] = $key;
596             else {
597                 trigger_error("Bad key in linktable: '$key'", E_USER_WARNING);
598             $db->delete($key);
599         }
600         }
601         foreach ($okeys as $key) {
602             $page = substr($key,1);
603             $links = $this->_get_links('o', $page);
604             $db->delete($key);
605             $this->set_links($page, $links);
606         }
607     }
608
609     function check() {
610         $db = &$this->_db;
611
612         // FIXME: check for sortedness and uniqueness in links lists.
613
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";
617                 continue;
618             }
619             $page = substr($key, 1);
620             if ($key[0] == 'o') {
621                 // Forward links.
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'";
626                 }
627             }
628             else {
629                 assert($key[0] == 'i');
630                 // Backlinks.
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'";
634                 }
635             }
636         }
637         //if ($errs) $this->rebuild();
638         return isset($errs) ? $errs : false;
639     }
640     
641     /* TODO: Add another lrRelationName key for relations.
642      * lrRelationName: frompage => topage
643      */
644
645     function _add_relation($page, $linkedfrom) {
646         $relations = $this->_get_links('r', $page);
647         $backlinks[] = $linkedfrom;
648         sort($backlinks);
649         $this->_set_links('i', $page, $backlinks);
650     }
651         
652     function _add_backlink($page, $linkedfrom) {
653         $backlinks = $this->_get_links('i', $page);
654         $backlinks[] = $linkedfrom;
655         sort($backlinks);
656         $this->_set_links('i', $page, $backlinks);
657     }
658     
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]);
664         }
665         $this->_set_links('i', $page, $backlinks);
666     }
667     
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)
673                 return true;
674         }
675         return false;
676     }
677     
678     function _get_links($which, $page) {
679         $data = $this->_db->get($which . $page);
680         return $data ? unserialize($data) : array();
681     }
682
683     function _set_links($which, $page, &$links) {
684         $key = $which . $page;
685         if ($links)
686             $this->_db->set($key, serialize($links));
687         else
688             $this->_db->set($key, false);
689     }
690 }
691
692 // $Log: not supported by cvs2svn $
693 // Revision 1.26  2006/12/22 00:27:37  rurban
694 // just add Log
695 //
696
697 // (c-file-style: "gnu")
698 // Local Variables:
699 // mode: php
700 // tab-width: 8
701 // c-basic-offset: 4
702 // c-hanging-comment-ender-p: nil
703 // indent-tabs-mode: nil
704 // End:   
705 ?>