summaryrefslogtreecommitdiff
path: root/includes/db
diff options
context:
space:
mode:
Diffstat (limited to 'includes/db')
-rw-r--r--includes/db/Database.php248
-rw-r--r--includes/db/DatabaseIbm_db2.php1796
-rw-r--r--includes/db/DatabasePostgres.php65
-rw-r--r--includes/db/DatabaseSqlite.php238
-rw-r--r--includes/db/LBFactory.php26
-rw-r--r--includes/db/LoadBalancer.php2
6 files changed, 2200 insertions, 175 deletions
diff --git a/includes/db/Database.php b/includes/db/Database.php
index 84b88643..52a59c11 100644
--- a/includes/db/Database.php
+++ b/includes/db/Database.php
@@ -26,6 +26,7 @@ class Database {
#------------------------------------------------------------------------------
protected $mLastQuery = '';
+ protected $mDoneWrites = false;
protected $mPHPError = false;
protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
@@ -210,7 +211,14 @@ class Database {
* @return String
*/
function lastQuery() { return $this->mLastQuery; }
-
+
+
+ /**
+ * Returns true if the connection may have been used for write queries.
+ * Should return true if unsure.
+ */
+ function doneWrites() { return $this->mDoneWrites; }
+
/**
* Is a connection to the database open?
* @return Boolean
@@ -493,6 +501,14 @@ class Database {
}
/**
+ * Determine whether a query writes to the DB.
+ * Should return true if unsure.
+ */
+ function isWriteQuery( $sql ) {
+ return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW)\b/i', $sql );
+ }
+
+ /**
* Usually aborts on failure. If errors are explicitly ignored, returns success.
*
* @param $sql String: SQL query
@@ -527,6 +543,11 @@ class Database {
}
$this->mLastQuery = $sql;
+ if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) {
+ // Set a flag indicating that writes have been done
+ wfDebug( __METHOD__.": Writes done: $sql\n" );
+ $this->mDoneWrites = true;
+ }
# Add a comment for easy SHOW PROCESSLIST interpretation
#if ( $fname ) {
@@ -566,11 +587,15 @@ class Database {
}
}
+ if ( istainted( $sql ) & TC_MYSQL ) {
+ throw new MWException( 'Tainted query found' );
+ }
+
# Do the query and handle errors
$ret = $this->doQuery( $commentedSql );
# Try reconnecting if the connection was lost
- if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
+ if ( false === $ret && $this->wasErrorReissuable() ) {
# Transaction is gone, like it or not
$this->mTrxLevel = 0;
wfDebug( "Connection lost, reconnecting...\n" );
@@ -1191,6 +1216,7 @@ class Database {
# SHOW INDEX should work for 3.x and up:
# http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
$table = $this->tableName( $table );
+ $index = $this->indexName( $index );
$sql = 'SHOW INDEX FROM '.$table;
$res = $this->query( $sql, $fname );
if ( !$res ) {
@@ -1396,7 +1422,7 @@ class Database {
} else {
$list .= $field." IN (".$this->makeList($value).") ";
}
- } elseif( is_null($value) ) {
+ } elseif( $value === null ) {
if ( $mode == LIST_AND || $mode == LIST_OR ) {
$list .= "$field IS ";
} elseif ( $mode == LIST_SET ) {
@@ -1574,6 +1600,23 @@ class Database {
}
/**
+ * Get the name of an index in a given table
+ */
+ function indexName( $index ) {
+ // Backwards-compatibility hack
+ $renamed = array(
+ 'ar_usertext_timestamp' => 'usertext_timestamp',
+ 'un_user_id' => 'user_id',
+ 'un_user_ip' => 'user_ip',
+ );
+ if( isset( $renamed[$index] ) ) {
+ return $renamed[$index];
+ } else {
+ return $index;
+ }
+ }
+
+ /**
* Wrapper for addslashes()
* @param $s String: to be slashed.
* @return String: slashed string.
@@ -1587,7 +1630,7 @@ class Database {
* Otherwise returns as-is
*/
function addQuotes( $s ) {
- if ( is_null( $s ) ) {
+ if ( $s === null ) {
return 'NULL';
} else {
# This will also quote numeric values. This should be harmless,
@@ -1602,6 +1645,7 @@ class Database {
* Escape string for safe LIKE usage
*/
function escapeLike( $s ) {
+ $s=str_replace('\\','\\\\',$s);
$s=$this->strencode( $s );
$s=str_replace(array('%','_'),array('\%','\_'),$s);
return $s;
@@ -1621,7 +1665,7 @@ class Database {
* PostgreSQL doesn't have them and returns ""
*/
function useIndexClause( $index ) {
- return "FORCE INDEX ($index)";
+ return "FORCE INDEX (" . $this->indexName( $index ) . ")";
}
/**
@@ -1817,6 +1861,14 @@ class Database {
}
/**
+ * Determines if the last query error was something that should be dealt
+ * with by pinging the connection and reissuing the query
+ */
+ function wasErrorReissuable() {
+ return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
+ }
+
+ /**
* Perform a deadlock-prone transaction.
*
* This function invokes a callback function to perform a set of write
@@ -2250,8 +2302,12 @@ class Database {
}
// Table prefixes
- $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-zA-Z_0-9]*)/',
- array( &$this, 'tableNameCallback' ), $ins );
+ $ins = preg_replace_callback( '!/\*(?:\$wgDBprefix|_)\*/([a-zA-Z_0-9]*)!',
+ array( $this, 'tableNameCallback' ), $ins );
+
+ // Index names
+ $ins = preg_replace_callback( '!/\*i\*/([a-zA-Z_0-9]*)!',
+ array( $this, 'indexNameCallback' ), $ins );
return $ins;
}
@@ -2263,6 +2319,13 @@ class Database {
return $this->tableName( $matches[1] );
}
+ /**
+ * Index name callback
+ */
+ protected function indexNameCallback( $matches ) {
+ return $this->indexName( $matches[1] );
+ }
+
/*
* Build a concatenation list to feed into a SQL query
*/
@@ -2480,44 +2543,27 @@ class DBConnectionError extends DBError {
}
function getPageTitle() {
- global $wgSitename;
- return "$wgSitename has a problem";
+ global $wgSitename, $wgLang;
+ $header = "$wgSitename has a problem";
+ if ( $wgLang instanceof Language ) {
+ $header = htmlspecialchars( $wgLang->getMessage( 'dberr-header' ) );
+ }
+
+ return $header;
}
function getHTML() {
- global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
- global $wgSitename, $wgServer, $wgMessageCache;
-
- # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
- # Hard coding strings instead.
+ global $wgLang, $wgMessageCache, $wgUseFileCache;
- $noconnect = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
- $mainpage = 'Main Page';
- $searchdisabled = <<<EOT
-<p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
-<span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
-EOT;
+ $sorry = 'Sorry! This site is experiencing technical difficulties.';
+ $again = 'Try waiting a few minutes and reloading.';
+ $info = '(Can\'t contact the database server: $1)';
- $googlesearch = "
-<!-- SiteSearch Google -->
-<FORM method=GET action=\"http://www.google.com/search\">
-<TABLE bgcolor=\"#FFFFFF\"><tr><td>
-<A HREF=\"http://www.google.com/\">
-<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
-border=\"0\" ALT=\"Google\"></A>
-</td>
-<td>
-<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
-<INPUT type=submit name=btnG VALUE=\"Google Search\">
-<font size=-1>
-<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
-<input type='hidden' name='ie' value='$2'>
-<input type='hidden' name='oe' value='$2'>
-</font>
-</td></tr></TABLE>
-</FORM>
-<!-- SiteSearch Google -->";
- $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
+ if ( $wgLang instanceof Language ) {
+ $sorry = htmlspecialchars( $wgLang->getMessage( 'dberr-problems' ) );
+ $again = htmlspecialchars( $wgLang->getMessage( 'dberr-again' ) );
+ $info = htmlspecialchars( $wgLang->getMessage( 'dberr-info' ) );
+ }
# No database access
if ( is_object( $wgMessageCache ) ) {
@@ -2528,6 +2574,7 @@ border=\"0\" ALT=\"Google\"></A>
$this->error = $this->db->getProperty('mServer');
}
+ $noconnect = "<p><strong>$sorry</strong><br />$again</p><p><small>$info</small></p>";
$text = str_replace( '$1', $this->error, $noconnect );
/*
@@ -2537,38 +2584,95 @@ border=\"0\" ALT=\"Google\"></A>
"</p>\n";
}*/
- if($wgUseFileCache) {
- if($wgTitle) {
- $t =& $wgTitle;
- } else {
- if($title) {
- $t = Title::newFromURL( $title );
- } elseif (@/**/$_REQUEST['search']) {
- $search = $_REQUEST['search'];
- return $searchdisabled .
- str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
- $wgInputEncoding ), $googlesearch );
- } else {
- $t = Title::newFromText( $mainpage );
+ $extra = $this->searchForm();
+
+ if( $wgUseFileCache ) {
+ $cache = $this->fileCachedPage();
+ # Cached version on file system?
+ if( $cache !== null ) {
+ # Hack: extend the body for error messages
+ $cache = str_replace( array('</html>','</body>'), '', $cache );
+ # Add cache notice...
+ $cachederror = "This is a cached copy of the requested page, and may not be up to date. ";
+ # Localize it if possible...
+ if( $wgLang instanceof Language ) {
+ $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) );
}
+ $warning = "<div style='color:red;font-size:150%;font-weight:bold;'>$cachederror</div>";
+ # Output cached page with notices on bottom and re-close body
+ return "{$cache}{$warning}<hr />$text<hr />$extra</body></html>";
}
+ }
+ # Headers needed here - output is just the error message
+ return $this->htmlHeader()."$text<hr />$extra".$this->htmlFooter();
+ }
- $cache = new HTMLFileCache( $t );
- if( $cache->isFileCached() ) {
- // @todo, FIXME: $msg is not defined on the next line.
- $msg = '<p style="color: red"><b>'.$text."<br />\n" .
- $cachederror . "</b></p>\n";
-
- $tag = '<div id="article">';
- $text = str_replace(
- $tag,
- $tag . $text,
- $cache->fetchPageText() );
- }
+ function searchForm() {
+ global $wgSitename, $wgServer, $wgLang, $wgInputEncoding;
+ $usegoogle = "You can try searching via Google in the meantime.";
+ $outofdate = "Note that their indexes of our content may be out of date.";
+ $googlesearch = "Search";
+
+ if ( $wgLang instanceof Language ) {
+ $usegoogle = htmlspecialchars( $wgLang->getMessage( 'dberr-usegoogle' ) );
+ $outofdate = htmlspecialchars( $wgLang->getMessage( 'dberr-outofdate' ) );
+ $googlesearch = htmlspecialchars( $wgLang->getMessage( 'searchbutton' ) );
+ }
+
+ $search = htmlspecialchars(@$_REQUEST['search']);
+
+ $trygoogle = <<<EOT
+<div style="margin: 1.5em">$usegoogle<br />
+<small>$outofdate</small></div>
+<!-- SiteSearch Google -->
+<form method="get" action="http://www.google.com/search" id="googlesearch">
+ <input type="hidden" name="domains" value="$wgServer" />
+ <input type="hidden" name="num" value="50" />
+ <input type="hidden" name="ie" value="$wgInputEncoding" />
+ <input type="hidden" name="oe" value="$wgInputEncoding" />
+
+ <img src="http://www.google.com/logos/Logo_40wht.gif" alt="" style="float:left; margin-left: 1.5em; margin-right: 1.5em;" />
+
+ <input type="text" name="q" size="31" maxlength="255" value="$search" />
+ <input type="submit" name="btnG" value="$googlesearch" />
+ <div>
+ <input type="radio" name="sitesearch" id="gwiki" value="$wgServer" checked="checked" /><label for="gwiki">$wgSitename</label>
+ <input type="radio" name="sitesearch" id="gWWW" value="" /><label for="gWWW">WWW</label>
+ </div>
+</form>
+<!-- SiteSearch Google -->
+EOT;
+ return $trygoogle;
+ }
+
+ function fileCachedPage() {
+ global $wgTitle, $title, $wgLang, $wgOut;
+ if( $wgOut->isDisabled() ) return; // Done already?
+ $mainpage = 'Main Page';
+ if ( $wgLang instanceof Language ) {
+ $mainpage = htmlspecialchars( $wgLang->getMessage( 'mainpage' ) );
}
- return $text;
+ if($wgTitle) {
+ $t =& $wgTitle;
+ } elseif($title) {
+ $t = Title::newFromURL( $title );
+ } else {
+ $t = Title::newFromText( $mainpage );
+ }
+
+ $cache = new HTMLFileCache( $t );
+ if( $cache->isFileCached() ) {
+ return $cache->fetchPageText();
+ } else {
+ return '';
+ }
}
+
+ function htmlBodyOnly() {
+ return true;
+ }
+
}
/**
@@ -2656,7 +2760,7 @@ class ResultWrapper implements Iterator {
* Get the number of rows in a result object
*/
function numRows() {
- return $this->db->numRows( $this->result );
+ return $this->db->numRows( $this );
}
/**
@@ -2669,7 +2773,7 @@ class ResultWrapper implements Iterator {
* @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchObject() {
- return $this->db->fetchObject( $this->result );
+ return $this->db->fetchObject( $this );
}
/**
@@ -2681,14 +2785,14 @@ class ResultWrapper implements Iterator {
* @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchRow() {
- return $this->db->fetchRow( $this->result );
+ return $this->db->fetchRow( $this );
}
/**
* Free a result object
*/
function free() {
- $this->db->freeResult( $this->result );
+ $this->db->freeResult( $this );
unset( $this->result );
unset( $this->db );
}
@@ -2698,7 +2802,7 @@ class ResultWrapper implements Iterator {
* See mysql_data_seek()
*/
function seek( $row ) {
- $this->db->dataSeek( $this->result, $row );
+ $this->db->dataSeek( $this, $row );
}
/*********************
@@ -2709,7 +2813,7 @@ class ResultWrapper implements Iterator {
function rewind() {
if ($this->numRows()) {
- $this->db->dataSeek($this->result, 0);
+ $this->db->dataSeek($this, 0);
}
$this->pos = 0;
$this->currentRow = null;
diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php
new file mode 100644
index 00000000..fcd0bc2d
--- /dev/null
+++ b/includes/db/DatabaseIbm_db2.php
@@ -0,0 +1,1796 @@
+<?php
+/**
+ * This script is the IBM DB2 database abstraction layer
+ *
+ * See maintenance/ibm_db2/README for development notes and other specific information
+ * @ingroup Database
+ * @file
+ * @author leo.petr+mediawiki@gmail.com
+ */
+
+/**
+ * Utility class for generating blank objects
+ * Intended as an equivalent to {} in Javascript
+ * @ingroup Database
+ */
+class BlankObject {
+}
+
+/**
+ * This represents a column in a DB2 database
+ * @ingroup Database
+ */
+class IBM_DB2Field {
+ private $name, $tablename, $type, $nullable, $max_length;
+
+ /**
+ * Builder method for the class
+ * @param Object $db Database interface
+ * @param string $table table name
+ * @param string $field column name
+ * @return IBM_DB2Field
+ */
+ static function fromText($db, $table, $field) {
+ global $wgDBmwschema;
+
+ $q = <<<END
+SELECT
+lcase(coltype) AS typname,
+nulls AS attnotnull, length AS attlen
+FROM sysibm.syscolumns
+WHERE tbcreator=%s AND tbname=%s AND name=%s;
+END;
+ $res = $db->query(sprintf($q,
+ $db->addQuotes($wgDBmwschema),
+ $db->addQuotes($table),
+ $db->addQuotes($field)));
+ $row = $db->fetchObject($res);
+ if (!$row)
+ return null;
+ $n = new IBM_DB2Field;
+ $n->type = $row->typname;
+ $n->nullable = ($row->attnotnull == 'N');
+ $n->name = $field;
+ $n->tablename = $table;
+ $n->max_length = $row->attlen;
+ return $n;
+ }
+ /**
+ * Get column name
+ * @return string column name
+ */
+ function name() { return $this->name; }
+ /**
+ * Get table name
+ * @return string table name
+ */
+ function tableName() { return $this->tablename; }
+ /**
+ * Get column type
+ * @return string column type
+ */
+ function type() { return $this->type; }
+ /**
+ * Can column be null?
+ * @return bool true or false
+ */
+ function nullable() { return $this->nullable; }
+ /**
+ * How much can you fit in the column per row?
+ * @return int length
+ */
+ function maxLength() { return $this->max_length; }
+}
+
+/**
+ * Wrapper around binary large objects
+ * @ingroup Database
+ */
+class IBM_DB2Blob {
+ private $mData;
+
+ function __construct($data) {
+ $this->mData = $data;
+ }
+
+ function getData() {
+ return $this->mData;
+ }
+}
+
+/**
+ * Primary database interface
+ * @ingroup Database
+ */
+class DatabaseIbm_db2 extends Database {
+ /*
+ * Inherited members
+ protected $mLastQuery = '';
+ protected $mPHPError = false;
+
+ protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
+ protected $mOut, $mOpened = false;
+
+ protected $mFailFunction;
+ protected $mTablePrefix;
+ protected $mFlags;
+ protected $mTrxLevel = 0;
+ protected $mErrorCount = 0;
+ protected $mLBInfo = array();
+ protected $mFakeSlaveLag = null, $mFakeMaster = false;
+ *
+ */
+
+ /// Server port for uncataloged connections
+ protected $mPort = NULL;
+ /// Whether connection is cataloged
+ protected $mCataloged = NULL;
+ /// Schema for tables, stored procedures, triggers
+ protected $mSchema = NULL;
+ /// Whether the schema has been applied in this session
+ protected $mSchemaSet = false;
+ /// Result of last query
+ protected $mLastResult = NULL;
+ /// Number of rows affected by last INSERT/UPDATE/DELETE
+ protected $mAffectedRows = NULL;
+ /// Number of rows returned by last SELECT
+ protected $mNumRows = NULL;
+
+
+ const CATALOGED = "cataloged";
+ const UNCATALOGED = "uncataloged";
+ const USE_GLOBAL = "get from global";
+
+ /// Last sequence value used for a primary key
+ protected $mInsertId = NULL;
+
+ /*
+ * These can be safely inherited
+ *
+ * Getter/Setter: (18)
+ * failFunction
+ * setOutputPage
+ * bufferResults
+ * ignoreErrors
+ * trxLevel
+ * errorCount
+ * getLBInfo
+ * setLBInfo
+ * lastQuery
+ * isOpen
+ * setFlag
+ * clearFlag
+ * getFlag
+ * getProperty
+ * getDBname
+ * getServer
+ * tableNameCallback
+ * tablePrefix
+ *
+ * Administrative: (8)
+ * debug
+ * installErrorHandler
+ * restoreErrorHandler
+ * connectionErrorHandler
+ * reportConnectionError
+ * sourceFile
+ * sourceStream
+ * replaceVars
+ *
+ * Database: (5)
+ * query
+ * set
+ * selectField
+ * generalizeSQL
+ * update
+ * strreplace
+ * deadlockLoop
+ *
+ * Prepared Statement: 6
+ * prepare
+ * freePrepared
+ * execute
+ * safeQuery
+ * fillPrepared
+ * fillPreparedArg
+ *
+ * Slave/Master: (4)
+ * masterPosWait
+ * getSlavePos
+ * getMasterPos
+ * getLag
+ *
+ * Generation: (9)
+ * tableNames
+ * tableNamesN
+ * tableNamesWithUseIndexOrJOIN
+ * escapeLike
+ * delete
+ * insertSelect
+ * timestampOrNull
+ * resultObject
+ * aggregateValue
+ * selectSQLText
+ * selectRow
+ * makeUpdateOptions
+ *
+ * Reflection: (1)
+ * indexExists
+ */
+
+ /*
+ * These need to be implemented TODO
+ *
+ * Administrative: 7 / 7
+ * constructor [Done]
+ * open [Done]
+ * openCataloged [Done]
+ * close [Done]
+ * newFromParams [Done]
+ * openUncataloged [Done]
+ * setup_database [Done]
+ *
+ * Getter/Setter: 13 / 13
+ * cascadingDeletes [Done]
+ * cleanupTriggers [Done]
+ * strictIPs [Done]
+ * realTimestamps [Done]
+ * impliciGroupby [Done]
+ * implicitOrderby [Done]
+ * searchableIPs [Done]
+ * functionalIndexes [Done]
+ * getWikiID [Done]
+ * isOpen [Done]
+ * getServerVersion [Done]
+ * getSoftwareLink [Done]
+ * getSearchEngine [Done]
+ *
+ * Database driver wrapper: 23 / 23
+ * lastError [Done]
+ * lastErrno [Done]
+ * doQuery [Done]
+ * tableExists [Done]
+ * fetchObject [Done]
+ * fetchRow [Done]
+ * freeResult [Done]
+ * numRows [Done]
+ * numFields [Done]
+ * fieldName [Done]
+ * insertId [Done]
+ * dataSeek [Done]
+ * affectedRows [Done]
+ * selectDB [Done]
+ * strencode [Done]
+ * conditional [Done]
+ * wasDeadlock [Done]
+ * ping [Done]
+ * getStatus [Done]
+ * setTimeout [Done]
+ * lock [Done]
+ * unlock [Done]
+ * insert [Done]
+ * select [Done]
+ *
+ * Slave/master: 2 / 2
+ * setFakeSlaveLag [Done]
+ * setFakeMaster [Done]
+ *
+ * Reflection: 6 / 6
+ * fieldExists [Done]
+ * indexInfo [Done]
+ * fieldInfo [Done]
+ * fieldType [Done]
+ * indexUnique [Done]
+ * textFieldSize [Done]
+ *
+ * Generation: 16 / 16
+ * tableName [Done]
+ * addQuotes [Done]
+ * makeList [Done]
+ * makeSelectOptions [Done]
+ * estimateRowCount [Done]
+ * nextSequenceValue [Done]
+ * useIndexClause [Done]
+ * replace [Done]
+ * deleteJoin [Done]
+ * lowPriorityOption [Done]
+ * limitResult [Done]
+ * limitResultForUpdate [Done]
+ * timestamp [Done]
+ * encodeBlob [Done]
+ * decodeBlob [Done]
+ * buildConcat [Done]
+ */
+
+ ######################################
+ # Getters and Setters
+ ######################################
+
+ /**
+ * Returns true if this database supports (and uses) cascading deletes
+ */
+ function cascadingDeletes() {
+ return true;
+ }
+
+ /**
+ * Returns true if this database supports (and uses) triggers (e.g. on the page table)
+ */
+ function cleanupTriggers() {
+ return true;
+ }
+
+ /**
+ * Returns true if this database is strict about what can be put into an IP field.
+ * Specifically, it uses a NULL value instead of an empty string.
+ */
+ function strictIPs() {
+ return true;
+ }
+
+ /**
+ * Returns true if this database uses timestamps rather than integers
+ */
+ function realTimestamps() {
+ return true;
+ }
+
+ /**
+ * Returns true if this database does an implicit sort when doing GROUP BY
+ */
+ function implicitGroupby() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database does an implicit order by when the column has an index
+ * For example: SELECT page_title FROM page LIMIT 1
+ */
+ function implicitOrderby() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database can do a native search on IP columns
+ * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
+ */
+ function searchableIPs() {
+ return true;
+ }
+
+ /**
+ * Returns true if this database can use functional indexes
+ */
+ function functionalIndexes() {
+ return true;
+ }
+
+ /**
+ * Returns a unique string representing the wiki on the server
+ */
+ function getWikiID() {
+ if( $this->mSchema ) {
+ return "{$this->mDBname}-{$this->mSchema}";
+ } else {
+ return $this->mDBname;
+ }
+ }
+
+
+ ######################################
+ # Setup
+ ######################################
+
+
+ /**
+ *
+ * @param string $server hostname of database server
+ * @param string $user username
+ * @param string $password
+ * @param string $dbName database name on the server
+ * @param function $failFunction (optional)
+ * @param integer $flags database behaviour flags (optional, unused)
+ */
+ public function DatabaseIbm_db2($server = false, $user = false, $password = false,
+ $dbName = false, $failFunction = false, $flags = 0,
+ $schema = self::USE_GLOBAL )
+ {
+
+ global $wgOut, $wgDBmwschema;
+ # Can't get a reference if it hasn't been set yet
+ if ( !isset( $wgOut ) ) {
+ $wgOut = NULL;
+ }
+ $this->mOut =& $wgOut;
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = DBO_TRX | $flags;
+
+ if ( $schema == self::USE_GLOBAL ) {
+ $this->mSchema = $wgDBmwschema;
+ }
+ else {
+ $this->mSchema = $schema;
+ }
+
+ $this->open( $server, $user, $password, $dbName);
+ }
+
+ /**
+ * Opens a database connection and returns it
+ * Closes any existing connection
+ * @return a fresh connection
+ * @param string $server hostname
+ * @param string $user
+ * @param string $password
+ * @param string $dbName database name
+ */
+ public function open( $server, $user, $password, $dbName )
+ {
+ // Load the port number
+ global $wgDBport_db2, $wgDBcataloged;
+ wfProfileIn( __METHOD__ );
+
+ // Load IBM DB2 driver if missing
+ if (!@extension_loaded('ibm_db2')) {
+ @dl('ibm_db2.so');
+ }
+ // Test for IBM DB2 support, to avoid suppressed fatal error
+ if ( !function_exists( 'db2_connect' ) ) {
+ $error = "DB2 functions missing, have you enabled the ibm_db2 extension for PHP?\n";
+ wfDebug($error);
+ $this->reportConnectionError($error);
+ }
+
+ if (!strlen($user)) { // Copied from Postgres
+ return null;
+ }
+
+ // Close existing connection
+ $this->close();
+ // Cache conn info
+ $this->mServer = $server;
+ $this->mPort = $port = $wgDBport_db2;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+ $this->mCataloged = $cataloged = $wgDBcataloged;
+
+ if ( $cataloged == self::CATALOGED ) {
+ $this->openCataloged($dbName, $user, $password);
+ }
+ elseif ( $cataloged == self::UNCATALOGED ) {
+ $this->openUncataloged($dbName, $user, $password, $server, $port);
+ }
+ // Don't do this
+ // Not all MediaWiki code is transactional
+ // Rather, turn it off in the begin function and turn on after a commit
+ // db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF);
+ db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON);
+
+ if ( $this->mConn == false ) {
+ wfDebug( "DB connection error\n" );
+ wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
+ wfDebug( $this->lastError()."\n" );
+ return null;
+ }
+
+ $this->mOpened = true;
+ $this->applySchema();
+
+ wfProfileOut( __METHOD__ );
+ return $this->mConn;
+ }
+
+ /**
+ * Opens a cataloged database connection, sets mConn
+ */
+ protected function openCataloged( $dbName, $user, $password )
+ {
+ @$this->mConn = db2_connect($dbName, $user, $password);
+ }
+
+ /**
+ * Opens an uncataloged database connection, sets mConn
+ */
+ protected function openUncataloged( $dbName, $user, $password, $server, $port )
+ {
+ $str = "DRIVER={IBM DB2 ODBC DRIVER};";
+ $str .= "DATABASE=$dbName;";
+ $str .= "HOSTNAME=$server;";
+ if ($port) $str .= "PORT=$port;";
+ $str .= "PROTOCOL=TCPIP;";
+ $str .= "UID=$user;";
+ $str .= "PWD=$password;";
+
+ @$this->mConn = db2_connect($str, $user, $password);
+ }
+
+ /**
+ * Closes a database connection, if it is open
+ * Returns success, true if already closed
+ */
+ public function close() {
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ if ($this->trxLevel() > 0) {
+ $this->commit();
+ }
+ return db2_close( $this->mConn );
+ }
+ else {
+ return true;
+ }
+ }
+
+ /**
+ * Returns a fresh instance of this class
+ * @static
+ * @return
+ * @param string $server hostname of database server
+ * @param string $user username
+ * @param string $password
+ * @param string $dbName database name on the server
+ * @param function $failFunction (optional)
+ * @param integer $flags database behaviour flags (optional, unused)
+ */
+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
+ {
+ return new DatabaseIbm_db2( $server, $user, $password, $dbName, $failFunction, $flags );
+ }
+
+ /**
+ * Retrieves the most current database error
+ * Forces a database rollback
+ */
+ public function lastError() {
+ if ($this->lastError2()) {
+ $this->rollback();
+ return true;
+ }
+ return false;
+ }
+
+ private function lastError2() {
+ $connerr = db2_conn_errormsg();
+ if ($connerr) return $connerr;
+ $stmterr = db2_stmt_errormsg();
+ if ($stmterr) return $stmterr;
+ if ($this->mConn) return "No open connection.";
+ if ($this->mOpened) return "No open connection allegedly.";
+
+ return false;
+ }
+
+ /**
+ * Get the last error number
+ * Return 0 if no error
+ * @return integer
+ */
+ public function lastErrno() {
+ $connerr = db2_conn_error();
+ if ($connerr) return $connerr;
+ $stmterr = db2_stmt_error();
+ if ($stmterr) return $stmterr;
+ return 0;
+ }
+
+ /**
+ * Is a database connection open?
+ * @return
+ */
+ public function isOpen() { return $this->mOpened; }
+
+ /**
+ * The DBMS-dependent part of query()
+ * @param $sql String: SQL query.
+ * @return object Result object to feed to fetchObject, fetchRow, ...; or false on failure
+ * @access private
+ */
+ /*private*/
+ public function doQuery( $sql ) {
+ //print "<li><pre>$sql</pre></li>";
+ // Switch into the correct namespace
+ $this->applySchema();
+
+ $ret = db2_exec( $this->mConn, $sql );
+ if( !$ret ) {
+ print "<br><pre>";
+ print $sql;
+ print "</pre><br>";
+ $error = db2_stmt_errormsg();
+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( $error ) );
+ }
+ $this->mLastResult = $ret;
+ $this->mAffectedRows = NULL; // Not calculated until asked for
+ return $ret;
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ public function getServerVersion() {
+ $info = db2_server_info( $this->mConn );
+ return $info->DBMS_VER;
+ }
+
+ /**
+ * Queries whether a given table exists
+ * @return boolean
+ */
+ public function tableExists( $table ) {
+ $schema = $this->mSchema;
+ $sql = <<< EOF
+SELECT COUNT(*) FROM SYSIBM.SYSTABLES ST
+WHERE ST.NAME = '$table' AND ST.CREATOR = '$schema'
+EOF;
+ $res = $this->query( $sql );
+ if (!$res) return false;
+
+ // If the table exists, there should be one of it
+ @$row = $this->fetchRow($res);
+ $count = $row[0];
+ if ($count == '1' or $count == 1) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return DB2 row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ public function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @$row = db2_fetch_object( $res );
+ if( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ // Make field names lowercase for compatibility with MySQL
+ if ($row)
+ {
+ $row2 = new BlankObject();
+ foreach ($row as $key => $value)
+ {
+ $keyu = strtolower($key);
+ $row2->$keyu = $value;
+ }
+ $row = $row2;
+ }
+ return $row;
+ }
+
+ /**
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return DB2 row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ public function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @$row = db2_fetch_array( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Override if introduced to base Database class
+ */
+ public function initial_setup() {
+ // do nothing
+ }
+
+ /**
+ * Create tables, stored procedures, and so on
+ */
+ public function setup_database() {
+ // Timeout was being changed earlier due to mysterious crashes
+ // Changing it now may cause more problems than not changing it
+ //set_time_limit(240);
+ try {
+ // TODO: switch to root login if available
+
+ // Switch into the correct namespace
+ $this->applySchema();
+ $this->begin();
+
+ $res = dbsource( "../maintenance/ibm_db2/tables.sql", $this);
+ $res = null;
+
+ // TODO: update mediawiki_version table
+
+ // TODO: populate interwiki links
+
+ $this->commit();
+ }
+ catch (MWException $mwe)
+ {
+ print "<br><pre>$mwe</pre><br>";
+ }
+ }
+
+ /**
+ * Escapes strings
+ * Doesn't escape numbers
+ * @param string s string to escape
+ * @return escaped string
+ */
+ public function addQuotes( $s ) {
+ //wfDebug("DB2::addQuotes($s)\n");
+ if ( is_null( $s ) ) {
+ return "NULL";
+ } else if ($s instanceof Blob) {
+ return "'".$s->fetch($s)."'";
+ }
+ $s = $this->strencode($s);
+ if ( is_numeric($s) ) {
+ return $s;
+ }
+ else {
+ return "'$s'";
+ }
+ }
+
+ /**
+ * Escapes strings
+ * Only escapes numbers going into non-numeric fields
+ * @param string s string to escape
+ * @return escaped string
+ */
+ public function addQuotesSmart( $table, $field, $s ) {
+ if ( is_null( $s ) ) {
+ return "NULL";
+ } else if ($s instanceof Blob) {
+ return "'".$s->fetch($s)."'";
+ }
+ $s = $this->strencode($s);
+ if ( is_numeric($s) ) {
+ // Check with the database if the column is actually numeric
+ // This allows for numbers in titles, etc
+ $res = $this->doQuery("SELECT $field FROM $table FETCH FIRST 1 ROWS ONLY");
+ $type = db2_field_type($res, strtoupper($field));
+ if ( $this->is_numeric_type( $type ) ) {
+ //wfDebug("DB2: Numeric value going in a numeric column: $s in $type $field in $table\n");
+ return $s;
+ }
+ else {
+ wfDebug("DB2: Numeric in non-numeric: '$s' in $type $field in $table\n");
+ return "'$s'";
+ }
+ }
+ else {
+ return "'$s'";
+ }
+ }
+
+ /**
+ * Verifies that a DB2 column/field type is numeric
+ * @return bool true if numeric
+ * @param string $type DB2 column type
+ */
+ public function is_numeric_type( $type ) {
+ switch (strtoupper($type)) {
+ case 'SMALLINT':
+ case 'INTEGER':
+ case 'INT':
+ case 'BIGINT':
+ case 'DECIMAL':
+ case 'REAL':
+ case 'DOUBLE':
+ case 'DECFLOAT':
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Alias for addQuotes()
+ * @param string s string to escape
+ * @return escaped string
+ */
+ public function strencode( $s ) {
+ // Bloody useless function
+ // Prepends backslashes to \x00, \n, \r, \, ', " and \x1a.
+ // But also necessary
+ $s = db2_escape_string($s);
+ // Wide characters are evil -- some of them look like '
+ $s = utf8_encode($s);
+ // Fix its stupidity
+ $from = array("\\\\", "\\'", '\\n', '\\t', '\\"', '\\r');
+ $to = array("\\", "''", "\n", "\t", '"', "\r");
+ $s = str_replace($from, $to, $s); // DB2 expects '', not \' escaping
+ return $s;
+ }
+
+ /**
+ * Switch into the database schema
+ */
+ protected function applySchema() {
+ if ( !($this->mSchemaSet) ) {
+ $this->mSchemaSet = true;
+ $this->begin();
+ $this->doQuery("SET SCHEMA = $this->mSchema");
+ $this->commit();
+ }
+ }
+
+ /**
+ * Start a transaction (mandatory)
+ */
+ public function begin() {
+ // turn off auto-commit
+ db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF);
+ $this->mTrxLevel = 1;
+ }
+
+ /**
+ * End a transaction
+ * Must have a preceding begin()
+ */
+ public function commit() {
+ db2_commit($this->mConn);
+ // turn auto-commit back on
+ db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON);
+ $this->mTrxLevel = 0;
+ }
+
+ /**
+ * Cancel a transaction
+ */
+ public function rollback() {
+ db2_rollback($this->mConn);
+ // turn auto-commit back on
+ // not sure if this is appropriate
+ db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON);
+ $this->mTrxLevel = 0;
+ }
+
+ /**
+ * Makes an encoded list of strings from an array
+ * $mode:
+ * LIST_COMMA - comma separated, no field names
+ * LIST_AND - ANDed WHERE clause (without the WHERE)
+ * LIST_OR - ORed WHERE clause (without the WHERE)
+ * LIST_SET - comma separated with field names, like a SET clause
+ * LIST_NAMES - comma separated field names
+ */
+ public function makeList( $a, $mode = LIST_COMMA ) {
+ wfDebug("DB2::makeList()\n");
+ if ( !is_array( $a ) ) {
+ throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
+ }
+
+ $first = true;
+ $list = '';
+ foreach ( $a as $field => $value ) {
+ if ( !$first ) {
+ if ( $mode == LIST_AND ) {
+ $list .= ' AND ';
+ } elseif($mode == LIST_OR) {
+ $list .= ' OR ';
+ } else {
+ $list .= ',';
+ }
+ } else {
+ $first = false;
+ }
+ if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
+ $list .= "($value)";
+ } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
+ $list .= "$value";
+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
+ if( count( $value ) == 0 ) {
+ throw new MWException( __METHOD__.': empty input' );
+ } elseif( count( $value ) == 1 ) {
+ // Special-case single values, as IN isn't terribly efficient
+ // Don't necessarily assume the single key is 0; we don't
+ // enforce linear numeric ordering on other arrays here.
+ $value = array_values( $value );
+ $list .= $field." = ".$this->addQuotes( $value[0] );
+ } else {
+ $list .= $field." IN (".$this->makeList($value).") ";
+ }
+ } elseif( is_null($value) ) {
+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
+ $list .= "$field IS ";
+ } elseif ( $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= 'NULL';
+ } else {
+ if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ if ( $mode == LIST_NAMES ) {
+ $list .= $value;
+ }
+ // Leo: Can't insert quoted numbers into numeric columns
+ // (?) Might cause other problems. May have to check column type before insertion.
+ else if ( is_numeric($value) ) {
+ $list .= $value;
+ }
+ else {
+ $list .= $this->addQuotes( $value );
+ }
+ }
+ }
+ return $list;
+ }
+
+ /**
+ * Makes an encoded list of strings from an array
+ * Quotes numeric values being inserted into non-numeric fields
+ * @return string
+ * @param string $table name of the table
+ * @param array $a list of values
+ * @param $mode:
+ * LIST_COMMA - comma separated, no field names
+ * LIST_AND - ANDed WHERE clause (without the WHERE)
+ * LIST_OR - ORed WHERE clause (without the WHERE)
+ * LIST_SET - comma separated with field names, like a SET clause
+ * LIST_NAMES - comma separated field names
+ */
+ public function makeListSmart( $table, $a, $mode = LIST_COMMA ) {
+ if ( !is_array( $a ) ) {
+ throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
+ }
+
+ $first = true;
+ $list = '';
+ foreach ( $a as $field => $value ) {
+ if ( !$first ) {
+ if ( $mode == LIST_AND ) {
+ $list .= ' AND ';
+ } elseif($mode == LIST_OR) {
+ $list .= ' OR ';
+ } else {
+ $list .= ',';
+ }
+ } else {
+ $first = false;
+ }
+ if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
+ $list .= "($value)";
+ } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
+ $list .= "$value";
+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
+ if( count( $value ) == 0 ) {
+ throw new MWException( __METHOD__.': empty input' );
+ } elseif( count( $value ) == 1 ) {
+ // Special-case single values, as IN isn't terribly efficient
+ // Don't necessarily assume the single key is 0; we don't
+ // enforce linear numeric ordering on other arrays here.
+ $value = array_values( $value );
+ $list .= $field." = ".$this->addQuotes( $value[0] );
+ } else {
+ $list .= $field." IN (".$this->makeList($value).") ";
+ }
+ } elseif( is_null($value) ) {
+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
+ $list .= "$field IS ";
+ } elseif ( $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= 'NULL';
+ } else {
+ if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ if ( $mode == LIST_NAMES ) {
+ $list .= $value;
+ }
+ else {
+ $list .= $this->addQuotesSmart( $table, $field, $value );
+ }
+ }
+ }
+ return $list;
+ }
+
+ /**
+ * Construct a LIMIT query with optional offset
+ * This is used for query pages
+ * $sql string SQL query we will append the limit too
+ * $limit integer the SQL limit
+ * $offset integer the SQL offset (default false)
+ */
+ public function limitResult($sql, $limit, $offset=false) {
+ if( !is_numeric($limit) ) {
+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
+ }
+ if( $offset ) {
+ wfDebug("Offset parameter not supported in limitResult()\n");
+ }
+ // TODO implement proper offset handling
+ // idea: get all the rows between 0 and offset, advance cursor to offset
+ return "$sql FETCH FIRST $limit ROWS ONLY ";
+ }
+
+ /**
+ * Handle reserved keyword replacement in table names
+ * @return
+ * @param $name Object
+ */
+ public function tableName( $name ) {
+ # Replace reserved words with better ones
+ switch( $name ) {
+ case 'user':
+ return 'mwuser';
+ case 'text':
+ return 'pagecontent';
+ default:
+ return $name;
+ }
+ }
+
+ /**
+ * Generates a timestamp in an insertable format
+ * @return string timestamp value
+ * @param timestamp $ts
+ */
+ public function timestamp( $ts=0 ) {
+ // TS_MW cannot be easily distinguished from an integer
+ return wfTimestamp(TS_DB2,$ts);
+ }
+
+ /**
+ * Return the next in a sequence, save the value for retrieval via insertId()
+ * @param string seqName Name of a defined sequence in the database
+ * @return next value in that sequence
+ */
+ public function nextSequenceValue( $seqName ) {
+ $safeseq = preg_replace( "/'/", "''", $seqName );
+ $res = $this->query( "VALUES NEXTVAL FOR $safeseq" );
+ $row = $this->fetchRow( $res );
+ $this->mInsertId = $row[0];
+ $this->freeResult( $res );
+ return $this->mInsertId;
+ }
+
+ /**
+ * This must be called after nextSequenceVal
+ * @return Last sequence value used as a primary key
+ */
+ public function insertId() {
+ return $this->mInsertId;
+ }
+
+ /**
+ * INSERT wrapper, inserts an array into a table
+ *
+ * $args may be a single associative array, or an array of these with numeric keys,
+ * for multi-row insert
+ *
+ * @param array $table String: Name of the table to insert to.
+ * @param array $args Array: Items to insert into the table.
+ * @param array $fname String: Name of the function, for profiling
+ * @param mixed $options String or Array. Valid options: IGNORE
+ *
+ * @return bool Success of insert operation. IGNORE always returns true.
+ */
+ public function insert( $table, $args, $fname = 'DatabaseIbm_db2::insert', $options = array() ) {
+ wfDebug("DB2::insert($table)\n");
+ if ( !count( $args ) ) {
+ return true;
+ }
+
+ $table = $this->tableName( $table );
+
+ if ( !is_array( $options ) )
+ $options = array( $options );
+
+ if ( isset( $args[0] ) && is_array( $args[0] ) ) {
+ }
+ else {
+ $args = array($args);
+ }
+ $keys = array_keys( $args[0] );
+
+ // If IGNORE is set, we use savepoints to emulate mysql's behavior
+ $ignore = in_array( 'IGNORE', $options ) ? 'mw' : '';
+
+ // Cache autocommit value at the start
+ $oldautocommit = db2_autocommit($this->mConn);
+
+ // If we are not in a transaction, we need to be for savepoint trickery
+ $didbegin = 0;
+ if (! $this->mTrxLevel) {
+ $this->begin();
+ $didbegin = 1;
+ }
+ if ( $ignore ) {
+ $olde = error_reporting( 0 );
+ // For future use, we may want to track the number of actual inserts
+ // Right now, insert (all writes) simply return true/false
+ $numrowsinserted = 0;
+ }
+
+ $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+ if ( !$ignore ) {
+ $first = true;
+ foreach ( $args as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeListSmart( $table, $row ) . ')';
+ }
+ $res = (bool)$this->query( $sql, $fname, $ignore );
+ }
+ else {
+ $res = true;
+ $origsql = $sql;
+ foreach ( $args as $row ) {
+ $tempsql = $origsql;
+ $tempsql .= '(' . $this->makeListSmart( $table, $row ) . ')';
+
+ if ( $ignore ) {
+ db2_exec($this->mConn, "SAVEPOINT $ignore");
+ }
+
+ $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
+
+ if ( $ignore ) {
+ $bar = db2_stmt_error();
+ if ($bar != false) {
+ db2_exec( $this->mConn, "ROLLBACK TO SAVEPOINT $ignore" );
+ }
+ else {
+ db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore" );
+ $numrowsinserted++;
+ }
+ }
+
+ // If any of them fail, we fail overall for this function call
+ // Note that this will be ignored if IGNORE is set
+ if (! $tempres)
+ $res = false;
+ }
+ }
+
+ if ($didbegin) {
+ $this->commit();
+ }
+ // if autocommit used to be on, it's ok to commit everything
+ else if ($oldautocommit)
+ {
+ $this->commit();
+ }
+
+ if ( $ignore ) {
+ $olde = error_reporting( $olde );
+ // Set the affected row count for the whole operation
+ $this->mAffectedRows = $numrowsinserted;
+
+ // IGNORE always returns true
+ return true;
+ }
+
+ return $res;
+ }
+
+ /**
+ * UPDATE wrapper, takes a condition array and a SET array
+ *
+ * @param string $table The table to UPDATE
+ * @param array $values An array of values to SET
+ * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
+ * @param string $fname The Class::Function calling this function
+ * (for the log)
+ * @param array $options An array of UPDATE options, can be one or
+ * more of IGNORE, LOW_PRIORITY
+ * @return bool
+ */
+ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
+ $table = $this->tableName( $table );
+ $opts = $this->makeUpdateOptions( $options );
+ $sql = "UPDATE $opts $table SET " . $this->makeListSmart( $table, $values, LIST_SET );
+ if ( $conds != '*' ) {
+ $sql .= " WHERE " . $this->makeListSmart( $table, $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * DELETE query wrapper
+ *
+ * Use $conds == "*" to delete all rows
+ */
+ function delete( $table, $conds, $fname = 'Database::delete' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
+ }
+ $table = $this->tableName( $table );
+ $sql = "DELETE FROM $table";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeListSmart( $table, $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Returns the number of rows affected by the last query or 0
+ * @return int the number of rows affected by the last query
+ */
+ public function affectedRows() {
+ if ( !is_null( $this->mAffectedRows ) ) {
+ // Forced result for simulated queries
+ return $this->mAffectedRows;
+ }
+ if( empty( $this->mLastResult ) )
+ return 0;
+ return db2_num_rows( $this->mLastResult );
+ }
+
+ /**
+ * USE INDEX clause
+ * DB2 doesn't have them and returns ""
+ * @param sting $index
+ */
+ public function useIndexClause( $index ) {
+ return "";
+ }
+
+ /**
+ * Simulates REPLACE with a DELETE followed by INSERT
+ * @param $table Object
+ * @param array $uniqueIndexes array consisting of indexes and arrays of indexes
+ * @param array $rows Rows to insert
+ * @param string $fname Name of the function for profiling
+ * @return nothing
+ */
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseIbm_db2::replace' ) {
+ $table = $this->tableName( $table );
+
+ if (count($rows)==0) {
+ return;
+ }
+
+ # Single row case
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
+ }
+
+ foreach( $rows as $row ) {
+ # Delete rows which collide
+ if ( $uniqueIndexes ) {
+ $sql = "DELETE FROM $table WHERE ";
+ $first = true;
+ foreach ( $uniqueIndexes as $index ) {
+ if ( $first ) {
+ $first = false;
+ $sql .= "(";
+ } else {
+ $sql .= ') OR (';
+ }
+ if ( is_array( $index ) ) {
+ $first2 = true;
+ foreach ( $index as $col ) {
+ if ( $first2 ) {
+ $first2 = false;
+ } else {
+ $sql .= ' AND ';
+ }
+ $sql .= $col.'=' . $this->addQuotes( $row[$col] );
+ }
+ } else {
+ $sql .= $index.'=' . $this->addQuotes( $row[$index] );
+ }
+ }
+ $sql .= ')';
+ $this->query( $sql, $fname );
+ }
+
+ # Now insert the row
+ $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
+ $this->makeList( $row, LIST_COMMA ) . ')';
+ $this->query( $sql, $fname );
+ }
+ }
+
+ /**
+ * Returns the number of rows in the result set
+ * Has to be called right after the corresponding select query
+ * @param Object $res result set
+ * @return int number of rows
+ */
+ public function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( $this->mNumRows ) {
+ return $this->mNumRows;
+ }
+ else {
+ return 0;
+ }
+ }
+
+ /**
+ * Moves the row pointer of the result set
+ * @param Object $res result set
+ * @param int $row row number
+ * @return success or failure
+ */
+ public function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return db2_fetch_row( $res, $row );
+ }
+
+ ###
+ # Fix notices in Block.php
+ ###
+
+ /**
+ * Frees memory associated with a statement resource
+ * @param Object $res Statement resource to free
+ * @return bool success or failure
+ */
+ public function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( !@db2_free_result( $res ) ) {
+ throw new DBUnexpectedError($this, "Unable to free DB2 result\n" );
+ }
+ }
+
+ /**
+ * Returns the number of columns in a resource
+ * @param Object $res Statement resource
+ * @return Number of fields/columns in resource
+ */
+ public function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return db2_num_fields( $res );
+ }
+
+ /**
+ * Returns the nth column name
+ * @param Object $res Statement resource
+ * @param int $n Index of field or column
+ * @return string name of nth column
+ */
+ public function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return db2_field_name( $res, $n );
+ }
+
+ /**
+ * SELECT wrapper
+ *
+ * @param mixed $table Array or string, table name(s) (prefix auto-added)
+ * @param mixed $vars Array or string, field name(s) to be retrieved
+ * @param mixed $conds Array or string, condition(s) for WHERE
+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
+ * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * see Database::makeSelectOptions code for list of supported stuff
+ * @param array $join_conds Associative array of table join conditions (optional)
+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+ * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
+ */
+ public function select( $table, $vars, $conds='', $fname = 'DatabaseIbm_db2::select', $options = array(), $join_conds = array() )
+ {
+ $res = parent::select( $table, $vars, $conds, $fname, $options, $join_conds );
+
+ // We must adjust for offset
+ if ( isset( $options['LIMIT'] ) ) {
+ if ( isset ($options['OFFSET'] ) ) {
+ $limit = $options['LIMIT'];
+ $offset = $options['OFFSET'];
+ }
+ }
+
+
+ // DB2 does not have a proper num_rows() function yet, so we must emulate it
+ // DB2 9.5.3/9.5.4 and the corresponding ibm_db2 driver will introduce a working one
+ // Yay!
+
+ // we want the count
+ $vars2 = array('count(*) as num_rows');
+ // respecting just the limit option
+ $options2 = array();
+ if ( isset( $options['LIMIT'] ) ) $options2['LIMIT'] = $options['LIMIT'];
+ // but don't try to emulate for GROUP BY
+ if ( isset( $options['GROUP BY'] ) ) return $res;
+
+ $res2 = parent::select( $table, $vars2, $conds, $fname, $options2, $join_conds );
+ $obj = $this->fetchObject($res2);
+ $this->mNumRows = $obj->num_rows;
+
+ wfDebug("DatabaseIbm_db2::select: There are $this->mNumRows rows.\n");
+
+ return $res;
+ }
+
+ /**
+ * Handles ordering, grouping, and having options ('GROUP BY' => colname)
+ * Has limited support for per-column options (colnum => 'DISTINCT')
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+ return array( $startOpts, '', $preLimitTail, $postLimitTail );
+ }
+
+ /**
+ * Returns link to IBM DB2 free download
+ * @return string wikitext of a link to the server software's web site
+ */
+ public function getSoftwareLink() {
+ return "[http://www.ibm.com/software/data/db2/express/?s_cmp=ECDDWW01&s_tact=MediaWiki IBM DB2]";
+ }
+
+ /**
+ * Does nothing
+ * @param object $db
+ * @return bool true
+ */
+ public function selectDB( $db ) {
+ return true;
+ }
+
+ /**
+ * Returns an SQL expression for a simple conditional.
+ * Uses CASE on DB2
+ *
+ * @param string $cond SQL expression which will result in a boolean value
+ * @param string $trueVal SQL expression to return if true
+ * @param string $falseVal SQL expression to return if false
+ * @return string SQL fragment
+ */
+ public function conditional( $cond, $trueVal, $falseVal ) {
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+ }
+
+ ###
+ # Fix search crash
+ ###
+ /**
+ * Get search engine class. All subclasses of this
+ * need to implement this if they wish to use searching.
+ *
+ * @return string
+ */
+ public function getSearchEngine() {
+ return "SearchIBM_DB2";
+ }
+
+ ###
+ # Tuesday the 14th of October, 2008
+ ###
+ /**
+ * Did the last database access fail because of deadlock?
+ * @return bool
+ */
+ public function wasDeadlock() {
+ // get SQLSTATE
+ $err = $this->lastErrno();
+ switch($err) {
+ case '40001': // sql0911n, Deadlock or timeout, rollback
+ case '57011': // sql0904n, Resource unavailable, no rollback
+ case '57033': // sql0913n, Deadlock or timeout, no rollback
+ wfDebug("In a deadlock because of SQLSTATE $err");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Ping the server and try to reconnect if it there is no connection
+ * The connection may be closed and reopened while this happens
+ * @return bool whether the connection exists
+ */
+ public function ping() {
+ // db2_ping() doesn't exist
+ // Emulate
+ $this->close();
+ if ($this->mCataloged == NULL) {
+ return false;
+ }
+ else if ($this->mCataloged) {
+ $this->mConn = $this->openCataloged($this->mDBName, $this->mUser, $this->mPassword);
+ }
+ else if (!$this->mCataloged) {
+ $this->mConn = $this->openUncataloged($this->mDBName, $this->mUser, $this->mPassword, $this->mServer, $this->mPort);
+ }
+ return false;
+ }
+ ######################################
+ # Unimplemented and not applicable
+ ######################################
+ /**
+ * Not implemented
+ * @return string ''
+ * @deprecated
+ */
+ public function getStatus( $which ) { wfDebug('Not implemented for DB2: getStatus()'); return ''; }
+ /**
+ * Not implemented
+ * @deprecated
+ */
+ public function setTimeout( $timeout ) { wfDebug('Not implemented for DB2: setTimeout()'); }
+ /**
+ * Not implemented
+ * TODO
+ * @return bool true
+ */
+ public function lock( $lockName, $method ) { wfDebug('Not implemented for DB2: lock()'); return true; }
+ /**
+ * Not implemented
+ * TODO
+ * @return bool true
+ */
+ public function unlock( $lockName, $method ) { wfDebug('Not implemented for DB2: unlock()'); return true; }
+ /**
+ * Not implemented
+ * @deprecated
+ */
+ public function setFakeSlaveLag( $lag ) { wfDebug('Not implemented for DB2: setFakeSlaveLag()'); }
+ /**
+ * Not implemented
+ * @deprecated
+ */
+ public function setFakeMaster( $enabled ) { wfDebug('Not implemented for DB2: setFakeMaster()'); }
+ /**
+ * Not implemented
+ * @return string $sql
+ * @deprecated
+ */
+ public function limitResultForUpdate($sql, $num) { return $sql; }
+ /**
+ * No such option
+ * @return string ''
+ * @deprecated
+ */
+ public function lowPriorityOption() { return ''; }
+
+ ######################################
+ # Reflection
+ ######################################
+
+ /**
+ * Query whether a given column exists in the mediawiki schema
+ * @param string $table name of the table
+ * @param string $field name of the column
+ * @param string $fname function name for logging and profiling
+ */
+ public function fieldExists( $table, $field, $fname = 'DatabaseIbm_db2::fieldExists' ) {
+ $table = $this->tableName( $table );
+ $schema = $this->mSchema;
+ $etable = preg_replace("/'/", "''", $table);
+ $eschema = preg_replace("/'/", "''", $schema);
+ $ecol = preg_replace("/'/", "''", $field);
+ $sql = <<<SQL
+SELECT 1 as fieldexists
+FROM sysibm.syscolumns sc
+WHERE sc.name='$ecol' AND sc.tbname='$etable' AND sc.tbcreator='$eschema'
+SQL;
+ $res = $this->query( $sql, $fname );
+ $count = $res ? $this->numRows($res) : 0;
+ if ($res)
+ $this->freeResult( $res );
+ return $count;
+ }
+
+ /**
+ * Returns information about an index
+ * If errors are explicitly ignored, returns NULL on failure
+ * @param string $table table name
+ * @param string $index index name
+ * @param string
+ * @return object query row in object form
+ */
+ public function indexInfo( $table, $index, $fname = 'DatabaseIbm_db2::indexExists' ) {
+ $table = $this->tableName( $table );
+ $sql = <<<SQL
+SELECT name as indexname
+FROM sysibm.sysindexes si
+WHERE si.name='$index' AND si.tbname='$table' AND sc.tbcreator='$this->mSchema'
+SQL;
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return NULL;
+ }
+ $row = $this->fetchObject( $res );
+ if ($row != NULL) return $row;
+ else return false;
+ }
+
+ /**
+ * Returns an information object on a table column
+ * @param string $table table name
+ * @param string $field column name
+ * @return IBM_DB2Field
+ */
+ public function fieldInfo( $table, $field ) {
+ return IBM_DB2Field::fromText($this, $table, $field);
+ }
+
+ /**
+ * db2_field_type() wrapper
+ * @param Object $res Result of executed statement
+ * @param mixed $index number or name of the column
+ * @return string column type
+ */
+ public function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return db2_field_type( $res, $index );
+ }
+
+ /**
+ * Verifies that an index was created as unique
+ * @param string $table table name
+ * @param string $index index name
+ * @param string $fnam function name for profiling
+ * @return bool
+ */
+ public function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
+ $table = $this->tableName( $table );
+ $sql = <<<SQL
+SELECT si.name as indexname
+FROM sysibm.sysindexes si
+WHERE si.name='$index' AND si.tbname='$table' AND sc.tbcreator='$this->mSchema'
+AND si.uniquerule IN ('U', 'P')
+SQL;
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return null;
+ }
+ if ($this->fetchObject( $res )) {
+ return true;
+ }
+ return false;
+
+ }
+
+ /**
+ * Returns the size of a text field, or -1 for "unlimited"
+ * @param string $table table name
+ * @param string $field column name
+ * @return int length or -1 for unlimited
+ */
+ public function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = <<<SQL
+SELECT length as size
+FROM sysibm.syscolumns sc
+WHERE sc.name='$field' AND sc.tbname='$table' AND sc.tbcreator='$this->mSchema'
+SQL;
+ $res = $this->query($sql);
+ $row = $this->fetchObject($res);
+ $size = $row->size;
+ $this->freeResult( $res );
+ return $size;
+ }
+
+ /**
+ * DELETE where the condition is a join
+ * @param string $delTable deleting from this table
+ * @param string $joinTable using data from this table
+ * @param string $delVar variable in deleteable table
+ * @param string $joinVar variable in data table
+ * @param array $conds conditionals for join table
+ * @param string $fname function name for profiling
+ */
+ public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseIbm_db2::deleteJoin" ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
+ if ( $conds != '*' ) {
+ $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= ')';
+
+ $this->query( $sql, $fname );
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on COUNT(*) output
+ * Takes same arguments as Database::select()
+ * @param string $table table name
+ * @param array $vars unused
+ * @param array $conds filters on the table
+ * @param string $fname function name for profiling
+ * @param array $options options for select
+ * @return int row count
+ */
+ public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ $rows = 0;
+ $res = $this->select ($table, 'COUNT(*) as mwrowcount', $conds, $fname, $options );
+ if ($res) {
+ $row = $this->fetchRow($res);
+ $rows = (isset($row['mwrowcount'])) ? $row['mwrowcount'] : 0;
+ }
+ $this->freeResult($res);
+ return $rows;
+ }
+
+ /**
+ * Description is left as an exercise for the reader
+ * @param mixed $b data to be encoded
+ * @return IBM_DB2Blob
+ */
+ public function encodeBlob($b) {
+ return new IBM_DB2Blob($b);
+ }
+
+ /**
+ * Description is left as an exercise for the reader
+ * @param IBM_DB2Blob $b data to be decoded
+ * @return mixed
+ */
+ public function decodeBlob($b) {
+ return $b->getData();
+ }
+
+ /**
+ * Convert into a list of string being concatenated
+ * @param array $stringList strings that need to be joined together by the SQL engine
+ * @return string joined by the concatenation operator
+ */
+ public function buildConcat( $stringList ) {
+ // || is equivalent to CONCAT
+ // Sample query: VALUES 'foo' CONCAT 'bar' CONCAT 'baz'
+ return implode( ' || ', $stringList );
+ }
+
+ /**
+ * Generates the SQL required to convert a DB2 timestamp into a Unix epoch
+ * @param string $column name of timestamp column
+ * @return string SQL code
+ */
+ public function extractUnixEpoch( $column ) {
+ // TODO
+ // see SpecialAncientpages
+ }
+}
+?> \ No newline at end of file
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
index 16a74b53..c940ad09 100644
--- a/includes/db/DatabasePostgres.php
+++ b/includes/db/DatabasePostgres.php
@@ -174,9 +174,11 @@ class DatabasePostgres extends Database {
global $wgCommandLineMode;
## If called from the command-line (e.g. importDump), only show errors
if ($wgCommandLineMode) {
- $this->doQuery("SET client_min_messages = 'ERROR'");
+ $this->doQuery( "SET client_min_messages = 'ERROR'" );
}
+ $this->doQuery( "SET client_encoding='UTF8'" );
+
global $wgDBmwschema, $wgDBts2schema;
if (isset( $wgDBmwschema ) && isset( $wgDBts2schema )
&& $wgDBmwschema !== 'mediawiki'
@@ -185,7 +187,7 @@ class DatabasePostgres extends Database {
) {
$safeschema = $this->quote_ident($wgDBmwschema);
$safeschema2 = $this->quote_ident($wgDBts2schema);
- $this->doQuery("SET search_path = $safeschema, $wgDBts2schema, public");
+ $this->doQuery( "SET search_path = $safeschema, $wgDBts2schema, public" );
}
return $this->mConn;
@@ -255,7 +257,7 @@ class DatabasePostgres extends Database {
print "<li>Database \"" . htmlspecialchars( $wgDBname ) . "\" already exists, skipping database creation.</li>";
}
else {
- if ($perms < 2) {
+ if ($perms < 1) {
print "<li>ERROR: the user \"" . htmlspecialchars( $wgDBsuperuser ) . "\" cannot create databases. ";
print 'Please use a different Postgres user.</li>';
dieout('</ul>');
@@ -687,7 +689,7 @@ class DatabasePostgres extends Database {
* Takes same arguments as Database::select()
*/
- function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ function estimateRowCount( $table, $vars='*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) {
$options['EXPLAIN'] = true;
$res = $this->select( $table, $vars, $conds, $fname, $options );
$rows = -1;
@@ -707,23 +709,25 @@ class DatabasePostgres extends Database {
* Returns information about an index
* If errors are explicitly ignored, returns NULL on failure
*/
- function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
+ function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) {
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
$res = $this->query( $sql, $fname );
if ( !$res ) {
return NULL;
}
while ( $row = $this->fetchObject( $res ) ) {
- if ( $row->indexname == $index ) {
+ if ( $row->indexname == $this->indexName( $index ) ) {
return $row;
}
}
return false;
}
- function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
+ function indexUnique ($table, $index, $fname = 'DatabasePostgres::indexUnique' ) {
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
- " AND indexdef LIKE 'CREATE UNIQUE%({$index})'";
+ " AND indexdef LIKE 'CREATE UNIQUE%(" .
+ $this->strencode( $this->indexName( $index ) ) .
+ ")'";
$res = $this->query( $sql, $fname );
if ( !$res )
return NULL;
@@ -921,7 +925,7 @@ class DatabasePostgres extends Database {
# It may be more efficient to leave off unique indexes which are unlikely to collide.
# However if you do this, you run the risk of encountering errors which wouldn't have
# occurred in MySQL
- function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabasePostgres::replace' ) {
$table = $this->tableName( $table );
if (count($rows)==0) {
@@ -971,7 +975,7 @@ class DatabasePostgres extends Database {
}
# DELETE where the condition is a join
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabasePostgres::deleteJoin' ) {
if ( !$conds ) {
throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
}
@@ -1072,12 +1076,15 @@ class DatabasePostgres extends Database {
*/
function getServerVersion() {
$versionInfo = pg_version( $this->mConn );
- if ( isset( $versionInfo['server'] ) ) {
+ if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
+ // Old client, abort install
+ $this->numeric_version = '7.3 or earlier';
+ } elseif ( isset( $versionInfo['server'] ) ) {
+ // Normal client
$this->numeric_version = $versionInfo['server'];
} else {
- // There's no way to identify the precise version before 7.4, but
- // it doesn't matter anyway since we're just going to give an error.
- $this->numeric_version = '7.3 or earlier';
+ // Bug 16937: broken pgsql extension from PHP<5.3
+ $this->numeric_version = pg_parameter_status( $this->mConn, 'server_version' );
}
return $this->numeric_version;
}
@@ -1088,12 +1095,12 @@ class DatabasePostgres extends Database {
*/
function relationExists( $table, $types, $schema = false ) {
global $wgDBmwschema;
- if (!is_array($types))
- $types = array($types);
- if (! $schema )
+ if ( !is_array( $types ) )
+ $types = array( $types );
+ if ( !$schema )
$schema = $wgDBmwschema;
- $etable = $this->addQuotes($table);
- $eschema = $this->addQuotes($schema);
+ $etable = $this->addQuotes( $table );
+ $eschema = $this->addQuotes( $schema );
$SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
. "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
. "AND c.relkind IN ('" . implode("','", $types) . "')";
@@ -1108,15 +1115,15 @@ class DatabasePostgres extends Database {
* For backward compatibility, this function checks both tables and
* views.
*/
- function tableExists ($table, $schema = false) {
- return $this->relationExists($table, array('r', 'v'), $schema);
+ function tableExists( $table, $schema = false ) {
+ return $this->relationExists( $table, array( 'r', 'v' ), $schema );
}
- function sequenceExists ($sequence, $schema = false) {
- return $this->relationExists($sequence, 'S', $schema);
+ function sequenceExists( $sequence, $schema = false ) {
+ return $this->relationExists( $sequence, 'S', $schema );
}
- function triggerExists($table, $trigger) {
+ function triggerExists( $table, $trigger ) {
global $wgDBmwschema;
$q = <<<END
@@ -1132,20 +1139,20 @@ END;
if (!$res)
return NULL;
$rows = $res->numRows();
- $this->freeResult($res);
+ $this->freeResult( $res );
return $rows;
}
- function ruleExists($table, $rule) {
+ function ruleExists( $table, $rule ) {
global $wgDBmwschema;
$exists = $this->selectField("pg_rules", "rulename",
array( "rulename" => $rule,
"tablename" => $table,
- "schemaname" => $wgDBmwschema));
+ "schemaname" => $wgDBmwschema ) );
return $exists === $rule;
}
- function constraintExists($table, $constraint) {
+ function constraintExists( $table, $constraint ) {
global $wgDBmwschema;
$SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ".
"WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
@@ -1224,7 +1231,7 @@ END;
}
/* Not even sure why this is used in the main codebase... */
- function limitResultForUpdate($sql, $num) {
+ function limitResultForUpdate( $sql, $num ) {
return $sql;
}
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
index dfc506cc..7a595697 100644
--- a/includes/db/DatabaseSqlite.php
+++ b/includes/db/DatabaseSqlite.php
@@ -15,6 +15,7 @@ class DatabaseSqlite extends Database {
var $mAffectedRows;
var $mLastResult;
var $mDatabaseFile;
+ var $mName;
/**
* Constructor
@@ -26,6 +27,7 @@ class DatabaseSqlite extends Database {
$this->mFailFunction = $failFunction;
$this->mFlags = $flags;
$this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite";
+ $this->mName = $dbName;
$this->open($server, $user, $password, $dbName);
}
@@ -89,8 +91,9 @@ class DatabaseSqlite extends Database {
*/
function doQuery($sql) {
$res = $this->mConn->query($sql);
- if ($res === false) $this->reportQueryError($this->lastError(),$this->lastErrno(),$sql,__FUNCTION__);
- else {
+ if ($res === false) {
+ return false;
+ } else {
$r = $res instanceof ResultWrapper ? $res->result : $res;
$this->mAffectedRows = $r->rowCount();
$res = new ResultWrapper($this,$r->fetchAll());
@@ -98,11 +101,11 @@ class DatabaseSqlite extends Database {
return $res;
}
- function freeResult(&$res) {
+ function freeResult($res) {
if ($res instanceof ResultWrapper) $res->result = NULL; else $res = NULL;
}
- function fetchObject(&$res) {
+ function fetchObject($res) {
if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
$cur = current($r);
if (is_array($cur)) {
@@ -114,7 +117,7 @@ class DatabaseSqlite extends Database {
return false;
}
- function fetchRow(&$res) {
+ function fetchRow($res) {
if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
$cur = current($r);
if (is_array($cur)) {
@@ -127,17 +130,17 @@ class DatabaseSqlite extends Database {
/**
* The PDO::Statement class implements the array interface so count() will work
*/
- function numRows(&$res) {
+ function numRows($res) {
$r = $res instanceof ResultWrapper ? $res->result : $res;
return count($r);
}
- function numFields(&$res) {
+ function numFields($res) {
$r = $res instanceof ResultWrapper ? $res->result : $res;
return is_array($r) ? count($r[0]) : 0;
}
- function fieldName(&$res,$n) {
+ function fieldName($res,$n) {
$r = $res instanceof ResultWrapper ? $res->result : $res;
if (is_array($r)) {
$keys = array_keys($r[0]);
@@ -154,13 +157,20 @@ class DatabaseSqlite extends Database {
}
/**
+ * Index names have DB scope
+ */
+ function indexName( $index ) {
+ return $index;
+ }
+
+ /**
* This must be called after nextSequenceVal
*/
function insertId() {
return $this->mConn->lastInsertId();
}
- function dataSeek(&$res,$row) {
+ function dataSeek($res,$row) {
if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
reset($r);
if ($row > 0) for ($i = 0; $i < $row; $i++) next($r);
@@ -173,8 +183,12 @@ class DatabaseSqlite extends Database {
}
function lastErrno() {
- if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
- return $this->mConn->errorCode();
+ if (!is_object($this->mConn)) {
+ return "Cannot return last error, no db connection";
+ } else {
+ $info = $this->mConn->errorInfo();
+ return $info[1];
+ }
}
function affectedRows() {
@@ -183,14 +197,43 @@ class DatabaseSqlite extends Database {
/**
* Returns information about an index
+ * Returns false if the index does not exist
* - if errors are explicitly ignored, returns NULL on failure
*/
function indexInfo($table, $index, $fname = 'Database::indexExists') {
- return false;
+ $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return null;
+ }
+ if ( $res->numRows() == 0 ) {
+ return false;
+ }
+ $info = array();
+ foreach ( $res as $row ) {
+ $info[] = $row->name;
+ }
+ return $info;
}
function indexUnique($table, $index, $fname = 'Database::indexUnique') {
- return false;
+ $row = $this->selectRow( 'sqlite_master', '*',
+ array(
+ 'type' => 'index',
+ 'name' => $this->indexName( $index ),
+ ), $fname );
+ if ( !$row || !isset( $row->sql ) ) {
+ return null;
+ }
+
+ // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
+ $indexPos = strpos( $row->sql, 'INDEX' );
+ if ( $indexPos === false ) {
+ return null;
+ }
+ $firstPart = substr( $row->sql, 0, $indexPos );
+ $options = explode( ' ', $firstPart );
+ return in_array( 'UNIQUE', $options );
}
/**
@@ -228,7 +271,10 @@ class DatabaseSqlite extends Database {
return '';
}
- # Returns the size of a text field, or -1 for "unlimited"
+ /**
+ * Returns the size of a text field, or -1 for "unlimited"
+ * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though.
+ */
function textFieldSize($table, $field) {
return -1;
}
@@ -252,6 +298,10 @@ class DatabaseSqlite extends Database {
return $this->lastErrno() == SQLITE_BUSY;
}
+ function wasErrorReissuable() {
+ return $this->lastErrno() == SQLITE_SCHEMA;
+ }
+
/**
* @return string wikitext of a link to the server software's web site
*/
@@ -265,38 +315,53 @@ class DatabaseSqlite extends Database {
function getServerVersion() {
global $wgContLang;
$ver = $this->mConn->getAttribute(PDO::ATTR_SERVER_VERSION);
- $size = $wgContLang->formatSize(filesize($this->mDatabaseFile));
- $file = basename($this->mDatabaseFile);
- return $ver." ($file: $size)";
+ return $ver;
}
/**
* Query whether a given column exists in the mediawiki schema
*/
- function fieldExists($table, $field) { return true; }
+ function fieldExists($table, $field, $fname = '') {
+ $info = $this->fieldInfo( $table, $field );
+ return (bool)$info;
+ }
- function fieldInfo($table, $field) { return SQLiteField::fromText($this, $table, $field); }
+ /**
+ * Get information about a given field
+ * Returns false if the field does not exist.
+ */
+ function fieldInfo($table, $field) {
+ $tableName = $this->tableName( $table );
+ $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
+ $res = $this->query( $sql, __METHOD__ );
+ foreach ( $res as $row ) {
+ if ( $row->name == $field ) {
+ return new SQLiteField( $row, $tableName );
+ }
+ }
+ return false;
+ }
- function begin() {
+ function begin( $fname = '' ) {
if ($this->mTrxLevel == 1) $this->commit();
$this->mConn->beginTransaction();
$this->mTrxLevel = 1;
}
- function commit() {
+ function commit( $fname = '' ) {
if ($this->mTrxLevel == 0) return;
$this->mConn->commit();
$this->mTrxLevel = 0;
}
- function rollback() {
+ function rollback( $fname = '' ) {
if ($this->mTrxLevel == 0) return;
$this->mConn->rollBack();
$this->mTrxLevel = 0;
}
function limitResultForUpdate($sql, $num) {
- return $sql;
+ return $this->limitResult( $sql, $num );
}
function strencode($s) {
@@ -325,17 +390,25 @@ class DatabaseSqlite extends Database {
function quote_ident($s) { return $s; }
/**
- * For now, does nothing
+ * Not possible in SQLite
+ * We have ATTACH_DATABASE but that requires database selectors before the
+ * table names and in any case is really a different concept to MySQL's USE
*/
- function selectDB($db) { return true; }
+ function selectDB($db) {
+ if ( $db != $this->mName ) {
+ throw new MWException( 'selectDB is not implemented in SQLite' );
+ }
+ }
/**
* not done
*/
public function setTimeout($timeout) { return; }
+ /**
+ * No-op for a non-networked database
+ */
function ping() {
- wfDebug("Function ping() not written for SQLite yet");
return true;
}
@@ -353,39 +426,16 @@ class DatabaseSqlite extends Database {
public function setup_database() {
global $IP,$wgSQLiteDataDir,$wgDBTableOptions;
$wgDBTableOptions = '';
- $mysql_tmpl = "$IP/maintenance/tables.sql";
- $mysql_iw = "$IP/maintenance/interwiki.sql";
- $sqlite_tmpl = "$IP/maintenance/sqlite/tables.sql";
-
- # Make an SQLite template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
- if (!file_exists($sqlite_tmpl)) {
- $sql = file_get_contents($mysql_tmpl);
- $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
- $sql = preg_replace('/^\s*(UNIQUE)?\s*(PRIMARY)?\s*KEY.+?$/m','',$sql);
- $sql = preg_replace('/^\s*(UNIQUE )?INDEX.+?$/m','',$sql); # These indexes should be created with a CREATE INDEX query
- $sql = preg_replace('/^\s*FULLTEXT.+?$/m','',$sql); # Full text indexes
- $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
- $sql = preg_replace('/binary\(\d+\)/','BLOB',$sql);
- $sql = preg_replace('/(TYPE|MAX_ROWS|AVG_ROW_LENGTH)=\w+/','',$sql);
- $sql = preg_replace('/,\s*\)/s',')',$sql); # removing previous items may leave a trailing comma
- $sql = str_replace('binary','',$sql);
- $sql = str_replace('auto_increment','PRIMARY KEY AUTOINCREMENT',$sql);
- $sql = str_replace(' unsigned','',$sql);
- $sql = str_replace(' int ',' INTEGER ',$sql);
- $sql = str_replace('NOT NULL','',$sql);
-
- # Tidy up and write file
- $sql = preg_replace('/^\s*^/m','',$sql); # Remove empty lines
- $sql = preg_replace('/;$/m',";\n",$sql); # Separate each statement with an empty line
- file_put_contents($sqlite_tmpl,$sql);
- }
- # Parse the SQLite template replacing inline variables such as /*$wgDBprefix*/
- $err = $this->sourceFile($sqlite_tmpl);
- if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
+ # Process common MySQL/SQLite table definitions
+ $err = $this->sourceFile( "$IP/maintenance/tables.sql" );
+ if ($err !== true) {
+ $this->reportQueryError($err,0,$sql,__FUNCTION__);
+ exit( 1 );
+ }
# Use DatabasePostgres's code to populate interwiki from MySQL template
- $f = fopen($mysql_iw,'r');
+ $f = fopen("$IP/maintenance/interwiki.sql",'r');
if ($f == false) dieout("<li>Could not find the interwiki.sql file");
$sql = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
while (!feof($f)) {
@@ -418,22 +468,80 @@ class DatabaseSqlite extends Database {
$function = array_shift( $args );
return call_user_func_array( $function, $args );
}
-}
+
+ protected function replaceVars( $s ) {
+ $s = parent::replaceVars( $s );
+ if ( preg_match( '/^\s*CREATE TABLE/i', $s ) ) {
+ // CREATE TABLE hacks to allow schema file sharing with MySQL
+
+ // binary/varbinary column type -> blob
+ $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'blob\1', $s );
+ // no such thing as unsigned
+ $s = preg_replace( '/\bunsigned\b/i', '', $s );
+ // INT -> INTEGER for primary keys
+ $s = preg_replacE( '/\bint\b/i', 'integer', $s );
+ // No ENUM type
+ $s = preg_replace( '/enum\([^)]*\)/i', 'blob', $s );
+ // binary collation type -> nothing
+ $s = preg_replace( '/\bbinary\b/i', '', $s );
+ // auto_increment -> autoincrement
+ $s = preg_replace( '/\bauto_increment\b/i', 'autoincrement', $s );
+ // No explicit options
+ $s = preg_replace( '/\)[^)]*$/', ')', $s );
+ } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
+ // No truncated indexes
+ $s = preg_replace( '/\(\d+\)/', '', $s );
+ // No FULLTEXT
+ $s = preg_replace( '/\bfulltext\b/i', '', $s );
+ }
+ return $s;
+ }
+
+} // end DatabaseSqlite class
/**
* @ingroup Database
*/
-class SQLiteField extends MySQLField {
+class SQLiteField {
+ private $info, $tableName;
+ function __construct( $info, $tableName ) {
+ $this->info = $info;
+ $this->tableName = $tableName;
+ }
- function __construct() {
+ function name() {
+ return $this->info->name;
}
- static function fromText($db, $table, $field) {
- $n = new SQLiteField;
- $n->name = $field;
- $n->tablename = $table;
- return $n;
+ function tableName() {
+ return $this->tableName;
}
-} // end DatabaseSqlite class
+ function defaultValue() {
+ if ( is_string( $this->info->dflt_value ) ) {
+ // Typically quoted
+ if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) {
+ return str_replace( "''", "'", $this->info->dflt_value );
+ }
+ }
+ return $this->info->dflt_value;
+ }
+
+ function maxLength() {
+ return -1;
+ }
+
+ function nullable() {
+ // SQLite dynamic types are always nullable
+ return true;
+ }
+
+ # isKey(), isMultipleKey() not implemented, MySQL-specific concept.
+ # Suggest removal from base class [TS]
+
+ function type() {
+ return $this->info->type;
+ }
+
+} // end SQLiteField
diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php
index 256875d7..3876d71f 100644
--- a/includes/db/LBFactory.php
+++ b/includes/db/LBFactory.php
@@ -236,15 +236,25 @@ class ChronologyProtector {
* @param LoadBalancer $lb
*/
function shutdownLB( $lb ) {
- if ( session_id() != '' && $lb->getServerCount() > 1 ) {
- $masterName = $lb->getServerName( 0 );
- if ( !isset( $this->shutdownPos[$masterName] ) ) {
- $pos = $lb->getMasterPos();
- $info = $lb->parentInfo();
- wfDebug( __METHOD__.": LB " . $info['id'] . " has master pos $pos\n" );
- $this->shutdownPos[$masterName] = $pos;
- }
+ // Don't start a session, don't bother with non-replicated setups
+ if ( strval( session_id() ) == '' || $lb->getServerCount() <= 1 ) {
+ return;
+ }
+ $masterName = $lb->getServerName( 0 );
+ if ( isset( $this->shutdownPos[$masterName] ) ) {
+ // Already done
+ return;
+ }
+ // Only save the position if writes have been done on the connection
+ $db = $lb->getAnyOpenConnection( 0 );
+ $info = $lb->parentInfo();
+ if ( !$db || !$db->doneWrites() ) {
+ wfDebug( __METHOD__.": LB {$info['id']}, no writes done\n" );
+ return;
}
+ $pos = $db->getMasterPos();
+ wfDebug( __METHOD__.": LB {$info['id']} has master pos $pos\n" );
+ $this->shutdownPos[$masterName] = $pos;
}
/**
diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php
index f847fe22..0b8ef05a 100644
--- a/includes/db/LoadBalancer.php
+++ b/includes/db/LoadBalancer.php
@@ -824,7 +824,7 @@ class LoadBalancer {
continue;
}
foreach ( $conns2[$masterIndex] as $conn ) {
- if ( $conn->lastQuery() != '' ) {
+ if ( $conn->doneWrites() ) {
$conn->commit();
}
}