DatabasePostgres.php 42 KB

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