4d14b0012bab4db96242611c2e7add5836b469ef
[lhc/web/wiklou.git] / includes / db / DatabasePostgres.php
1 <?php
2 /**
3 * @ingroup Database
4 * @file
5 * This is the Postgres database abstraction layer.
6 *
7 */
8 class PostgresField {
9 private $name, $tablename, $type, $nullable, $max_length;
10
11 static function fromText($db, $table, $field) {
12 global $wgDBmwschema;
13
14 $q = <<<END
15 SELECT
16 CASE WHEN typname = 'int2' THEN 'smallint'
17 WHEN typname = 'int4' THEN 'integer'
18 WHEN typname = 'int8' THEN 'bigint'
19 WHEN typname = 'bpchar' THEN 'char'
20 ELSE typname END AS typname,
21 attnotnull, attlen
22 FROM pg_class, pg_namespace, pg_attribute, pg_type
23 WHERE relnamespace=pg_namespace.oid
24 AND relkind='r'
25 AND attrelid=pg_class.oid
26 AND atttypid=pg_type.oid
27 AND nspname=%s
28 AND relname=%s
29 AND attname=%s;
30 END;
31 $res = $db->query(sprintf($q,
32 $db->addQuotes($wgDBmwschema),
33 $db->addQuotes($table),
34 $db->addQuotes($field)));
35 $row = $db->fetchObject($res);
36 if (!$row)
37 return null;
38 $n = new PostgresField;
39 $n->type = $row->typname;
40 $n->nullable = ($row->attnotnull == 'f');
41 $n->name = $field;
42 $n->tablename = $table;
43 $n->max_length = $row->attlen;
44 return $n;
45 }
46
47 function name() {
48 return $this->name;
49 }
50
51 function tableName() {
52 return $this->tablename;
53 }
54
55 function type() {
56 return $this->type;
57 }
58
59 function nullable() {
60 return $this->nullable;
61 }
62
63 function maxLength() {
64 return $this->max_length;
65 }
66 }
67
68 /**
69 * @ingroup Database
70 */
71 class DatabasePostgres extends Database {
72 var $mInsertId = NULL;
73 var $mLastResult = NULL;
74 var $numeric_version = NULL;
75
76 function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false,
77 $failFunction = false, $flags = 0 )
78 {
79
80 global $wgOut;
81 # Can't get a reference if it hasn't been set yet
82 if ( !isset( $wgOut ) ) {
83 $wgOut = NULL;
84 }
85 $this->mOut =& $wgOut;
86 $this->mFailFunction = $failFunction;
87 $this->mFlags = $flags;
88 $this->open( $server, $user, $password, $dbName);
89
90 }
91
92 function cascadingDeletes() {
93 return true;
94 }
95 function cleanupTriggers() {
96 return true;
97 }
98 function strictIPs() {
99 return true;
100 }
101 function realTimestamps() {
102 return true;
103 }
104 function implicitGroupby() {
105 return false;
106 }
107 function implicitOrderby() {
108 return false;
109 }
110 function searchableIPs() {
111 return true;
112 }
113 function functionalIndexes() {
114 return true;
115 }
116
117 function hasConstraint( $name ) {
118 global $wgDBmwschema;
119 $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . pg_escape_string( $name ) . "' AND n.nspname = '" . pg_escape_string($wgDBmwschema) ."'";
120 return $this->numRows($res = $this->doQuery($SQL));
121 }
122
123 static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
124 {
125 return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags );
126 }
127
128 /**
129 * Usually aborts on failure
130 * If the failFunction is set to a non-zero integer, returns success
131 */
132 function open( $server, $user, $password, $dbName ) {
133 # Test for Postgres support, to avoid suppressed fatal error
134 if ( !function_exists( 'pg_connect' ) ) {
135 throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
136 }
137
138 global $wgDBport;
139
140 if (!strlen($user)) { ## e.g. the class is being loaded
141 return;
142 }
143
144 $this->close();
145 $this->mServer = $server;
146 $this->mPort = $port = $wgDBport;
147 $this->mUser = $user;
148 $this->mPassword = $password;
149 $this->mDBname = $dbName;
150
151 $hstring="";
152 if ($server!=false && $server!="") {
153 $hstring="host=$server ";
154 }
155 if ($port!=false && $port!="") {
156 $hstring .= "port=$port ";
157 }
158
159 error_reporting( E_ALL );
160 @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password");
161
162 if ( $this->mConn == false ) {
163 wfDebug( "DB connection error\n" );
164 wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
165 wfDebug( $this->lastError()."\n" );
166 return false;
167 }
168
169 $this->mOpened = true;
170
171 global $wgCommandLineMode;
172 ## If called from the command-line (e.g. importDump), only show errors
173 if ($wgCommandLineMode) {
174 $this->doQuery("SET client_min_messages = 'ERROR'");
175 }
176
177 global $wgDBmwschema, $wgDBts2schema;
178 if (isset( $wgDBmwschema ) && isset( $wgDBts2schema )
179 && $wgDBmwschema !== 'mediawiki'
180 && preg_match( '/^\w+$/', $wgDBmwschema )
181 && preg_match( '/^\w+$/', $wgDBts2schema )
182 ) {
183 $safeschema = $this->quote_ident($wgDBmwschema);
184 $safeschema2 = $this->quote_ident($wgDBts2schema);
185 $this->doQuery("SET search_path = $safeschema, $wgDBts2schema, public");
186 }
187
188 return $this->mConn;
189 }
190
191
192 function initial_setup($password, $dbName) {
193 // If this is the initial connection, setup the schema stuff and possibly create the user
194 global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema;
195
196 print "<li>Checking the version of Postgres...";
197 $version = $this->getServerVersion();
198 $PGMINVER = '8.1';
199 if ($this->numeric_version < $PGMINVER) {
200 print "<b>FAILED</b>. Required version is $PGMINVER. You have $this->numeric_version ($version)</li>\n";
201 dieout("</ul>");
202 }
203 print "version $this->numeric_version is OK.</li>\n";
204
205 $safeuser = $this->quote_ident($wgDBuser);
206 // Are we connecting as a superuser for the first time?
207 if ($wgDBsuperuser) {
208 // Are we really a superuser? Check out our rights
209 $SQL = "SELECT
210 CASE WHEN usesuper IS TRUE THEN
211 CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END
212 ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END
213 END AS rights
214 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser);
215 $rows = $this->numRows($res = $this->doQuery($SQL));
216 if (!$rows) {
217 print "<li>ERROR: Could not read permissions for user \"$wgDBsuperuser\"</li>\n";
218 dieout('</ul>');
219 }
220 $perms = pg_fetch_result($res, 0, 0);
221
222 $SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser);
223 $rows = $this->numRows($this->doQuery($SQL));
224 if ($rows) {
225 print "<li>User \"$wgDBuser\" already exists, skipping account creation.</li>";
226 }
227 else {
228 if ($perms != 1 and $perms != 3) {
229 print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create other users. ";
230 print 'Please use a different Postgres user.</li>';
231 dieout('</ul>');
232 }
233 print "<li>Creating user <b>$wgDBuser</b>...";
234 $safepass = $this->addQuotes($wgDBpassword);
235 $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass";
236 $this->doQuery($SQL);
237 print "OK</li>\n";
238 }
239 // User now exists, check out the database
240 if ($dbName != $wgDBname) {
241 $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $this->addQuotes($wgDBname);
242 $rows = $this->numRows($this->doQuery($SQL));
243 if ($rows) {
244 print "<li>Database \"$wgDBname\" already exists, skipping database creation.</li>";
245 }
246 else {
247 if ($perms < 2) {
248 print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create databases. ";
249 print 'Please use a different Postgres user.</li>';
250 dieout('</ul>');
251 }
252 print "<li>Creating database <b>$wgDBname</b>...";
253 $safename = $this->quote_ident($wgDBname);
254 $SQL = "CREATE DATABASE $safename OWNER $safeuser ";
255 $this->doQuery($SQL);
256 print "OK</li>\n";
257 // Hopefully tsearch2 and plpgsql are in template1...
258 }
259
260 // Reconnect to check out tsearch2 rights for this user
261 print "<li>Connecting to \"$wgDBname\" as superuser \"$wgDBsuperuser\" to check rights...";
262
263 $hstring="";
264 if ($this->mServer!=false && $this->mServer!="") {
265 $hstring="host=$this->mServer ";
266 }
267 if ($this->mPort!=false && $this->mPort!="") {
268 $hstring .= "port=$this->mPort ";
269 }
270
271 @$this->mConn = pg_connect("$hstring dbname=$wgDBname user=$wgDBsuperuser password=$password");
272 if ( $this->mConn == false ) {
273 print "<b>FAILED TO CONNECT!</b></li>";
274 dieout("</ul>");
275 }
276 print "OK</li>\n";
277 }
278
279 if ($this->numeric_version < 8.3) {
280 // Tsearch2 checks
281 print "<li>Checking that tsearch2 is installed in the database \"$wgDBname\"...";
282 if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) {
283 print "<b>FAILED</b>. tsearch2 must be installed in the database \"$wgDBname\".";
284 print "Please see <a href='http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
285 print " for instructions or ask on #postgresql on irc.freenode.net</li>\n";
286 dieout("</ul>");
287 }
288 print "OK</li>\n";
289 print "<li>Ensuring that user \"$wgDBuser\" has select rights on the tsearch2 tables...";
290 foreach (array('cfg','cfgmap','dict','parser') as $table) {
291 $SQL = "GRANT SELECT ON pg_ts_$table TO $safeuser";
292 $this->doQuery($SQL);
293 }
294 print "OK</li>\n";
295 }
296
297 // Setup the schema for this user if needed
298 $result = $this->schemaExists($wgDBmwschema);
299 $safeschema = $this->quote_ident($wgDBmwschema);
300 if (!$result) {
301 print "<li>Creating schema <b>$wgDBmwschema</b> ...";
302 $result = $this->doQuery("CREATE SCHEMA $safeschema AUTHORIZATION $safeuser");
303 if (!$result) {
304 print "<b>FAILED</b>.</li>\n";
305 dieout("</ul>");
306 }
307 print "OK</li>\n";
308 }
309 else {
310 print "<li>Schema already exists, explicitly granting rights...\n";
311 $safeschema2 = $this->addQuotes($wgDBmwschema);
312 $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n".
313 "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n".
314 "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n".
315 "AND p.relkind IN ('r','S','v')\n";
316 $SQL .= "UNION\n";
317 $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n".
318 "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n".
319 "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n".
320 "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2";
321 $res = $this->doQuery($SQL);
322 if (!$res) {
323 print "<b>FAILED</b>. Could not set rights for the user.</li>\n";
324 dieout("</ul>");
325 }
326 $this->doQuery("SET search_path = $safeschema");
327 $rows = $this->numRows($res);
328 while ($rows) {
329 $rows--;
330 $this->doQuery(pg_fetch_result($res, $rows, 0));
331 }
332 print "OK</li>";
333 }
334
335 // Install plpgsql if needed
336 $this->setup_plpgsql();
337
338 $wgDBsuperuser = '';
339 return true; // Reconnect as regular user
340
341 } // end superuser
342
343 if (!defined('POSTGRES_SEARCHPATH')) {
344
345 if ($this->numeric_version < 8.3) {
346 // Do we have the basic tsearch2 table?
347 print "<li>Checking for tsearch2 in the schema \"$wgDBts2schema\"...";
348 if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) {
349 print "<b>FAILED</b>. Make sure tsearch2 is installed. See <a href=";
350 print "'http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
351 print " for instructions.</li>\n";
352 dieout("</ul>");
353 }
354 print "OK</li>\n";
355
356 // Does this user have the rights to the tsearch2 tables?
357 $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
358 print "<li>Checking tsearch2 permissions...";
359 // Let's check all four, just to be safe
360 error_reporting( 0 );
361 $ts2tables = array('cfg','cfgmap','dict','parser');
362 $safetsschema = $this->quote_ident($wgDBts2schema);
363 foreach ( $ts2tables AS $tname ) {
364 $SQL = "SELECT count(*) FROM $safetsschema.pg_ts_$tname";
365 $res = $this->doQuery($SQL);
366 if (!$res) {
367 print "<b>FAILED</b> to access pg_ts_$tname. Make sure that the user ".
368 "\"$wgDBuser\" has SELECT access to all four tsearch2 tables</li>\n";
369 dieout("</ul>");
370 }
371 }
372 $SQL = "SELECT ts_name FROM $safetsschema.pg_ts_cfg WHERE locale = '$ctype'";
373 $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END";
374 $res = $this->doQuery($SQL);
375 error_reporting( E_ALL );
376 if (!$res) {
377 print "<b>FAILED</b>. Could not determine the tsearch2 locale information</li>\n";
378 dieout("</ul>");
379 }
380 print "OK</li>";
381
382 // Will the current locale work? Can we force it to?
383 print "<li>Verifying tsearch2 locale with $ctype...";
384 $rows = $this->numRows($res);
385 $resetlocale = 0;
386 if (!$rows) {
387 print "<b>not found</b></li>\n";
388 print "<li>Attempting to set default tsearch2 locale to \"$ctype\"...";
389 $resetlocale = 1;
390 }
391 else {
392 $tsname = pg_fetch_result($res, 0, 0);
393 if ($tsname != 'default') {
394 print "<b>not set to default ($tsname)</b>";
395 print "<li>Attempting to change tsearch2 default locale to \"$ctype\"...";
396 $resetlocale = 1;
397 }
398 }
399 if ($resetlocale) {
400 $SQL = "UPDATE $safetsschema.pg_ts_cfg SET locale = '$ctype' WHERE ts_name = 'default'";
401 $res = $this->doQuery($SQL);
402 if (!$res) {
403 print "<b>FAILED</b>. ";
404 print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"$ctype\"</li>\n";
405 dieout("</ul>");
406 }
407 print "OK</li>";
408 }
409
410 // Final test: try out a simple tsearch2 query
411 $SQL = "SELECT $safetsschema.to_tsvector('default','MediaWiki tsearch2 testing')";
412 $res = $this->doQuery($SQL);
413 if (!$res) {
414 print "<b>FAILED</b>. Specifically, \"$SQL\" did not work.</li>";
415 dieout("</ul>");
416 }
417 print "OK</li>";
418 }
419
420 // Install plpgsql if needed
421 $this->setup_plpgsql();
422
423 // Does the schema already exist? Who owns it?
424 $result = $this->schemaExists($wgDBmwschema);
425 if (!$result) {
426 print "<li>Creating schema <b>$wgDBmwschema</b> ...";
427 error_reporting( 0 );
428 $safeschema = $this->quote_ident($wgDBmwschema);
429 $result = $this->doQuery("CREATE SCHEMA $safeschema");
430 error_reporting( E_ALL );
431 if (!$result) {
432 print "<b>FAILED</b>. The user \"$wgDBuser\" must be able to access the schema. ".
433 "You can try making them the owner of the database, or try creating the schema with a ".
434 "different user, and then grant access to the \"$wgDBuser\" user.</li>\n";
435 dieout("</ul>");
436 }
437 print "OK</li>\n";
438 }
439 else if ($result != $wgDBuser) {
440 print "<li>Schema \"$wgDBmwschema\" exists but is not owned by \"$wgDBuser\". Not ideal.</li>\n";
441 }
442 else {
443 print "<li>Schema \"$wgDBmwschema\" exists and is owned by \"$wgDBuser\". Excellent.</li>\n";
444 }
445
446 // Always return GMT time to accomodate the existing integer-based timestamp assumption
447 print "<li>Setting the timezone to GMT for user \"$wgDBuser\" ...";
448 $SQL = "ALTER USER $safeuser SET timezone = 'GMT'";
449 $result = pg_query($this->mConn, $SQL);
450 if (!$result) {
451 print "<b>FAILED</b>.</li>\n";
452 dieout("</ul>");
453 }
454 print "OK</li>\n";
455 // Set for the rest of this session
456 $SQL = "SET timezone = 'GMT'";
457 $result = pg_query($this->mConn, $SQL);
458 if (!$result) {
459 print "<li>Failed to set timezone</li>\n";
460 dieout("</ul>");
461 }
462
463 print "<li>Setting the datestyle to ISO, YMD for user \"$wgDBuser\" ...";
464 $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'";
465 $result = pg_query($this->mConn, $SQL);
466 if (!$result) {
467 print "<b>FAILED</b>.</li>\n";
468 dieout("</ul>");
469 }
470 print "OK</li>\n";
471 // Set for the rest of this session
472 $SQL = "SET datestyle = 'ISO, YMD'";
473 $result = pg_query($this->mConn, $SQL);
474 if (!$result) {
475 print "<li>Failed to set datestyle</li>\n";
476 dieout("</ul>");
477 }
478
479 // Fix up the search paths if needed
480 print "<li>Setting the search path for user \"$wgDBuser\" ...";
481 $path = $this->quote_ident($wgDBmwschema);
482 if ($wgDBts2schema !== $wgDBmwschema)
483 $path .= ", ". $this->quote_ident($wgDBts2schema);
484 if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public')
485 $path .= ", public";
486 $SQL = "ALTER USER $safeuser SET search_path = $path";
487 $result = pg_query($this->mConn, $SQL);
488 if (!$result) {
489 print "<b>FAILED</b>.</li>\n";
490 dieout("</ul>");
491 }
492 print "OK</li>\n";
493 // Set for the rest of this session
494 $SQL = "SET search_path = $path";
495 $result = pg_query($this->mConn, $SQL);
496 if (!$result) {
497 print "<li>Failed to set search_path</li>\n";
498 dieout("</ul>");
499 }
500 define( "POSTGRES_SEARCHPATH", $path );
501 }
502 }
503
504
505 function setup_plpgsql() {
506 print "<li>Checking for Pl/Pgsql ...";
507 $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'";
508 $rows = $this->numRows($this->doQuery($SQL));
509 if ($rows < 1) {
510 // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it
511 print "not installed. Attempting to install Pl/Pgsql ...";
512 $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ".
513 "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'";
514 $rows = $this->numRows($this->doQuery($SQL));
515 if ($rows >= 1) {
516 $olde = error_reporting(0);
517 error_reporting($olde - E_WARNING);
518 $result = $this->doQuery("CREATE LANGUAGE plpgsql");
519 error_reporting($olde);
520 if (!$result) {
521 print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>";
522 dieout("</ul>");
523 }
524 }
525 else {
526 print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>";
527 dieout("</ul>");
528 }
529 }
530 print "OK</li>\n";
531 }
532
533
534 /**
535 * Closes a database connection, if it is open
536 * Returns success, true if already closed
537 */
538 function close() {
539 $this->mOpened = false;
540 if ( $this->mConn ) {
541 return pg_close( $this->mConn );
542 } else {
543 return true;
544 }
545 }
546
547 function doQuery( $sql ) {
548 if (function_exists('mb_convert_encoding')) {
549 return $this->mLastResult=pg_query( $this->mConn , mb_convert_encoding($sql,'UTF-8') );
550 }
551 return $this->mLastResult=pg_query( $this->mConn , $sql);
552 }
553
554 function queryIgnore( $sql, $fname = '' ) {
555 return $this->query( $sql, $fname, true );
556 }
557
558 function freeResult( $res ) {
559 if ( $res instanceof ResultWrapper ) {
560 $res = $res->result;
561 }
562 if ( !@pg_free_result( $res ) ) {
563 throw new DBUnexpectedError($this, "Unable to free Postgres result\n" );
564 }
565 }
566
567 function fetchObject( $res ) {
568 if ( $res instanceof ResultWrapper ) {
569 $res = $res->result;
570 }
571 @$row = pg_fetch_object( $res );
572 # FIXME: HACK HACK HACK HACK debug
573
574 # TODO:
575 # hashar : not sure if the following test really trigger if the object
576 # fetching failed.
577 if( pg_last_error($this->mConn) ) {
578 throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
579 }
580 return $row;
581 }
582
583 function fetchRow( $res ) {
584 if ( $res instanceof ResultWrapper ) {
585 $res = $res->result;
586 }
587 @$row = pg_fetch_array( $res );
588 if( pg_last_error($this->mConn) ) {
589 throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
590 }
591 return $row;
592 }
593
594 function numRows( $res ) {
595 if ( $res instanceof ResultWrapper ) {
596 $res = $res->result;
597 }
598 @$n = pg_num_rows( $res );
599 if( pg_last_error($this->mConn) ) {
600 throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
601 }
602 return $n;
603 }
604 function numFields( $res ) {
605 if ( $res instanceof ResultWrapper ) {
606 $res = $res->result;
607 }
608 return pg_num_fields( $res );
609 }
610 function fieldName( $res, $n ) {
611 if ( $res instanceof ResultWrapper ) {
612 $res = $res->result;
613 }
614 return pg_field_name( $res, $n );
615 }
616
617 /**
618 * This must be called after nextSequenceVal
619 */
620 function insertId() {
621 return $this->mInsertId;
622 }
623
624 function dataSeek( $res, $row ) {
625 if ( $res instanceof ResultWrapper ) {
626 $res = $res->result;
627 }
628 return pg_result_seek( $res, $row );
629 }
630
631 function lastError() {
632 if ( $this->mConn ) {
633 return pg_last_error();
634 }
635 else {
636 return "No database connection";
637 }
638 }
639 function lastErrno() {
640 return pg_last_error() ? 1 : 0;
641 }
642
643 function affectedRows() {
644 if( !isset( $this->mLastResult ) or ! $this->mLastResult )
645 return 0;
646
647 return pg_affected_rows( $this->mLastResult );
648 }
649
650 /**
651 * Estimate rows in dataset
652 * Returns estimated count, based on EXPLAIN output
653 * This is not necessarily an accurate estimate, so use sparingly
654 * Returns -1 if count cannot be found
655 * Takes same arguments as Database::select()
656 */
657
658 function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
659 $options['EXPLAIN'] = true;
660 $res = $this->select( $table, $vars, $conds, $fname, $options );
661 $rows = -1;
662 if ( $res ) {
663 $row = $this->fetchRow( $res );
664 $count = array();
665 if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
666 $rows = $count[1];
667 }
668 $this->freeResult($res);
669 }
670 return $rows;
671 }
672
673
674 /**
675 * Returns information about an index
676 * If errors are explicitly ignored, returns NULL on failure
677 */
678 function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
679 $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
680 $res = $this->query( $sql, $fname );
681 if ( !$res ) {
682 return NULL;
683 }
684 while ( $row = $this->fetchObject( $res ) ) {
685 if ( $row->indexname == $index ) {
686 return $row;
687 }
688 }
689 return false;
690 }
691
692 function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
693 $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
694 " AND indexdef LIKE 'CREATE UNIQUE%({$index})'";
695 $res = $this->query( $sql, $fname );
696 if ( !$res )
697 return NULL;
698 while ($row = $this->fetchObject( $res ))
699 return true;
700 return false;
701
702 }
703
704 /**
705 * INSERT wrapper, inserts an array into a table
706 *
707 * $args may be a single associative array, or an array of these with numeric keys,
708 * for multi-row insert (Postgres version 8.2 and above only).
709 *
710 * @param array $table String: Name of the table to insert to.
711 * @param array $args Array: Items to insert into the table.
712 * @param array $fname String: Name of the function, for profiling
713 * @param mixed $options String or Array. Valid options: IGNORE
714 *
715 * @return bool Success of insert operation. IGNORE always returns true.
716 */
717 function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
718 global $wgDBversion;
719
720 if ( !count( $args ) ) {
721 return true;
722 }
723
724 $table = $this->tableName( $table );
725 if (! isset( $wgDBversion ) ) {
726 $this->getServerVersion();
727 $wgDBversion = $this->numeric_version;
728 }
729
730 if ( !is_array( $options ) )
731 $options = array( $options );
732
733 if ( isset( $args[0] ) && is_array( $args[0] ) ) {
734 $multi = true;
735 $keys = array_keys( $args[0] );
736 }
737 else {
738 $multi = false;
739 $keys = array_keys( $args );
740 }
741
742 // If IGNORE is set, we use savepoints to emulate mysql's behavior
743 $ignore = in_array( 'IGNORE', $options ) ? 'mw' : '';
744
745 // If we are not in a transaction, we need to be for savepoint trickery
746 $didbegin = 0;
747 if ( $ignore ) {
748 if (! $this->mTrxLevel) {
749 $this->begin();
750 $didbegin = 1;
751 }
752 $olde = error_reporting( 0 );
753 // For future use, we may want to track the number of actual inserts
754 // Right now, insert (all writes) simply return true/false
755 $numrowsinserted = 0;
756 }
757
758 $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
759
760 if ( $multi ) {
761 if ( $wgDBversion >= 8.2 && !$ignore ) {
762 $first = true;
763 foreach ( $args as $row ) {
764 if ( $first ) {
765 $first = false;
766 } else {
767 $sql .= ',';
768 }
769 $sql .= '(' . $this->makeList( $row ) . ')';
770 }
771 $res = (bool)$this->query( $sql, $fname, $ignore );
772 }
773 else {
774 $res = true;
775 $origsql = $sql;
776 foreach ( $args as $row ) {
777 $tempsql = $origsql;
778 $tempsql .= '(' . $this->makeList( $row ) . ')';
779
780 if ( $ignore ) {
781 pg_query($this->mConn, "SAVEPOINT $ignore");
782 }
783
784 $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
785
786 if ( $ignore ) {
787 $bar = pg_last_error();
788 if ($bar != false) {
789 pg_query( $this->mConn, "ROLLBACK TO $ignore" );
790 }
791 else {
792 pg_query( $this->mConn, "RELEASE $ignore" );
793 $numrowsinserted++;
794 }
795 }
796
797 // If any of them fail, we fail overall for this function call
798 // Note that this will be ignored if IGNORE is set
799 if (! $tempres)
800 $res = false;
801 }
802 }
803 }
804 else {
805 // Not multi, just a lone insert
806 if ( $ignore ) {
807 pg_query($this->mConn, "SAVEPOINT $ignore");
808 }
809
810 $sql .= '(' . $this->makeList( $args ) . ')';
811 $res = (bool)$this->query( $sql, $fname, $ignore );
812
813 if ( $ignore ) {
814 $bar = pg_last_error();
815 if ($bar != false) {
816 pg_query( $this->mConn, "ROLLBACK TO $ignore" );
817 }
818 else {
819 pg_query( $this->mConn, "RELEASE $ignore" );
820 $numrowsinserted++;
821 }
822 }
823 }
824
825 if ( $ignore ) {
826 $olde = error_reporting( $olde );
827 if ($didbegin) {
828 $this->commit();
829 }
830
831 // IGNORE always returns true
832 return true;
833 }
834
835
836 return $res;
837
838 }
839
840 function tableName( $name ) {
841 # Replace reserved words with better ones
842 switch( $name ) {
843 case 'user':
844 return 'mwuser';
845 case 'text':
846 return 'pagecontent';
847 default:
848 return $name;
849 }
850 }
851
852 /**
853 * Return the next in a sequence, save the value for retrieval via insertId()
854 */
855 function nextSequenceValue( $seqName ) {
856 $safeseq = preg_replace( "/'/", "''", $seqName );
857 $res = $this->query( "SELECT nextval('$safeseq')" );
858 $row = $this->fetchRow( $res );
859 $this->mInsertId = $row[0];
860 $this->freeResult( $res );
861 return $this->mInsertId;
862 }
863
864 /**
865 * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
866 */
867 function currentSequenceValue( $seqName ) {
868 $safeseq = preg_replace( "/'/", "''", $seqName );
869 $res = $this->query( "SELECT currval('$safeseq')" );
870 $row = $this->fetchRow( $res );
871 $currval = $row[0];
872 $this->freeResult( $res );
873 return $currval;
874 }
875
876 /**
877 * Postgres does not have a "USE INDEX" clause, so return an empty string
878 */
879 function useIndexClause( $index ) {
880 return '';
881 }
882
883 # REPLACE query wrapper
884 # Postgres simulates this with a DELETE followed by INSERT
885 # $row is the row to insert, an associative array
886 # $uniqueIndexes is an array of indexes. Each element may be either a
887 # field name or an array of field names
888 #
889 # It may be more efficient to leave off unique indexes which are unlikely to collide.
890 # However if you do this, you run the risk of encountering errors which wouldn't have
891 # occurred in MySQL
892 function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
893 $table = $this->tableName( $table );
894
895 if (count($rows)==0) {
896 return;
897 }
898
899 # Single row case
900 if ( !is_array( reset( $rows ) ) ) {
901 $rows = array( $rows );
902 }
903
904 foreach( $rows as $row ) {
905 # Delete rows which collide
906 if ( $uniqueIndexes ) {
907 $sql = "DELETE FROM $table WHERE ";
908 $first = true;
909 foreach ( $uniqueIndexes as $index ) {
910 if ( $first ) {
911 $first = false;
912 $sql .= "(";
913 } else {
914 $sql .= ') OR (';
915 }
916 if ( is_array( $index ) ) {
917 $first2 = true;
918 foreach ( $index as $col ) {
919 if ( $first2 ) {
920 $first2 = false;
921 } else {
922 $sql .= ' AND ';
923 }
924 $sql .= $col.'=' . $this->addQuotes( $row[$col] );
925 }
926 } else {
927 $sql .= $index.'=' . $this->addQuotes( $row[$index] );
928 }
929 }
930 $sql .= ')';
931 $this->query( $sql, $fname );
932 }
933
934 # Now insert the row
935 $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
936 $this->makeList( $row, LIST_COMMA ) . ')';
937 $this->query( $sql, $fname );
938 }
939 }
940
941 # DELETE where the condition is a join
942 function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
943 if ( !$conds ) {
944 throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
945 }
946
947 $delTable = $this->tableName( $delTable );
948 $joinTable = $this->tableName( $joinTable );
949 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
950 if ( $conds != '*' ) {
951 $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
952 }
953 $sql .= ')';
954
955 $this->query( $sql, $fname );
956 }
957
958 # Returns the size of a text field, or -1 for "unlimited"
959 function textFieldSize( $table, $field ) {
960 $table = $this->tableName( $table );
961 $sql = "SELECT t.typname as ftype,a.atttypmod as size
962 FROM pg_class c, pg_attribute a, pg_type t
963 WHERE relname='$table' AND a.attrelid=c.oid AND
964 a.atttypid=t.oid and a.attname='$field'";
965 $res =$this->query($sql);
966 $row=$this->fetchObject($res);
967 if ($row->ftype=="varchar") {
968 $size=$row->size-4;
969 } else {
970 $size=$row->size;
971 }
972 $this->freeResult( $res );
973 return $size;
974 }
975
976 function lowPriorityOption() {
977 return '';
978 }
979
980 function limitResult($sql, $limit, $offset=false) {
981 return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":"");
982 }
983
984 /**
985 * Returns an SQL expression for a simple conditional.
986 * Uses CASE on Postgres
987 *
988 * @param string $cond SQL expression which will result in a boolean value
989 * @param string $trueVal SQL expression to return if true
990 * @param string $falseVal SQL expression to return if false
991 * @return string SQL fragment
992 */
993 function conditional( $cond, $trueVal, $falseVal ) {
994 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
995 }
996
997 function wasDeadlock() {
998 return $this->lastErrno() == '40P01';
999 }
1000
1001 function timestamp( $ts=0 ) {
1002 return wfTimestamp(TS_POSTGRES,$ts);
1003 }
1004
1005 /**
1006 * Return aggregated value function call
1007 */
1008 function aggregateValue ($valuedata,$valuename='value') {
1009 return $valuedata;
1010 }
1011
1012
1013 function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1014 // Ignore errors during error handling to avoid infinite recursion
1015 $ignore = $this->ignoreErrors( true );
1016 $this->mErrorCount++;
1017
1018 if ($ignore || $tempIgnore) {
1019 wfDebug("SQL ERROR (ignored): $error\n");
1020 $this->ignoreErrors( $ignore );
1021 }
1022 else {
1023 $message = "A database error has occurred\n" .
1024 "Query: $sql\n" .
1025 "Function: $fname\n" .
1026 "Error: $errno $error\n";
1027 throw new DBUnexpectedError($this, $message);
1028 }
1029 }
1030
1031 /**
1032 * @return string wikitext of a link to the server software's web site
1033 */
1034 function getSoftwareLink() {
1035 return "[http://www.postgresql.org/ PostgreSQL]";
1036 }
1037
1038 /**
1039 * @return string Version information from the database
1040 */
1041 function getServerVersion() {
1042 $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0);
1043 $thisver = array();
1044 if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) {
1045 die("Could not determine the numeric version from $version!");
1046 }
1047 $this->numeric_version = $thisver[1];
1048 return $version;
1049 }
1050
1051
1052 /**
1053 * Query whether a given relation exists (in the given schema, or the
1054 * default mw one if not given)
1055 */
1056 function relationExists( $table, $types, $schema = false ) {
1057 global $wgDBmwschema;
1058 if (!is_array($types))
1059 $types = array($types);
1060 if (! $schema )
1061 $schema = $wgDBmwschema;
1062 $etable = $this->addQuotes($table);
1063 $eschema = $this->addQuotes($schema);
1064 $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
1065 . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
1066 . "AND c.relkind IN ('" . implode("','", $types) . "')";
1067 $res = $this->query( $SQL );
1068 $count = $res ? $res->numRows() : 0;
1069 if ($res)
1070 $this->freeResult( $res );
1071 return $count ? true : false;
1072 }
1073
1074 /*
1075 * For backward compatibility, this function checks both tables and
1076 * views.
1077 */
1078 function tableExists ($table, $schema = false) {
1079 return $this->relationExists($table, array('r', 'v'), $schema);
1080 }
1081
1082 function sequenceExists ($sequence, $schema = false) {
1083 return $this->relationExists($sequence, 'S', $schema);
1084 }
1085
1086 function triggerExists($table, $trigger) {
1087 global $wgDBmwschema;
1088
1089 $q = <<<END
1090 SELECT 1 FROM pg_class, pg_namespace, pg_trigger
1091 WHERE relnamespace=pg_namespace.oid AND relkind='r'
1092 AND tgrelid=pg_class.oid
1093 AND nspname=%s AND relname=%s AND tgname=%s
1094 END;
1095 $res = $this->query(sprintf($q,
1096 $this->addQuotes($wgDBmwschema),
1097 $this->addQuotes($table),
1098 $this->addQuotes($trigger)));
1099 if (!$res)
1100 return NULL;
1101 $rows = $res->numRows();
1102 $this->freeResult($res);
1103 return $rows;
1104 }
1105
1106 function ruleExists($table, $rule) {
1107 global $wgDBmwschema;
1108 $exists = $this->selectField("pg_rules", "rulename",
1109 array( "rulename" => $rule,
1110 "tablename" => $table,
1111 "schemaname" => $wgDBmwschema));
1112 return $exists === $rule;
1113 }
1114
1115 function constraintExists($table, $constraint) {
1116 global $wgDBmwschema;
1117 $SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ".
1118 "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
1119 $this->addQuotes($wgDBmwschema),
1120 $this->addQuotes($table),
1121 $this->addQuotes($constraint));
1122 $res = $this->query($SQL);
1123 if (!$res)
1124 return NULL;
1125 $rows = $res->numRows();
1126 $this->freeResult($res);
1127 return $rows;
1128 }
1129
1130 /**
1131 * Query whether a given schema exists. Returns the name of the owner
1132 */
1133 function schemaExists( $schema ) {
1134 $eschema = preg_replace("/'/", "''", $schema);
1135 $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r "
1136 ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'";
1137 $res = $this->query( $SQL );
1138 if ( $res && $res->numRows() ) {
1139 $row = $res->fetchObject();
1140 $owner = $row->rolname;
1141 } else {
1142 $owner = false;
1143 }
1144 if ($res)
1145 $this->freeResult($res);
1146 return $owner;
1147 }
1148
1149 /**
1150 * Query whether a given column exists in the mediawiki schema
1151 */
1152 function fieldExists( $table, $field, $fname = 'DatabasePostgres::fieldExists' ) {
1153 global $wgDBmwschema;
1154 $etable = preg_replace("/'/", "''", $table);
1155 $eschema = preg_replace("/'/", "''", $wgDBmwschema);
1156 $ecol = preg_replace("/'/", "''", $field);
1157 $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a "
1158 . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' "
1159 . "AND a.attrelid = c.oid AND a.attname = '$ecol'";
1160 $res = $this->query( $SQL, $fname );
1161 $count = $res ? $res->numRows() : 0;
1162 if ($res)
1163 $this->freeResult( $res );
1164 return $count;
1165 }
1166
1167 function fieldInfo( $table, $field ) {
1168 return PostgresField::fromText($this, $table, $field);
1169 }
1170
1171 function begin( $fname = 'DatabasePostgres::begin' ) {
1172 $this->query( 'BEGIN', $fname );
1173 $this->mTrxLevel = 1;
1174 }
1175 function immediateCommit( $fname = 'DatabasePostgres::immediateCommit' ) {
1176 return true;
1177 }
1178 function commit( $fname = 'DatabasePostgres::commit' ) {
1179 $this->query( 'COMMIT', $fname );
1180 $this->mTrxLevel = 0;
1181 }
1182
1183 /* Not even sure why this is used in the main codebase... */
1184 function limitResultForUpdate($sql, $num) {
1185 return $sql;
1186 }
1187
1188 function setup_database() {
1189 global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
1190
1191 // Make sure that we can write to the correct schema
1192 // If not, Postgres will happily and silently go to the next search_path item
1193 $ctest = "mediawiki_test_table";
1194 $safeschema = $this->quote_ident($wgDBmwschema);
1195 if ($this->tableExists($ctest, $wgDBmwschema)) {
1196 $this->doQuery("DROP TABLE $safeschema.$ctest");
1197 }
1198 $SQL = "CREATE TABLE $safeschema.$ctest(a int)";
1199 $olde = error_reporting( 0 );
1200 $res = $this->doQuery($SQL);
1201 error_reporting( $olde );
1202 if (!$res) {
1203 print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" can write to the schema \"$wgDBmwschema\"</li>\n";
1204 dieout("</ul>");
1205 }
1206 $this->doQuery("DROP TABLE $safeschema.$ctest");
1207
1208 $res = dbsource( "../maintenance/postgres/tables.sql", $this);
1209
1210 ## Update version information
1211 $mwv = $this->addQuotes($wgVersion);
1212 $pgv = $this->addQuotes($this->getServerVersion());
1213 $pgu = $this->addQuotes($this->mUser);
1214 $mws = $this->addQuotes($wgDBmwschema);
1215 $tss = $this->addQuotes($wgDBts2schema);
1216 $pgp = $this->addQuotes($wgDBport);
1217 $dbn = $this->addQuotes($this->mDBname);
1218 $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
1219
1220 $SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ".
1221 "mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ".
1222 "ctype = '$ctype' ".
1223 "WHERE type = 'Creation'";
1224 $this->query($SQL);
1225
1226 ## Avoid the non-standard "REPLACE INTO" syntax
1227 $f = fopen( "../maintenance/interwiki.sql", 'r' );
1228 if ($f == false ) {
1229 dieout( "<li>Could not find the interwiki.sql file");
1230 }
1231 ## We simply assume it is already empty as we have just created it
1232 $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
1233 while ( ! feof( $f ) ) {
1234 $line = fgets($f,1024);
1235 $matches = array();
1236 if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) {
1237 continue;
1238 }
1239 $this->query("$SQL $matches[1],$matches[2])");
1240 }
1241 print " (table interwiki successfully populated)...\n";
1242
1243 $this->doQuery("COMMIT");
1244 }
1245
1246 function encodeBlob( $b ) {
1247 return new Blob ( pg_escape_bytea( $b ) ) ;
1248 }
1249
1250 function decodeBlob( $b ) {
1251 if ($b instanceof Blob) {
1252 $b = $b->fetch();
1253 }
1254 return pg_unescape_bytea( $b );
1255 }
1256
1257 function strencode( $s ) { ## Should not be called by us
1258 return pg_escape_string( $s );
1259 }
1260
1261 function addQuotes( $s ) {
1262 if ( is_null( $s ) ) {
1263 return 'NULL';
1264 } else if ($s instanceof Blob) {
1265 return "'".$s->fetch($s)."'";
1266 }
1267 return "'" . pg_escape_string($s) . "'";
1268 }
1269
1270 function quote_ident( $s ) {
1271 return '"' . preg_replace( '/"/', '""', $s) . '"';
1272 }
1273
1274 /* For now, does nothing */
1275 function selectDB( $db ) {
1276 return true;
1277 }
1278
1279 /**
1280 * Postgres specific version of replaceVars.
1281 * Calls the parent version in Database.php
1282 *
1283 * @private
1284 *
1285 * @param string $com SQL string, read from a stream (usually tables.sql)
1286 *
1287 * @return string SQL string
1288 */
1289 protected function replaceVars( $ins ) {
1290
1291 $ins = parent::replaceVars( $ins );
1292
1293 if ($this->numeric_version >= 8.3) {
1294 // Thanks for not providing backwards-compatibility, 8.3
1295 $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
1296 }
1297
1298 if ($this->numeric_version <= 8.1) { // Our minimum version
1299 $ins = str_replace( 'USING gin', 'USING gist', $ins );
1300 }
1301
1302 return $ins;
1303 }
1304
1305 /**
1306 * Various select options
1307 *
1308 * @private
1309 *
1310 * @param array $options an associative array of options to be turned into
1311 * an SQL query, valid keys are listed in the function.
1312 * @return array
1313 */
1314 function makeSelectOptions( $options ) {
1315 $preLimitTail = $postLimitTail = '';
1316 $startOpts = $useIndex = '';
1317
1318 $noKeyOptions = array();
1319 foreach ( $options as $key => $option ) {
1320 if ( is_numeric( $key ) ) {
1321 $noKeyOptions[$option] = true;
1322 }
1323 }
1324
1325 if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY'];
1326 if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
1327 if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY'];
1328
1329 //if (isset($options['LIMIT'])) {
1330 // $tailOpts .= $this->limitResult('', $options['LIMIT'],
1331 // isset($options['OFFSET']) ? $options['OFFSET']
1332 // : false);
1333 //}
1334
1335 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
1336 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
1337 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
1338
1339 return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
1340 }
1341
1342 public function setTimeout( $timeout ) {
1343 // @todo fixme no-op
1344 }
1345
1346 function ping() {
1347 wfDebug( "Function ping() not written for DatabasePostgres.php yet");
1348 return true;
1349 }
1350
1351 /**
1352 * How lagged is this slave?
1353 *
1354 */
1355 public function getLag() {
1356 # Not implemented for PostgreSQL
1357 return false;
1358 }
1359
1360 function setFakeSlaveLag( $lag ) {}
1361 function setFakeMaster( $enabled = true ) {}
1362
1363 function getDBname() {
1364 return $this->mDBname;
1365 }
1366
1367 function getServer() {
1368 return $this->mServer;
1369 }
1370
1371 function buildConcat( $stringList ) {
1372 return implode( ' || ', $stringList );
1373 }
1374
1375 /* These are not used yet, but we know we don't want the default version */
1376
1377 public function lock( $lockName, $method ) {
1378 return true;
1379 }
1380 public function unlock( $lockName, $method ) {
1381 return true;
1382 }
1383
1384 } // end DatabasePostgres class