]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/PearDB_ffpgsql.php
Add comments
[SourceForge/phpwiki.git] / lib / WikiDB / backend / PearDB_ffpgsql.php
1 <?php
2
3 /*
4  * Copyright (C) 2001-2009 $ThePhpWikiProgrammingTeam
5  * Copyright (C) 2010 Alain Peyrat, Alcatel-Lucent
6  *
7  * This file is part of PhpWiki.
8  *
9  * PhpWiki is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * PhpWiki is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License along
20  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 */
23
24 /*
25  * Standard Alcatel-Lucent disclaimer for contributing to open source
26  *
27  * "The Fusionforge backend ("Contribution") has not been tested and/or
28  * validated for release as or in products, combinations with products or
29  * other commercial use. Any use of the Contribution is entirely made at
30  * the user's own responsibility and the user can not rely on any features,
31  * functionalities or performances Alcatel-Lucent has attributed to the
32  * Contribution.
33  *
34  * THE CONTRIBUTION BY ALCATEL-LUCENT IS PROVIDED AS IS, WITHOUT WARRANTY
35  * OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
36  * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, COMPLIANCE,
37  * NON-INTERFERENCE AND/OR INTERWORKING WITH THE SOFTWARE TO WHICH THE
38  * CONTRIBUTION HAS BEEN MADE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL
39  * ALCATEL-LUCENT BE LIABLE FOR ANY DAMAGES OR OTHER LIABLITY, WHETHER IN
40  * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
41  * CONTRIBUTION OR THE USE OR OTHER DEALINGS IN THE CONTRIBUTION, WHETHER
42  * TOGETHER WITH THE SOFTWARE TO WHICH THE CONTRIBUTION RELATES OR ON A STAND
43  * ALONE BASIS."
44  */
45
46 require_once 'lib/ErrorManager.php';
47 require_once 'lib/WikiDB/backend/PearDB_pgsql.php';
48
49 class WikiDB_backend_PearDB_ffpgsql
50     extends WikiDB_backend_PearDB_pgsql
51 {
52     function __construct($dbparams)
53     {
54         $dbparams['dsn'] = str_replace('ffpgsql:', 'pgsql:', $dbparams['dsn']);
55         parent::__construct($dbparams);
56
57         global $page_prefix;
58         $p = strlen($page_prefix) + 1;
59         $page_tbl = $this->_table_names['page_tbl'];
60         $this->page_tbl_fields = "$page_tbl.id AS id, substring($page_tbl.pagename from $p) AS pagename, $page_tbl.hits AS hits";
61
62         pg_set_client_encoding("iso-8859-1");
63     }
64
65     function get_all_pagenames()
66     {
67         $dbh = &$this->_dbh;
68         extract($this->_table_names);
69         global $page_prefix;
70         $pat = $page_prefix;
71         $p = strlen($pat) + 1;
72         return $dbh->getCol("SELECT substring(pagename from $p)"
73             . " FROM $nonempty_tbl, $page_tbl"
74             . " WHERE $nonempty_tbl.id=$page_tbl.id"
75             . " AND substring($page_tbl.pagename from 0 for $p) = '$pat'");
76     }
77
78     /*
79      * filter (nonempty pages) currently ignored
80      */
81     function numPages($filter = false, $exclude = '')
82     {
83         $dbh = &$this->_dbh;
84         extract($this->_table_names);
85         global $page_prefix;
86         $pat = $page_prefix;
87         $p = strlen($pat) + 1;
88         return $dbh->getOne("SELECT count(*)"
89             . " FROM $nonempty_tbl, $page_tbl"
90             . " WHERE $nonempty_tbl.id=$page_tbl.id"
91             . " AND substring($page_tbl.pagename from 0 for $p) = '$pat'");
92     }
93
94     /*
95      * Read page information from database.
96      */
97     function get_pagedata($pagename)
98     {
99         global $page_prefix;
100         return parent::get_pagedata($page_prefix . $pagename);
101     }
102
103     function update_pagedata($pagename, $newdata)
104     {
105         $dbh = &$this->_dbh;
106         $page_tbl = $this->_table_names['page_tbl'];
107
108         // Hits is the only thing we can update in a fast manner.
109         if (count($newdata) == 1 && isset($newdata['hits'])) {
110             // Note that this will fail silently if the page does not
111             // have a record in the page table.  Since it's just the
112             // hit count, who cares?
113             global $page_prefix;
114             $pagename = $page_prefix . $pagename;
115             $dbh->query(sprintf("UPDATE $page_tbl SET hits=%d WHERE pagename='%s'",
116                 $newdata['hits'], $dbh->escapeSimple($pagename)));
117             return;
118         }
119
120         $this->lock(array($page_tbl), true);
121         $data = $this->get_pagedata($pagename);
122         if (!$data) {
123             $data = array();
124             $this->_get_pageid($pagename, true); // Creates page record
125         }
126
127         if (isset($data['hits'])) {
128             $hits = (int)$data['hits'];
129             unset($data['hits']);
130         } else {
131             $hits = 0;
132         }
133
134         foreach ($newdata as $key => $val) {
135             if ($key == 'hits')
136                 $hits = (int)$val;
137             else if (empty($val))
138                 unset($data[$key]);
139             else
140                 $data[$key] = $val;
141         }
142
143         /* Portability issue -- not all DBMS supports huge strings
144          * so we need to 'bind' instead of building a simple SQL statment.
145          * Note that we do not need to escapeSimple when we bind
146         $dbh->query(sprintf("UPDATE $page_tbl"
147                             . " SET hits=%d, pagedata='%s'"
148                             . " WHERE pagename='%s'",
149                             $hits,
150                             $dbh->escapeSimple($this->_serialize($data)),
151                             $dbh->escapeSimple($pagename)));
152         */
153         global $page_prefix;
154         $pagename = $page_prefix . $pagename;
155         $dbh->query("UPDATE $page_tbl"
156                 . " SET hits=?, pagedata=?"
157                 . " WHERE pagename=?",
158             array($hits, $this->_serialize($data), $pagename));
159         $this->unlock(array($page_tbl));
160     }
161
162     function get_latest_version($pagename)
163     {
164         global $page_prefix;
165         return parent::get_latest_version($page_prefix . $pagename);
166     }
167
168     function get_previous_version($pagename, $version)
169     {
170         global $page_prefix;
171         return parent::get_previous_version($page_prefix . $pagename, $version);
172     }
173
174     /**
175      * Get version data.
176      *
177      * @param string $pagename Name of the page
178      * @param int $version Which version to get
179      * @param bool $want_content Do we need content?
180      *
181      * @return array hash The version data, or false if specified version does not
182      *              exist.
183      */
184     function get_versiondata($pagename, $version, $want_content = false)
185     {
186         $dbh = &$this->_dbh;
187         extract($this->_table_names);
188         extract($this->_expressions);
189
190         // assert(is_string($pagename) and $pagename != "");
191         // assert($version > 0);
192
193         // FIXME: optimization: sometimes don't get page data?
194         if ($want_content) {
195             $fields = $this->page_tbl_fields
196                 . ",$page_tbl.pagedata AS pagedata,"
197                 . $this->version_tbl_fields;
198         } else {
199             $fields = $this->page_tbl_fields . ","
200                 . "mtime, minor_edit, versiondata,"
201                 . "$iscontent AS have_content";
202         }
203
204         global $page_prefix;
205         $pagename = $page_prefix . $pagename;
206         $result = $dbh->getRow(sprintf("SELECT $fields"
207                     . " FROM $page_tbl, $version_tbl"
208                     . " WHERE $page_tbl.id=$version_tbl.id"
209                     . "  AND pagename='%s'"
210                     . "  AND version=%d",
211                 $dbh->escapeSimple($pagename), $version),
212             DB_FETCHMODE_ASSOC);
213
214         return $this->_extract_version_data($result);
215     }
216
217     function get_cached_html($pagename)
218     {
219         global $page_prefix;
220         return parent::get_cached_html($page_prefix . $pagename);
221     }
222
223     function set_cached_html($pagename, $data)
224     {
225         global $page_prefix;
226         parent::set_cached_html($page_prefix . $pagename, $data);
227     }
228
229     function _get_pageid($pagename, $create_if_missing = false)
230     {
231
232         // check id_cache
233         global $request;
234         $cache =& $request->_dbi->_cache->_id_cache;
235         if (isset($cache[$pagename])) {
236             if ($cache[$pagename] or !$create_if_missing) {
237                 return $cache[$pagename];
238             }
239         }
240
241         // attributes play this game.
242         if ($pagename === '') return 0;
243
244         $dbh = &$this->_dbh;
245         $page_tbl = $this->_table_names['page_tbl'];
246         global $page_prefix;
247         $pagename = $page_prefix . $pagename;
248
249         $query = sprintf("SELECT id FROM $page_tbl WHERE pagename='%s'",
250             $dbh->escapeSimple($pagename));
251
252         if (!$create_if_missing)
253             return $dbh->getOne($query);
254
255         $id = $dbh->getOne($query);
256         if (empty($id)) {
257             $this->lock(array($page_tbl), true); // write lock
258             $max_id = $dbh->getOne("SELECT MAX(id) FROM $page_tbl");
259             $id = $max_id + 1;
260             // requires createSequence and on mysql lock the interim table ->getSequenceName
261             //$id = $dbh->nextId($page_tbl . "_id");
262             $dbh->query(sprintf("INSERT INTO $page_tbl"
263                     . " (id,pagename,hits)"
264                     . " VALUES (%d,'%s',0)",
265                 $id, $dbh->escapeSimple($pagename)));
266             $this->unlock(array($page_tbl));
267         }
268         return $id;
269     }
270
271     /*
272      * Delete page completely from the database.
273      */
274     function purge_page($pagename)
275     {
276         $dbh = &$this->_dbh;
277         extract($this->_table_names);
278
279         $this->lock();
280         if (($id = $this->_get_pageid($pagename, false))) {
281             $dbh->query("DELETE FROM $nonempty_tbl WHERE id=$id");
282             $dbh->query("DELETE FROM $recent_tbl   WHERE id=$id");
283             $dbh->query("DELETE FROM $version_tbl  WHERE id=$id");
284             $dbh->query("DELETE FROM $link_tbl     WHERE linkfrom=$id");
285             $nlinks = $dbh->getOne("SELECT COUNT(*) FROM $link_tbl WHERE linkto=$id");
286             if ($nlinks) {
287                 // We're still in the link table (dangling link) so we can't delete this
288                 // altogether.
289                 $dbh->query("UPDATE $page_tbl SET hits=0, pagedata='' WHERE id=$id");
290                 $result = 0;
291             } else {
292                 $dbh->query("DELETE FROM $page_tbl WHERE id=$id");
293                 $result = 1;
294             }
295         } else {
296             $result = -1; // already purged or not existing
297         }
298         $this->unlock();
299         return $result;
300     }
301
302     /*
303      * Find pages which link to or are linked from a page.
304      *
305      * TESTME relations: get_links is responsible to add the relation to the pagehash
306      * as 'linkrelation' key as pagename. See WikiDB_PageIterator::next
307      *   if (isset($next['linkrelation']))
308      */
309     function get_links($pagename, $reversed = true, $include_empty = false,
310                        $sortby = '', $limit = '', $exclude = '',
311                        $want_relations = false)
312     {
313         $dbh = &$this->_dbh;
314         extract($this->_table_names);
315
316         if ($reversed)
317             list($have, $want) = array('linkee', 'linker');
318         else
319             list($have, $want) = array('linker', 'linkee');
320         $orderby = $this->sortby($sortby, 'db', array('pagename'));
321         if ($orderby) $orderby = " ORDER BY $want." . $orderby;
322         if ($exclude) // array of pagenames
323             $exclude = " AND $want.pagename NOT IN " . $this->_sql_set($exclude);
324         else
325             $exclude = '';
326
327         global $page_prefix;
328         $pat = $page_prefix;
329         $p = strlen($pat) + 1;
330
331         $qpagename = $dbh->escapeSimple($pagename);
332         // MeV+APe 2007-11-14
333         // added "dummyname" so that database accepts "ORDER BY"
334         $sql = "SELECT DISTINCT $want.id AS id, substring($want.pagename from $p) AS pagename, $want.pagename AS dummyname,"
335             . ($want_relations ? " related.pagename as linkrelation" : " $want.hits AS hits")
336             . " FROM "
337             . (!$include_empty ? "$nonempty_tbl, " : '')
338             . " $page_tbl linkee, $page_tbl linker, $link_tbl "
339             . ($want_relations ? " JOIN $page_tbl related ON ($link_tbl.relation=related.id)" : '')
340             . " WHERE linkfrom=linker.id AND linkto=linkee.id"
341             . " AND $have.pagename='$pat$qpagename'"
342             . " AND substring($want.pagename from 0 for $p) = '$pat'"
343             . (!$include_empty ? " AND $nonempty_tbl.id=$want.id" : "")
344             //. " GROUP BY $want.id"
345             . $exclude
346             . $orderby;
347         if ($limit) {
348             // extract from,count from limit
349             list($from, $count) = $this->limit($limit);
350             $result = $dbh->limitQuery($sql, $from, $count);
351         } else {
352             $result = $dbh->query($sql);
353         }
354
355         return new WikiDB_backend_PearDB_iter($this, $result);
356     }
357
358     public function get_all_pages($include_empty = false,
359                                   $sortby = '', $limit = '', $exclude = '')
360     {
361         $dbh = &$this->_dbh;
362         extract($this->_table_names);
363
364         global $page_prefix;
365         $pat = $page_prefix;
366         $p = strlen($pat) + 1;
367
368         $orderby = $this->sortby($sortby, 'db');
369         if ($orderby) $orderby = ' ORDER BY ' . $orderby;
370         if ($exclude) // array of pagenames
371             $exclude = " AND $page_tbl.pagename NOT IN " . $this->_sql_set($exclude);
372         else
373             $exclude = '';
374
375         if (strstr($orderby, 'mtime ')) { // multiple columns possible
376             if ($include_empty) {
377                 $sql = "SELECT "
378                     . $this->page_tbl_fields
379                     . " FROM $page_tbl, $recent_tbl, $version_tbl"
380                     . " WHERE $page_tbl.id=$recent_tbl.id"
381                     . " AND $page_tbl.id=$version_tbl.id AND latestversion=version"
382                     . " AND substring($page_tbl.pagename from 0 for $p) = '$pat'"
383                     . $exclude
384                     . $orderby;
385             } else {
386                 $sql = "SELECT "
387                     . $this->page_tbl_fields
388                     . " FROM $nonempty_tbl, $page_tbl, $recent_tbl, $version_tbl"
389                     . " WHERE $nonempty_tbl.id=$page_tbl.id"
390                     . " AND $page_tbl.id=$recent_tbl.id"
391                     . " AND $page_tbl.id=$version_tbl.id AND latestversion=version"
392                     . " AND substring($page_tbl.pagename from 0 for $p) = '$pat'"
393                     . $exclude
394                     . $orderby;
395             }
396         } else {
397             if ($include_empty) {
398                 $sql = "SELECT "
399                     . $this->page_tbl_fields
400                     . " FROM $page_tbl"
401                     . ($exclude ? " WHERE $exclude" : '')
402                     . ($exclude ? " AND " : " WHERE ")
403                     . " substring($page_tbl.pagename from 0 for $p) = '$pat'"
404                     . $orderby;
405             } else {
406                 $sql = "SELECT "
407                     . $this->page_tbl_fields
408                     . " FROM $nonempty_tbl, $page_tbl"
409                     . " WHERE $nonempty_tbl.id=$page_tbl.id"
410                     . " AND substring($page_tbl.pagename from 0 for $p) = '$pat'"
411                     . $exclude
412                     . $orderby;
413             }
414         }
415         if ($limit && $orderby) {
416             // extract from,count from limit
417             list($from, $count) = $this->limit($limit);
418             $result = $dbh->limitQuery($sql, $from, $count);
419             $options = array('limit_by_db' => 1);
420         } else {
421             $result = $dbh->query($sql);
422             $options = array('limit_by_db' => 0);
423         }
424         return new WikiDB_backend_PearDB_iter($this, $result, $options);
425     }
426
427     /*
428      * Find highest or lowest hit counts.
429      */
430     public function most_popular($limit = 20, $sortby = '-hits')
431     {
432         $dbh = &$this->_dbh;
433         extract($this->_table_names);
434         global $page_prefix;
435         $pat = $page_prefix;
436         $p = strlen($pat) + 1;
437         if ($limit < 0) {
438             $order = "hits ASC";
439             $limit = -$limit;
440             $where = "";
441         } else {
442             $order = "hits DESC";
443             $where = " AND hits > 0";
444         }
445         $orderby = '';
446         if ($sortby != '-hits') {
447             if ($order = $this->sortby($sortby, 'db'))
448                 $orderby = " ORDER BY " . $order;
449         } else {
450             $orderby = " ORDER BY $order";
451         }
452         //$limitclause = $limit ? " LIMIT $limit" : '';
453         $sql = "SELECT "
454             . $this->page_tbl_fields
455             . " FROM $nonempty_tbl, $page_tbl"
456             . " WHERE $nonempty_tbl.id=$page_tbl.id"
457             . " AND substring($page_tbl.pagename from 0 for $p) = '$pat'"
458             . $where
459             . $orderby;
460         if ($limit) {
461             list($from, $count) = $this->limit($limit);
462             $result = $dbh->limitQuery($sql, $from, $count);
463         } else {
464             $result = $dbh->query($sql);
465         }
466         return new WikiDB_backend_PearDB_iter($this, $result);
467     }
468
469     /*
470      * Find recent changes.
471      */
472     public function most_recent($params)
473     {
474         $limit = 0;
475         $since = 0;
476         $include_minor_revisions = false;
477         $exclude_major_revisions = false;
478         $include_all_revisions = false;
479         extract($params);
480
481         $dbh = &$this->_dbh;
482         extract($this->_table_names);
483
484         $pick = array();
485         if ($since)
486             $pick[] = "mtime >= $since";
487
488         if ($include_all_revisions) {
489             // Include all revisions of each page.
490             $table = "$page_tbl, $version_tbl";
491             $join_clause = "$page_tbl.id=$version_tbl.id";
492
493             if ($exclude_major_revisions) {
494                 // Include only minor revisions
495                 $pick[] = "minor_edit <> 0";
496             } elseif (!$include_minor_revisions) {
497                 // Include only major revisions
498                 $pick[] = "minor_edit = 0";
499             }
500         } else {
501             $table = "$page_tbl, $recent_tbl";
502             $join_clause = "$page_tbl.id=$recent_tbl.id";
503             $table .= ", $version_tbl";
504             $join_clause .= " AND $version_tbl.id=$page_tbl.id";
505
506             if ($exclude_major_revisions) {
507                 // Include only most recent minor revision
508                 $pick[] = 'version=latestminor';
509             } elseif (!$include_minor_revisions) {
510                 // Include only most recent major revision
511                 $pick[] = 'version=latestmajor';
512             } else {
513                 // Include only the latest revision (whether major or minor).
514                 $pick[] = 'version=latestversion';
515             }
516         }
517         $order = "DESC";
518         if ($limit < 0) {
519             $order = "ASC";
520             $limit = -$limit;
521         }
522         // $limitclause = $limit ? " LIMIT $limit" : '';
523         $where_clause = $join_clause;
524         if ($pick)
525             $where_clause .= " AND " . join(" AND ", $pick);
526
527         global $page_prefix;
528         $pat = $page_prefix;
529         $p = strlen($pat) + 1;
530
531         // FIXME: use SQL_BUFFER_RESULT for mysql?
532         $sql = "SELECT "
533             . $this->page_tbl_fields . ", " . $this->version_tbl_fields
534             . " FROM $table"
535             . " WHERE $where_clause"
536             . " AND substring($page_tbl.pagename from 0 for $p) = '$pat'"
537             . " ORDER BY mtime $order";
538         if ($limit) {
539             list($from, $count) = $this->limit($limit);
540             $result = $dbh->limitQuery($sql, $from, $count);
541         } else {
542             $result = $dbh->query($sql);
543         }
544         return new WikiDB_backend_PearDB_iter($this, $result);
545     }
546
547     /*
548      * Find referenced empty pages.
549      */
550     function wanted_pages($exclude_from = '', $exclude = '', $sortby = '', $limit = '')
551     {
552         $dbh = &$this->_dbh;
553         extract($this->_table_names);
554         global $page_prefix;
555         $pat = $page_prefix;
556         $p = strlen($pat) + 1;
557         if ($orderby = $this->sortby($sortby, 'db', array('pagename', 'wantedfrom')))
558             $orderby = 'ORDER BY ' . $orderby;
559
560         if ($exclude_from) // array of pagenames
561             $exclude_from = " AND pp.pagename NOT IN " . $this->_sql_set($exclude_from);
562         if ($exclude) // array of pagenames
563             $exclude = " AND p.pagename NOT IN " . $this->_sql_set($exclude);
564         $p = strlen($page_prefix) + 1;
565         $sql = "SELECT substring(p.pagename from $p) AS wantedfrom, substring(pp.pagename from $p) AS pagename"
566             . " FROM $page_tbl p, $link_tbl linked"
567             . " LEFT JOIN $page_tbl pp ON linked.linkto = pp.id"
568             . " LEFT JOIN $nonempty_tbl ne ON linked.linkto = ne.id"
569             . " WHERE ne.id IS NULL"
570             . " AND p.id = linked.linkfrom"
571             . " AND substring(p.pagename from 0 for $p) = '$pat'"
572             . " AND substring(pp.pagename from 0 for $p) = '$pat'"
573             . $exclude_from
574             . $exclude
575             . $orderby;
576         if ($limit) {
577             // oci8 error: WHERE NULL = NULL appended
578             list($from, $count) = $this->limit($limit);
579             $result = $dbh->limitQuery($sql, $from, $count * 3);
580         } else {
581             $result = $dbh->query($sql);
582         }
583         return new WikiDB_backend_PearDB_generic_iter($this, $result);
584     }
585
586     /*
587      * Rename page in the database.
588      */
589     function rename_page($pagename, $to)
590     {
591         $dbh = &$this->_dbh;
592         extract($this->_table_names);
593
594         $this->lock();
595         if (($id = $this->_get_pageid($pagename, false))) {
596             if ($new = $this->_get_pageid($to, false)) {
597                 // Cludge Alert!
598                 // This page does not exist (already verified before), but exists in the page table.
599                 // So we delete this page.
600                 $dbh->query("DELETE FROM $nonempty_tbl WHERE id=$new");
601                 $dbh->query("DELETE FROM $recent_tbl WHERE id=$new");
602                 $dbh->query("DELETE FROM $version_tbl WHERE id=$new");
603                 // We have to fix all referring tables to the old id
604                 $dbh->query("UPDATE $link_tbl SET linkfrom=$id WHERE linkfrom=$new");
605                 $dbh->query("UPDATE $link_tbl SET linkto=$id WHERE linkto=$new");
606                 $dbh->query("DELETE FROM $page_tbl WHERE id=$new");
607             }
608             global $page_prefix;
609             $dbh->query(sprintf("UPDATE $page_tbl SET pagename='%s' WHERE id=$id",
610                 $dbh->escapeSimple($page_prefix . $to)));
611         }
612         $this->unlock();
613         return $id;
614     }
615
616     function is_wiki_page($pagename)
617     {
618         global $page_prefix;
619         return parent::is_wiki_page($page_prefix . $pagename);
620     }
621
622     function increaseHitCount($pagename)
623     {
624         global $page_prefix;
625         parent::increaseHitCount($page_prefix . $pagename);
626     }
627
628     /*
629      * Serialize data
630      */
631     function _serialize($data)
632     {
633         return WikiDB_backend_PearDB::_serialize($data);
634     }
635
636     /**
637      * Pack tables.
638      * NOTE: Disable vacuum, wikiuser is not the table owner
639      */
640     function optimize()
641     {
642         return true;
643     }
644
645     /*
646      * Text search (title or full text)
647      */
648     public function text_search($search, $fulltext = false,
649                                 $sortby = '', $limit = '', $exclude = '')
650     {
651         $dbh = &$this->_dbh;
652         extract($this->_table_names);
653         global $page_prefix;
654         $pat = $page_prefix;
655         $len = strlen($pat) + 1;
656         $orderby = $this->sortby($sortby, 'db');
657         if ($sortby and $orderby) $orderby = ' ORDER BY ' . $orderby;
658
659         $searchclass = get_class($this) . "_search";
660         // no need to define it everywhere and then fallback. memory!
661         if (!class_exists($searchclass))
662             $searchclass = "WikiDB_backend_PearDB_search";
663         $searchobj = new $searchclass($search, $dbh);
664
665         $table = "$nonempty_tbl, $page_tbl";
666         $join_clause = "$nonempty_tbl.id=$page_tbl.id";
667         $fields = $this->page_tbl_fields;
668
669         if ($fulltext) {
670             $table .= ", $recent_tbl";
671             $join_clause .= " AND $page_tbl.id=$recent_tbl.id";
672
673             $table .= ", $version_tbl";
674             $join_clause .= " AND $page_tbl.id=$version_tbl.id AND latestversion=version";
675
676             $fields .= ", $page_tbl.pagedata as pagedata, " . $this->version_tbl_fields;
677             // TODO: title still ignored, need better rank and subselect
678             $callback = new WikiMethodCb($searchobj, "_fulltext_match_clause");
679             $search_string = $search->makeTsearch2SqlClauseObj($callback);
680             $search_string = str_replace('%', '', $search_string);
681             $search_clause = "substring(plugin_wiki_page.pagename from 0 for $len) = '$pat') AND (";
682
683             $search_clause .= "idxFTI @@ plainto_tsquery('english', '$search_string')";
684             if (!$orderby)
685                 $orderby = " ORDER BY ts_rank(idxFTI, plainto_tsquery('english', '$search_string')) DESC";
686         } else {
687             $callback = new WikiMethodCb($searchobj, "_pagename_match_clause");
688             $search_clause = "substring(plugin_wiki_page.pagename from 0 for $len) = '$pat') AND (";
689             $search_clause .= $search->makeSqlClauseObj($callback);
690         }
691
692         $sql = "SELECT $fields FROM $table"
693             . " WHERE $join_clause"
694             . "  AND ($search_clause)"
695             . $orderby;
696         if ($limit) {
697             list($from, $count) = $this->limit($limit);
698             $result = $dbh->limitQuery($sql, $from, $count);
699         } else {
700             $result = $dbh->query($sql);
701         }
702
703         $iter = new WikiDB_backend_PearDB_iter($this, $result);
704         $iter->stoplisted = @$searchobj->stoplisted;
705         return $iter;
706     }
707
708     function exists_link($pagename, $link, $reversed = false)
709     {
710         $dbh = &$this->_dbh;
711         extract($this->_table_names);
712
713         if ($reversed)
714             list($have, $want) = array('linkee', 'linker');
715         else
716             list($have, $want) = array('linker', 'linkee');
717         $qpagename = $dbh->escapeSimple($pagename);
718         $qlink = $dbh->escapeSimple($link);
719         $row = $dbh->GetRow("SELECT $want.pagename as result"
720             . " FROM $link_tbl, $page_tbl linker, $page_tbl linkee, $nonempty_tbl"
721             . " WHERE linkfrom=linker.id AND linkto=linkee.id"
722             . " AND $have.pagename='$qpagename'"
723             . " AND $want.pagename='$qlink'"
724             . " LIMIT 1");
725         return $row['result'] ? 1 : 0;
726     }
727 }
728
729 class WikiDB_backend_PearDB_ffpgsql_search
730     extends WikiDB_backend_PearDB_pgsql_search
731 {
732     function _pagename_match_clause($node)
733     {
734         $word = $node->sql();
735         // @alu: use _quote maybe instead of direct pg_escape_string
736         $word = pg_escape_string($word);
737         global $page_prefix;
738         $len = strlen($page_prefix) + 1;
739         if ($node->op == 'REGEX') { // posix regex extensions
740             return ($this->_case_exact
741                 ? "substring(pagename from $len) ~* '$word'"
742                 : "substring(pagename from $len) ~ '$word'");
743         } else {
744             return ($this->_case_exact
745                 ? "substring(pagename from $len) LIKE '$word'"
746                 : "substring(pagename from $len) ILIKE '$word'");
747         }
748     }
749
750     /*
751      * use tsearch2. See schemas/psql-tsearch2.sql and /usr/share/postgresql/contrib/tsearch2.sql
752      * TODO: don't parse the words into nodes. rather replace "[ +]" with & and "-" with "!" and " or " with "|"
753      * tsearch2 query language: @@ "word | word", "word & word", ! word
754      * ~* '.*something that does not exist.*'
755      *
756      * phrase search for "history lesson":
757      *
758      * SELECT id FROM tab WHERE ts_idx_col @@ to_tsquery('history&lesson')
759      * AND text_col ~* '.*history\\s+lesson.*';
760      *
761      * The full-text index will still be used, and the regex will be used to
762      * prune the results afterwards.
763      */
764     function _fulltext_match_clause($node)
765     {
766         $word = strtolower($node->word);
767         // $word = str_replace(" ", "&", $word); // phrase fix
768
769         // @alu: use _quote maybe instead of direct pg_escape_string
770         $word = pg_escape_string($word);
771
772         return $word;
773     }
774 }
775
776 // Local Variables:
777 // mode: php
778 // tab-width: 8
779 // c-basic-offset: 4
780 // c-hanging-comment-ender-p: nil
781 // indent-tabs-mode: nil
782 // End: