]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/PearDB_pgsql.php
Reformat code
[SourceForge/phpwiki.git] / lib / WikiDB / backend / PearDB_pgsql.php
1 <?php
2
3 require_once 'lib/ErrorManager.php';
4 require_once 'lib/WikiDB/backend/PearDB.php';
5
6 if (!defined("USE_BYTEA")) // see schemas/psql-initialize.sql
7     define("USE_BYTEA", true);
8 //define("USE_BYTEA", false);
9
10 /*
11 Since 1.3.12 changed to use:
12  * Foreign Keys
13  * ON DELETE CASCADE
14  * tsearch2
15 */
16
17 class WikiDB_backend_PearDB_pgsql
18     extends WikiDB_backend_PearDB
19 {
20     function WikiDB_backend_PearDB_pgsql($dbparams)
21     {
22         // The pgsql handler of (at least my version of) the PEAR::DB
23         // library generates three warnings when a database is opened:
24         //
25         //     Undefined index: options
26         //     Undefined index: tty
27         //     Undefined index: port
28         //
29         // This stuff is all just to catch and ignore these warnings,
30         // so that they don't get reported to the user.  (They are
31         // not consequential.)
32
33         global $ErrorManager;
34         $ErrorManager->pushErrorHandler(new WikiMethodCb($this, '_pgsql_open_error'));
35         $this->WikiDB_backend_PearDB($dbparams);
36         $ErrorManager->popErrorHandler();
37     }
38
39     function _pgsql_open_error($error)
40     {
41         if (preg_match('/^Undefined\s+index:\s+(options|tty|port)/',
42             $error->errstr)
43         )
44             return true; // Ignore error
45         return false;
46     }
47
48     /**
49      * Pack tables.
50      * NOTE: Only the table owner can do this. Either fix the schema or setup autovacuum.
51      */
52     function optimize()
53     {
54         return 0; // if the wikiuser is not the table owner
55
56         foreach ($this->_table_names as $table) {
57             $this->_dbh->query("VACUUM ANALYZE $table");
58         }
59         return 1;
60     }
61
62     function _quote($s)
63     {
64         if (USE_BYTEA)
65             return pg_escape_bytea($s);
66         if (function_exists('pg_escape_string'))
67             return pg_escape_string($s);
68         else
69             return base64_encode($s);
70     }
71
72     function _unquote($s)
73     {
74         if (USE_BYTEA)
75             return pg_unescape_bytea($s);
76         if (function_exists('pg_escape_string'))
77             return $s;
78         else
79             return base64_decode($s);
80     }
81
82     // Until the binary escape problems on pear pgsql are solved */
83     function get_cached_html($pagename)
84     {
85         $dbh = &$this->_dbh;
86         $page_tbl = $this->_table_names['page_tbl'];
87         $data = $dbh->GetOne(sprintf("SELECT cached_html FROM $page_tbl WHERE pagename='%s'",
88             $dbh->escapeSimple($pagename)));
89         if ($data) return $this->_unquote($data);
90         else return '';
91     }
92
93     function set_cached_html($pagename, $data)
94     {
95         $dbh = &$this->_dbh;
96         $page_tbl = $this->_table_names['page_tbl'];
97         if (USE_BYTEA)
98             $sth = $dbh->query(sprintf("UPDATE $page_tbl"
99                     . " SET cached_html='%s'"
100                     . " WHERE pagename='%s'",
101                 $this->_quote($data),
102                 $dbh->escapeSimple($pagename)));
103         else
104             $sth = $dbh->query("UPDATE $page_tbl"
105                     . " SET cached_html=?"
106                     . " WHERE pagename=?",
107                 // PearDB does NOT use pg_escape_string()! Oh dear.
108                 array($this->_quote($data), $pagename));
109     }
110
111     /**
112      * Create a new revision of a page.
113      */
114     function _todo_set_versiondata($pagename, $version, $data)
115     {
116         $dbh = &$this->_dbh;
117         $version_tbl = $this->_table_names['version_tbl'];
118
119         $minor_edit = (int)!empty($data['is_minor_edit']);
120         unset($data['is_minor_edit']);
121
122         $mtime = (int)$data['mtime'];
123         unset($data['mtime']);
124         assert(!empty($mtime));
125
126         @$content = (string)$data['%content'];
127         unset($data['%content']);
128         unset($data['%pagedata']);
129
130         $this->lock();
131         $id = $this->_get_pageid($pagename, true);
132         $dbh->query(sprintf("DELETE FROM version WHERE id=%d AND version=%d", $id, $version));
133         $dbh->query(sprintf("INSERT INTO version (id,version,mtime,minor_edit,content,versiondata)" .
134                 " VALUES (%d, %d, %d, %d, '%s', '%s')",
135             $id, $version, $mtime, $minor_edit,
136             $this->_quote($content),
137             $this->_serialize($data)));
138         // TODO: This function does not work yet
139         $dbh->query(sprintf("SELECT update_recent (%d, %d)", $id, $version));
140         $this->unlock();
141     }
142
143     /**
144      * Delete an old revision of a page.
145      */
146     function _todo_delete_versiondata($pagename, $version)
147     {
148         $dbh = &$this->_dbh;
149         // TODO: This function was removed
150         $dbh->query(sprintf("SELECT delete_versiondata (%d, %d)", $id, $version));
151     }
152
153     /**
154      * Rename page in the database.
155      */
156     function _todo_rename_page($pagename, $to)
157     {
158         $dbh = &$this->_dbh;
159         extract($this->_table_names);
160
161         $this->lock();
162         if (($id = $this->_get_pageid($pagename, false))) {
163             if ($new = $this->_get_pageid($to, false)) {
164                 // Cludge Alert!
165                 // This page does not exist (already verified before), but exists in the page table.
166                 // So we delete this page in one step.
167                 $dbh->query("SELECT prepare_rename_page($id, $new)");
168             }
169             $dbh->query(sprintf("UPDATE $page_tbl SET pagename='%s' WHERE id=$id",
170                 $dbh->escapeSimple($to)));
171         }
172         $this->unlock();
173         return $id;
174     }
175
176     /**
177      * Lock all tables we might use.
178      */
179     function _lock_tables($write_lock = true)
180     {
181         $this->_dbh->query("BEGIN");
182     }
183
184     /**
185      * Unlock all tables.
186      */
187     function _unlock_tables()
188     {
189         $this->_dbh->query("COMMIT");
190     }
191
192     /**
193      * Serialize data
194      */
195     function _serialize($data)
196     {
197         if (empty($data))
198             return '';
199         assert(is_array($data));
200         return $this->_quote(serialize($data));
201     }
202
203     /**
204      * Unserialize data
205      */
206     function _unserialize($data)
207     {
208         if (empty($data))
209             return array();
210         // Base64 encoded data does not contain colons.
211         //  (only alphanumerics and '+' and '/'.)
212         if (substr($data, 0, 2) == 'a:')
213             return unserialize($data);
214         return unserialize($this->_unquote($data));
215     }
216
217     /**
218      * Title search.
219      */
220     function text_search($search, $fulltext = false, $sortby = '', $limit = '',
221                          $exclude = '')
222     {
223         $dbh = &$this->_dbh;
224         extract($this->_table_names);
225         $orderby = $this->sortby($sortby, 'db');
226         if ($sortby and $orderby) $orderby = ' ORDER BY ' . $orderby;
227
228         $searchclass = get_class($this) . "_search";
229         // no need to define it everywhere and then fallback. memory!
230         if (!class_exists($searchclass))
231             $searchclass = "WikiDB_backend_PearDB_search";
232         $searchobj = new $searchclass($search, $dbh);
233
234         $table = "$nonempty_tbl, $page_tbl";
235         $join_clause = "$nonempty_tbl.id=$page_tbl.id";
236         $fields = $this->page_tbl_fields;
237
238         if ($fulltext) {
239             $table .= ", $recent_tbl";
240             $join_clause .= " AND $page_tbl.id=$recent_tbl.id";
241
242             $table .= ", $version_tbl";
243             $join_clause .= " AND $page_tbl.id=$version_tbl.id AND latestversion=version";
244
245             $fields .= ", $page_tbl.pagedata as pagedata, " . $this->version_tbl_fields;
246             // TODO: title still ignored, need better rank and subselect
247             $callback = new WikiMethodCb($searchobj, "_fulltext_match_clause");
248             $search_string = $search->makeTsearch2SqlClauseObj($callback);
249             $search_string = str_replace(array("%", " "), array("", "&"), $search_string);
250             $search_clause = "idxFTI @@ to_tsquery('$search_string')";
251             if (!$orderby)
252                 $orderby = " ORDER BY rank(idxFTI, to_tsquery('$search_string')) DESC";
253         } else {
254             $callback = new WikiMethodCb($searchobj, "_pagename_match_clause");
255             $search_clause = $search->makeSqlClauseObj($callback);
256         }
257
258         $sql = "SELECT $fields FROM $table"
259             . " WHERE $join_clause"
260             . "  AND ($search_clause)"
261             . $orderby;
262         if ($limit) {
263             list($from, $count) = $this->limit($limit);
264             $result = $dbh->limitQuery($sql, $from, $count);
265         } else {
266             $result = $dbh->query($sql);
267         }
268
269         $iter = new WikiDB_backend_PearDB_iter($this, $result);
270         $iter->stoplisted = @$searchobj->stoplisted;
271         return $iter;
272     }
273
274 }
275
276 ;
277
278 class WikiDB_backend_PearDB_pgsql_search
279     extends WikiDB_backend_PearDB_search
280 {
281     function _pagename_match_clause($node)
282     {
283         $word = $node->sql();
284         if ($node->op == 'REGEX') { // posix regex extensions
285             return ($this->_case_exact
286                 ? "pagename ~* '$word'"
287                 : "pagename ~ '$word'");
288         } else {
289             return ($this->_case_exact
290                 ? "pagename LIKE '$word'"
291                 : "pagename ILIKE '$word'");
292         }
293     }
294
295     /*
296      most used words:
297 select * from stat('select idxfti from version') order by ndoc desc, nentry desc, word limit 10;
298       word       | ndoc | nentry
299 -----------------+------+--------
300  plugin          |  112 |    418
301  page            |   85 |    446
302  phpwikidocument |   62 |     62
303  use             |   48 |    169
304  help            |   46 |     96
305  wiki            |   44 |    102
306  name            |   43 |    131
307  phpwiki         |   42 |    173
308  see             |   42 |     69
309  default         |   39 |    124
310     */
311
312     /**
313      * use tsearch2. See schemas/psql-tsearch2.sql and /usr/share/postgresql/contrib/tsearch2.sql
314      * TODO: don't parse the words into nodes. rather replace "[ +]" with & and "-" with "!" and " or " with "|"
315      * tsearch2 query language: @@ "word | word", "word & word", ! word
316      * ~* '.*something that does not exist.*'
317      */
318     /*
319      phrase search for "history lesson":
320
321      SELECT id FROM tab WHERE ts_idx_col @@ to_tsquery('history&lesson')
322      AND text_col ~* '.*history\\s+lesson.*';
323
324      The full-text index will still be used, and the regex will be used to
325      prune the results afterwards.
326     */
327     function _fulltext_match_clause($node)
328     {
329         $word = strtolower($node->word);
330         $word = str_replace(" ", "&", $word); // phrase fix
331         return $word;
332
333         // clause specified above.
334         return $this->_pagename_match_clause($node) . " OR idxFTI @@ to_tsquery('$word')";
335     }
336 }
337
338 // Local Variables:
339 // mode: php
340 // tab-width: 8
341 // c-basic-offset: 4
342 // c-hanging-comment-ender-p: nil
343 // indent-tabs-mode: nil
344 // End: