(bug 17028) Added support for IBM DB2 database. config/index.php has new interface...
authorLeons Petrazickis <leonsp@users.mediawiki.org>
Wed, 14 Jan 2009 22:20:15 +0000 (22:20 +0000)
committerLeons Petrazickis <leonsp@users.mediawiki.org>
Wed, 14 Jan 2009 22:20:15 +0000 (22:20 +0000)
config/index.php
includes/AutoLoader.php
includes/GlobalFunctions.php
includes/Revision.php
includes/SearchIBM_DB2.php [new file with mode: 0644]
includes/db/DatabaseIbm_db2.php [new file with mode: 0644]
maintenance/ibm_db2/README [new file with mode: 0644]
maintenance/ibm_db2/tables.sql [new file with mode: 0644]

index 0a2d290..915b8a3 100644 (file)
@@ -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';
+
 ?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
@@ -615,6 +621,12 @@ print "<li style='font-weight:bold;color:green;font-size:110%'>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( "<li>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() . "</li>\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( "<li>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 "<li style=\"list-style: none\">\n";
@@ -1459,6 +1495,25 @@ if( count( $errs ) ) {
                <p>Avoid exotic characters; something like <tt>mw_</tt> is good.</p>
        </div>
        </fieldset>
+       
+       <?php database_switcher('ibm_db2'); ?>
+       <div class="config-input"><?php
+               aField( $conf, "DBport_db2", "Database port:" );
+       ?></div>
+       <div class="config-input"><?php
+               aField( $conf, "DBmwschema", "Schema for mediawiki:" );
+       ?></div>
+       <div>Select one:</div>
+               <ul class="plain">
+               <li><?php aField( $conf, "DBcataloged", "Cataloged (DB2 installed locally)", "radio", "cataloged" ); ?></li>
+               <li><?php aField( $conf, "DBcataloged", "Uncataloged (remote DB2 through ODBC)", "radio", "uncataloged" ); ?></li>
+               </ul>
+       <div class="config-desc">
+               <p>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.</p>
+       </div>
+       </fieldset>
 
        <div class="config-input" style="padding:2em 0 3em">
                <label class='column'>&nbsp;</label>
@@ -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 = '';
index 8c47db0..009d984 100644 (file)
@@ -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',
index 878f3bc..a828e98 100644 (file)
@@ -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.');
        }
index 7938d88..37c2260 100644 (file)
@@ -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 (file)
index 0000000..eba401c
--- /dev/null
@@ -0,0 +1,247 @@
+<?php\r
+# Copyright (C) 2004 Brion Vibber <brion@pobox.com>\r
+# http://www.mediawiki.org/\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License along\r
+# with this program; if not, write to the Free Software Foundation, Inc.,\r
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+# http://www.gnu.org/copyleft/gpl.html\r
+\r
+/**\r
+ * @file\r
+ * @ingroup Search\r
+ */\r
+\r
+/**\r
+ * Search engine hook base class for IBM DB2\r
+ * @ingroup Search\r
+ */\r
+class SearchIBM_DB2 extends SearchEngine {\r
+       function __construct($db) {\r
+               $this->db = $db;\r
+       }\r
+\r
+       /**\r
+        * Perform a full text search query and return a result set.\r
+        *\r
+        * @param string $term - Raw search term\r
+        * @return IBM_DB2SearchResultSet\r
+        * @access public\r
+        */\r
+       function searchText( $term ) {\r
+               $resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), true)));\r
+               return new IBM_DB2SearchResultSet($resultSet, $this->searchTerms);\r
+       }\r
+\r
+       /**\r
+        * Perform a title-only search query and return a result set.\r
+        *\r
+        * @param string $term - Raw search term\r
+        * @return IBM_DB2SearchResultSet\r
+        * @access public\r
+        */\r
+       function searchTitle($term) {\r
+               $resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), false)));\r
+               return new MySQLSearchResultSet($resultSet, $this->searchTerms);\r
+       }\r
+\r
+\r
+       /**\r
+        * Return a partial WHERE clause to exclude redirects, if so set\r
+        * @return string\r
+        * @private\r
+        */\r
+       function queryRedirect() {\r
+               if ($this->showRedirects) {\r
+                       return '';\r
+               } else {\r
+                       return 'AND page_is_redirect=0';\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Return a partial WHERE clause to limit the search to the given namespaces\r
+        * @return string\r
+        * @private\r
+        */\r
+       function queryNamespaces() {\r
+               if( is_null($this->namespaces) )\r
+                       return '';\r
+               $namespaces = implode(',', $this->namespaces);\r
+               if ($namespaces == '') {\r
+                       $namespaces = '0';\r
+               }\r
+               return 'AND page_namespace IN (' . $namespaces . ')';\r
+       }\r
+\r
+       /**\r
+        * Return a LIMIT clause to limit results on the query.\r
+        * @return string\r
+        * @private\r
+        */\r
+       function queryLimit($sql) {\r
+               return $this->db->limitResult($sql, $this->limit, $this->offset);\r
+       }\r
+\r
+       /**\r
+        * Does not do anything for generic search engine\r
+        * subclasses may define this though\r
+        * @return string\r
+        * @private\r
+        */\r
+       function queryRanking($filteredTerm, $fulltext) {\r
+               // requires Net Search Extender or equivalent\r
+               // return ' ORDER BY score(1)';\r
+               return '';\r
+       }\r
+\r
+       /**\r
+        * Construct the full SQL query to do the search.\r
+        * The guts shoulds be constructed in queryMain()\r
+        * @param string $filteredTerm\r
+        * @param bool $fulltext\r
+        * @private\r
+        */\r
+       function getQuery( $filteredTerm, $fulltext ) {\r
+               return $this->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' .\r
+                       $this->queryRedirect() . ' ' .\r
+                       $this->queryNamespaces() . ' ' .\r
+                       $this->queryRanking( $filteredTerm, $fulltext ) . ' ');\r
+       }\r
+\r
+\r
+       /**\r
+        * Picks which field to index on, depending on what type of query.\r
+        * @param bool $fulltext\r
+        * @return string\r
+        */\r
+       function getIndexField($fulltext) {\r
+               return $fulltext ? 'si_text' : 'si_title';\r
+       }\r
+\r
+       /**\r
+        * Get the base part of the search query.\r
+        *\r
+        * @param string $filteredTerm\r
+        * @param bool $fulltext\r
+        * @return string\r
+        * @private\r
+        */\r
+       function queryMain( $filteredTerm, $fulltext ) {\r
+               $match = $this->parseQuery($filteredTerm, $fulltext);\r
+               $page        = $this->db->tableName('page');\r
+               $searchindex = $this->db->tableName('searchindex');\r
+               return 'SELECT page_id, page_namespace, page_title ' .\r
+                       "FROM $page,$searchindex " .\r
+                       'WHERE page_id=si_page AND ' . $match;\r
+       }\r
+\r
+       /** @todo document */\r
+       function parseQuery($filteredText, $fulltext) {\r
+               global $wgContLang;\r
+               $lc = SearchEngine::legalSearchChars();\r
+               $this->searchTerms = array();\r
+\r
+               # FIXME: This doesn't handle parenthetical expressions.\r
+               $m = array();\r
+               $q = array();\r
+\r
+               if (preg_match_all('/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',\r
+                         $filteredText, $m, PREG_SET_ORDER)) {\r
+                       foreach($m as $terms) {\r
+                               $q[] = $terms[1] . $wgContLang->stripForSearch($terms[2]);\r
+\r
+                               if (!empty($terms[3])) {\r
+                                       $regexp = preg_quote( $terms[3], '/' );\r
+                                       if ($terms[4])\r
+                                               $regexp .= "[0-9A-Za-z_]+";\r
+                               } else {\r
+                                       $regexp = preg_quote(str_replace('"', '', $terms[2]), '/');\r
+                               }\r
+                               $this->searchTerms[] = $regexp;\r
+                       }\r
+               }\r
+\r
+               $searchon = $this->db->strencode(join(',', $q));\r
+               $field = $this->getIndexField($fulltext);\r
+               \r
+               // requires Net Search Extender or equivalent\r
+               //return " CONTAINS($field, '$searchon') > 0 ";\r
+               \r
+               return " lcase($field) LIKE lcase('%$searchon%')";\r
+       }\r
+\r
+       /**\r
+        * Create or update the search index record for the given page.\r
+        * Title and text should be pre-processed.\r
+        *\r
+        * @param int $id\r
+        * @param string $title\r
+        * @param string $text\r
+        */\r
+       function update($id, $title, $text) {\r
+               $dbw = wfGetDB(DB_MASTER);\r
+               $dbw->replace('searchindex',\r
+                       array('si_page'),\r
+                       array(\r
+                               'si_page' => $id,\r
+                               'si_title' => $title,\r
+                               'si_text' => $text\r
+                       ), 'SearchIBM_DB2::update' );\r
+               // ?\r
+               //$dbw->query("CALL ctx_ddl.sync_index('si_text_idx')");\r
+               //$dbw->query("CALL ctx_ddl.sync_index('si_title_idx')");\r
+       }\r
+\r
+       /**\r
+        * Update a search index record's title only.\r
+        * Title should be pre-processed.\r
+        *\r
+        * @param int $id\r
+        * @param string $title\r
+        */\r
+       function updateTitle($id, $title) {\r
+               $dbw = wfGetDB(DB_MASTER);\r
+\r
+               $dbw->update('searchindex',\r
+                       array('si_title' => $title),\r
+                       array('si_page'  => $id),\r
+                       'SearchIBM_DB2::updateTitle',\r
+                       array());\r
+       }\r
+}\r
+\r
+/**\r
+ * @ingroup Search\r
+ */\r
+class IBM_DB2SearchResultSet extends SearchResultSet {\r
+       function __construct($resultSet, $terms) {\r
+               $this->mResultSet = $resultSet;\r
+               $this->mTerms = $terms;\r
+       }\r
+\r
+       function termMatches() {\r
+               return $this->mTerms;\r
+       }\r
+\r
+       function numRows() {\r
+               return $this->mResultSet->numRows();\r
+       }\r
+\r
+       function next() {\r
+               $row = $this->mResultSet->fetchObject();\r
+               if ($row === false)\r
+                       return false;\r
+               return new SearchResult($row);\r
+       }\r
+}\r
diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php
new file mode 100644 (file)
index 0000000..56dc3c6
--- /dev/null
@@ -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;
+       }\r
+       \r
+       /**\r
+        * Override if introduced to base Database class\r
+        */\r
+       public function initial_setup() {\r
+               // do nothing\r
+       }
+       
+       /**
+        * 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>";
+               }
+       }
+\r
+       /**\r
+        * Escapes strings
+        * Doesn't escape numbers\r
+        * @param string s string to escape\r
+        * @return escaped string\r
+        */
+       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;
+       }
+       \r
+       /**\r
+        * Switch into the database schema\r
+        */
+       protected function applySchema() {
+               if ( !($this->mSchemaSet) ) {\r
+                       $this->mSchemaSet = true;
+                       $this->begin();
+                       $this->doQuery("SET SCHEMA = $this->mSchema");\r
+                       $this->commit();
+               }       
+       }\r
+       \r
+       /**\r
+        * Start a transaction (mandatory)\r
+        */\r
+       public function begin() {\r
+               // turn off auto-commit
+               db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF);
+               $this->mTrxLevel = 1;\r
+       }\r
+       \r
+       /**\r
+        * End a transaction
+        * Must have a preceding begin()\r
+        */\r
+       public function commit() {\r
+               db2_commit($this->mConn);
+               // turn auto-commit back on
+               db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON);
+               $this->mTrxLevel = 0;\r
+       }\r
+       \r
+       /**\r
+        * Cancel a transaction\r
+        */\r
+       public function rollback() {\r
+               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;\r
+       }
+       
+       /**
+        * 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/maintenance/ibm_db2/README b/maintenance/ibm_db2/README
new file mode 100644 (file)
index 0000000..bbd076f
--- /dev/null
@@ -0,0 +1,41 @@
+== Syntax differences between other databases and IBM DB2 ==\r
+{| border cellspacing=0 cellpadding=4\r
+!MySQL!!IBM DB2\r
+|-\r
+\r
+|SELECT 1 FROM $table LIMIT 1\r
+|SELECT COUNT(*) FROM SYSIBM.SYSTABLES ST\r
+WHERE ST.NAME = '$table' AND ST.CREATOR = '$schema'\r
+|-\r
+|MySQL code tries to read one row and interprets lack of error as proof of existence.\r
+|DB2 code counts the number of TABLES of that name in the database. There ought to be 1 for it to exist.\r
+|-\r
+|BEGIN\r
+|(implicit)\r
+|-\r
+|TEXT\r
+|VARCHAR(255) or CLOB\r
+|-\r
+|TIMESTAMPTZ\r
+|TIMESTAMP\r
+|-\r
+|BYTEA\r
+|VARGRAPHIC(255)\r
+|-\r
+|DEFAULT nextval('some_kind_of_sequence'),\r
+|GENERATED ALWAYS AS IDENTITY (START WITH 0, INCREMENT BY 1),\r
+|-\r
+|CIDR\r
+|VARCHAR(255)\r
+|-\r
+|LIMIT 10\r
+|FETCH FIRST 10 ROWS ONLY\r
+|-\r
+|ROLLBACK TO\r
+|ROLLBACK TO SAVEPOINT\r
+|-\r
+|RELEASE\r
+|RELEASE SAVEPOINT\r
+|}\r
+== See also ==\r
+*[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 (file)
index 0000000..d0da55d
--- /dev/null
@@ -0,0 +1,604 @@
+-- DB2\r
+\r
+-- SQL to create the initial tables for the MediaWiki database.\r
+-- This is read and executed by the install script; you should\r
+-- not have to run it by itself unless doing a manual install.\r
+-- This is the IBM DB2 version.\r
+-- For information about each table, please see the notes in maintenance/tables.sql\r
+-- Please make sure all dollar-quoting uses $mw$ at the start of the line\r
+-- TODO: Change CHAR/SMALLINT to BOOL (still used in a non-bool fashion in PHP code)\r
+\r
+\r
+\r
+\r
+CREATE SEQUENCE user_user_id_seq AS INTEGER START WITH 0 INCREMENT BY 1;\r
+CREATE TABLE mwuser ( -- replace reserved word 'user'\r
+  user_id                   INTEGER  NOT NULL PRIMARY KEY, -- DEFAULT nextval('user_user_id_seq'),\r
+  user_name                 VARCHAR(255)     NOT NULL  UNIQUE,\r
+  user_real_name            VARCHAR(255),\r
+  user_password             clob(1K),\r
+  user_newpassword          clob(1K),\r
+  user_newpass_time         TIMESTAMP,\r
+  user_token                VARCHAR(255),\r
+  user_email                VARCHAR(255),\r
+  user_email_token          VARCHAR(255),\r
+  user_email_token_expires  TIMESTAMP,\r
+  user_email_authenticated  TIMESTAMP,\r
+  user_options              CLOB(64K),\r
+  user_touched              TIMESTAMP,\r
+  user_registration         TIMESTAMP,\r
+  user_editcount            INTEGER\r
+);\r
+CREATE INDEX user_email_token_idx ON mwuser (user_email_token);\r
+\r
+-- Create a dummy user to satisfy fk contraints especially with revisions\r
+INSERT INTO mwuser\r
+  VALUES (NEXTVAL FOR user_user_id_seq,'Anonymous','', NULL,NULL,CURRENT_TIMESTAMP,NULL, NULL,NULL,NULL,NULL, NULL,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0);\r
+\r
+CREATE TABLE user_groups (\r
+  ug_user   INTEGER    REFERENCES mwuser(user_id) ON DELETE CASCADE,\r
+  ug_group  VARCHAR(255)     NOT NULL\r
+);\r
+CREATE UNIQUE INDEX user_groups_unique ON user_groups (ug_user, ug_group);\r
+\r
+CREATE TABLE user_newtalk (\r
+  user_id              INTEGER      NOT NULL  REFERENCES mwuser(user_id) ON DELETE CASCADE,\r
+  user_ip              VARCHAR(255),\r
+  user_last_timestamp  TIMESTAMP\r
+);\r
+CREATE INDEX user_newtalk_id_idx ON user_newtalk (user_id);\r
+CREATE INDEX user_newtalk_ip_idx ON user_newtalk (user_ip);\r
+\r
+\r
+CREATE SEQUENCE page_page_id_seq;\r
+CREATE TABLE page (\r
+  page_id            INTEGER        NOT NULL  PRIMARY KEY, -- DEFAULT NEXT VALUE FOR user_user_id_seq,\r
+  page_namespace     SMALLINT       NOT NULL,\r
+  page_title         VARCHAR(255)   NOT NULL,\r
+  page_restrictions  clob(1K),\r
+  page_counter       BIGINT         NOT NULL  DEFAULT 0,\r
+  page_is_redirect   SMALLINT       NOT NULL  DEFAULT 0,\r
+  page_is_new        SMALLINT       NOT NULL  DEFAULT 0,\r
+  page_random        NUMERIC(15,14) NOT NULL,\r
+  page_touched       TIMESTAMP,\r
+  page_latest        INTEGER        NOT NULL, -- FK?\r
+  page_len           INTEGER        NOT NULL\r
+);\r
+CREATE UNIQUE INDEX page_unique_name ON page (page_namespace, page_title);\r
+--CREATE INDEX page_main_title         ON page (page_title) WHERE page_namespace = 0;\r
+--CREATE INDEX page_talk_title         ON page (page_title) WHERE page_namespace = 1;\r
+--CREATE INDEX page_user_title         ON page (page_title) WHERE page_namespace = 2;\r
+--CREATE INDEX page_utalk_title        ON page (page_title) WHERE page_namespace = 3;\r
+--CREATE INDEX page_project_title      ON page (page_title) WHERE page_namespace = 4;\r
+CREATE INDEX page_random_idx         ON page (page_random);\r
+CREATE INDEX page_len_idx            ON page (page_len);\r
+\r
+--CREATE FUNCTION page_deleted() RETURNS TRIGGER LANGUAGE plpgsql AS\r
+--$mw$\r
+--BEGIN\r
+--DELETE FROM recentchanges WHERE rc_namespace = OLD.page_namespace AND rc_title = OLD.page_title;\r
+--RETURN NULL;\r
+--END;\r
+--$mw$;\r
+\r
+--CREATE TRIGGER page_deleted AFTER DELETE ON page\r
+--  FOR EACH ROW EXECUTE PROCEDURE page_deleted();\r
+\r
+CREATE SEQUENCE rev_rev_id_val;\r
+CREATE TABLE revision (\r
+  rev_id          INTEGER      NOT NULL  UNIQUE, --DEFAULT nextval('rev_rev_id_val'),\r
+  rev_page        INTEGER      REFERENCES page (page_id) ON DELETE CASCADE,\r
+  rev_text_id     INTEGER, -- FK\r
+  rev_comment     clob(1K),                    -- changed from VARCHAR(255)\r
+  rev_user        INTEGER      NOT NULL  REFERENCES mwuser(user_id) ON DELETE RESTRICT,\r
+  rev_user_text   VARCHAR(255) NOT NULL,\r
+  rev_timestamp   TIMESTAMP    NOT NULL,\r
+  rev_minor_edit  SMALLINT     NOT NULL  DEFAULT 0,\r
+  rev_deleted     SMALLINT     NOT NULL  DEFAULT 0,\r
+  rev_len         INTEGER,\r
+  rev_parent_id   INTEGER\r
+);\r
+CREATE UNIQUE INDEX revision_unique ON revision (rev_page, rev_id);\r
+CREATE INDEX rev_text_id_idx        ON revision (rev_text_id);\r
+CREATE INDEX rev_timestamp_idx      ON revision (rev_timestamp);\r
+CREATE INDEX rev_user_idx           ON revision (rev_user);\r
+CREATE INDEX rev_user_text_idx      ON revision (rev_user_text);\r
+\r
+\r
+CREATE SEQUENCE text_old_id_val;\r
+CREATE TABLE pagecontent ( -- replaces reserved word 'text'\r
+  old_id     INTEGER  NOT NULL,\r
+  --PRIMARY KEY DEFAULT nextval('text_old_id_val'),\r
+  old_text   CLOB(16M),\r
+  old_flags  clob(1K)\r
+);\r
+\r
+CREATE SEQUENCE pr_id_val;\r
+CREATE TABLE page_restrictions (\r
+  pr_id      INTEGER      NOT NULL  UNIQUE,\r
+  --DEFAULT nextval('pr_id_val'),\r
+  pr_page    INTEGER              NOT NULL\r
+  --(used to be nullable)\r
+    REFERENCES page (page_id) ON DELETE CASCADE,\r
+  pr_type    VARCHAR(255)         NOT NULL,\r
+  pr_level   VARCHAR(255)         NOT NULL,\r
+  pr_cascade SMALLINT             NOT NULL,\r
+  pr_user    INTEGER,\r
+  pr_expiry  TIMESTAMP,\r
+  PRIMARY KEY (pr_page, pr_type)\r
+);\r
+--ALTER TABLE page_restrictions ADD CONSTRAINT page_restrictions_pk PRIMARY KEY (pr_page,pr_type);\r
+\r
+CREATE TABLE page_props (\r
+  pp_page      INTEGER  NOT NULL  REFERENCES page (page_id) ON DELETE CASCADE,\r
+  pp_propname  VARCHAR(255)     NOT NULL,\r
+  pp_value     CLOB(64K)     NOT NULL,\r
+  PRIMARY KEY (pp_page,pp_propname) \r
+);\r
+--ALTER TABLE page_props ADD CONSTRAINT page_props_pk PRIMARY KEY (pp_page,pp_propname);\r
+CREATE INDEX page_props_propname ON page_props (pp_propname);\r
+\r
+\r
+\r
+CREATE TABLE archive (\r
+  ar_namespace   SMALLINT     NOT NULL,\r
+  ar_title       VARCHAR(255)         NOT NULL,\r
+  ar_text        CLOB(16M),\r
+  ar_page_id     INTEGER,\r
+  ar_parent_id   INTEGER,\r
+  ar_comment     clob(1K),\r
+  ar_user        INTEGER  REFERENCES mwuser(user_id) ON DELETE SET NULL,\r
+  ar_user_text   VARCHAR(255)         NOT NULL,\r
+  ar_timestamp   TIMESTAMP  NOT NULL,\r
+  ar_minor_edit  SMALLINT     NOT NULL  DEFAULT 0,\r
+  ar_flags       clob(1K),\r
+  ar_rev_id      INTEGER,\r
+  ar_text_id     INTEGER,\r
+  ar_deleted     SMALLINT     NOT NULL  DEFAULT 0,\r
+  ar_len         INTEGER\r
+);\r
+CREATE INDEX archive_name_title_timestamp ON archive (ar_namespace,ar_title,ar_timestamp);\r
+CREATE INDEX archive_user_text            ON archive (ar_user_text);\r
+\r
+\r
+\r
+CREATE TABLE redirect (\r
+  rd_from       INTEGER  NOT NULL  REFERENCES page(page_id) ON DELETE CASCADE,\r
+  rd_namespace  SMALLINT NOT NULL,\r
+  rd_title      VARCHAR(255)     NOT NULL\r
+);\r
+CREATE INDEX redirect_ns_title ON redirect (rd_namespace,rd_title,rd_from);\r
+\r
+\r
+CREATE TABLE pagelinks (\r
+  pl_from       INTEGER   NOT NULL  REFERENCES page(page_id) ON DELETE CASCADE,\r
+  pl_namespace  SMALLINT  NOT NULL,\r
+  pl_title      VARCHAR(255)      NOT NULL\r
+);\r
+CREATE UNIQUE INDEX pagelink_unique ON pagelinks (pl_from,pl_namespace,pl_title);\r
+\r
+CREATE TABLE templatelinks (\r
+  tl_from       INTEGER  NOT NULL  REFERENCES page(page_id) ON DELETE CASCADE,\r
+  tl_namespace  SMALLINT NOT NULL,\r
+  tl_title      VARCHAR(255)     NOT NULL\r
+);\r
+CREATE UNIQUE INDEX templatelinks_unique ON templatelinks (tl_namespace,tl_title,tl_from);\r
+\r
+CREATE TABLE imagelinks (\r
+  il_from  INTEGER  NOT NULL  REFERENCES page(page_id) ON DELETE CASCADE,\r
+  il_to    VARCHAR(255)     NOT NULL\r
+);\r
+CREATE UNIQUE INDEX il_from ON imagelinks (il_to,il_from);\r
+\r
+CREATE TABLE categorylinks (\r
+  cl_from       INTEGER      NOT NULL  REFERENCES page(page_id) ON DELETE CASCADE,\r
+  cl_to         VARCHAR(255)         NOT NULL,\r
+  cl_sortkey    VARCHAR(255),\r
+  cl_timestamp  TIMESTAMP  NOT NULL\r
+);\r
+CREATE UNIQUE INDEX cl_from ON categorylinks (cl_from, cl_to);\r
+CREATE INDEX cl_sortkey     ON categorylinks (cl_to, cl_sortkey, cl_from);\r
+\r
+\r
+\r
+CREATE TABLE externallinks (\r
+  el_from   INTEGER  NOT NULL  REFERENCES page(page_id) ON DELETE CASCADE,\r
+  el_to     VARCHAR(255)     NOT NULL,\r
+  el_index  VARCHAR(255)     NOT NULL\r
+);\r
+CREATE INDEX externallinks_from_to ON externallinks (el_from,el_to);\r
+CREATE INDEX externallinks_index   ON externallinks (el_index);\r
+\r
+CREATE TABLE langlinks (\r
+  ll_from    INTEGER  NOT NULL  REFERENCES page (page_id) ON DELETE CASCADE,\r
+  ll_lang    VARCHAR(255),\r
+  ll_title   VARCHAR(255)\r
+);\r
+CREATE UNIQUE INDEX langlinks_unique ON langlinks (ll_from,ll_lang);\r
+CREATE INDEX langlinks_lang_title    ON langlinks (ll_lang,ll_title);\r
+\r
+\r
+CREATE TABLE site_stats (\r
+  ss_row_id         INTEGER  NOT NULL  UNIQUE,\r
+  ss_total_views    INTEGER            DEFAULT 0,\r
+  ss_total_edits    INTEGER            DEFAULT 0,\r
+  ss_good_articles  INTEGER             DEFAULT 0,\r
+  ss_total_pages    INTEGER            DEFAULT -1,\r
+  ss_users          INTEGER            DEFAULT -1,\r
+  ss_admins         INTEGER            DEFAULT -1,\r
+  ss_images         INTEGER            DEFAULT 0\r
+);\r
+\r
+CREATE TABLE hitcounter (\r
+  hc_id  BIGINT  NOT NULL\r
+);\r
+\r
+CREATE SEQUENCE ipblocks_ipb_id_val;\r
+CREATE TABLE ipblocks (\r
+  ipb_id                INTEGER      NOT NULL  PRIMARY KEY,\r
+  --DEFAULT nextval('ipblocks_ipb_id_val'),\r
+  ipb_address           VARCHAR(255),\r
+  ipb_user              INTEGER            REFERENCES mwuser(user_id) ON DELETE SET NULL,\r
+  ipb_by                INTEGER      NOT NULL  REFERENCES mwuser(user_id) ON DELETE CASCADE,\r
+  ipb_by_text           VARCHAR(255)         NOT NULL  DEFAULT '',\r
+  ipb_reason            VARCHAR(255)         NOT NULL,\r
+  ipb_timestamp         TIMESTAMP  NOT NULL,\r
+  ipb_auto              SMALLINT     NOT NULL  DEFAULT 0,\r
+  ipb_anon_only         SMALLINT     NOT NULL  DEFAULT 0,\r
+  ipb_create_account    SMALLINT     NOT NULL  DEFAULT 1,\r
+  ipb_enable_autoblock  SMALLINT     NOT NULL  DEFAULT 1,\r
+  ipb_expiry            TIMESTAMP  NOT NULL,\r
+  ipb_range_start       VARCHAR(255),\r
+  ipb_range_end         VARCHAR(255),\r
+  ipb_deleted           SMALLINT     NOT NULL  DEFAULT 0,\r
+  ipb_block_email       SMALLINT     NOT NULL  DEFAULT 0\r
+\r
+);\r
+CREATE INDEX ipb_address ON ipblocks (ipb_address);\r
+CREATE INDEX ipb_user    ON ipblocks (ipb_user);\r
+CREATE INDEX ipb_range   ON ipblocks (ipb_range_start,ipb_range_end);\r
+\r
+\r
+\r
+CREATE TABLE image (\r
+  img_name         VARCHAR(255)      NOT NULL  PRIMARY KEY,\r
+  img_size         INTEGER   NOT NULL,\r
+  img_width        INTEGER   NOT NULL,\r
+  img_height       INTEGER   NOT NULL,\r
+  img_metadata     CLOB(16M)     NOT NULL  DEFAULT '',\r
+  img_bits         SMALLINT,\r
+  img_media_type   VARCHAR(255),\r
+  img_major_mime   VARCHAR(255)                DEFAULT 'unknown',\r
+  img_minor_mime   VARCHAR(255)                DEFAULT 'unknown',\r
+  img_description  clob(1K)      NOT NULL      DEFAULT '',\r
+  img_user         INTEGER         REFERENCES mwuser(user_id) ON DELETE SET NULL,\r
+  img_user_text    VARCHAR(255)      NOT NULL DEFAULT '',\r
+  img_timestamp    TIMESTAMP,\r
+  img_sha1         VARCHAR(255)      NOT NULL  DEFAULT ''\r
+);\r
+CREATE INDEX img_size_idx      ON image (img_size);\r
+CREATE INDEX img_timestamp_idx ON image (img_timestamp);\r
+CREATE INDEX img_sha1          ON image (img_sha1);\r
+\r
+CREATE TABLE oldimage (\r
+  oi_name          VARCHAR(255)         NOT NULL,\r
+  oi_archive_name  VARCHAR(255)         NOT NULL,\r
+  oi_size          INTEGER      NOT NULL,\r
+  oi_width         INTEGER      NOT NULL,\r
+  oi_height        INTEGER      NOT NULL,\r
+  oi_bits          SMALLINT     NOT NULL,\r
+  oi_description   clob(1K),\r
+  oi_user          INTEGER            REFERENCES mwuser(user_id) ON DELETE SET NULL,\r
+  oi_user_text     VARCHAR(255)         NOT NULL,\r
+  oi_timestamp     TIMESTAMP  NOT NULL,\r
+  oi_metadata      CLOB(16M)        NOT NULL DEFAULT '',\r
+  oi_media_type    VARCHAR(255)             ,\r
+  oi_major_mime    VARCHAR(255)         NOT NULL DEFAULT 'unknown',\r
+  oi_minor_mime    VARCHAR(255)         NOT NULL DEFAULT 'unknown',\r
+  oi_deleted       SMALLINT     NOT NULL DEFAULT 0,\r
+  oi_sha1          VARCHAR(255)         NOT NULL DEFAULT '',\r
+  FOREIGN KEY (oi_name) REFERENCES image(img_name) ON DELETE CASCADE\r
+);\r
+--ALTER TABLE oldimage ADD CONSTRAINT oldimage_oi_name_fkey_cascade FOREIGN KEY (oi_name) REFERENCES image(img_name) ON DELETE CASCADE;\r
+CREATE INDEX oi_name_timestamp    ON oldimage (oi_name,oi_timestamp);\r
+CREATE INDEX oi_name_archive_name ON oldimage (oi_name,oi_archive_name);\r
+CREATE INDEX oi_sha1              ON oldimage (oi_sha1);\r
+\r
+\r
+CREATE SEQUENCE filearchive_fa_id_seq;\r
+CREATE TABLE filearchive (\r
+  fa_id                 INTEGER      NOT NULL PRIMARY KEY,\r
+  --PRIMARY KEY DEFAULT nextval('filearchive_fa_id_seq'),\r
+  fa_name               VARCHAR(255)         NOT NULL,\r
+  fa_archive_name       VARCHAR(255),\r
+  fa_storage_group      VARCHAR(255),\r
+  fa_storage_key        VARCHAR(255),\r
+  fa_deleted_user       INTEGER            REFERENCES mwuser(user_id) ON DELETE SET NULL,\r
+  fa_deleted_timestamp  TIMESTAMP  NOT NULL,\r
+  fa_deleted_reason     VARCHAR(255),\r
+  fa_size               INTEGER      NOT NULL,\r
+  fa_width              INTEGER      NOT NULL,\r
+  fa_height             INTEGER      NOT NULL,\r
+  fa_metadata           CLOB(16M)        NOT NULL  DEFAULT '',\r
+  fa_bits               SMALLINT,\r
+  fa_media_type         VARCHAR(255),\r
+  fa_major_mime         VARCHAR(255)                   DEFAULT 'unknown',\r
+  fa_minor_mime         VARCHAR(255)                   DEFAULT 'unknown',\r
+  fa_description        clob(1K)         NOT NULL,\r
+  fa_user               INTEGER            REFERENCES mwuser(user_id) ON DELETE SET NULL,\r
+  fa_user_text          VARCHAR(255)         NOT NULL,\r
+  fa_timestamp          TIMESTAMP,\r
+  fa_deleted            SMALLINT     NOT NULL DEFAULT 0\r
+);\r
+CREATE INDEX fa_name_time ON filearchive (fa_name, fa_timestamp);\r
+CREATE INDEX fa_dupe      ON filearchive (fa_storage_group, fa_storage_key);\r
+CREATE INDEX fa_notime    ON filearchive (fa_deleted_timestamp);\r
+CREATE INDEX fa_nouser    ON filearchive (fa_deleted_user);\r
+\r
+CREATE SEQUENCE rc_rc_id_seq;\r
+CREATE TABLE recentchanges (\r
+  rc_id              INTEGER      NOT NULL PRIMARY KEY,\r
+  --PRIMARY KEY DEFAULT nextval('rc_rc_id_seq'),\r
+  rc_timestamp       TIMESTAMP  NOT NULL,\r
+  rc_cur_time        TIMESTAMP  NOT NULL,\r
+  rc_user            INTEGER        REFERENCES mwuser(user_id) ON DELETE SET NULL,\r
+  rc_user_text       VARCHAR(255)         NOT NULL,\r
+  rc_namespace       SMALLINT     NOT NULL,\r
+  rc_title           VARCHAR(255)         NOT NULL,\r
+  rc_comment         VARCHAR(255),\r
+  rc_minor           SMALLINT     NOT NULL  DEFAULT 0,\r
+  rc_bot             SMALLINT     NOT NULL  DEFAULT 0,\r
+  rc_new             SMALLINT     NOT NULL  DEFAULT 0,\r
+  rc_cur_id          INTEGER            REFERENCES page(page_id) ON DELETE SET NULL,\r
+  rc_this_oldid      INTEGER      NOT NULL,\r
+  rc_last_oldid      INTEGER      NOT NULL,\r
+  rc_type            SMALLINT     NOT NULL  DEFAULT 0,\r
+  rc_moved_to_ns     SMALLINT,\r
+  rc_moved_to_title  VARCHAR(255),\r
+  rc_patrolled       SMALLINT     NOT NULL  DEFAULT 0,\r
+  rc_ip              VARCHAR(255),     -- was CIDR type\r
+  rc_old_len         INTEGER,\r
+  rc_new_len         INTEGER,\r
+  rc_deleted         SMALLINT     NOT NULL  DEFAULT 0,\r
+  rc_logid           INTEGER      NOT NULL  DEFAULT 0,\r
+  rc_log_type        VARCHAR(255),\r
+  rc_log_action      VARCHAR(255),\r
+  rc_params          CLOB(64K)\r
+);\r
+CREATE INDEX rc_timestamp       ON recentchanges (rc_timestamp);\r
+CREATE INDEX rc_namespace_title ON recentchanges (rc_namespace, rc_title);\r
+CREATE INDEX rc_cur_id          ON recentchanges (rc_cur_id);\r
+CREATE INDEX new_name_timestamp ON recentchanges (rc_new, rc_namespace, rc_timestamp);\r
+CREATE INDEX rc_ip              ON recentchanges (rc_ip);\r
+\r
+\r
+\r
+CREATE TABLE watchlist (\r
+  wl_user                   INTEGER     NOT NULL  REFERENCES mwuser(user_id) ON DELETE CASCADE,\r
+  wl_namespace              SMALLINT    NOT NULL  DEFAULT 0,\r
+  wl_title                  VARCHAR(255)        NOT NULL,\r
+  wl_notificationtimestamp  TIMESTAMP\r
+);\r
+CREATE UNIQUE INDEX wl_user_namespace_title ON watchlist (wl_namespace, wl_title, wl_user);\r
+\r
+\r
+CREATE TABLE math (\r
+  math_inputhash              VARGRAPHIC(255)     NOT NULL  UNIQUE,\r
+  math_outputhash             VARGRAPHIC(255)     NOT NULL,\r
+  math_html_conservativeness  SMALLINT  NOT NULL,\r
+  math_html                   VARCHAR(255),\r
+  math_mathml                 VARCHAR(255)\r
+);\r
+\r
+\r
+CREATE TABLE interwiki (\r
+  iw_prefix  VARCHAR(255)      NOT NULL  UNIQUE,\r
+  iw_url     CLOB(64K)      NOT NULL,\r
+  iw_local   SMALLINT  NOT NULL,\r
+  iw_trans   SMALLINT  NOT NULL  DEFAULT 0\r
+);\r
+\r
+\r
+CREATE TABLE querycache (\r
+  qc_type       VARCHAR(255)      NOT NULL,\r
+  qc_value      INTEGER   NOT NULL,\r
+  qc_namespace  SMALLINT  NOT NULL,\r
+  qc_title      VARCHAR(255)      NOT NULL\r
+);\r
+CREATE INDEX querycache_type_value ON querycache (qc_type, qc_value);\r
+\r
+\r
+\r
+CREATE  TABLE querycache_info (\r
+  qci_type        VARCHAR(255)              UNIQUE NOT NULL,\r
+  qci_timestamp  TIMESTAMP\r
+);\r
+\r
+\r
+CREATE TABLE querycachetwo (\r
+  qcc_type           VARCHAR(255)     NOT NULL,\r
+  qcc_value         INTEGER  NOT NULL  DEFAULT 0,\r
+  qcc_namespace     INTEGER  NOT NULL  DEFAULT 0,\r
+  qcc_title          VARCHAR(255)     NOT NULL  DEFAULT '',\r
+  qcc_namespacetwo  INTEGER  NOT NULL  DEFAULT 0,\r
+  qcc_titletwo       VARCHAR(255)     NOT NULL  DEFAULT ''\r
+);\r
+CREATE INDEX querycachetwo_type_value ON querycachetwo (qcc_type, qcc_value);\r
+CREATE INDEX querycachetwo_title      ON querycachetwo (qcc_type,qcc_namespace,qcc_title);\r
+CREATE INDEX querycachetwo_titletwo   ON querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo);\r
+\r
+CREATE TABLE objectcache (\r
+  keyname   VARCHAR(255)           NOT NULL        UNIQUE, -- was nullable\r
+  value     CLOB(16M)                   NOT NULL  DEFAULT '',\r
+  exptime  TIMESTAMP               NOT NULL\r
+);\r
+CREATE INDEX objectcacache_exptime ON objectcache (exptime);\r
+\r
+\r
+\r
+CREATE TABLE transcache (\r
+  tc_url       VARCHAR(255)         NOT NULL  UNIQUE,\r
+  tc_contents  VARCHAR(255)         NOT NULL,\r
+  tc_time      TIMESTAMP  NOT NULL\r
+);\r
+\r
+CREATE SEQUENCE log_log_id_seq;\r
+CREATE TABLE logging (\r
+  log_id          INTEGER      NOT NULL PRIMARY KEY,\r
+  --PRIMARY KEY DEFAULT nextval('log_log_id_seq'),\r
+  log_type        VARCHAR(255)         NOT NULL,\r
+  log_action      VARCHAR(255)         NOT NULL,\r
+  log_timestamp   TIMESTAMP  NOT NULL,\r
+  log_user        INTEGER                REFERENCES mwuser(user_id) ON DELETE SET NULL,\r
+  log_namespace   SMALLINT     NOT NULL,\r
+  log_title       VARCHAR(255)         NOT NULL,\r
+  log_comment     VARCHAR(255),\r
+  log_params      CLOB(64K),\r
+  log_deleted     SMALLINT     NOT NULL DEFAULT 0\r
+);\r
+CREATE INDEX logging_type_name ON logging (log_type, log_timestamp);\r
+CREATE INDEX logging_user_time ON logging (log_timestamp, log_user);\r
+CREATE INDEX logging_page_time ON logging (log_namespace, log_title, log_timestamp);\r
+\r
+CREATE SEQUENCE trackbacks_tb_id_seq;\r
+CREATE TABLE trackbacks (\r
+  tb_id     INTEGER  NOT NULL PRIMARY KEY,\r
+  --PRIMARY KEY DEFAULT nextval('trackbacks_tb_id_seq'),\r
+  tb_page   INTEGER            REFERENCES page(page_id) ON DELETE CASCADE,\r
+  tb_title  VARCHAR(255)     NOT NULL,\r
+  tb_url    CLOB(64K)       NOT NULL,\r
+  tb_ex     VARCHAR(255),\r
+  tb_name   VARCHAR(255)\r
+);\r
+CREATE INDEX trackback_page ON trackbacks (tb_page);\r
+\r
+\r
+CREATE SEQUENCE job_job_id_seq;\r
+CREATE TABLE job (\r
+  job_id         INTEGER   NOT NULL PRIMARY KEY,\r
+  --PRIMARY KEY DEFAULT nextval('job_job_id_seq'),\r
+  job_cmd        VARCHAR(255)      NOT NULL,\r
+  job_namespace  SMALLINT  NOT NULL,\r
+  job_title      VARCHAR(255)      NOT NULL,\r
+  job_params     CLOB(64K)      NOT NULL\r
+);\r
+CREATE INDEX job_cmd_namespace_title ON job (job_cmd, job_namespace, job_title);\r
+\r
+\r
+\r
+-- Postgres' Tsearch2 dropped\r
+--ALTER TABLE page ADD titlevector tsvector;\r
+--CREATE FUNCTION ts2_page_title() RETURNS TRIGGER LANGUAGE plpgsql AS\r
+--$mw$\r
+--BEGIN\r
+--IF TG_OP = 'INSERT' THEN\r
+--  NEW.titlevector = to_tsvector('default',REPLACE(NEW.page_title,'/',' '));\r
+--ELSIF NEW.page_title != OLD.page_title THEN\r
+--  NEW.titlevector := to_tsvector('default',REPLACE(NEW.page_title,'/',' '));\r
+--END IF;\r
+--RETURN NEW;\r
+--END;\r
+--$mw$;\r
+\r
+--CREATE TRIGGER ts2_page_title BEFORE INSERT OR UPDATE ON page\r
+--  FOR EACH ROW EXECUTE PROCEDURE ts2_page_title();\r
+\r
+\r
+--ALTER TABLE pagecontent ADD textvector tsvector;\r
+--CREATE FUNCTION ts2_page_text() RETURNS TRIGGER LANGUAGE plpgsql AS\r
+--$mw$\r
+--BEGIN\r
+--IF TG_OP = 'INSERT' THEN\r
+--  NEW.textvector = to_tsvector('default',NEW.old_text);\r
+--ELSIF NEW.old_text != OLD.old_text THEN\r
+--  NEW.textvector := to_tsvector('default',NEW.old_text);\r
+--END IF;\r
+--RETURN NEW;\r
+--END;\r
+--$mw$;\r
+\r
+--CREATE TRIGGER ts2_page_text BEFORE INSERT OR UPDATE ON pagecontent\r
+--  FOR EACH ROW EXECUTE PROCEDURE ts2_page_text();\r
+\r
+-- These are added by the setup script due to version compatibility issues\r
+-- If using 8.1, we switch from "gin" to "gist"\r
+\r
+--CREATE INDEX ts2_page_title ON page USING gin(titlevector);\r
+--CREATE INDEX ts2_page_text ON pagecontent USING gin(textvector);\r
+\r
+--TODO\r
+--CREATE FUNCTION add_interwiki (TEXT,INT,SMALLINT) RETURNS INT LANGUAGE SQL AS\r
+--$mw$\r
+--  INSERT INTO interwiki (iw_prefix, iw_url, iw_local) VALUES ($1,$2,$3);\r
+--  SELECT 1;\r
+--$mw$;\r
+\r
+-- hack implementation\r
+-- should be replaced with OmniFind, Contains(), etc\r
+CREATE TABLE searchindex (\r
+  si_page int NOT NULL,\r
+  si_title varchar(255) NOT NULL default '',\r
+  si_text clob NOT NULL\r
+);\r
+\r
+-- This table is not used unless profiling is turned on\r
+CREATE TABLE profiling (\r
+  pf_count   INTEGER         NOT NULL DEFAULT 0,\r
+  pf_time    NUMERIC(18,10)  NOT NULL DEFAULT 0,\r
+  pf_memory  NUMERIC(18,10)  NOT NULL DEFAULT 0,\r
+  pf_name    VARCHAR(255)            NOT NULL,\r
+  pf_server  VARCHAR(255)            \r
+);\r
+CREATE UNIQUE INDEX pf_name_server ON profiling (pf_name, pf_server);\r
+\r
+CREATE TABLE protected_titles (\r
+  pt_namespace   SMALLINT    NOT NULL,\r
+  pt_title       VARCHAR(255)        NOT NULL,\r
+  pt_user        INTEGER       REFERENCES mwuser(user_id) ON DELETE SET NULL,\r
+  pt_reason      clob(1K),\r
+  pt_timestamp   TIMESTAMP NOT NULL,\r
+  pt_expiry      TIMESTAMP     ,\r
+  pt_create_perm VARCHAR(255)        NOT NULL DEFAULT ''\r
+);\r
+CREATE UNIQUE INDEX protected_titles_unique ON protected_titles(pt_namespace, pt_title);\r
+\r
+\r
+\r
+CREATE TABLE updatelog (\r
+  ul_key VARCHAR(255) NOT NULL PRIMARY KEY\r
+);\r
+\r
+CREATE SEQUENCE category_id_seq;\r
+CREATE TABLE category (\r
+  cat_id       INTEGER  NOT NULL PRIMARY KEY,\r
+  --PRIMARY KEY DEFAULT nextval('category_id_seq'),\r
+  cat_title    VARCHAR(255)     NOT NULL,\r
+  cat_pages    INTEGER  NOT NULL  DEFAULT 0,\r
+  cat_subcats  INTEGER  NOT NULL  DEFAULT 0,\r
+  cat_files    INTEGER  NOT NULL  DEFAULT 0,\r
+  cat_hidden   SMALLINT NOT NULL  DEFAULT 0\r
+);\r
+CREATE UNIQUE INDEX category_title ON category(cat_title);\r
+CREATE INDEX category_pages ON category(cat_pages);\r
+\r
+CREATE TABLE mediawiki_version (\r
+  type         VARCHAR(255)         NOT NULL,\r
+  mw_version   VARCHAR(255)         NOT NULL,\r
+  notes        VARCHAR(255)         ,\r
+\r
+  pg_version   VARCHAR(255)         ,\r
+  pg_dbname    VARCHAR(255)         ,\r
+  pg_user      VARCHAR(255)         ,\r
+  pg_port      VARCHAR(255)         ,\r
+  mw_schema    VARCHAR(255)         ,\r
+  ts2_schema   VARCHAR(255)         ,\r
+  ctype        VARCHAR(255)         ,\r
+\r
+  sql_version  VARCHAR(255)         ,\r
+  sql_date     VARCHAR(255)         ,\r
+  cdate        TIMESTAMP  NOT NULL DEFAULT CURRENT TIMESTAMP\r
+);\r
+\r
+INSERT INTO mediawiki_version (type,mw_version,sql_version,sql_date)\r
+  VALUES ('Creation','??','$LastChangedRevision: 34049 $','$LastChangedDate: 2008-04-30 10:20:36 -0400 (Wed, 30 Apr 2008) $');\r
+\r