From: Leons Petrazickis Date: Wed, 14 Jan 2009 22:20:15 +0000 (+0000) Subject: (bug 17028) Added support for IBM DB2 database. config/index.php has new interface... X-Git-Tag: 1.31.0-rc.0~43415 X-Git-Url: https://git.cyclocoop.org/%7B%24admin_url%7Dmembres/fiche.php?a=commitdiff_plain;h=5c7431a08ee1ae66e4ef7aec4317df5613a9323f;p=lhc%2Fweb%2Fwiklou.git (bug 17028) Added support for IBM DB2 database. config/index.php has new interface elements that only show up if PHP has ibm_db2 module enabled. AutoLoader knows about the new DB2 classes. GlobalFunctions has a new constant for DB2 time format. Revision class fixed slightly. Also includes new PHP files containing the Database and Search API implementations for IBM DB2. --- diff --git a/config/index.php b/config/index.php index 0a2d290b64..915b8a3589 100644 --- a/config/index.php +++ b/config/index.php @@ -79,6 +79,12 @@ $ourdb['mssql']['compile'] = 'mssql not ready'; # Change to 'mssql' after $ourdb['mssql']['bgcolor'] = '#ffc0cb'; $ourdb['mssql']['rootuser'] = 'administrator'; +$ourdb['ibm_db2']['fullname'] = 'DB2'; +$ourdb['ibm_db2']['havedriver'] = 0; +$ourdb['ibm_db2']['compile'] = 'ibm_db2'; +$ourdb['ibm_db2']['bgcolor'] = '#ffeba1'; +$ourdb['ibm_db2']['rootuser'] = 'db2admin'; + ?> @@ -615,6 +621,12 @@ print "
  • Environment check ## MSSQL specific // We need a second field so it doesn't overwrite the MySQL one $conf->DBprefix2 = importPost( "DBprefix2" ); + + ## DB2 specific: + // New variable in order to have a different default port number + $conf->DBport_db2 = importPost( "DBport_db2", "50000" ); + $conf->DBmwschema = importPost( "DBmwschema", "mediawiki" ); + $conf->DBcataloged = importPost( "DBcataloged", "cataloged" ); $conf->ShellLocale = getShellLocale( $conf->LanguageCode ); @@ -786,6 +798,9 @@ if( $conf->posted && ( 0 == count( $errs ) ) ) { $wgDBprefix = $conf->DBprefix2; } + ## DB2 specific: + $wgDBcataloged = $conf->DBcataloged; + $wgCommandLineMode = true; if (! defined ( 'STDERR' ) ) define( 'STDERR', fopen("php://stderr", "wb")); @@ -861,12 +876,31 @@ if( $conf->posted && ( 0 == count( $errs ) ) ) { } #conn. att. if( !$ok ) { continue; } - + } + else if( $conf->DBtype == 'ibm_db2' ) { + if( $useRoot ) { + $db_user = $conf->RootUser; + $db_pass = $conf->RootPW; + } else { + $db_user = $wgDBuser; + $db_pass = $wgDBpassword; + } + + echo( "
  • Attempting to connect to database \"$wgDBname\" as \"$db_user\"..." ); + $wgDatabase = $dbc->newFromParams($wgDBserver, $db_user, $db_pass, $wgDBname, 1); + if (!$wgDatabase->isOpen()) { + print " error: " . $wgDatabase->lastError() . "
  • \n"; + } else { + $myver = $wgDatabase->getServerVersion(); + } + if (is_callable(array($wgDatabase, 'initial_setup'))) $wgDatabase->initial_setup('', $wgDBname); + } else { # not mysql error_reporting( E_ALL ); $wgSuperUser = ''; ## Possible connect as a superuser - if( $useRoot && $conf->DBtype != 'sqlite' ) { + // Changed !mysql to postgres check since it seems to only apply to postgres + if( $useRoot && $conf->DBtype == 'postgres' ) { $wgDBsuperuser = $conf->RootUser; echo( "
  • Attempting to connect to database \"postgres\" as superuser \"$wgDBsuperuser\"..." ); $wgDatabase = $dbc->newFromParams($wgDBserver, $wgDBsuperuser, $conf->RootPW, "postgres", 1); @@ -1113,6 +1147,8 @@ if( $conf->posted && ( 0 == count( $errs ) ) ) { $revid = $revision->insertOn( $wgDatabase ); $article->updateRevisionOn( $wgDatabase, $revision ); } + // Now that all database work is done, make sure everything is committed + $wgDatabase->commit(); /* Write out the config file now that all is well */ print "
  • \n"; @@ -1459,6 +1495,25 @@ if( count( $errs ) ) {

    Avoid exotic characters; something like mw_ is good.

    + + +
    +
    +
    Select one:
    + +
    +

    If you need to share one database between multiple wikis, or + between MediaWiki and another web application, you may specify + a different schema to avoid conflicts.

    +
    +
    @@ -1629,6 +1684,12 @@ function writeLocalSettings( $conf ) { $dbsettings = "# MSSQL specific settings \$wgDBprefix = \"{$slconf['DBprefix2']}\";"; + } elseif( $conf->DBtype == 'ibm_db2' ) { + $dbsettings = +"# DB2 specific settings +\$wgDBport_db2 = \"{$slconf['DBport_db2']}\"; +\$wgDBmwschema = \"{$slconf['DBmwschema']}\"; +\$wgDBcataloged = \"{$slconf['DBcataloged']}\";"; } else { // ummm... :D $dbsettings = ''; diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 8c47db0b6f..009d984e8f 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -320,6 +320,11 @@ $wgAutoloadLocalClasses = array( 'PostgresField' => 'includes/db/DatabasePostgres.php', 'ResultWrapper' => 'includes/db/Database.php', 'SQLiteField' => 'includes/db/DatabaseSqlite.php', + + 'DatabaseIbm_db2' => 'includes/db/DatabaseIbm_db2.php', + 'IBM_DB2Field' => 'includes/db/DatabaseIbm_db2.php', + 'IBM_DB2SearchResultSet' => 'includes/SearchIBM_DB2.php', + 'SearchIBM_DB2' => 'includes/SearchIBM_DB2.php', # includes/diff 'AncestorComparator' => 'includes/diff/HTMLDiff.php', diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 878f3bcf89..a828e9829a 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -1692,6 +1692,11 @@ define('TS_ORACLE', 6); */ define('TS_POSTGRES', 7); +/** + * DB2 format time + */ +define('TS_DB2', 8); + /** * @param mixed $outputtype A timestamp in one of the supported formats, the * function will autodetect which format is supplied @@ -1753,6 +1758,8 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { return gmdate( 'd-M-y h.i.s A', $uts) . ' +00:00'; case TS_POSTGRES: return gmdate( 'Y-m-d H:i:s', $uts) . ' GMT'; + case TS_DB2: + return gmdate( 'Y-m-d H:i:s', $uts); default: throw new MWException( 'wfTimestamp() called with illegal output type.'); } diff --git a/includes/Revision.php b/includes/Revision.php index 7938d88a8d..37c22607b0 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -961,6 +961,10 @@ class Revision { */ static function getTimestampFromId( $title, $id ) { $dbr = wfGetDB( DB_SLAVE ); + // Casting fix for DB2 + if ($id == '') { + $id = 0; + } $conds = array( 'rev_id' => $id ); $conds['rev_page'] = $title->getArticleId(); $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); diff --git a/includes/SearchIBM_DB2.php b/includes/SearchIBM_DB2.php new file mode 100644 index 0000000000..eba401ce7c --- /dev/null +++ b/includes/SearchIBM_DB2.php @@ -0,0 +1,247 @@ + +# http://www.mediawiki.org/ +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# http://www.gnu.org/copyleft/gpl.html + +/** + * @file + * @ingroup Search + */ + +/** + * Search engine hook base class for IBM DB2 + * @ingroup Search + */ +class SearchIBM_DB2 extends SearchEngine { + function __construct($db) { + $this->db = $db; + } + + /** + * Perform a full text search query and return a result set. + * + * @param string $term - Raw search term + * @return IBM_DB2SearchResultSet + * @access public + */ + function searchText( $term ) { + $resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), true))); + return new IBM_DB2SearchResultSet($resultSet, $this->searchTerms); + } + + /** + * Perform a title-only search query and return a result set. + * + * @param string $term - Raw search term + * @return IBM_DB2SearchResultSet + * @access public + */ + function searchTitle($term) { + $resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), false))); + return new MySQLSearchResultSet($resultSet, $this->searchTerms); + } + + + /** + * Return a partial WHERE clause to exclude redirects, if so set + * @return string + * @private + */ + function queryRedirect() { + if ($this->showRedirects) { + return ''; + } else { + return 'AND page_is_redirect=0'; + } + } + + /** + * Return a partial WHERE clause to limit the search to the given namespaces + * @return string + * @private + */ + function queryNamespaces() { + if( is_null($this->namespaces) ) + return ''; + $namespaces = implode(',', $this->namespaces); + if ($namespaces == '') { + $namespaces = '0'; + } + return 'AND page_namespace IN (' . $namespaces . ')'; + } + + /** + * Return a LIMIT clause to limit results on the query. + * @return string + * @private + */ + function queryLimit($sql) { + return $this->db->limitResult($sql, $this->limit, $this->offset); + } + + /** + * Does not do anything for generic search engine + * subclasses may define this though + * @return string + * @private + */ + function queryRanking($filteredTerm, $fulltext) { + // requires Net Search Extender or equivalent + // return ' ORDER BY score(1)'; + return ''; + } + + /** + * Construct the full SQL query to do the search. + * The guts shoulds be constructed in queryMain() + * @param string $filteredTerm + * @param bool $fulltext + * @private + */ + function getQuery( $filteredTerm, $fulltext ) { + return $this->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' . + $this->queryRedirect() . ' ' . + $this->queryNamespaces() . ' ' . + $this->queryRanking( $filteredTerm, $fulltext ) . ' '); + } + + + /** + * Picks which field to index on, depending on what type of query. + * @param bool $fulltext + * @return string + */ + function getIndexField($fulltext) { + return $fulltext ? 'si_text' : 'si_title'; + } + + /** + * Get the base part of the search query. + * + * @param string $filteredTerm + * @param bool $fulltext + * @return string + * @private + */ + function queryMain( $filteredTerm, $fulltext ) { + $match = $this->parseQuery($filteredTerm, $fulltext); + $page = $this->db->tableName('page'); + $searchindex = $this->db->tableName('searchindex'); + return 'SELECT page_id, page_namespace, page_title ' . + "FROM $page,$searchindex " . + 'WHERE page_id=si_page AND ' . $match; + } + + /** @todo document */ + function parseQuery($filteredText, $fulltext) { + global $wgContLang; + $lc = SearchEngine::legalSearchChars(); + $this->searchTerms = array(); + + # FIXME: This doesn't handle parenthetical expressions. + $m = array(); + $q = array(); + + if (preg_match_all('/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', + $filteredText, $m, PREG_SET_ORDER)) { + foreach($m as $terms) { + $q[] = $terms[1] . $wgContLang->stripForSearch($terms[2]); + + if (!empty($terms[3])) { + $regexp = preg_quote( $terms[3], '/' ); + if ($terms[4]) + $regexp .= "[0-9A-Za-z_]+"; + } else { + $regexp = preg_quote(str_replace('"', '', $terms[2]), '/'); + } + $this->searchTerms[] = $regexp; + } + } + + $searchon = $this->db->strencode(join(',', $q)); + $field = $this->getIndexField($fulltext); + + // requires Net Search Extender or equivalent + //return " CONTAINS($field, '$searchon') > 0 "; + + return " lcase($field) LIKE lcase('%$searchon%')"; + } + + /** + * Create or update the search index record for the given page. + * Title and text should be pre-processed. + * + * @param int $id + * @param string $title + * @param string $text + */ + function update($id, $title, $text) { + $dbw = wfGetDB(DB_MASTER); + $dbw->replace('searchindex', + array('si_page'), + array( + 'si_page' => $id, + 'si_title' => $title, + 'si_text' => $text + ), 'SearchIBM_DB2::update' ); + // ? + //$dbw->query("CALL ctx_ddl.sync_index('si_text_idx')"); + //$dbw->query("CALL ctx_ddl.sync_index('si_title_idx')"); + } + + /** + * Update a search index record's title only. + * Title should be pre-processed. + * + * @param int $id + * @param string $title + */ + function updateTitle($id, $title) { + $dbw = wfGetDB(DB_MASTER); + + $dbw->update('searchindex', + array('si_title' => $title), + array('si_page' => $id), + 'SearchIBM_DB2::updateTitle', + array()); + } +} + +/** + * @ingroup Search + */ +class IBM_DB2SearchResultSet extends SearchResultSet { + function __construct($resultSet, $terms) { + $this->mResultSet = $resultSet; + $this->mTerms = $terms; + } + + function termMatches() { + return $this->mTerms; + } + + function numRows() { + return $this->mResultSet->numRows(); + } + + function next() { + $row = $this->mResultSet->fetchObject(); + if ($row === false) + return false; + return new SearchResult($row); + } +} diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php new file mode 100644 index 0000000000..56dc3c67b1 --- /dev/null +++ b/includes/db/DatabaseIbm_db2.php @@ -0,0 +1,1796 @@ +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 "
  • $sql
  • "; + // Switch into the correct namespace + $this->applySchema(); + + $ret = db2_exec( $this->mConn, $sql ); + if( !$ret ) { + print "
    ";
    +			print $sql;
    +			print "

    "; + $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 "
    $mwe

    "; + } + } + + /** + * 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 = <<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 = <<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 = <<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 = <<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/maintenance/ibm_db2/README b/maintenance/ibm_db2/README new file mode 100644 index 0000000000..bbd076f193 --- /dev/null +++ b/maintenance/ibm_db2/README @@ -0,0 +1,41 @@ +== Syntax differences between other databases and IBM DB2 == +{| border cellspacing=0 cellpadding=4 +!MySQL!!IBM DB2 +|- + +|SELECT 1 FROM $table LIMIT 1 +|SELECT COUNT(*) FROM SYSIBM.SYSTABLES ST +WHERE ST.NAME = '$table' AND ST.CREATOR = '$schema' +|- +|MySQL code tries to read one row and interprets lack of error as proof of existence. +|DB2 code counts the number of TABLES of that name in the database. There ought to be 1 for it to exist. +|- +|BEGIN +|(implicit) +|- +|TEXT +|VARCHAR(255) or CLOB +|- +|TIMESTAMPTZ +|TIMESTAMP +|- +|BYTEA +|VARGRAPHIC(255) +|- +|DEFAULT nextval('some_kind_of_sequence'), +|GENERATED ALWAYS AS IDENTITY (START WITH 0, INCREMENT BY 1), +|- +|CIDR +|VARCHAR(255) +|- +|LIMIT 10 +|FETCH FIRST 10 ROWS ONLY +|- +|ROLLBACK TO +|ROLLBACK TO SAVEPOINT +|- +|RELEASE +|RELEASE SAVEPOINT +|} +== See also == +*[http://ca.php.net/manual/en/function.db2-connect.php PHP Manual for DB2 functions] \ No newline at end of file diff --git a/maintenance/ibm_db2/tables.sql b/maintenance/ibm_db2/tables.sql new file mode 100644 index 0000000000..d0da55d6eb --- /dev/null +++ b/maintenance/ibm_db2/tables.sql @@ -0,0 +1,604 @@ +-- DB2 + +-- SQL to create the initial tables for the MediaWiki database. +-- This is read and executed by the install script; you should +-- not have to run it by itself unless doing a manual install. +-- This is the IBM DB2 version. +-- For information about each table, please see the notes in maintenance/tables.sql +-- Please make sure all dollar-quoting uses $mw$ at the start of the line +-- TODO: Change CHAR/SMALLINT to BOOL (still used in a non-bool fashion in PHP code) + + + + +CREATE SEQUENCE user_user_id_seq AS INTEGER START WITH 0 INCREMENT BY 1; +CREATE TABLE mwuser ( -- replace reserved word 'user' + user_id INTEGER NOT NULL PRIMARY KEY, -- DEFAULT nextval('user_user_id_seq'), + user_name VARCHAR(255) NOT NULL UNIQUE, + user_real_name VARCHAR(255), + user_password clob(1K), + user_newpassword clob(1K), + user_newpass_time TIMESTAMP, + user_token VARCHAR(255), + user_email VARCHAR(255), + user_email_token VARCHAR(255), + user_email_token_expires TIMESTAMP, + user_email_authenticated TIMESTAMP, + user_options CLOB(64K), + user_touched TIMESTAMP, + user_registration TIMESTAMP, + user_editcount INTEGER +); +CREATE INDEX user_email_token_idx ON mwuser (user_email_token); + +-- Create a dummy user to satisfy fk contraints especially with revisions +INSERT INTO mwuser + VALUES (NEXTVAL FOR user_user_id_seq,'Anonymous','', NULL,NULL,CURRENT_TIMESTAMP,NULL, NULL,NULL,NULL,NULL, NULL,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0); + +CREATE TABLE user_groups ( + ug_user INTEGER REFERENCES mwuser(user_id) ON DELETE CASCADE, + ug_group VARCHAR(255) NOT NULL +); +CREATE UNIQUE INDEX user_groups_unique ON user_groups (ug_user, ug_group); + +CREATE TABLE user_newtalk ( + user_id INTEGER NOT NULL REFERENCES mwuser(user_id) ON DELETE CASCADE, + user_ip VARCHAR(255), + user_last_timestamp TIMESTAMP +); +CREATE INDEX user_newtalk_id_idx ON user_newtalk (user_id); +CREATE INDEX user_newtalk_ip_idx ON user_newtalk (user_ip); + + +CREATE SEQUENCE page_page_id_seq; +CREATE TABLE page ( + page_id INTEGER NOT NULL PRIMARY KEY, -- DEFAULT NEXT VALUE FOR user_user_id_seq, + page_namespace SMALLINT NOT NULL, + page_title VARCHAR(255) NOT NULL, + page_restrictions clob(1K), + page_counter BIGINT NOT NULL DEFAULT 0, + page_is_redirect SMALLINT NOT NULL DEFAULT 0, + page_is_new SMALLINT NOT NULL DEFAULT 0, + page_random NUMERIC(15,14) NOT NULL, + page_touched TIMESTAMP, + page_latest INTEGER NOT NULL, -- FK? + page_len INTEGER NOT NULL +); +CREATE UNIQUE INDEX page_unique_name ON page (page_namespace, page_title); +--CREATE INDEX page_main_title ON page (page_title) WHERE page_namespace = 0; +--CREATE INDEX page_talk_title ON page (page_title) WHERE page_namespace = 1; +--CREATE INDEX page_user_title ON page (page_title) WHERE page_namespace = 2; +--CREATE INDEX page_utalk_title ON page (page_title) WHERE page_namespace = 3; +--CREATE INDEX page_project_title ON page (page_title) WHERE page_namespace = 4; +CREATE INDEX page_random_idx ON page (page_random); +CREATE INDEX page_len_idx ON page (page_len); + +--CREATE FUNCTION page_deleted() RETURNS TRIGGER LANGUAGE plpgsql AS +--$mw$ +--BEGIN +--DELETE FROM recentchanges WHERE rc_namespace = OLD.page_namespace AND rc_title = OLD.page_title; +--RETURN NULL; +--END; +--$mw$; + +--CREATE TRIGGER page_deleted AFTER DELETE ON page +-- FOR EACH ROW EXECUTE PROCEDURE page_deleted(); + +CREATE SEQUENCE rev_rev_id_val; +CREATE TABLE revision ( + rev_id INTEGER NOT NULL UNIQUE, --DEFAULT nextval('rev_rev_id_val'), + rev_page INTEGER REFERENCES page (page_id) ON DELETE CASCADE, + rev_text_id INTEGER, -- FK + rev_comment clob(1K), -- changed from VARCHAR(255) + rev_user INTEGER NOT NULL REFERENCES mwuser(user_id) ON DELETE RESTRICT, + rev_user_text VARCHAR(255) NOT NULL, + rev_timestamp TIMESTAMP NOT NULL, + rev_minor_edit SMALLINT NOT NULL DEFAULT 0, + rev_deleted SMALLINT NOT NULL DEFAULT 0, + rev_len INTEGER, + rev_parent_id INTEGER +); +CREATE UNIQUE INDEX revision_unique ON revision (rev_page, rev_id); +CREATE INDEX rev_text_id_idx ON revision (rev_text_id); +CREATE INDEX rev_timestamp_idx ON revision (rev_timestamp); +CREATE INDEX rev_user_idx ON revision (rev_user); +CREATE INDEX rev_user_text_idx ON revision (rev_user_text); + + +CREATE SEQUENCE text_old_id_val; +CREATE TABLE pagecontent ( -- replaces reserved word 'text' + old_id INTEGER NOT NULL, + --PRIMARY KEY DEFAULT nextval('text_old_id_val'), + old_text CLOB(16M), + old_flags clob(1K) +); + +CREATE SEQUENCE pr_id_val; +CREATE TABLE page_restrictions ( + pr_id INTEGER NOT NULL UNIQUE, + --DEFAULT nextval('pr_id_val'), + pr_page INTEGER NOT NULL + --(used to be nullable) + REFERENCES page (page_id) ON DELETE CASCADE, + pr_type VARCHAR(255) NOT NULL, + pr_level VARCHAR(255) NOT NULL, + pr_cascade SMALLINT NOT NULL, + pr_user INTEGER, + pr_expiry TIMESTAMP, + PRIMARY KEY (pr_page, pr_type) +); +--ALTER TABLE page_restrictions ADD CONSTRAINT page_restrictions_pk PRIMARY KEY (pr_page,pr_type); + +CREATE TABLE page_props ( + pp_page INTEGER NOT NULL REFERENCES page (page_id) ON DELETE CASCADE, + pp_propname VARCHAR(255) NOT NULL, + pp_value CLOB(64K) NOT NULL, + PRIMARY KEY (pp_page,pp_propname) +); +--ALTER TABLE page_props ADD CONSTRAINT page_props_pk PRIMARY KEY (pp_page,pp_propname); +CREATE INDEX page_props_propname ON page_props (pp_propname); + + + +CREATE TABLE archive ( + ar_namespace SMALLINT NOT NULL, + ar_title VARCHAR(255) NOT NULL, + ar_text CLOB(16M), + ar_page_id INTEGER, + ar_parent_id INTEGER, + ar_comment clob(1K), + ar_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, + ar_user_text VARCHAR(255) NOT NULL, + ar_timestamp TIMESTAMP NOT NULL, + ar_minor_edit SMALLINT NOT NULL DEFAULT 0, + ar_flags clob(1K), + ar_rev_id INTEGER, + ar_text_id INTEGER, + ar_deleted SMALLINT NOT NULL DEFAULT 0, + ar_len INTEGER +); +CREATE INDEX archive_name_title_timestamp ON archive (ar_namespace,ar_title,ar_timestamp); +CREATE INDEX archive_user_text ON archive (ar_user_text); + + + +CREATE TABLE redirect ( + rd_from INTEGER NOT NULL REFERENCES page(page_id) ON DELETE CASCADE, + rd_namespace SMALLINT NOT NULL, + rd_title VARCHAR(255) NOT NULL +); +CREATE INDEX redirect_ns_title ON redirect (rd_namespace,rd_title,rd_from); + + +CREATE TABLE pagelinks ( + pl_from INTEGER NOT NULL REFERENCES page(page_id) ON DELETE CASCADE, + pl_namespace SMALLINT NOT NULL, + pl_title VARCHAR(255) NOT NULL +); +CREATE UNIQUE INDEX pagelink_unique ON pagelinks (pl_from,pl_namespace,pl_title); + +CREATE TABLE templatelinks ( + tl_from INTEGER NOT NULL REFERENCES page(page_id) ON DELETE CASCADE, + tl_namespace SMALLINT NOT NULL, + tl_title VARCHAR(255) NOT NULL +); +CREATE UNIQUE INDEX templatelinks_unique ON templatelinks (tl_namespace,tl_title,tl_from); + +CREATE TABLE imagelinks ( + il_from INTEGER NOT NULL REFERENCES page(page_id) ON DELETE CASCADE, + il_to VARCHAR(255) NOT NULL +); +CREATE UNIQUE INDEX il_from ON imagelinks (il_to,il_from); + +CREATE TABLE categorylinks ( + cl_from INTEGER NOT NULL REFERENCES page(page_id) ON DELETE CASCADE, + cl_to VARCHAR(255) NOT NULL, + cl_sortkey VARCHAR(255), + cl_timestamp TIMESTAMP NOT NULL +); +CREATE UNIQUE INDEX cl_from ON categorylinks (cl_from, cl_to); +CREATE INDEX cl_sortkey ON categorylinks (cl_to, cl_sortkey, cl_from); + + + +CREATE TABLE externallinks ( + el_from INTEGER NOT NULL REFERENCES page(page_id) ON DELETE CASCADE, + el_to VARCHAR(255) NOT NULL, + el_index VARCHAR(255) NOT NULL +); +CREATE INDEX externallinks_from_to ON externallinks (el_from,el_to); +CREATE INDEX externallinks_index ON externallinks (el_index); + +CREATE TABLE langlinks ( + ll_from INTEGER NOT NULL REFERENCES page (page_id) ON DELETE CASCADE, + ll_lang VARCHAR(255), + ll_title VARCHAR(255) +); +CREATE UNIQUE INDEX langlinks_unique ON langlinks (ll_from,ll_lang); +CREATE INDEX langlinks_lang_title ON langlinks (ll_lang,ll_title); + + +CREATE TABLE site_stats ( + ss_row_id INTEGER NOT NULL UNIQUE, + ss_total_views INTEGER DEFAULT 0, + ss_total_edits INTEGER DEFAULT 0, + ss_good_articles INTEGER DEFAULT 0, + ss_total_pages INTEGER DEFAULT -1, + ss_users INTEGER DEFAULT -1, + ss_admins INTEGER DEFAULT -1, + ss_images INTEGER DEFAULT 0 +); + +CREATE TABLE hitcounter ( + hc_id BIGINT NOT NULL +); + +CREATE SEQUENCE ipblocks_ipb_id_val; +CREATE TABLE ipblocks ( + ipb_id INTEGER NOT NULL PRIMARY KEY, + --DEFAULT nextval('ipblocks_ipb_id_val'), + ipb_address VARCHAR(255), + ipb_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, + ipb_by INTEGER NOT NULL REFERENCES mwuser(user_id) ON DELETE CASCADE, + ipb_by_text VARCHAR(255) NOT NULL DEFAULT '', + ipb_reason VARCHAR(255) NOT NULL, + ipb_timestamp TIMESTAMP NOT NULL, + ipb_auto SMALLINT NOT NULL DEFAULT 0, + ipb_anon_only SMALLINT NOT NULL DEFAULT 0, + ipb_create_account SMALLINT NOT NULL DEFAULT 1, + ipb_enable_autoblock SMALLINT NOT NULL DEFAULT 1, + ipb_expiry TIMESTAMP NOT NULL, + ipb_range_start VARCHAR(255), + ipb_range_end VARCHAR(255), + ipb_deleted SMALLINT NOT NULL DEFAULT 0, + ipb_block_email SMALLINT NOT NULL DEFAULT 0 + +); +CREATE INDEX ipb_address ON ipblocks (ipb_address); +CREATE INDEX ipb_user ON ipblocks (ipb_user); +CREATE INDEX ipb_range ON ipblocks (ipb_range_start,ipb_range_end); + + + +CREATE TABLE image ( + img_name VARCHAR(255) NOT NULL PRIMARY KEY, + img_size INTEGER NOT NULL, + img_width INTEGER NOT NULL, + img_height INTEGER NOT NULL, + img_metadata CLOB(16M) NOT NULL DEFAULT '', + img_bits SMALLINT, + img_media_type VARCHAR(255), + img_major_mime VARCHAR(255) DEFAULT 'unknown', + img_minor_mime VARCHAR(255) DEFAULT 'unknown', + img_description clob(1K) NOT NULL DEFAULT '', + img_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, + img_user_text VARCHAR(255) NOT NULL DEFAULT '', + img_timestamp TIMESTAMP, + img_sha1 VARCHAR(255) NOT NULL DEFAULT '' +); +CREATE INDEX img_size_idx ON image (img_size); +CREATE INDEX img_timestamp_idx ON image (img_timestamp); +CREATE INDEX img_sha1 ON image (img_sha1); + +CREATE TABLE oldimage ( + oi_name VARCHAR(255) NOT NULL, + oi_archive_name VARCHAR(255) NOT NULL, + oi_size INTEGER NOT NULL, + oi_width INTEGER NOT NULL, + oi_height INTEGER NOT NULL, + oi_bits SMALLINT NOT NULL, + oi_description clob(1K), + oi_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, + oi_user_text VARCHAR(255) NOT NULL, + oi_timestamp TIMESTAMP NOT NULL, + oi_metadata CLOB(16M) NOT NULL DEFAULT '', + oi_media_type VARCHAR(255) , + oi_major_mime VARCHAR(255) NOT NULL DEFAULT 'unknown', + oi_minor_mime VARCHAR(255) NOT NULL DEFAULT 'unknown', + oi_deleted SMALLINT NOT NULL DEFAULT 0, + oi_sha1 VARCHAR(255) NOT NULL DEFAULT '', + FOREIGN KEY (oi_name) REFERENCES image(img_name) ON DELETE CASCADE +); +--ALTER TABLE oldimage ADD CONSTRAINT oldimage_oi_name_fkey_cascade FOREIGN KEY (oi_name) REFERENCES image(img_name) ON DELETE CASCADE; +CREATE INDEX oi_name_timestamp ON oldimage (oi_name,oi_timestamp); +CREATE INDEX oi_name_archive_name ON oldimage (oi_name,oi_archive_name); +CREATE INDEX oi_sha1 ON oldimage (oi_sha1); + + +CREATE SEQUENCE filearchive_fa_id_seq; +CREATE TABLE filearchive ( + fa_id INTEGER NOT NULL PRIMARY KEY, + --PRIMARY KEY DEFAULT nextval('filearchive_fa_id_seq'), + fa_name VARCHAR(255) NOT NULL, + fa_archive_name VARCHAR(255), + fa_storage_group VARCHAR(255), + fa_storage_key VARCHAR(255), + fa_deleted_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, + fa_deleted_timestamp TIMESTAMP NOT NULL, + fa_deleted_reason VARCHAR(255), + fa_size INTEGER NOT NULL, + fa_width INTEGER NOT NULL, + fa_height INTEGER NOT NULL, + fa_metadata CLOB(16M) NOT NULL DEFAULT '', + fa_bits SMALLINT, + fa_media_type VARCHAR(255), + fa_major_mime VARCHAR(255) DEFAULT 'unknown', + fa_minor_mime VARCHAR(255) DEFAULT 'unknown', + fa_description clob(1K) NOT NULL, + fa_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, + fa_user_text VARCHAR(255) NOT NULL, + fa_timestamp TIMESTAMP, + fa_deleted SMALLINT NOT NULL DEFAULT 0 +); +CREATE INDEX fa_name_time ON filearchive (fa_name, fa_timestamp); +CREATE INDEX fa_dupe ON filearchive (fa_storage_group, fa_storage_key); +CREATE INDEX fa_notime ON filearchive (fa_deleted_timestamp); +CREATE INDEX fa_nouser ON filearchive (fa_deleted_user); + +CREATE SEQUENCE rc_rc_id_seq; +CREATE TABLE recentchanges ( + rc_id INTEGER NOT NULL PRIMARY KEY, + --PRIMARY KEY DEFAULT nextval('rc_rc_id_seq'), + rc_timestamp TIMESTAMP NOT NULL, + rc_cur_time TIMESTAMP NOT NULL, + rc_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, + rc_user_text VARCHAR(255) NOT NULL, + rc_namespace SMALLINT NOT NULL, + rc_title VARCHAR(255) NOT NULL, + rc_comment VARCHAR(255), + rc_minor SMALLINT NOT NULL DEFAULT 0, + rc_bot SMALLINT NOT NULL DEFAULT 0, + rc_new SMALLINT NOT NULL DEFAULT 0, + rc_cur_id INTEGER REFERENCES page(page_id) ON DELETE SET NULL, + rc_this_oldid INTEGER NOT NULL, + rc_last_oldid INTEGER NOT NULL, + rc_type SMALLINT NOT NULL DEFAULT 0, + rc_moved_to_ns SMALLINT, + rc_moved_to_title VARCHAR(255), + rc_patrolled SMALLINT NOT NULL DEFAULT 0, + rc_ip VARCHAR(255), -- was CIDR type + rc_old_len INTEGER, + rc_new_len INTEGER, + rc_deleted SMALLINT NOT NULL DEFAULT 0, + rc_logid INTEGER NOT NULL DEFAULT 0, + rc_log_type VARCHAR(255), + rc_log_action VARCHAR(255), + rc_params CLOB(64K) +); +CREATE INDEX rc_timestamp ON recentchanges (rc_timestamp); +CREATE INDEX rc_namespace_title ON recentchanges (rc_namespace, rc_title); +CREATE INDEX rc_cur_id ON recentchanges (rc_cur_id); +CREATE INDEX new_name_timestamp ON recentchanges (rc_new, rc_namespace, rc_timestamp); +CREATE INDEX rc_ip ON recentchanges (rc_ip); + + + +CREATE TABLE watchlist ( + wl_user INTEGER NOT NULL REFERENCES mwuser(user_id) ON DELETE CASCADE, + wl_namespace SMALLINT NOT NULL DEFAULT 0, + wl_title VARCHAR(255) NOT NULL, + wl_notificationtimestamp TIMESTAMP +); +CREATE UNIQUE INDEX wl_user_namespace_title ON watchlist (wl_namespace, wl_title, wl_user); + + +CREATE TABLE math ( + math_inputhash VARGRAPHIC(255) NOT NULL UNIQUE, + math_outputhash VARGRAPHIC(255) NOT NULL, + math_html_conservativeness SMALLINT NOT NULL, + math_html VARCHAR(255), + math_mathml VARCHAR(255) +); + + +CREATE TABLE interwiki ( + iw_prefix VARCHAR(255) NOT NULL UNIQUE, + iw_url CLOB(64K) NOT NULL, + iw_local SMALLINT NOT NULL, + iw_trans SMALLINT NOT NULL DEFAULT 0 +); + + +CREATE TABLE querycache ( + qc_type VARCHAR(255) NOT NULL, + qc_value INTEGER NOT NULL, + qc_namespace SMALLINT NOT NULL, + qc_title VARCHAR(255) NOT NULL +); +CREATE INDEX querycache_type_value ON querycache (qc_type, qc_value); + + + +CREATE TABLE querycache_info ( + qci_type VARCHAR(255) UNIQUE NOT NULL, + qci_timestamp TIMESTAMP +); + + +CREATE TABLE querycachetwo ( + qcc_type VARCHAR(255) NOT NULL, + qcc_value INTEGER NOT NULL DEFAULT 0, + qcc_namespace INTEGER NOT NULL DEFAULT 0, + qcc_title VARCHAR(255) NOT NULL DEFAULT '', + qcc_namespacetwo INTEGER NOT NULL DEFAULT 0, + qcc_titletwo VARCHAR(255) NOT NULL DEFAULT '' +); +CREATE INDEX querycachetwo_type_value ON querycachetwo (qcc_type, qcc_value); +CREATE INDEX querycachetwo_title ON querycachetwo (qcc_type,qcc_namespace,qcc_title); +CREATE INDEX querycachetwo_titletwo ON querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo); + +CREATE TABLE objectcache ( + keyname VARCHAR(255) NOT NULL UNIQUE, -- was nullable + value CLOB(16M) NOT NULL DEFAULT '', + exptime TIMESTAMP NOT NULL +); +CREATE INDEX objectcacache_exptime ON objectcache (exptime); + + + +CREATE TABLE transcache ( + tc_url VARCHAR(255) NOT NULL UNIQUE, + tc_contents VARCHAR(255) NOT NULL, + tc_time TIMESTAMP NOT NULL +); + +CREATE SEQUENCE log_log_id_seq; +CREATE TABLE logging ( + log_id INTEGER NOT NULL PRIMARY KEY, + --PRIMARY KEY DEFAULT nextval('log_log_id_seq'), + log_type VARCHAR(255) NOT NULL, + log_action VARCHAR(255) NOT NULL, + log_timestamp TIMESTAMP NOT NULL, + log_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, + log_namespace SMALLINT NOT NULL, + log_title VARCHAR(255) NOT NULL, + log_comment VARCHAR(255), + log_params CLOB(64K), + log_deleted SMALLINT NOT NULL DEFAULT 0 +); +CREATE INDEX logging_type_name ON logging (log_type, log_timestamp); +CREATE INDEX logging_user_time ON logging (log_timestamp, log_user); +CREATE INDEX logging_page_time ON logging (log_namespace, log_title, log_timestamp); + +CREATE SEQUENCE trackbacks_tb_id_seq; +CREATE TABLE trackbacks ( + tb_id INTEGER NOT NULL PRIMARY KEY, + --PRIMARY KEY DEFAULT nextval('trackbacks_tb_id_seq'), + tb_page INTEGER REFERENCES page(page_id) ON DELETE CASCADE, + tb_title VARCHAR(255) NOT NULL, + tb_url CLOB(64K) NOT NULL, + tb_ex VARCHAR(255), + tb_name VARCHAR(255) +); +CREATE INDEX trackback_page ON trackbacks (tb_page); + + +CREATE SEQUENCE job_job_id_seq; +CREATE TABLE job ( + job_id INTEGER NOT NULL PRIMARY KEY, + --PRIMARY KEY DEFAULT nextval('job_job_id_seq'), + job_cmd VARCHAR(255) NOT NULL, + job_namespace SMALLINT NOT NULL, + job_title VARCHAR(255) NOT NULL, + job_params CLOB(64K) NOT NULL +); +CREATE INDEX job_cmd_namespace_title ON job (job_cmd, job_namespace, job_title); + + + +-- Postgres' Tsearch2 dropped +--ALTER TABLE page ADD titlevector tsvector; +--CREATE FUNCTION ts2_page_title() RETURNS TRIGGER LANGUAGE plpgsql AS +--$mw$ +--BEGIN +--IF TG_OP = 'INSERT' THEN +-- NEW.titlevector = to_tsvector('default',REPLACE(NEW.page_title,'/',' ')); +--ELSIF NEW.page_title != OLD.page_title THEN +-- NEW.titlevector := to_tsvector('default',REPLACE(NEW.page_title,'/',' ')); +--END IF; +--RETURN NEW; +--END; +--$mw$; + +--CREATE TRIGGER ts2_page_title BEFORE INSERT OR UPDATE ON page +-- FOR EACH ROW EXECUTE PROCEDURE ts2_page_title(); + + +--ALTER TABLE pagecontent ADD textvector tsvector; +--CREATE FUNCTION ts2_page_text() RETURNS TRIGGER LANGUAGE plpgsql AS +--$mw$ +--BEGIN +--IF TG_OP = 'INSERT' THEN +-- NEW.textvector = to_tsvector('default',NEW.old_text); +--ELSIF NEW.old_text != OLD.old_text THEN +-- NEW.textvector := to_tsvector('default',NEW.old_text); +--END IF; +--RETURN NEW; +--END; +--$mw$; + +--CREATE TRIGGER ts2_page_text BEFORE INSERT OR UPDATE ON pagecontent +-- FOR EACH ROW EXECUTE PROCEDURE ts2_page_text(); + +-- These are added by the setup script due to version compatibility issues +-- If using 8.1, we switch from "gin" to "gist" + +--CREATE INDEX ts2_page_title ON page USING gin(titlevector); +--CREATE INDEX ts2_page_text ON pagecontent USING gin(textvector); + +--TODO +--CREATE FUNCTION add_interwiki (TEXT,INT,SMALLINT) RETURNS INT LANGUAGE SQL AS +--$mw$ +-- INSERT INTO interwiki (iw_prefix, iw_url, iw_local) VALUES ($1,$2,$3); +-- SELECT 1; +--$mw$; + +-- hack implementation +-- should be replaced with OmniFind, Contains(), etc +CREATE TABLE searchindex ( + si_page int NOT NULL, + si_title varchar(255) NOT NULL default '', + si_text clob NOT NULL +); + +-- This table is not used unless profiling is turned on +CREATE TABLE profiling ( + pf_count INTEGER NOT NULL DEFAULT 0, + pf_time NUMERIC(18,10) NOT NULL DEFAULT 0, + pf_memory NUMERIC(18,10) NOT NULL DEFAULT 0, + pf_name VARCHAR(255) NOT NULL, + pf_server VARCHAR(255) +); +CREATE UNIQUE INDEX pf_name_server ON profiling (pf_name, pf_server); + +CREATE TABLE protected_titles ( + pt_namespace SMALLINT NOT NULL, + pt_title VARCHAR(255) NOT NULL, + pt_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, + pt_reason clob(1K), + pt_timestamp TIMESTAMP NOT NULL, + pt_expiry TIMESTAMP , + pt_create_perm VARCHAR(255) NOT NULL DEFAULT '' +); +CREATE UNIQUE INDEX protected_titles_unique ON protected_titles(pt_namespace, pt_title); + + + +CREATE TABLE updatelog ( + ul_key VARCHAR(255) NOT NULL PRIMARY KEY +); + +CREATE SEQUENCE category_id_seq; +CREATE TABLE category ( + cat_id INTEGER NOT NULL PRIMARY KEY, + --PRIMARY KEY DEFAULT nextval('category_id_seq'), + cat_title VARCHAR(255) NOT NULL, + cat_pages INTEGER NOT NULL DEFAULT 0, + cat_subcats INTEGER NOT NULL DEFAULT 0, + cat_files INTEGER NOT NULL DEFAULT 0, + cat_hidden SMALLINT NOT NULL DEFAULT 0 +); +CREATE UNIQUE INDEX category_title ON category(cat_title); +CREATE INDEX category_pages ON category(cat_pages); + +CREATE TABLE mediawiki_version ( + type VARCHAR(255) NOT NULL, + mw_version VARCHAR(255) NOT NULL, + notes VARCHAR(255) , + + pg_version VARCHAR(255) , + pg_dbname VARCHAR(255) , + pg_user VARCHAR(255) , + pg_port VARCHAR(255) , + mw_schema VARCHAR(255) , + ts2_schema VARCHAR(255) , + ctype VARCHAR(255) , + + sql_version VARCHAR(255) , + sql_date VARCHAR(255) , + cdate TIMESTAMP NOT NULL DEFAULT CURRENT TIMESTAMP +); + +INSERT INTO mediawiki_version (type,mw_version,sql_version,sql_date) + VALUES ('Creation','??','$LastChangedRevision: 34049 $','$LastChangedDate: 2008-04-30 10:20:36 -0400 (Wed, 30 Apr 2008) $'); +