diff options
Diffstat (limited to 'includes/db/DatabaseIbm_db2.php')
-rw-r--r-- | includes/db/DatabaseIbm_db2.php | 1796 |
1 files changed, 1796 insertions, 0 deletions
diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php new file mode 100644 index 00000000..fcd0bc2d --- /dev/null +++ b/includes/db/DatabaseIbm_db2.php @@ -0,0 +1,1796 @@ +<?php +/** + * This script is the IBM DB2 database abstraction layer + * + * See maintenance/ibm_db2/README for development notes and other specific information + * @ingroup Database + * @file + * @author leo.petr+mediawiki@gmail.com + */ + +/** + * Utility class for generating blank objects + * Intended as an equivalent to {} in Javascript + * @ingroup Database + */ +class BlankObject { +} + +/** + * This represents a column in a DB2 database + * @ingroup Database + */ +class IBM_DB2Field { + private $name, $tablename, $type, $nullable, $max_length; + + /** + * Builder method for the class + * @param Object $db Database interface + * @param string $table table name + * @param string $field column name + * @return IBM_DB2Field + */ + static function fromText($db, $table, $field) { + global $wgDBmwschema; + + $q = <<<END +SELECT +lcase(coltype) AS typname, +nulls AS attnotnull, length AS attlen +FROM sysibm.syscolumns +WHERE tbcreator=%s AND tbname=%s AND name=%s; +END; + $res = $db->query(sprintf($q, + $db->addQuotes($wgDBmwschema), + $db->addQuotes($table), + $db->addQuotes($field))); + $row = $db->fetchObject($res); + if (!$row) + return null; + $n = new IBM_DB2Field; + $n->type = $row->typname; + $n->nullable = ($row->attnotnull == 'N'); + $n->name = $field; + $n->tablename = $table; + $n->max_length = $row->attlen; + return $n; + } + /** + * Get column name + * @return string column name + */ + function name() { return $this->name; } + /** + * Get table name + * @return string table name + */ + function tableName() { return $this->tablename; } + /** + * Get column type + * @return string column type + */ + function type() { return $this->type; } + /** + * Can column be null? + * @return bool true or false + */ + function nullable() { return $this->nullable; } + /** + * How much can you fit in the column per row? + * @return int length + */ + function maxLength() { return $this->max_length; } +} + +/** + * Wrapper around binary large objects + * @ingroup Database + */ +class IBM_DB2Blob { + private $mData; + + function __construct($data) { + $this->mData = $data; + } + + function getData() { + return $this->mData; + } +} + +/** + * Primary database interface + * @ingroup Database + */ +class DatabaseIbm_db2 extends Database { + /* + * Inherited members + protected $mLastQuery = ''; + protected $mPHPError = false; + + protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname; + protected $mOut, $mOpened = false; + + protected $mFailFunction; + protected $mTablePrefix; + protected $mFlags; + protected $mTrxLevel = 0; + protected $mErrorCount = 0; + protected $mLBInfo = array(); + protected $mFakeSlaveLag = null, $mFakeMaster = false; + * + */ + + /// Server port for uncataloged connections + protected $mPort = NULL; + /// Whether connection is cataloged + protected $mCataloged = NULL; + /// Schema for tables, stored procedures, triggers + protected $mSchema = NULL; + /// Whether the schema has been applied in this session + protected $mSchemaSet = false; + /// Result of last query + protected $mLastResult = NULL; + /// Number of rows affected by last INSERT/UPDATE/DELETE + protected $mAffectedRows = NULL; + /// Number of rows returned by last SELECT + protected $mNumRows = NULL; + + + const CATALOGED = "cataloged"; + const UNCATALOGED = "uncataloged"; + const USE_GLOBAL = "get from global"; + + /// Last sequence value used for a primary key + protected $mInsertId = NULL; + + /* + * These can be safely inherited + * + * Getter/Setter: (18) + * failFunction + * setOutputPage + * bufferResults + * ignoreErrors + * trxLevel + * errorCount + * getLBInfo + * setLBInfo + * lastQuery + * isOpen + * setFlag + * clearFlag + * getFlag + * getProperty + * getDBname + * getServer + * tableNameCallback + * tablePrefix + * + * Administrative: (8) + * debug + * installErrorHandler + * restoreErrorHandler + * connectionErrorHandler + * reportConnectionError + * sourceFile + * sourceStream + * replaceVars + * + * Database: (5) + * query + * set + * selectField + * generalizeSQL + * update + * strreplace + * deadlockLoop + * + * Prepared Statement: 6 + * prepare + * freePrepared + * execute + * safeQuery + * fillPrepared + * fillPreparedArg + * + * Slave/Master: (4) + * masterPosWait + * getSlavePos + * getMasterPos + * getLag + * + * Generation: (9) + * tableNames + * tableNamesN + * tableNamesWithUseIndexOrJOIN + * escapeLike + * delete + * insertSelect + * timestampOrNull + * resultObject + * aggregateValue + * selectSQLText + * selectRow + * makeUpdateOptions + * + * Reflection: (1) + * indexExists + */ + + /* + * These need to be implemented TODO + * + * Administrative: 7 / 7 + * constructor [Done] + * open [Done] + * openCataloged [Done] + * close [Done] + * newFromParams [Done] + * openUncataloged [Done] + * setup_database [Done] + * + * Getter/Setter: 13 / 13 + * cascadingDeletes [Done] + * cleanupTriggers [Done] + * strictIPs [Done] + * realTimestamps [Done] + * impliciGroupby [Done] + * implicitOrderby [Done] + * searchableIPs [Done] + * functionalIndexes [Done] + * getWikiID [Done] + * isOpen [Done] + * getServerVersion [Done] + * getSoftwareLink [Done] + * getSearchEngine [Done] + * + * Database driver wrapper: 23 / 23 + * lastError [Done] + * lastErrno [Done] + * doQuery [Done] + * tableExists [Done] + * fetchObject [Done] + * fetchRow [Done] + * freeResult [Done] + * numRows [Done] + * numFields [Done] + * fieldName [Done] + * insertId [Done] + * dataSeek [Done] + * affectedRows [Done] + * selectDB [Done] + * strencode [Done] + * conditional [Done] + * wasDeadlock [Done] + * ping [Done] + * getStatus [Done] + * setTimeout [Done] + * lock [Done] + * unlock [Done] + * insert [Done] + * select [Done] + * + * Slave/master: 2 / 2 + * setFakeSlaveLag [Done] + * setFakeMaster [Done] + * + * Reflection: 6 / 6 + * fieldExists [Done] + * indexInfo [Done] + * fieldInfo [Done] + * fieldType [Done] + * indexUnique [Done] + * textFieldSize [Done] + * + * Generation: 16 / 16 + * tableName [Done] + * addQuotes [Done] + * makeList [Done] + * makeSelectOptions [Done] + * estimateRowCount [Done] + * nextSequenceValue [Done] + * useIndexClause [Done] + * replace [Done] + * deleteJoin [Done] + * lowPriorityOption [Done] + * limitResult [Done] + * limitResultForUpdate [Done] + * timestamp [Done] + * encodeBlob [Done] + * decodeBlob [Done] + * buildConcat [Done] + */ + + ###################################### + # Getters and Setters + ###################################### + + /** + * Returns true if this database supports (and uses) cascading deletes + */ + function cascadingDeletes() { + return true; + } + + /** + * Returns true if this database supports (and uses) triggers (e.g. on the page table) + */ + function cleanupTriggers() { + return true; + } + + /** + * Returns true if this database is strict about what can be put into an IP field. + * Specifically, it uses a NULL value instead of an empty string. + */ + function strictIPs() { + return true; + } + + /** + * Returns true if this database uses timestamps rather than integers + */ + function realTimestamps() { + return true; + } + + /** + * Returns true if this database does an implicit sort when doing GROUP BY + */ + function implicitGroupby() { + return false; + } + + /** + * Returns true if this database does an implicit order by when the column has an index + * For example: SELECT page_title FROM page LIMIT 1 + */ + function implicitOrderby() { + return false; + } + + /** + * Returns true if this database can do a native search on IP columns + * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32'; + */ + function searchableIPs() { + return true; + } + + /** + * Returns true if this database can use functional indexes + */ + function functionalIndexes() { + return true; + } + + /** + * Returns a unique string representing the wiki on the server + */ + function getWikiID() { + if( $this->mSchema ) { + return "{$this->mDBname}-{$this->mSchema}"; + } else { + return $this->mDBname; + } + } + + + ###################################### + # Setup + ###################################### + + + /** + * + * @param string $server hostname of database server + * @param string $user username + * @param string $password + * @param string $dbName database name on the server + * @param function $failFunction (optional) + * @param integer $flags database behaviour flags (optional, unused) + */ + public function DatabaseIbm_db2($server = false, $user = false, $password = false, + $dbName = false, $failFunction = false, $flags = 0, + $schema = self::USE_GLOBAL ) + { + + global $wgOut, $wgDBmwschema; + # Can't get a reference if it hasn't been set yet + if ( !isset( $wgOut ) ) { + $wgOut = NULL; + } + $this->mOut =& $wgOut; + $this->mFailFunction = $failFunction; + $this->mFlags = DBO_TRX | $flags; + + if ( $schema == self::USE_GLOBAL ) { + $this->mSchema = $wgDBmwschema; + } + else { + $this->mSchema = $schema; + } + + $this->open( $server, $user, $password, $dbName); + } + + /** + * Opens a database connection and returns it + * Closes any existing connection + * @return a fresh connection + * @param string $server hostname + * @param string $user + * @param string $password + * @param string $dbName database name + */ + public function open( $server, $user, $password, $dbName ) + { + // Load the port number + global $wgDBport_db2, $wgDBcataloged; + wfProfileIn( __METHOD__ ); + + // Load IBM DB2 driver if missing + if (!@extension_loaded('ibm_db2')) { + @dl('ibm_db2.so'); + } + // Test for IBM DB2 support, to avoid suppressed fatal error + if ( !function_exists( 'db2_connect' ) ) { + $error = "DB2 functions missing, have you enabled the ibm_db2 extension for PHP?\n"; + wfDebug($error); + $this->reportConnectionError($error); + } + + if (!strlen($user)) { // Copied from Postgres + return null; + } + + // Close existing connection + $this->close(); + // Cache conn info + $this->mServer = $server; + $this->mPort = $port = $wgDBport_db2; + $this->mUser = $user; + $this->mPassword = $password; + $this->mDBname = $dbName; + $this->mCataloged = $cataloged = $wgDBcataloged; + + if ( $cataloged == self::CATALOGED ) { + $this->openCataloged($dbName, $user, $password); + } + elseif ( $cataloged == self::UNCATALOGED ) { + $this->openUncataloged($dbName, $user, $password, $server, $port); + } + // Don't do this + // Not all MediaWiki code is transactional + // Rather, turn it off in the begin function and turn on after a commit + // db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF); + db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON); + + if ( $this->mConn == false ) { + wfDebug( "DB connection error\n" ); + wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); + wfDebug( $this->lastError()."\n" ); + return null; + } + + $this->mOpened = true; + $this->applySchema(); + + wfProfileOut( __METHOD__ ); + return $this->mConn; + } + + /** + * Opens a cataloged database connection, sets mConn + */ + protected function openCataloged( $dbName, $user, $password ) + { + @$this->mConn = db2_connect($dbName, $user, $password); + } + + /** + * Opens an uncataloged database connection, sets mConn + */ + protected function openUncataloged( $dbName, $user, $password, $server, $port ) + { + $str = "DRIVER={IBM DB2 ODBC DRIVER};"; + $str .= "DATABASE=$dbName;"; + $str .= "HOSTNAME=$server;"; + if ($port) $str .= "PORT=$port;"; + $str .= "PROTOCOL=TCPIP;"; + $str .= "UID=$user;"; + $str .= "PWD=$password;"; + + @$this->mConn = db2_connect($str, $user, $password); + } + + /** + * Closes a database connection, if it is open + * Returns success, true if already closed + */ + public function close() { + $this->mOpened = false; + if ( $this->mConn ) { + if ($this->trxLevel() > 0) { + $this->commit(); + } + return db2_close( $this->mConn ); + } + else { + return true; + } + } + + /** + * Returns a fresh instance of this class + * @static + * @return + * @param string $server hostname of database server + * @param string $user username + * @param string $password + * @param string $dbName database name on the server + * @param function $failFunction (optional) + * @param integer $flags database behaviour flags (optional, unused) + */ + static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0) + { + return new DatabaseIbm_db2( $server, $user, $password, $dbName, $failFunction, $flags ); + } + + /** + * Retrieves the most current database error + * Forces a database rollback + */ + public function lastError() { + if ($this->lastError2()) { + $this->rollback(); + return true; + } + return false; + } + + private function lastError2() { + $connerr = db2_conn_errormsg(); + if ($connerr) return $connerr; + $stmterr = db2_stmt_errormsg(); + if ($stmterr) return $stmterr; + if ($this->mConn) return "No open connection."; + if ($this->mOpened) return "No open connection allegedly."; + + return false; + } + + /** + * Get the last error number + * Return 0 if no error + * @return integer + */ + public function lastErrno() { + $connerr = db2_conn_error(); + if ($connerr) return $connerr; + $stmterr = db2_stmt_error(); + if ($stmterr) return $stmterr; + return 0; + } + + /** + * Is a database connection open? + * @return + */ + public function isOpen() { return $this->mOpened; } + + /** + * The DBMS-dependent part of query() + * @param $sql String: SQL query. + * @return object Result object to feed to fetchObject, fetchRow, ...; or false on failure + * @access private + */ + /*private*/ + public function doQuery( $sql ) { + //print "<li><pre>$sql</pre></li>"; + // Switch into the correct namespace + $this->applySchema(); + + $ret = db2_exec( $this->mConn, $sql ); + if( !$ret ) { + print "<br><pre>"; + print $sql; + print "</pre><br>"; + $error = db2_stmt_errormsg(); + throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( $error ) ); + } + $this->mLastResult = $ret; + $this->mAffectedRows = NULL; // Not calculated until asked for + return $ret; + } + + /** + * @return string Version information from the database + */ + public function getServerVersion() { + $info = db2_server_info( $this->mConn ); + return $info->DBMS_VER; + } + + /** + * Queries whether a given table exists + * @return boolean + */ + public function tableExists( $table ) { + $schema = $this->mSchema; + $sql = <<< EOF +SELECT COUNT(*) FROM SYSIBM.SYSTABLES ST +WHERE ST.NAME = '$table' AND ST.CREATOR = '$schema' +EOF; + $res = $this->query( $sql ); + if (!$res) return false; + + // If the table exists, there should be one of it + @$row = $this->fetchRow($res); + $count = $row[0]; + if ($count == '1' or $count == 1) { + return true; + } + + return false; + } + + /** + * Fetch the next row from the given result object, in object form. + * Fields can be retrieved with $row->fieldname, with fields acting like + * member variables. + * + * @param $res SQL result object as returned from Database::query(), etc. + * @return DB2 row object + * @throws DBUnexpectedError Thrown if the database returns an error + */ + public function fetchObject( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + @$row = db2_fetch_object( $res ); + if( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); + } + // Make field names lowercase for compatibility with MySQL + if ($row) + { + $row2 = new BlankObject(); + foreach ($row as $key => $value) + { + $keyu = strtolower($key); + $row2->$keyu = $value; + } + $row = $row2; + } + return $row; + } + + /** + * Fetch the next row from the given result object, in associative array + * form. Fields are retrieved with $row['fieldname']. + * + * @param $res SQL result object as returned from Database::query(), etc. + * @return DB2 row object + * @throws DBUnexpectedError Thrown if the database returns an error + */ + public function fetchRow( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + @$row = db2_fetch_array( $res ); + if ( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); + } + return $row; + } + + /** + * Override if introduced to base Database class + */ + public function initial_setup() { + // do nothing + } + + /** + * Create tables, stored procedures, and so on + */ + public function setup_database() { + // Timeout was being changed earlier due to mysterious crashes + // Changing it now may cause more problems than not changing it + //set_time_limit(240); + try { + // TODO: switch to root login if available + + // Switch into the correct namespace + $this->applySchema(); + $this->begin(); + + $res = dbsource( "../maintenance/ibm_db2/tables.sql", $this); + $res = null; + + // TODO: update mediawiki_version table + + // TODO: populate interwiki links + + $this->commit(); + } + catch (MWException $mwe) + { + print "<br><pre>$mwe</pre><br>"; + } + } + + /** + * Escapes strings + * Doesn't escape numbers + * @param string s string to escape + * @return escaped string + */ + public function addQuotes( $s ) { + //wfDebug("DB2::addQuotes($s)\n"); + if ( is_null( $s ) ) { + return "NULL"; + } else if ($s instanceof Blob) { + return "'".$s->fetch($s)."'"; + } + $s = $this->strencode($s); + if ( is_numeric($s) ) { + return $s; + } + else { + return "'$s'"; + } + } + + /** + * Escapes strings + * Only escapes numbers going into non-numeric fields + * @param string s string to escape + * @return escaped string + */ + public function addQuotesSmart( $table, $field, $s ) { + if ( is_null( $s ) ) { + return "NULL"; + } else if ($s instanceof Blob) { + return "'".$s->fetch($s)."'"; + } + $s = $this->strencode($s); + if ( is_numeric($s) ) { + // Check with the database if the column is actually numeric + // This allows for numbers in titles, etc + $res = $this->doQuery("SELECT $field FROM $table FETCH FIRST 1 ROWS ONLY"); + $type = db2_field_type($res, strtoupper($field)); + if ( $this->is_numeric_type( $type ) ) { + //wfDebug("DB2: Numeric value going in a numeric column: $s in $type $field in $table\n"); + return $s; + } + else { + wfDebug("DB2: Numeric in non-numeric: '$s' in $type $field in $table\n"); + return "'$s'"; + } + } + else { + return "'$s'"; + } + } + + /** + * Verifies that a DB2 column/field type is numeric + * @return bool true if numeric + * @param string $type DB2 column type + */ + public function is_numeric_type( $type ) { + switch (strtoupper($type)) { + case 'SMALLINT': + case 'INTEGER': + case 'INT': + case 'BIGINT': + case 'DECIMAL': + case 'REAL': + case 'DOUBLE': + case 'DECFLOAT': + return true; + } + return false; + } + + /** + * Alias for addQuotes() + * @param string s string to escape + * @return escaped string + */ + public function strencode( $s ) { + // Bloody useless function + // Prepends backslashes to \x00, \n, \r, \, ', " and \x1a. + // But also necessary + $s = db2_escape_string($s); + // Wide characters are evil -- some of them look like ' + $s = utf8_encode($s); + // Fix its stupidity + $from = array("\\\\", "\\'", '\\n', '\\t', '\\"', '\\r'); + $to = array("\\", "''", "\n", "\t", '"', "\r"); + $s = str_replace($from, $to, $s); // DB2 expects '', not \' escaping + return $s; + } + + /** + * Switch into the database schema + */ + protected function applySchema() { + if ( !($this->mSchemaSet) ) { + $this->mSchemaSet = true; + $this->begin(); + $this->doQuery("SET SCHEMA = $this->mSchema"); + $this->commit(); + } + } + + /** + * Start a transaction (mandatory) + */ + public function begin() { + // turn off auto-commit + db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF); + $this->mTrxLevel = 1; + } + + /** + * End a transaction + * Must have a preceding begin() + */ + public function commit() { + db2_commit($this->mConn); + // turn auto-commit back on + db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON); + $this->mTrxLevel = 0; + } + + /** + * Cancel a transaction + */ + public function rollback() { + db2_rollback($this->mConn); + // turn auto-commit back on + // not sure if this is appropriate + db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON); + $this->mTrxLevel = 0; + } + + /** + * Makes an encoded list of strings from an array + * $mode: + * LIST_COMMA - comma separated, no field names + * LIST_AND - ANDed WHERE clause (without the WHERE) + * LIST_OR - ORed WHERE clause (without the WHERE) + * LIST_SET - comma separated with field names, like a SET clause + * LIST_NAMES - comma separated field names + */ + public function makeList( $a, $mode = LIST_COMMA ) { + wfDebug("DB2::makeList()\n"); + if ( !is_array( $a ) ) { + throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' ); + } + + $first = true; + $list = ''; + foreach ( $a as $field => $value ) { + if ( !$first ) { + if ( $mode == LIST_AND ) { + $list .= ' AND '; + } elseif($mode == LIST_OR) { + $list .= ' OR '; + } else { + $list .= ','; + } + } else { + $first = false; + } + if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) { + $list .= "($value)"; + } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) { + $list .= "$value"; + } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) { + if( count( $value ) == 0 ) { + throw new MWException( __METHOD__.': empty input' ); + } elseif( count( $value ) == 1 ) { + // Special-case single values, as IN isn't terribly efficient + // Don't necessarily assume the single key is 0; we don't + // enforce linear numeric ordering on other arrays here. + $value = array_values( $value ); + $list .= $field." = ".$this->addQuotes( $value[0] ); + } else { + $list .= $field." IN (".$this->makeList($value).") "; + } + } elseif( is_null($value) ) { + if ( $mode == LIST_AND || $mode == LIST_OR ) { + $list .= "$field IS "; + } elseif ( $mode == LIST_SET ) { + $list .= "$field = "; + } + $list .= 'NULL'; + } else { + if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { + $list .= "$field = "; + } + if ( $mode == LIST_NAMES ) { + $list .= $value; + } + // Leo: Can't insert quoted numbers into numeric columns + // (?) Might cause other problems. May have to check column type before insertion. + else if ( is_numeric($value) ) { + $list .= $value; + } + else { + $list .= $this->addQuotes( $value ); + } + } + } + return $list; + } + + /** + * Makes an encoded list of strings from an array + * Quotes numeric values being inserted into non-numeric fields + * @return string + * @param string $table name of the table + * @param array $a list of values + * @param $mode: + * LIST_COMMA - comma separated, no field names + * LIST_AND - ANDed WHERE clause (without the WHERE) + * LIST_OR - ORed WHERE clause (without the WHERE) + * LIST_SET - comma separated with field names, like a SET clause + * LIST_NAMES - comma separated field names + */ + public function makeListSmart( $table, $a, $mode = LIST_COMMA ) { + if ( !is_array( $a ) ) { + throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' ); + } + + $first = true; + $list = ''; + foreach ( $a as $field => $value ) { + if ( !$first ) { + if ( $mode == LIST_AND ) { + $list .= ' AND '; + } elseif($mode == LIST_OR) { + $list .= ' OR '; + } else { + $list .= ','; + } + } else { + $first = false; + } + if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) { + $list .= "($value)"; + } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) { + $list .= "$value"; + } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) { + if( count( $value ) == 0 ) { + throw new MWException( __METHOD__.': empty input' ); + } elseif( count( $value ) == 1 ) { + // Special-case single values, as IN isn't terribly efficient + // Don't necessarily assume the single key is 0; we don't + // enforce linear numeric ordering on other arrays here. + $value = array_values( $value ); + $list .= $field." = ".$this->addQuotes( $value[0] ); + } else { + $list .= $field." IN (".$this->makeList($value).") "; + } + } elseif( is_null($value) ) { + if ( $mode == LIST_AND || $mode == LIST_OR ) { + $list .= "$field IS "; + } elseif ( $mode == LIST_SET ) { + $list .= "$field = "; + } + $list .= 'NULL'; + } else { + if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { + $list .= "$field = "; + } + if ( $mode == LIST_NAMES ) { + $list .= $value; + } + else { + $list .= $this->addQuotesSmart( $table, $field, $value ); + } + } + } + return $list; + } + + /** + * Construct a LIMIT query with optional offset + * This is used for query pages + * $sql string SQL query we will append the limit too + * $limit integer the SQL limit + * $offset integer the SQL offset (default false) + */ + public function limitResult($sql, $limit, $offset=false) { + if( !is_numeric($limit) ) { + throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" ); + } + if( $offset ) { + wfDebug("Offset parameter not supported in limitResult()\n"); + } + // TODO implement proper offset handling + // idea: get all the rows between 0 and offset, advance cursor to offset + return "$sql FETCH FIRST $limit ROWS ONLY "; + } + + /** + * Handle reserved keyword replacement in table names + * @return + * @param $name Object + */ + public function tableName( $name ) { + # Replace reserved words with better ones + switch( $name ) { + case 'user': + return 'mwuser'; + case 'text': + return 'pagecontent'; + default: + return $name; + } + } + + /** + * Generates a timestamp in an insertable format + * @return string timestamp value + * @param timestamp $ts + */ + public function timestamp( $ts=0 ) { + // TS_MW cannot be easily distinguished from an integer + return wfTimestamp(TS_DB2,$ts); + } + + /** + * Return the next in a sequence, save the value for retrieval via insertId() + * @param string seqName Name of a defined sequence in the database + * @return next value in that sequence + */ + public function nextSequenceValue( $seqName ) { + $safeseq = preg_replace( "/'/", "''", $seqName ); + $res = $this->query( "VALUES NEXTVAL FOR $safeseq" ); + $row = $this->fetchRow( $res ); + $this->mInsertId = $row[0]; + $this->freeResult( $res ); + return $this->mInsertId; + } + + /** + * This must be called after nextSequenceVal + * @return Last sequence value used as a primary key + */ + public function insertId() { + return $this->mInsertId; + } + + /** + * INSERT wrapper, inserts an array into a table + * + * $args may be a single associative array, or an array of these with numeric keys, + * for multi-row insert + * + * @param array $table String: Name of the table to insert to. + * @param array $args Array: Items to insert into the table. + * @param array $fname String: Name of the function, for profiling + * @param mixed $options String or Array. Valid options: IGNORE + * + * @return bool Success of insert operation. IGNORE always returns true. + */ + public function insert( $table, $args, $fname = 'DatabaseIbm_db2::insert', $options = array() ) { + wfDebug("DB2::insert($table)\n"); + if ( !count( $args ) ) { + return true; + } + + $table = $this->tableName( $table ); + + if ( !is_array( $options ) ) + $options = array( $options ); + + if ( isset( $args[0] ) && is_array( $args[0] ) ) { + } + else { + $args = array($args); + } + $keys = array_keys( $args[0] ); + + // If IGNORE is set, we use savepoints to emulate mysql's behavior + $ignore = in_array( 'IGNORE', $options ) ? 'mw' : ''; + + // Cache autocommit value at the start + $oldautocommit = db2_autocommit($this->mConn); + + // If we are not in a transaction, we need to be for savepoint trickery + $didbegin = 0; + if (! $this->mTrxLevel) { + $this->begin(); + $didbegin = 1; + } + if ( $ignore ) { + $olde = error_reporting( 0 ); + // For future use, we may want to track the number of actual inserts + // Right now, insert (all writes) simply return true/false + $numrowsinserted = 0; + } + + $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES '; + + if ( !$ignore ) { + $first = true; + foreach ( $args as $row ) { + if ( $first ) { + $first = false; + } else { + $sql .= ','; + } + $sql .= '(' . $this->makeListSmart( $table, $row ) . ')'; + } + $res = (bool)$this->query( $sql, $fname, $ignore ); + } + else { + $res = true; + $origsql = $sql; + foreach ( $args as $row ) { + $tempsql = $origsql; + $tempsql .= '(' . $this->makeListSmart( $table, $row ) . ')'; + + if ( $ignore ) { + db2_exec($this->mConn, "SAVEPOINT $ignore"); + } + + $tempres = (bool)$this->query( $tempsql, $fname, $ignore ); + + if ( $ignore ) { + $bar = db2_stmt_error(); + if ($bar != false) { + db2_exec( $this->mConn, "ROLLBACK TO SAVEPOINT $ignore" ); + } + else { + db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore" ); + $numrowsinserted++; + } + } + + // If any of them fail, we fail overall for this function call + // Note that this will be ignored if IGNORE is set + if (! $tempres) + $res = false; + } + } + + if ($didbegin) { + $this->commit(); + } + // if autocommit used to be on, it's ok to commit everything + else if ($oldautocommit) + { + $this->commit(); + } + + if ( $ignore ) { + $olde = error_reporting( $olde ); + // Set the affected row count for the whole operation + $this->mAffectedRows = $numrowsinserted; + + // IGNORE always returns true + return true; + } + + return $res; + } + + /** + * UPDATE wrapper, takes a condition array and a SET array + * + * @param string $table The table to UPDATE + * @param array $values An array of values to SET + * @param array $conds An array of conditions (WHERE). Use '*' to update all rows. + * @param string $fname The Class::Function calling this function + * (for the log) + * @param array $options An array of UPDATE options, can be one or + * more of IGNORE, LOW_PRIORITY + * @return bool + */ + function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) { + $table = $this->tableName( $table ); + $opts = $this->makeUpdateOptions( $options ); + $sql = "UPDATE $opts $table SET " . $this->makeListSmart( $table, $values, LIST_SET ); + if ( $conds != '*' ) { + $sql .= " WHERE " . $this->makeListSmart( $table, $conds, LIST_AND ); + } + return $this->query( $sql, $fname ); + } + + /** + * DELETE query wrapper + * + * Use $conds == "*" to delete all rows + */ + function delete( $table, $conds, $fname = 'Database::delete' ) { + if ( !$conds ) { + throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' ); + } + $table = $this->tableName( $table ); + $sql = "DELETE FROM $table"; + if ( $conds != '*' ) { + $sql .= ' WHERE ' . $this->makeListSmart( $table, $conds, LIST_AND ); + } + return $this->query( $sql, $fname ); + } + + /** + * Returns the number of rows affected by the last query or 0 + * @return int the number of rows affected by the last query + */ + public function affectedRows() { + if ( !is_null( $this->mAffectedRows ) ) { + // Forced result for simulated queries + return $this->mAffectedRows; + } + if( empty( $this->mLastResult ) ) + return 0; + return db2_num_rows( $this->mLastResult ); + } + + /** + * USE INDEX clause + * DB2 doesn't have them and returns "" + * @param sting $index + */ + public function useIndexClause( $index ) { + return ""; + } + + /** + * Simulates REPLACE with a DELETE followed by INSERT + * @param $table Object + * @param array $uniqueIndexes array consisting of indexes and arrays of indexes + * @param array $rows Rows to insert + * @param string $fname Name of the function for profiling + * @return nothing + */ + function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseIbm_db2::replace' ) { + $table = $this->tableName( $table ); + + if (count($rows)==0) { + return; + } + + # Single row case + if ( !is_array( reset( $rows ) ) ) { + $rows = array( $rows ); + } + + foreach( $rows as $row ) { + # Delete rows which collide + if ( $uniqueIndexes ) { + $sql = "DELETE FROM $table WHERE "; + $first = true; + foreach ( $uniqueIndexes as $index ) { + if ( $first ) { + $first = false; + $sql .= "("; + } else { + $sql .= ') OR ('; + } + if ( is_array( $index ) ) { + $first2 = true; + foreach ( $index as $col ) { + if ( $first2 ) { + $first2 = false; + } else { + $sql .= ' AND '; + } + $sql .= $col.'=' . $this->addQuotes( $row[$col] ); + } + } else { + $sql .= $index.'=' . $this->addQuotes( $row[$index] ); + } + } + $sql .= ')'; + $this->query( $sql, $fname ); + } + + # Now insert the row + $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' . + $this->makeList( $row, LIST_COMMA ) . ')'; + $this->query( $sql, $fname ); + } + } + + /** + * Returns the number of rows in the result set + * Has to be called right after the corresponding select query + * @param Object $res result set + * @return int number of rows + */ + public function numRows( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + if ( $this->mNumRows ) { + return $this->mNumRows; + } + else { + return 0; + } + } + + /** + * Moves the row pointer of the result set + * @param Object $res result set + * @param int $row row number + * @return success or failure + */ + public function dataSeek( $res, $row ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return db2_fetch_row( $res, $row ); + } + + ### + # Fix notices in Block.php + ### + + /** + * Frees memory associated with a statement resource + * @param Object $res Statement resource to free + * @return bool success or failure + */ + public function freeResult( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + if ( !@db2_free_result( $res ) ) { + throw new DBUnexpectedError($this, "Unable to free DB2 result\n" ); + } + } + + /** + * Returns the number of columns in a resource + * @param Object $res Statement resource + * @return Number of fields/columns in resource + */ + public function numFields( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return db2_num_fields( $res ); + } + + /** + * Returns the nth column name + * @param Object $res Statement resource + * @param int $n Index of field or column + * @return string name of nth column + */ + public function fieldName( $res, $n ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return db2_field_name( $res, $n ); + } + + /** + * SELECT wrapper + * + * @param mixed $table Array or string, table name(s) (prefix auto-added) + * @param mixed $vars Array or string, field name(s) to be retrieved + * @param mixed $conds Array or string, condition(s) for WHERE + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), + * see Database::makeSelectOptions code for list of supported stuff + * @param array $join_conds Associative array of table join conditions (optional) + * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) + * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure + */ + public function select( $table, $vars, $conds='', $fname = 'DatabaseIbm_db2::select', $options = array(), $join_conds = array() ) + { + $res = parent::select( $table, $vars, $conds, $fname, $options, $join_conds ); + + // We must adjust for offset + if ( isset( $options['LIMIT'] ) ) { + if ( isset ($options['OFFSET'] ) ) { + $limit = $options['LIMIT']; + $offset = $options['OFFSET']; + } + } + + + // DB2 does not have a proper num_rows() function yet, so we must emulate it + // DB2 9.5.3/9.5.4 and the corresponding ibm_db2 driver will introduce a working one + // Yay! + + // we want the count + $vars2 = array('count(*) as num_rows'); + // respecting just the limit option + $options2 = array(); + if ( isset( $options['LIMIT'] ) ) $options2['LIMIT'] = $options['LIMIT']; + // but don't try to emulate for GROUP BY + if ( isset( $options['GROUP BY'] ) ) return $res; + + $res2 = parent::select( $table, $vars2, $conds, $fname, $options2, $join_conds ); + $obj = $this->fetchObject($res2); + $this->mNumRows = $obj->num_rows; + + wfDebug("DatabaseIbm_db2::select: There are $this->mNumRows rows.\n"); + + return $res; + } + + /** + * Handles ordering, grouping, and having options ('GROUP BY' => colname) + * Has limited support for per-column options (colnum => 'DISTINCT') + * + * @private + * + * @param array $options an associative array of options to be turned into + * an SQL query, valid keys are listed in the function. + * @return array + */ + function makeSelectOptions( $options ) { + $preLimitTail = $postLimitTail = ''; + $startOpts = ''; + + $noKeyOptions = array(); + foreach ( $options as $key => $option ) { + if ( is_numeric( $key ) ) { + $noKeyOptions[$option] = true; + } + } + + if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; + if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}"; + if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; + + if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; + + return array( $startOpts, '', $preLimitTail, $postLimitTail ); + } + + /** + * Returns link to IBM DB2 free download + * @return string wikitext of a link to the server software's web site + */ + public function getSoftwareLink() { + return "[http://www.ibm.com/software/data/db2/express/?s_cmp=ECDDWW01&s_tact=MediaWiki IBM DB2]"; + } + + /** + * Does nothing + * @param object $db + * @return bool true + */ + public function selectDB( $db ) { + return true; + } + + /** + * Returns an SQL expression for a simple conditional. + * Uses CASE on DB2 + * + * @param string $cond SQL expression which will result in a boolean value + * @param string $trueVal SQL expression to return if true + * @param string $falseVal SQL expression to return if false + * @return string SQL fragment + */ + public function conditional( $cond, $trueVal, $falseVal ) { + return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; + } + + ### + # Fix search crash + ### + /** + * Get search engine class. All subclasses of this + * need to implement this if they wish to use searching. + * + * @return string + */ + public function getSearchEngine() { + return "SearchIBM_DB2"; + } + + ### + # Tuesday the 14th of October, 2008 + ### + /** + * Did the last database access fail because of deadlock? + * @return bool + */ + public function wasDeadlock() { + // get SQLSTATE + $err = $this->lastErrno(); + switch($err) { + case '40001': // sql0911n, Deadlock or timeout, rollback + case '57011': // sql0904n, Resource unavailable, no rollback + case '57033': // sql0913n, Deadlock or timeout, no rollback + wfDebug("In a deadlock because of SQLSTATE $err"); + return true; + } + return false; + } + + /** + * Ping the server and try to reconnect if it there is no connection + * The connection may be closed and reopened while this happens + * @return bool whether the connection exists + */ + public function ping() { + // db2_ping() doesn't exist + // Emulate + $this->close(); + if ($this->mCataloged == NULL) { + return false; + } + else if ($this->mCataloged) { + $this->mConn = $this->openCataloged($this->mDBName, $this->mUser, $this->mPassword); + } + else if (!$this->mCataloged) { + $this->mConn = $this->openUncataloged($this->mDBName, $this->mUser, $this->mPassword, $this->mServer, $this->mPort); + } + return false; + } + ###################################### + # Unimplemented and not applicable + ###################################### + /** + * Not implemented + * @return string '' + * @deprecated + */ + public function getStatus( $which ) { wfDebug('Not implemented for DB2: getStatus()'); return ''; } + /** + * Not implemented + * @deprecated + */ + public function setTimeout( $timeout ) { wfDebug('Not implemented for DB2: setTimeout()'); } + /** + * Not implemented + * TODO + * @return bool true + */ + public function lock( $lockName, $method ) { wfDebug('Not implemented for DB2: lock()'); return true; } + /** + * Not implemented + * TODO + * @return bool true + */ + public function unlock( $lockName, $method ) { wfDebug('Not implemented for DB2: unlock()'); return true; } + /** + * Not implemented + * @deprecated + */ + public function setFakeSlaveLag( $lag ) { wfDebug('Not implemented for DB2: setFakeSlaveLag()'); } + /** + * Not implemented + * @deprecated + */ + public function setFakeMaster( $enabled ) { wfDebug('Not implemented for DB2: setFakeMaster()'); } + /** + * Not implemented + * @return string $sql + * @deprecated + */ + public function limitResultForUpdate($sql, $num) { return $sql; } + /** + * No such option + * @return string '' + * @deprecated + */ + public function lowPriorityOption() { return ''; } + + ###################################### + # Reflection + ###################################### + + /** + * Query whether a given column exists in the mediawiki schema + * @param string $table name of the table + * @param string $field name of the column + * @param string $fname function name for logging and profiling + */ + public function fieldExists( $table, $field, $fname = 'DatabaseIbm_db2::fieldExists' ) { + $table = $this->tableName( $table ); + $schema = $this->mSchema; + $etable = preg_replace("/'/", "''", $table); + $eschema = preg_replace("/'/", "''", $schema); + $ecol = preg_replace("/'/", "''", $field); + $sql = <<<SQL +SELECT 1 as fieldexists +FROM sysibm.syscolumns sc +WHERE sc.name='$ecol' AND sc.tbname='$etable' AND sc.tbcreator='$eschema' +SQL; + $res = $this->query( $sql, $fname ); + $count = $res ? $this->numRows($res) : 0; + if ($res) + $this->freeResult( $res ); + return $count; + } + + /** + * Returns information about an index + * If errors are explicitly ignored, returns NULL on failure + * @param string $table table name + * @param string $index index name + * @param string + * @return object query row in object form + */ + public function indexInfo( $table, $index, $fname = 'DatabaseIbm_db2::indexExists' ) { + $table = $this->tableName( $table ); + $sql = <<<SQL +SELECT name as indexname +FROM sysibm.sysindexes si +WHERE si.name='$index' AND si.tbname='$table' AND sc.tbcreator='$this->mSchema' +SQL; + $res = $this->query( $sql, $fname ); + if ( !$res ) { + return NULL; + } + $row = $this->fetchObject( $res ); + if ($row != NULL) return $row; + else return false; + } + + /** + * Returns an information object on a table column + * @param string $table table name + * @param string $field column name + * @return IBM_DB2Field + */ + public function fieldInfo( $table, $field ) { + return IBM_DB2Field::fromText($this, $table, $field); + } + + /** + * db2_field_type() wrapper + * @param Object $res Result of executed statement + * @param mixed $index number or name of the column + * @return string column type + */ + public function fieldType( $res, $index ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return db2_field_type( $res, $index ); + } + + /** + * Verifies that an index was created as unique + * @param string $table table name + * @param string $index index name + * @param string $fnam function name for profiling + * @return bool + */ + public function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) { + $table = $this->tableName( $table ); + $sql = <<<SQL +SELECT si.name as indexname +FROM sysibm.sysindexes si +WHERE si.name='$index' AND si.tbname='$table' AND sc.tbcreator='$this->mSchema' +AND si.uniquerule IN ('U', 'P') +SQL; + $res = $this->query( $sql, $fname ); + if ( !$res ) { + return null; + } + if ($this->fetchObject( $res )) { + return true; + } + return false; + + } + + /** + * Returns the size of a text field, or -1 for "unlimited" + * @param string $table table name + * @param string $field column name + * @return int length or -1 for unlimited + */ + public function textFieldSize( $table, $field ) { + $table = $this->tableName( $table ); + $sql = <<<SQL +SELECT length as size +FROM sysibm.syscolumns sc +WHERE sc.name='$field' AND sc.tbname='$table' AND sc.tbcreator='$this->mSchema' +SQL; + $res = $this->query($sql); + $row = $this->fetchObject($res); + $size = $row->size; + $this->freeResult( $res ); + return $size; + } + + /** + * DELETE where the condition is a join + * @param string $delTable deleting from this table + * @param string $joinTable using data from this table + * @param string $delVar variable in deleteable table + * @param string $joinVar variable in data table + * @param array $conds conditionals for join table + * @param string $fname function name for profiling + */ + public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseIbm_db2::deleteJoin" ) { + if ( !$conds ) { + throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' ); + } + + $delTable = $this->tableName( $delTable ); + $joinTable = $this->tableName( $joinTable ); + $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; + if ( $conds != '*' ) { + $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); + } + $sql .= ')'; + + $this->query( $sql, $fname ); + } + + /** + * Estimate rows in dataset + * Returns estimated count, based on COUNT(*) output + * Takes same arguments as Database::select() + * @param string $table table name + * @param array $vars unused + * @param array $conds filters on the table + * @param string $fname function name for profiling + * @param array $options options for select + * @return int row count + */ + public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { + $rows = 0; + $res = $this->select ($table, 'COUNT(*) as mwrowcount', $conds, $fname, $options ); + if ($res) { + $row = $this->fetchRow($res); + $rows = (isset($row['mwrowcount'])) ? $row['mwrowcount'] : 0; + } + $this->freeResult($res); + return $rows; + } + + /** + * Description is left as an exercise for the reader + * @param mixed $b data to be encoded + * @return IBM_DB2Blob + */ + public function encodeBlob($b) { + return new IBM_DB2Blob($b); + } + + /** + * Description is left as an exercise for the reader + * @param IBM_DB2Blob $b data to be decoded + * @return mixed + */ + public function decodeBlob($b) { + return $b->getData(); + } + + /** + * Convert into a list of string being concatenated + * @param array $stringList strings that need to be joined together by the SQL engine + * @return string joined by the concatenation operator + */ + public function buildConcat( $stringList ) { + // || is equivalent to CONCAT + // Sample query: VALUES 'foo' CONCAT 'bar' CONCAT 'baz' + return implode( ' || ', $stringList ); + } + + /** + * Generates the SQL required to convert a DB2 timestamp into a Unix epoch + * @param string $column name of timestamp column + * @return string SQL code + */ + public function extractUnixEpoch( $column ) { + // TODO + // see SpecialAncientpages + } +} +?>
\ No newline at end of file |