diff options
Diffstat (limited to 'includes/db')
-rw-r--r-- | includes/db/Database.php | 248 | ||||
-rw-r--r-- | includes/db/DatabaseIbm_db2.php | 1796 | ||||
-rw-r--r-- | includes/db/DatabasePostgres.php | 65 | ||||
-rw-r--r-- | includes/db/DatabaseSqlite.php | 238 | ||||
-rw-r--r-- | includes/db/LBFactory.php | 26 | ||||
-rw-r--r-- | includes/db/LoadBalancer.php | 2 |
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(); } } |