diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2010-07-28 11:52:48 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2010-07-28 11:52:48 +0200 |
commit | 222b01f5169f1c7e69762e0e8904c24f78f71882 (patch) | |
tree | 8e932e12546bb991357ec48eb1638d1770be7a35 /includes/db | |
parent | 00ab76a6b686e98a914afc1975812d2b1aaa7016 (diff) |
update to MediaWiki 1.16.0
Diffstat (limited to 'includes/db')
-rw-r--r-- | includes/db/Database.php | 812 | ||||
-rw-r--r-- | includes/db/DatabaseIbm_db2.php | 908 | ||||
-rw-r--r-- | includes/db/DatabaseMssql.php | 86 | ||||
-rw-r--r-- | includes/db/DatabaseMysql.php | 453 | ||||
-rw-r--r-- | includes/db/DatabaseOracle.php | 1049 | ||||
-rw-r--r-- | includes/db/DatabasePostgres.php | 184 | ||||
-rw-r--r-- | includes/db/DatabaseSqlite.php | 432 | ||||
-rw-r--r-- | includes/db/LBFactory.php | 14 | ||||
-rw-r--r-- | includes/db/LoadBalancer.php | 21 |
9 files changed, 2511 insertions, 1448 deletions
diff --git a/includes/db/Database.php b/includes/db/Database.php index 52a59c11..ea5d77da 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -19,7 +19,7 @@ define( 'DEADLOCK_DELAY_MAX', 1500000 ); * Database abstraction object * @ingroup Database */ -class Database { +abstract class DatabaseBase { #------------------------------------------------------------------------------ # Variables @@ -39,6 +39,7 @@ class Database { protected $mErrorCount = 0; protected $mLBInfo = array(); protected $mFakeSlaveLag = null, $mFakeMaster = false; + protected $mDefaultBigSelects = null; #------------------------------------------------------------------------------ # Accessors @@ -49,7 +50,7 @@ class Database { * Fail function, takes a Database as a parameter * Set to false for default, 1 for ignore errors */ - function failFunction( $function = NULL ) { + function failFunction( $function = null ) { return wfSetVar( $this->mFailFunction, $function ); } @@ -64,7 +65,7 @@ class Database { /** * Boolean, controls output of large amounts of debug information */ - function debug( $debug = NULL ) { + function debug( $debug = null ) { return wfSetBit( $this->mFlags, DBO_DEBUG, $debug ); } @@ -72,7 +73,7 @@ class Database { * Turns buffering of SQL result sets on (true) or off (false). * Default is "on" and it should not be changed without good reasons. */ - function bufferResults( $buffer = NULL ) { + function bufferResults( $buffer = null ) { if ( is_null( $buffer ) ) { return !(bool)( $this->mFlags & DBO_NOBUFFER ); } else { @@ -87,7 +88,7 @@ class Database { * code should use lastErrno() and lastError() to handle the * situation as appropriate. */ - function ignoreErrors( $ignoreErrors = NULL ) { + function ignoreErrors( $ignoreErrors = null ) { return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors ); } @@ -95,14 +96,14 @@ class Database { * The current depth of nested transactions * @param $level Integer: , default NULL. */ - function trxLevel( $level = NULL ) { + function trxLevel( $level = null ) { return wfSetVar( $this->mTrxLevel, $level ); } /** * Number of errors logged, only useful when errors are ignored */ - function errorCount( $count = NULL ) { + function errorCount( $count = null ) { return wfSetVar( $this->mErrorCount, $count ); } @@ -113,19 +114,19 @@ class Database { /** * Properties passed down from the server info array of the load balancer */ - function getLBInfo( $name = NULL ) { + function getLBInfo( $name = null ) { if ( is_null( $name ) ) { return $this->mLBInfo; } else { if ( array_key_exists( $name, $this->mLBInfo ) ) { return $this->mLBInfo[$name]; } else { - return NULL; + return null; } } } - function setLBInfo( $name, $value = NULL ) { + function setLBInfo( $name, $value = null ) { if ( is_null( $value ) ) { $this->mLBInfo = $name; } else { @@ -192,6 +193,14 @@ class Database { } /** + * Returns true if this database requires that SELECT DISTINCT queries require that all + ORDER BY expressions occur in the SELECT list per the SQL92 standard + */ + function standardSelectDistinct() { + return true; + } + + /** * 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'; */ @@ -225,14 +234,37 @@ class Database { */ function isOpen() { return $this->mOpened; } + /** + * Set a flag for this connection + * + * @param $flag Integer: DBO_* constants from Defines.php: + * - DBO_DEBUG: output some debug info (same as debug()) + * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults()) + * - DBO_IGNORE: ignore errors (same as ignoreErrors()) + * - DBO_TRX: automatically start transactions + * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode + * and removes it in command line mode + * - DBO_PERSISTENT: use persistant database connection + */ function setFlag( $flag ) { $this->mFlags |= $flag; } + /** + * Clear a flag for this connection + * + * @param $flag: same as setFlag()'s $flag param + */ function clearFlag( $flag ) { $this->mFlags &= ~$flag; } + /** + * Returns a boolean whether the flag $flag is set for this connection + * + * @param $flag: same as setFlag()'s $flag param + * @return Boolean + */ function getFlag( $flag ) { return !!($this->mFlags & $flag); } @@ -252,6 +284,11 @@ class Database { } } + /** + * Get the type of the DBMS, as it appears in $wgDBtype. + */ + abstract function getType(); + #------------------------------------------------------------------------------ # Other functions #------------------------------------------------------------------------------ @@ -272,7 +309,7 @@ class Database { global $wgOut, $wgDBprefix, $wgCommandLineMode; # Can't get a reference if it hasn't been set yet if ( !isset( $wgOut ) ) { - $wgOut = NULL; + $wgOut = null; } $this->mFailFunction = $failFunction; @@ -306,7 +343,7 @@ class Database { } /** - * Same as new Database( ... ), kept for backward compatibility + * Same as new DatabaseMysql( ... ), kept for backward compatibility * @param $server String: database server host * @param $user String: database user name * @param $password String: database user password @@ -316,7 +353,7 @@ class Database { */ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) { - return new Database( $server, $user, $password, $dbName, $failFunction, $flags ); + return new DatabaseMysql( $server, $user, $password, $dbName, $failFunction, $flags ); } /** @@ -327,114 +364,7 @@ class Database { * @param $password String: database user password * @param $dbName String: database name */ - function open( $server, $user, $password, $dbName ) { - global $wgAllDBsAreLocalhost; - wfProfileIn( __METHOD__ ); - - # Test for missing mysql.so - # First try to load it - if (!@extension_loaded('mysql')) { - @dl('mysql.so'); - } - - # Fail now - # Otherwise we get a suppressed fatal error, which is very hard to track down - if ( !function_exists( 'mysql_connect' ) ) { - throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" ); - } - - # Debugging hack -- fake cluster - if ( $wgAllDBsAreLocalhost ) { - $realServer = 'localhost'; - } else { - $realServer = $server; - } - $this->close(); - $this->mServer = $server; - $this->mUser = $user; - $this->mPassword = $password; - $this->mDBname = $dbName; - - $success = false; - - wfProfileIn("dbconnect-$server"); - - # The kernel's default SYN retransmission period is far too slow for us, - # so we use a short timeout plus a manual retry. Retrying means that a small - # but finite rate of SYN packet loss won't cause user-visible errors. - $this->mConn = false; - if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) { - $numAttempts = 2; - } else { - $numAttempts = 1; - } - $this->installErrorHandler(); - for ( $i = 0; $i < $numAttempts && !$this->mConn; $i++ ) { - if ( $i > 1 ) { - usleep( 1000 ); - } - if ( $this->mFlags & DBO_PERSISTENT ) { - $this->mConn = mysql_pconnect( $realServer, $user, $password ); - } else { - # Create a new connection... - $this->mConn = mysql_connect( $realServer, $user, $password, true ); - } - if ($this->mConn === false) { - #$iplus = $i + 1; - #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); - } - } - $phpError = $this->restoreErrorHandler(); - # Always log connection errors - if ( !$this->mConn ) { - $error = $this->lastError(); - if ( !$error ) { - $error = $phpError; - } - wfLogDBError( "Error connecting to {$this->mServer}: $error\n" ); - wfDebug( "DB connection error\n" ); - wfDebug( "Server: $server, User: $user, Password: " . - substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" ); - $success = false; - } - - wfProfileOut("dbconnect-$server"); - - if ( $dbName != '' && $this->mConn !== false ) { - $success = @/**/mysql_select_db( $dbName, $this->mConn ); - if ( !$success ) { - $error = "Error selecting database $dbName on server {$this->mServer} " . - "from client host " . wfHostname() . "\n"; - wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n"); - wfDebug( $error ); - } - } else { - # Delay USE query - $success = (bool)$this->mConn; - } - - if ( $success ) { - $version = $this->getServerVersion(); - if ( version_compare( $version, '4.1' ) >= 0 ) { - // Tell the server we're communicating with it in UTF-8. - // This may engage various charset conversions. - global $wgDBmysql5; - if( $wgDBmysql5 ) { - $this->query( 'SET NAMES utf8', __METHOD__ ); - } - // Turn off strict mode - $this->query( "SET sql_mode = ''", __METHOD__ ); - } - - // Turn off strict mode if it is on - } else { - $this->reportConnectionError( $phpError ); - } - - $this->mOpened = $success; - wfProfileOut( __METHOD__ ); - return $success; - } + abstract function open( $server, $user, $password, $dbName ); protected function installErrorHandler() { $this->mPHPError = false; @@ -466,17 +396,9 @@ class Database { * * @return Bool operation success. true if already closed. */ - function close() - { - $this->mOpened = false; - if ( $this->mConn ) { - if ( $this->trxLevel() ) { - $this->immediateCommit(); - } - return mysql_close( $this->mConn ); - } else { - return true; - } + function close() { + # Stub, should probably be overridden + return true; } /** @@ -505,7 +427,7 @@ class Database { * Should return true if unsure. */ function isWriteQuery( $sql ) { - return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW)\b/i', $sql ); + return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW|\(SELECT)\b/i', $sql ); } /** @@ -629,14 +551,7 @@ class Database { * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure * @private */ - /*private*/ function doQuery( $sql ) { - if( $this->bufferResults() ) { - $ret = mysql_query( $sql, $this->mConn ); - } else { - $ret = mysql_unbuffered_query( $sql, $this->mConn ); - } - return $ret; - } + /*private*/ abstract function doQuery( $sql ); /** * @param $error String @@ -762,12 +677,8 @@ class Database { * @param $res Mixed: A SQL result */ function freeResult( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - if ( !@/**/mysql_free_result( $res ) ) { - throw new DBUnexpectedError( $this, "Unable to free MySQL result" ); - } + # Stub. Might not really need to be overridden, since results should + # be freed by PHP when the variable goes out of scope anyway. } /** @@ -779,16 +690,7 @@ class Database { * @return MySQL row object * @throws DBUnexpectedError Thrown if the database returns an error */ - function fetchObject( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - @/**/$row = mysql_fetch_object( $res ); - if( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); - } - return $row; - } + abstract function fetchObject( $res ); /** * Fetch the next row from the given result object, in associative array @@ -798,43 +700,20 @@ class Database { * @return MySQL row object * @throws DBUnexpectedError Thrown if the database returns an error */ - function fetchRow( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - @/**/$row = mysql_fetch_array( $res ); - if ( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); - } - return $row; - } + abstract function fetchRow( $res ); /** * Get the number of rows in a result object * @param $res Mixed: A SQL result */ - function numRows( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - @/**/$n = mysql_num_rows( $res ); - if( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) ); - } - return $n; - } + abstract function numRows( $res ); /** * Get the number of fields in a result object * See documentation for mysql_num_fields() * @param $res Mixed: A SQL result */ - function numFields( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - return mysql_num_fields( $res ); - } + abstract function numFields( $res ); /** * Get a field name in a result object @@ -843,12 +722,7 @@ class Database { * @param $res Mixed: A SQL result * @param $n Integer */ - function fieldName( $res, $n ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - return mysql_field_name( $res, $n ); - } + abstract function fieldName( $res, $n ); /** * Get the inserted value of an auto-increment row @@ -860,7 +734,7 @@ class Database { * $dbw->insert('page',array('page_id' => $id)); * $id = $dbw->insertId(); */ - function insertId() { return mysql_insert_id( $this->mConn ); } + abstract function insertId(); /** * Change the position of the cursor in a result object @@ -868,51 +742,25 @@ class Database { * @param $res Mixed: A SQL result * @param $row Mixed: Either MySQL row or ResultWrapper */ - function dataSeek( $res, $row ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - return mysql_data_seek( $res, $row ); - } + abstract function dataSeek( $res, $row ); /** * Get the last error number * See mysql_errno() */ - function lastErrno() { - if ( $this->mConn ) { - return mysql_errno( $this->mConn ); - } else { - return mysql_errno(); - } - } + abstract function lastErrno(); /** * Get a description of the last error * See mysql_error() for more details */ - function lastError() { - if ( $this->mConn ) { - # Even if it's non-zero, it can still be invalid - wfSuppressWarnings(); - $error = mysql_error( $this->mConn ); - if ( !$error ) { - $error = mysql_error(); - } - wfRestoreWarnings(); - } else { - $error = mysql_error(); - } - if( $error ) { - $error .= ' (' . $this->mServer . ')'; - } - return $error; - } + abstract function lastError(); + /** * Get the number of rows affected by the last write query * See mysql_affected_rows() for more details */ - function affectedRows() { return mysql_affected_rows( $this->mConn ); } + abstract function affectedRows(); /** * Simple UPDATE wrapper @@ -1095,7 +943,7 @@ class Database { * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" => * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where * $obj- >page_id is the ID of the Astronomy article - * @param $fname String: Calling functio name + * @param $fname String: Calling function name * @param $options Array * @param $join_conds Array * @@ -1118,30 +966,27 @@ class Database { /** * Estimate rows in dataset - * Returns estimated count, based on EXPLAIN output + * Returns estimated count - not necessarily an accurate estimate across different databases, + * so use sparingly * Takes same arguments as Database::select() - */ - - function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { - $options['EXPLAIN']=true; - $res = $this->select ($table, $vars, $conds, $fname, $options ); - if ( $res === false ) - return false; - if (!$this->numRows($res)) { - $this->freeResult($res); - return 0; - } - - $rows=1; - - while( $plan = $this->fetchObject( $res ) ) { - $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero + * + * @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 rowcount', $conds, $fname, $options ); + if ( $res ) { + $row = $this->fetchRow( $res ); + $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0; } - - $this->freeResult($res); - return $rows; + $this->freeResult( $res ); + return $rows; } - /** * Removes most variables from an SQL query and replaces them with X or N for numbers. @@ -1178,7 +1023,7 @@ class Database { $table = $this->tableName( $table ); $res = $this->query( 'DESCRIBE '.$table, $fname ); if ( !$res ) { - return NULL; + return null; } $found = false; @@ -1200,7 +1045,7 @@ class Database { function indexExists( $table, $index, $fname = 'Database::indexExists' ) { $info = $this->indexInfo( $table, $index, $fname ); if ( is_null( $info ) ) { - return NULL; + return null; } else { return $info !== false; } @@ -1220,7 +1065,7 @@ class Database { $sql = 'SHOW INDEX FROM '.$table; $res = $this->query( $sql, $fname ); if ( !$res ) { - return NULL; + return null; } $result = array(); @@ -1257,18 +1102,7 @@ class Database { * @param $table * @param $field */ - function fieldInfo( $table, $field ) { - $table = $this->tableName( $table ); - $res = $this->query( "SELECT * FROM $table LIMIT 1" ); - $n = mysql_num_fields( $res->result ); - for( $i = 0; $i < $n; $i++ ) { - $meta = mysql_fetch_field( $res->result, $i ); - if( $field == $meta->name ) { - return new MySQLField($meta); - } - } - return false; - } + abstract function fieldInfo( $table, $field ); /** * mysql_field_type() wrapper @@ -1286,7 +1120,7 @@ class Database { function indexUnique( $table, $index ) { $indexInfo = $this->indexInfo( $table, $index ); if ( !$indexInfo ) { - return NULL; + return null; } return !$indexInfo[0]->Non_unique; } @@ -1440,11 +1274,32 @@ class Database { } /** + * Bitwise operations + */ + + function bitNot($field) { + return "(~$bitField)"; + } + + function bitAnd($fieldLeft, $fieldRight) { + return "($fieldLeft & $fieldRight)"; + } + + function bitOr($fieldLeft, $fieldRight) { + return "($fieldLeft | $fieldRight)"; + } + + /** * Change the current database + * + * @return bool Success or failure */ function selectDB( $db ) { - $this->mDBname = $db; - return mysql_select_db( $db, $this->mConn ); + # Stub. Shouldn't cause serious problems if it's not overridden, but + # if your database engine supports a concept similar to MySQL's + # databases you may as well. TODO: explain what exactly will fail if + # this is not overridden. + return true; } /** @@ -1621,9 +1476,7 @@ class Database { * @param $s String: to be slashed. * @return String: slashed string. */ - function strencode( $s ) { - return mysql_real_escape_string( $s, $this->mConn ); - } + abstract function strencode( $s ); /** * If it's a string, adds quotes and backslashes @@ -1642,30 +1495,78 @@ class Database { } /** - * Escape string for safe LIKE usage + * Escape string for safe LIKE usage. + * WARNING: you should almost never use this function directly, + * instead use buildLike() that escapes everything automatically */ function escapeLike( $s ) { - $s=str_replace('\\','\\\\',$s); - $s=$this->strencode( $s ); - $s=str_replace(array('%','_'),array('\%','\_'),$s); + $s = str_replace( '\\', '\\\\', $s ); + $s = $this->strencode( $s ); + $s = str_replace( array( '%', '_' ), array( '\%', '\_' ), $s ); return $s; } /** + * LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match + * containing either string literals that will be escaped or tokens returned by anyChar() or anyString(). + * Alternatively, the function could be provided with an array of aforementioned parameters. + * + * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns a LIKE clause that searches + * for subpages of 'My page title'. + * Alternatively: $pattern = array( 'My_page_title/', $dbr->anyString() ); $query .= $dbr->buildLike( $pattern ); + * + * @ return String: fully built LIKE statement + */ + function buildLike() { + $params = func_get_args(); + if (count($params) > 0 && is_array($params[0])) { + $params = $params[0]; + } + + $s = ''; + foreach( $params as $value) { + if( $value instanceof LikeMatch ) { + $s .= $value->toString(); + } else { + $s .= $this->escapeLike( $value ); + } + } + return " LIKE '" . $s . "' "; + } + + /** + * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query + */ + function anyChar() { + return new LikeMatch( '_' ); + } + + /** + * Returns a token for buildLike() that denotes a '%' to be used in a LIKE query + */ + function anyString() { + return new LikeMatch( '%' ); + } + + /** * Returns an appropriately quoted sequence value for inserting a new row. * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL * subclass will return an integer, and save the value for insertId() */ function nextSequenceValue( $seqName ) { - return NULL; + return null; } /** - * USE INDEX clause - * PostgreSQL doesn't have them and returns "" + * USE INDEX clause. Unlikely to be useful for anything but MySQL. This + * is only needed because a) MySQL must be as efficient as possible due to + * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about + * which index to pick. Anyway, other databases might have different + * indexes on a given table. So don't bother overriding this unless you're + * MySQL. */ function useIndexClause( $index ) { - return "FORCE INDEX (" . $this->indexName( $index ) . ")"; + return ''; } /** @@ -1753,10 +1654,14 @@ class Database { } /** + * A string to insert into queries to show that they're low-priority, like + * MySQL's LOW_PRIORITY. If no such feature exists, return an empty + * string and nothing bad should happen. + * * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise */ function lowPriorityOption() { - return 'LOW_PRIORITY'; + return ''; } /** @@ -1810,27 +1715,60 @@ class Database { } /** - * Construct a LIMIT query with optional offset - * This is used for query pages + * Construct a LIMIT query with optional offset. This is used for query + * pages. The SQL should be adjusted so that only the first $limit rows + * are returned. If $offset is provided as well, then the first $offset + * rows should be discarded, and the next $limit rows should be returned. + * If the result of the query is not ordered, then the rows to be returned + * are theoretically arbitrary. + * + * $sql is expected to be a SELECT, if that makes a difference. For + * UPDATE, limitResultForUpdate should be used. + * + * The version provided by default works in MySQL and SQLite. It will very + * likely need to be overridden for most other DBMSes. + * * @param $sql String: SQL query we will append the limit too * @param $limit Integer: the SQL limit * @param $offset Integer the SQL offset (default false) */ - function limitResult($sql, $limit, $offset=false) { - if( !is_numeric($limit) ) { + function limitResult( $sql, $limit, $offset=false ) { + if( !is_numeric( $limit ) ) { throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" ); } return "$sql LIMIT " . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" ) . "{$limit} "; } - function limitResultForUpdate($sql, $num) { - return $this->limitResult($sql, $num, 0); + function limitResultForUpdate( $sql, $num ) { + return $this->limitResult( $sql, $num, 0 ); } /** - * Returns an SQL expression for a simple conditional. - * Uses IF on MySQL. + * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries + * within the UNION construct. + * @return Boolean + */ + function unionSupportsOrderAndLimit() { + return true; // True for almost every DB supported + } + + /** + * Construct a UNION query + * This is used for providing overload point for other DB abstractions + * not compatible with the MySQL syntax. + * @param $sqls Array: SQL statements to combine + * @param $all Boolean: use UNION ALL + * @return String: SQL fragment + */ + function unionQueries($sqls, $all) { + $glue = $all ? ') UNION ALL (' : ') UNION ('; + return '('.implode( $glue, $sqls ) . ')'; + } + + /** + * Returns an SQL expression for a simple conditional. This doesn't need + * to be overridden unless CASE isn't supported in your DBMS. * * @param $cond String: SQL expression which will result in a boolean value * @param $trueVal String: SQL expression to return if true @@ -1838,7 +1776,7 @@ class Database { * @return String: SQL fragment */ function conditional( $cond, $trueVal, $falseVal ) { - return " IF($cond, $trueVal, $falseVal) "; + return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; } /** @@ -1855,17 +1793,27 @@ class Database { /** * Determines if the last failure was due to a deadlock + * STUB */ function wasDeadlock() { - return $this->lastErrno() == 1213; + return false; } /** * Determines if the last query error was something that should be dealt - * with by pinging the connection and reissuing the query + * with by pinging the connection and reissuing the query. + * STUB */ function wasErrorReissuable() { - return $this->lastErrno() == 2013 || $this->lastErrno() == 2006; + return false; + } + + /** + * Determines if the last failure was due to the database being read-only. + * STUB + */ + function wasReadOnlyError() { + return false; } /** @@ -1935,7 +1883,7 @@ class Database { # Commit any open transactions if ( $this->mTrxLevel ) { - $this->immediateCommit(); + $this->commit(); } if ( !is_null( $this->mFakeSlaveLag ) ) { @@ -2048,6 +1996,21 @@ class Database { } /** + * Creates a new table with structure copied from existing table + * Note that unlike most database abstraction functions, this function does not + * automatically append database prefix, because it works at a lower + * abstraction level. + * + * @param $oldName String: name of table whose structure should be copied + * @param $newName String: name of table to be created + * @param $temporary Boolean: whether the new table should be temporary + * @return Boolean: true if operation was successful + */ + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'Database::duplicateTableStructure' ) { + throw new MWException( 'DatabaseBase::duplicateTableStructure is not implemented in descendant class' ); + } + + /** * Return MW-style timestamp used for MySQL schema */ function timestamp( $ts=0 ) { @@ -2089,41 +2052,31 @@ class Database { } /** + * Returns a wikitext link to the DB's website, e.g., + * return "[http://www.mysql.com/ MySQL]"; + * Should at least contain plain text, if for some reason + * your database has no website. + * * @return String: wikitext of a link to the server software's web site */ - function getSoftwareLink() { - return "[http://www.mysql.com/ MySQL]"; - } + abstract function getSoftwareLink(); /** + * A string describing the current software version, like from + * mysql_get_server_info(). Will be listed on Special:Version, etc. + * * @return String: Version information from the database */ - function getServerVersion() { - return mysql_get_server_info( $this->mConn ); - } + abstract function getServerVersion(); /** * Ping the server and try to reconnect if it there is no connection + * + * @return bool Success or failure */ function ping() { - if( !function_exists( 'mysql_ping' ) ) { - wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" ); - return true; - } - $ping = mysql_ping( $this->mConn ); - if ( $ping ) { - return true; - } - - // Need to reconnect manually in MySQL client 5.0.13+ - if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) { - mysql_close( $this->mConn ); - $this->mOpened = false; - $this->mConn = false; - $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname ); - return true; - } - return false; + # Stub. Not essential to override. + return true; } /** @@ -2135,7 +2088,7 @@ class Database { wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" ); return $this->mFakeSlaveLag; } - $res = $this->query( 'SHOW PROCESSLIST' ); + $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ ); # Find slave SQL thread while ( $row = $this->fetchObject( $res ) ) { /* This should work for most situations - when default db @@ -2149,7 +2102,10 @@ class Database { $row->State != 'Connecting to master' && $row->State != 'Queueing master event to the relay log' && $row->State != 'Waiting for master update' && - $row->State != 'Requesting binlog dump' + $row->State != 'Requesting binlog dump' && + $row->State != 'Waiting to reconnect after a failed master event read' && + $row->State != 'Reconnecting after a failed master event read' && + $row->State != 'Registering slave on master' ) { # This is it, return the time (except -ve) if ( $row->Time > 0x7fffffff ) { @@ -2190,16 +2146,14 @@ class Database { } /** - * Override database's default connection timeout. - * May be useful for very long batch queries such as - * full-wiki dumps, where a single query reads out - * over hours or days. + * Override database's default connection timeout. May be useful for very + * long batch queries such as full-wiki dumps, where a single query reads + * out over hours or days. May or may not be necessary for non-MySQL + * databases. For most purposes, leaving it as a no-op should be fine. + * * @param $timeout Integer in seconds */ - public function setTimeout( $timeout ) { - $this->query( "SET net_read_timeout=$timeout" ); - $this->query( "SET net_write_timeout=$timeout" ); - } + public function setTimeout( $timeout ) {} /** * Read and execute SQL commands from a file. @@ -2211,14 +2165,45 @@ class Database { function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) { $fp = fopen( $filename, 'r' ); if ( false === $fp ) { - throw new MWException( "Could not open \"{$filename}\".\n" ); + if (!defined("MEDIAWIKI_INSTALL")) + throw new MWException( "Could not open \"{$filename}\".\n" ); + else + return "Could not open \"{$filename}\".\n"; + } + try { + $error = $this->sourceStream( $fp, $lineCallback, $resultCallback ); } - $error = $this->sourceStream( $fp, $lineCallback, $resultCallback ); + catch( MWException $e ) { + if ( defined("MEDIAWIKI_INSTALL") ) { + $error = $e->getMessage(); + } else { + fclose( $fp ); + throw $e; + } + } + fclose( $fp ); return $error; } /** + * Get the full path of a patch file. Originally based on archive() + * from updaters.inc. Keep in mind this always returns a patch, as + * it fails back to MySQL if no DB-specific patch can be found + * + * @param $patch String The name of the patch, like patch-something.sql + * @return String Full path to patch file + */ + public static function patchPath( $patch ) { + global $wgDBtype, $IP; + if ( file_exists( "$IP/maintenance/$wgDBtype/archives/$patch" ) ) { + return "$IP/maintenance/$wgDBtype/archives/$patch"; + } else { + return "$IP/maintenance/archives/$patch"; + } + } + + /** * Read and execute commands from an open file handle * Returns true on success, error string or exception on failure (depending on object's error ignore settings) * @param $fp String: File handle @@ -2257,7 +2242,7 @@ class Database { } } - if ( '' != $cmd ) { $cmd .= ' '; } + if ( $cmd != '' ) { $cmd .= ' '; } $cmd .= "$line\n"; if ( $done ) { @@ -2326,15 +2311,17 @@ class Database { return $this->indexName( $matches[1] ); } - /* + /** * Build a concatenation list to feed into a SQL query - */ + * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting + * @return String + */ function buildConcat( $stringList ) { return 'CONCAT(' . implode( ',', $stringList ) . ')'; } /** - * Acquire a lock + * Acquire a named lock * * Abstracted from Filestore::lock() so child classes can implement for * their own needs. @@ -2343,32 +2330,44 @@ class Database { * @param $method String: Name of method calling us * @return bool */ - public function lock( $lockName, $method ) { - $lockName = $this->addQuotes( $lockName ); - $result = $this->query( "SELECT GET_LOCK($lockName, 5) AS lockstatus", $method ); - $row = $this->fetchObject( $result ); - $this->freeResult( $result ); - - if( $row->lockstatus == 1 ) { - return true; - } else { - wfDebug( __METHOD__." failed to acquire lock\n" ); - return false; - } + public function lock( $lockName, $method, $timeout = 5 ) { + return true; } + /** * Release a lock. * - * @todo fixme - Figure out a way to return a bool - * based on successful lock release. - * * @param $lockName String: Name of lock to release * @param $method String: Name of method calling us + * + * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock + * @return Returns 1 if the lock was released, 0 if the lock was not established + * by this thread (in which case the lock is not released), and NULL if the named + * lock did not exist */ public function unlock( $lockName, $method ) { - $lockName = $this->addQuotes( $lockName ); - $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method ); - $this->freeResult( $result ); + return true; + } + + /** + * Lock specific tables + * + * @param $read Array of tables to lock for read access + * @param $write Array of tables to lock for write access + * @param $method String name of caller + * @param $lowPriority bool Whether to indicate writes to be LOW PRIORITY + */ + public function lockTables( $read, $write, $method, $lowPriority = true ) { + return true; + } + + /** + * Unlock specific tables + * + * @param $method String the caller + */ + public function unlockTables( $method ) { + return true; } /** @@ -2380,19 +2379,21 @@ class Database { public function getSearchEngine() { return "SearchMySQL"; } -} -/** - * Database abstraction object for mySQL - * Inherit all methods and properties of Database::Database() - * - * @ingroup Database - * @see Database - */ -class DatabaseMysql extends Database { - # Inherit all + /** + * Allow or deny "big selects" for this session only. This is done by setting + * the sql_big_selects session variable. + * + * This is a MySQL-specific feature. + * + * @param mixed $value true for allow, false for deny, or "default" to restore the initial value + */ + public function setBigSelects( $value = true ) { + // no-op + } } + /****************************************************************************** * Utility classes *****************************************************************************/ @@ -2502,10 +2503,19 @@ class DBError extends MWException { * @param $db Database object which threw the error * @param $error A simple error message to be used for debugging */ - function __construct( Database &$db, $error ) { + function __construct( DatabaseBase &$db, $error ) { $this->db =& $db; parent::__construct( $error ); } + + function getText() { + global $wgShowDBErrorBacktrace; + $s = $this->getMessage() . "\n"; + if ( $wgShowDBErrorBacktrace ) { + $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n"; + } + return $s; + } } /** @@ -2514,7 +2524,7 @@ class DBError extends MWException { class DBConnectionError extends DBError { public $error; - function __construct( Database &$db, $error = 'unknown error' ) { + function __construct( DatabaseBase &$db, $error = 'unknown error' ) { $msg = 'DB connection error'; if ( trim( $error ) != '' ) { $msg .= ": $error"; @@ -2533,10 +2543,6 @@ class DBConnectionError extends DBError { return false; } - function getText() { - return $this->getMessage() . "\n"; - } - function getLogMessage() { # Don't send to the exception log return false; @@ -2553,7 +2559,7 @@ class DBConnectionError extends DBError { } function getHTML() { - global $wgLang, $wgMessageCache, $wgUseFileCache; + global $wgLang, $wgMessageCache, $wgUseFileCache, $wgShowDBErrorBacktrace; $sorry = 'Sorry! This site is experiencing technical difficulties.'; $again = 'Try waiting a few minutes and reloading.'; @@ -2577,30 +2583,31 @@ class DBConnectionError extends DBError { $noconnect = "<p><strong>$sorry</strong><br />$again</p><p><small>$info</small></p>"; $text = str_replace( '$1', $this->error, $noconnect ); - /* - if ( $GLOBALS['wgShowExceptionDetails'] ) { - $text .= '</p><p>Backtrace:</p><p>' . - nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . - "</p>\n"; - }*/ + if ( $wgShowDBErrorBacktrace ) { + $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ); + } $extra = $this->searchForm(); if( $wgUseFileCache ) { - $cache = $this->fileCachedPage(); - # Cached version on file system? - if( $cache !== null ) { - # Hack: extend the body for error messages - $cache = str_replace( array('</html>','</body>'), '', $cache ); - # Add cache notice... - $cachederror = "This is a cached copy of the requested page, and may not be up to date. "; - # Localize it if possible... - if( $wgLang instanceof Language ) { - $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) ); + try { + $cache = $this->fileCachedPage(); + # Cached version on file system? + if( $cache !== null ) { + # Hack: extend the body for error messages + $cache = str_replace( array('</html>','</body>'), '', $cache ); + # Add cache notice... + $cachederror = "This is a cached copy of the requested page, and may not be up to date. "; + # Localize it if possible... + if( $wgLang instanceof Language ) { + $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) ); + } + $warning = "<div style='color:red;font-size:150%;font-weight:bold;'>$cachederror</div>"; + # Output cached page with notices on bottom and re-close body + return "{$cache}{$warning}<hr />$text<hr />$extra</body></html>"; } - $warning = "<div style='color:red;font-size:150%;font-weight:bold;'>$cachederror</div>"; - # Output cached page with notices on bottom and re-close body - return "{$cache}{$warning}<hr />$text<hr />$extra</body></html>"; + } catch( MWException $e ) { + // Do nothing, just use the default page } } # Headers needed here - output is just the error message @@ -2631,8 +2638,6 @@ class DBConnectionError extends DBError { <input type="hidden" name="ie" value="$wgInputEncoding" /> <input type="hidden" name="oe" value="$wgInputEncoding" /> - <img src="http://www.google.com/logos/Logo_40wht.gif" alt="" style="float:left; margin-left: 1.5em; margin-right: 1.5em;" /> - <input type="text" name="q" size="31" maxlength="255" value="$search" /> <input type="submit" name="btnG" value="$googlesearch" /> <div> @@ -2653,9 +2658,9 @@ EOT; $mainpage = htmlspecialchars( $wgLang->getMessage( 'mainpage' ) ); } - if($wgTitle) { + if( $wgTitle ) { $t =& $wgTitle; - } elseif($title) { + } elseif( $title ) { $t = Title::newFromURL( $title ); } else { $t = Title::newFromText( $mainpage ); @@ -2681,7 +2686,7 @@ EOT; class DBQueryError extends DBError { public $error, $errno, $sql, $fname; - function __construct( Database &$db, $error, $errno, $sql, $fname ) { + function __construct( DatabaseBase &$db, $error, $errno, $sql, $fname ) { $message = "A database error has occurred\n" . "Query: $sql\n" . "Function: $fname\n" . @@ -2695,11 +2700,16 @@ class DBQueryError extends DBError { } function getText() { + global $wgShowDBErrorBacktrace; if ( $this->useMessageCache() ) { - return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ), - htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n"; + $s = wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ), + htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n"; + if ( $wgShowDBErrorBacktrace ) { + $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n"; + } + return $s; } else { - return $this->getMessage(); + return parent::getText(); } } @@ -2722,12 +2732,17 @@ class DBQueryError extends DBError { } function getHTML() { + global $wgShowDBErrorBacktrace; if ( $this->useMessageCache() ) { - return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ), + $s = wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ), htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ); } else { - return nl2br( htmlspecialchars( $this->getMessage() ) ); + $s = nl2br( htmlspecialchars( $this->getMessage() ) ); } + if ( $wgShowDBErrorBacktrace ) { + $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ); + } + return $s; } } @@ -2841,15 +2856,18 @@ class ResultWrapper implements Iterator { } } -class MySQLMasterPos { - var $file, $pos; +/** + * Used by DatabaseBase::buildLike() to represent characters that have special meaning in SQL LIKE clauses + * and thus need no escaping. Don't instantiate it manually, use Database::anyChar() and anyString() instead. + */ +class LikeMatch { + private $str; - function __construct( $file, $pos ) { - $this->file = $file; - $this->pos = $pos; + public function __construct( $s ) { + $this->str = $s; } - function __toString() { - return "{$this->file}/{$this->pos}"; + public function toString() { + return $this->str; } } diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php index fcd0bc2d..9b62af82 100644 --- a/includes/db/DatabaseIbm_db2.php +++ b/includes/db/DatabaseIbm_db2.php @@ -9,37 +9,33 @@ */ /** - * 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; + private $name = ''; + private $tablename = ''; + private $type = ''; + private $nullable = false; + private $max_length = 0; /** * Builder method for the class - * @param Object $db Database interface - * @param string $table table name - * @param string $field column name + * @param $db DatabaseIbm_db2: Database interface + * @param $table String: table name + * @param $field String: column name * @return IBM_DB2Field */ static function fromText($db, $table, $field) { global $wgDBmwschema; - $q = <<<END + $q = <<<SQL SELECT lcase(coltype) AS typname, nulls AS attnotnull, length AS attlen FROM sysibm.syscolumns WHERE tbcreator=%s AND tbname=%s AND name=%s; -END; +SQL; $res = $db->query(sprintf($q, $db->addQuotes($wgDBmwschema), $db->addQuotes($table), @@ -89,20 +85,25 @@ END; class IBM_DB2Blob { private $mData; - function __construct($data) { + public function __construct($data) { $this->mData = $data; } - function getData() { + public function getData() { return $this->mData; } + + public function __toString() + { + return $this->mData; + } } /** * Primary database interface * @ingroup Database */ -class DatabaseIbm_db2 extends Database { +class DatabaseIbm_db2 extends DatabaseBase { /* * Inherited members protected $mLastQuery = ''; @@ -122,27 +123,42 @@ class DatabaseIbm_db2 extends Database { */ /// Server port for uncataloged connections - protected $mPort = NULL; + protected $mPort = null; /// Whether connection is cataloged - protected $mCataloged = NULL; + protected $mCataloged = null; /// Schema for tables, stored procedures, triggers - protected $mSchema = NULL; + protected $mSchema = null; /// Whether the schema has been applied in this session protected $mSchemaSet = false; /// Result of last query - protected $mLastResult = NULL; + protected $mLastResult = null; /// Number of rows affected by last INSERT/UPDATE/DELETE - protected $mAffectedRows = NULL; + protected $mAffectedRows = null; /// Number of rows returned by last SELECT - protected $mNumRows = NULL; + protected $mNumRows = null; + + /// Connection config options - see constructor + public $mConnOptions = array(); + /// Statement config options -- see constructor + public $mStmtOptions = array(); const CATALOGED = "cataloged"; const UNCATALOGED = "uncataloged"; const USE_GLOBAL = "get from global"; + const NONE_OPTION = 0x00; + const CONN_OPTION = 0x01; + const STMT_OPTION = 0x02; + + const REGULAR_MODE = 'regular'; + const INSTALL_MODE = 'install'; + + // Whether this is regular operation or the initial installation + protected $mMode = self::REGULAR_MODE; + /// Last sequence value used for a primary key - protected $mInsertId = NULL; + protected $mInsertId = null; /* * These can be safely inherited @@ -219,7 +235,7 @@ class DatabaseIbm_db2 extends Database { */ /* - * These need to be implemented TODO + * These have been implemented * * Administrative: 7 / 7 * constructor [Done] @@ -375,7 +391,10 @@ class DatabaseIbm_db2 extends Database { return $this->mDBname; } } - + + function getType() { + return 'ibm_db2'; + } ###################################### # Setup @@ -384,12 +403,13 @@ class DatabaseIbm_db2 extends Database { /** * - * @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) + * @param $server String: hostname of database server + * @param $user String: username + * @param $password String: password + * @param $dbName String: database name on the server + * @param $failFunction Callback (optional) + * @param $flags Integer: database behaviour flags (optional, unused) + * @param $schema String */ public function DatabaseIbm_db2($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0, @@ -399,7 +419,7 @@ class DatabaseIbm_db2 extends Database { global $wgOut, $wgDBmwschema; # Can't get a reference if it hasn't been set yet if ( !isset( $wgOut ) ) { - $wgOut = NULL; + $wgOut = null; } $this->mOut =& $wgOut; $this->mFailFunction = $failFunction; @@ -412,17 +432,50 @@ class DatabaseIbm_db2 extends Database { $this->mSchema = $schema; } + // configure the connection and statement objects + $this->setDB2Option('db2_attr_case', 'DB2_CASE_LOWER', self::CONN_OPTION | self::STMT_OPTION); + $this->setDB2Option('deferred_prepare', 'DB2_DEFERRED_PREPARE_ON', self::STMT_OPTION); + $this->setDB2Option('rowcount', 'DB2_ROWCOUNT_PREFETCH_ON', self::STMT_OPTION); + $this->open( $server, $user, $password, $dbName); } /** + * Enables options only if the ibm_db2 extension version supports them + * @param $name String: name of the option in the options array + * @param $const String: name of the constant holding the right option value + * @param $type Integer: whether this is a Connection or Statement otion + */ + private function setDB2Option($name, $const, $type) { + if (defined($const)) { + if ($type & self::CONN_OPTION) $this->mConnOptions[$name] = constant($const); + if ($type & self::STMT_OPTION) $this->mStmtOptions[$name] = constant($const); + } + else { + $this->installPrint("$const is not defined. ibm_db2 version is likely too low."); + } + } + + /** + * Outputs debug information in the appropriate place + * @param $string String: the relevant debug message + */ + private function installPrint($string) { + wfDebug("$string"); + if ($this->mMode == self::INSTALL_MODE) { + print "<li>$string</li>"; + flush(); + } + } + + /** * 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 + * @param $server String: hostname + * @param $user String + * @param $password String + * @param $dbName String: database name */ public function open( $server, $user, $password, $dbName ) { @@ -437,7 +490,7 @@ class DatabaseIbm_db2 extends Database { // 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->installPrint($error); $this->reportConnectionError($error); } @@ -461,16 +514,16 @@ class DatabaseIbm_db2 extends Database { elseif ( $cataloged == self::UNCATALOGED ) { $this->openUncataloged($dbName, $user, $password, $server, $port); } - // Don't do this + // Apply connection config + db2_set_option($this->mConn, $this->mConnOptions, 1); // 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); + // Rather, turn autocommit off in the begin function and turn on after a commit 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" ); + $this->installPrint( "DB connection error\n" ); + $this->installPrint( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); + $this->installPrint( $this->lastError()."\n" ); return null; } @@ -524,14 +577,14 @@ class DatabaseIbm_db2 extends Database { /** * 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) + * + * @param $server String: hostname of database server + * @param $user String: username + * @param $password String + * @param $dbName String: database name on the server + * @param $failFunction Callback (optional) + * @param $flags Integer: database behaviour flags (optional, unused) + * @return DatabaseIbm_db2 object */ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0) { @@ -543,20 +596,16 @@ class DatabaseIbm_db2 extends Database { * 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; + if ($connerr) { + //$this->rollback(); + 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."; + if ($stmterr) { + //$this->rollback(); + return $stmterr; + } return false; } @@ -592,7 +641,7 @@ class DatabaseIbm_db2 extends Database { // Switch into the correct namespace $this->applySchema(); - $ret = db2_exec( $this->mConn, $sql ); + $ret = db2_exec( $this->mConn, $sql, $this->mStmtOptions ); if( !$ret ) { print "<br><pre>"; print $sql; @@ -601,7 +650,7 @@ class DatabaseIbm_db2 extends Database { throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( $error ) ); } $this->mLastResult = $ret; - $this->mAffectedRows = NULL; // Not calculated until asked for + $this->mAffectedRows = null; // Not calculated until asked for return $ret; } @@ -653,17 +702,6 @@ EOF; 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; } @@ -707,14 +745,26 @@ EOF; $this->applySchema(); $this->begin(); - $res = dbsource( "../maintenance/ibm_db2/tables.sql", $this); + $res = $this->sourceFile( "../maintenance/ibm_db2/tables.sql" ); + if ($res !== true) { + print " <b>FAILED</b>: " . htmlspecialchars( $res ) . "</li>"; + } else { + print " done</li>"; + } $res = null; // TODO: update mediawiki_version table // TODO: populate interwiki links - $this->commit(); + if ($this->lastError()) { + print "<li>Errors encountered during table creation -- rolled back</li>\n"; + print "<li>Please install again</li>\n"; + $this->rollback(); + } + else { + $this->commit(); + } } catch (MWException $mwe) { @@ -725,15 +775,17 @@ EOF; /** * Escapes strings * Doesn't escape numbers - * @param string s string to escape + * @param $s String: string to escape * @return escaped string */ public function addQuotes( $s ) { - //wfDebug("DB2::addQuotes($s)\n"); + //$this->installPrint("DB2::addQuotes($s)\n"); if ( is_null( $s ) ) { return "NULL"; } else if ($s instanceof Blob) { return "'".$s->fetch($s)."'"; + } else if ($s instanceof IBM_DB2Blob) { + return "'".$this->decodeBlob($s)."'"; } $s = $this->strencode($s); if ( is_numeric($s) ) { @@ -745,41 +797,9 @@ EOF; } /** - * 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 + * @param $type String: DB2 column type */ public function is_numeric_type( $type ) { switch (strtoupper($type)) { @@ -798,7 +818,7 @@ EOF; /** * Alias for addQuotes() - * @param string s string to escape + * @param $s String: string to escape * @return escaped string */ public function strencode( $s ) { @@ -830,7 +850,7 @@ EOF; /** * Start a transaction (mandatory) */ - public function begin() { + public function begin( $fname = 'DatabaseIbm_db2::begin' ) { // turn off auto-commit db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF); $this->mTrxLevel = 1; @@ -840,7 +860,7 @@ EOF; * End a transaction * Must have a preceding begin() */ - public function commit() { + public function commit( $fname = 'DatabaseIbm_db2::commit' ) { db2_commit($this->mConn); // turn auto-commit back on db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON); @@ -850,7 +870,7 @@ EOF; /** * Cancel a transaction */ - public function rollback() { + public function rollback( $fname = 'DatabaseIbm_db2::rollback' ) { db2_rollback($this->mConn); // turn auto-commit back on // not sure if this is appropriate @@ -868,7 +888,6 @@ EOF; * 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' ); } @@ -931,88 +950,18 @@ EOF; } /** - * 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) + * @param $sql string SQL query we will append the limit too + * @param $limit integer the SQL limit + * @param $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"); + $this->installPrint("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 @@ -1026,20 +975,22 @@ EOF; */ public function tableName( $name ) { # Replace reserved words with better ones - switch( $name ) { - case 'user': - return 'mwuser'; - case 'text': - return 'pagecontent'; - default: - return $name; - } +// switch( $name ) { +// case 'user': +// return 'mwuser'; +// case 'text': +// return 'pagecontent'; +// default: +// return $name; +// } + // we want maximum compatibility with MySQL schema + return $name; } /** * Generates a timestamp in an insertable format * @return string timestamp value - * @param timestamp $ts + * @param $ts timestamp */ public function timestamp( $ts=0 ) { // TS_MW cannot be easily distinguished from an integer @@ -1048,16 +999,21 @@ EOF; /** * Return the next in a sequence, save the value for retrieval via insertId() - * @param string seqName Name of a defined sequence in the database + * @param $seqName String: name of a defined sequence in the database * @return next value in that sequence */ public function nextSequenceValue( $seqName ) { + // Not using sequences in the primary schema to allow for easy third-party migration scripts + // Emulating MySQL behaviour of using NULL to signal that sequences aren't used + /* $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; + */ + return null; } /** @@ -1069,139 +1025,180 @@ EOF; } /** + * Updates the mInsertId property with the value of the last insert into a generated column + * @param $table String: sanitized table name + * @param $primaryKey Mixed: string name of the primary key or a bool if this call is a do-nothing + * @param $stmt Resource: prepared statement resource + * of the SELECT primary_key FROM FINAL TABLE ( INSERT ... ) form + */ + private function calcInsertId($table, $primaryKey, $stmt) { + if ($primaryKey) { + $id_row = $this->fetchRow($stmt); + $this->mInsertId = $id_row[0]; + } + } + + /** * 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 + * @param $table String: Name of the table to insert to. + * @param $args Array: Items to insert into the table. + * @param $fname String: Name of the function, for profiling + * @param $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; } - + // get database-specific table name (not used) $table = $this->tableName( $table ); - - if ( !is_array( $options ) ) - $options = array( $options ); - - if ( isset( $args[0] ) && is_array( $args[0] ) ) { - } - else { + // format options as an array + if ( !is_array( $options ) ) $options = array( $options ); + // format args as an array of arrays + if ( !( isset( $args[0] ) && is_array( $args[0] ) ) ) { $args = array($args); } + // prevent insertion of NULL into primary key columns + list($args, $primaryKeys) = $this->removeNullPrimaryKeys($table, $args); + // if there's only one primary key + // we'll be able to read its value after insertion + $primaryKey = false; + if (count($primaryKeys) == 1) { + $primaryKey = $primaryKeys[0]; + } + + // get column names $keys = array_keys( $args[0] ); + $key_count = count($keys); // 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); + // assume success + $res = true; // 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 '; + switch($key_count) { + //case 0 impossible + case 1: + $sql .= '(?)'; + break; + default: + $sql .= '(?' . str_repeat(',?', $key_count-1) . ')'; + } + // add logic to read back the new primary key value + if ($primaryKey) { + $sql = "SELECT $primaryKey FROM FINAL TABLE($sql)"; + } + $stmt = $this->prepare($sql); + + // start a transaction/enter transaction mode + $this->begin(); if ( !$ignore ) { $first = true; foreach ( $args as $row ) { - if ( $first ) { - $first = false; - } else { - $sql .= ','; - } - $sql .= '(' . $this->makeListSmart( $table, $row ) . ')'; + // insert each row into the database + $res = $res & $this->execute($stmt, $row); + // get the last inserted value into a generated column + $this->calcInsertId($table, $primaryKey, $stmt); } - $res = (bool)$this->query( $sql, $fname, $ignore ); } else { + $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; + + // always return true $res = true; - $origsql = $sql; + foreach ( $args as $row ) { - $tempsql = $origsql; - $tempsql .= '(' . $this->makeListSmart( $table, $row ) . ')'; - - if ( $ignore ) { - db2_exec($this->mConn, "SAVEPOINT $ignore"); + $overhead = "SAVEPOINT $ignore ON ROLLBACK RETAIN CURSORS"; + db2_exec($this->mConn, $overhead, $this->mStmtOptions); + + $res2 = $this->execute($stmt, $row); + // get the last inserted value into a generated column + $this->calcInsertId($table, $primaryKey, $stmt); + + $errNum = $this->lastErrno(); + if ($errNum) { + db2_exec( $this->mConn, "ROLLBACK TO SAVEPOINT $ignore", $this->mStmtOptions ); } - - $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++; - } + else { + db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore", $this->mStmtOptions ); + $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; } + // commit either way + $this->commit(); return $res; } /** + * Given a table name and a hash of columns with values + * Removes primary key columns from the hash where the value is NULL + * + * @param $table String: name of the table + * @param $args Array of hashes of column names with values + * @return Array: tuple containing filtered array of columns, array of primary keys + */ + private function removeNullPrimaryKeys($table, $args) { + $schema = $this->mSchema; + // find out the primary keys + $keyres = db2_primary_keys($this->mConn, null, strtoupper($schema), strtoupper($table)); + $keys = array(); + for ($row = $this->fetchObject($keyres); $row != null; $row = $this->fetchRow($keyres)) { + $keys[] = strtolower($row->column_name); + } + // remove primary keys + foreach ($args as $ai => $row) { + foreach ($keys as $ki => $key) { + if ($row[$key] == null) { + unset($row[$key]); + } + } + $args[$ai] = $row; + } + // return modified hash + return array($args, $keys); + } + + /** * 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() ) { + * @param $table String: The table to UPDATE + * @param $values An array of values to SET + * @param $conds An array of conditions (WHERE). Use '*' to update all rows. + * @param $fname String: The Class::Function calling this function + * (for the log) + * @param $options An array of UPDATE options, can be one or + * more of IGNORE, LOW_PRIORITY + * @return Boolean + */ + public 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 ); + $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET ); if ( $conds != '*' ) { - $sql .= " WHERE " . $this->makeListSmart( $table, $conds, LIST_AND ); + $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); } return $this->query( $sql, $fname ); } @@ -1211,21 +1208,21 @@ EOF; * * Use $conds == "*" to delete all rows */ - function delete( $table, $conds, $fname = 'Database::delete' ) { + public 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 ); + $sql .= ' WHERE ' . $this->makeList( $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 + * @return Integer: the number of rows affected by the last query */ public function affectedRows() { if ( !is_null( $this->mAffectedRows ) ) { @@ -1238,20 +1235,11 @@ EOF; } /** - * 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 + * @param $uniqueIndexes Array consisting of indexes and arrays of indexes + * @param $rows Array: rows to insert + * @param $fname String: name of the function for profiling * @return nothing */ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseIbm_db2::replace' ) { @@ -1306,8 +1294,8 @@ EOF; /** * 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 + * @param $res Object result set + * @return Integer: number of rows */ public function numRows( $res ) { if ( $res instanceof ResultWrapper ) { @@ -1323,8 +1311,8 @@ EOF; /** * Moves the row pointer of the result set - * @param Object $res result set - * @param int $row row number + * @param $res Object: result set + * @param $row Integer: row number * @return success or failure */ public function dataSeek( $res, $row ) { @@ -1340,8 +1328,8 @@ EOF; /** * Frees memory associated with a statement resource - * @param Object $res Statement resource to free - * @return bool success or failure + * @param $res Object: statement resource to free + * @return Boolean success or failure */ public function freeResult( $res ) { if ( $res instanceof ResultWrapper ) { @@ -1354,7 +1342,7 @@ EOF; /** * Returns the number of columns in a resource - * @param Object $res Statement resource + * @param $res Object: statement resource * @return Number of fields/columns in resource */ public function numFields( $res ) { @@ -1366,9 +1354,9 @@ EOF; /** * Returns the nth column name - * @param Object $res Statement resource - * @param int $n Index of field or column - * @return string name of nth column + * @param $res Object: statement resource + * @param $n Integer: Index of field or column + * @return String name of nth column */ public function fieldName( $res, $n ) { if ( $res instanceof ResultWrapper ) { @@ -1380,15 +1368,15 @@ EOF; /** * 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 + * @param $table Array or string, table name(s) (prefix auto-added) + * @param $vars Array or string, field name(s) to be retrieved + * @param $conds Array or string, condition(s) for WHERE + * @param $fname String: calling function name (use __METHOD__) for logs/profiling + * @param $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), + * see Database::makeSelectOptions code for list of supported stuff + * @param $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() ) { @@ -1419,7 +1407,6 @@ EOF; $obj = $this->fetchObject($res2); $this->mNumRows = $obj->num_rows; - wfDebug("DatabaseIbm_db2::select: There are $this->mNumRows rows.\n"); return $res; } @@ -1430,9 +1417,9 @@ EOF; * * @private * - * @param array $options an associative array of options to be turned into + * @param $options Associative array of options to be turned into * an SQL query, valid keys are listed in the function. - * @return array + * @return Array */ function makeSelectOptions( $options ) { $preLimitTail = $postLimitTail = ''; @@ -1463,46 +1450,18 @@ EOF; } /** - * 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 + * @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 + * @return Boolean */ public function wasDeadlock() { // get SQLSTATE @@ -1511,7 +1470,7 @@ EOF; 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"); + $this->installPrint("In a deadlock because of SQLSTATE $err"); return true; } return false; @@ -1520,13 +1479,13 @@ EOF; /** * 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 + * @return Boolean: whether the connection exists */ public function ping() { // db2_ping() doesn't exist // Emulate $this->close(); - if ($this->mCataloged == NULL) { + if ($this->mCataloged == null) { return false; } else if ($this->mCataloged) { @@ -1545,46 +1504,34 @@ EOF; * @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()'); } + public function getStatus( $which="%" ) { $this->installPrint('Not implemented for DB2: getStatus()'); return ''; } /** * 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()'); } + public function setFakeSlaveLag( $lag ) { $this->installPrint('Not implemented for DB2: setFakeSlaveLag()'); } /** * Not implemented * @deprecated */ - public function setFakeMaster( $enabled ) { wfDebug('Not implemented for DB2: setFakeMaster()'); } + public function setFakeMaster( $enabled = true ) { $this->installPrint('Not implemented for DB2: setFakeMaster()'); } /** * Not implemented * @return string $sql * @deprecated */ - public function limitResultForUpdate($sql, $num) { return $sql; } + public function limitResultForUpdate($sql, $num) { $this->installPrint('Not implemented for DB2: limitResultForUpdate()'); return $sql; } + /** - * No such option - * @return string '' - * @deprecated + * Only useful with fake prepare like in base Database class + * @return string */ - public function lowPriorityOption() { return ''; } + public function fillPreparedArg( $matches ) { $this->installPrint('Not useful for DB2: fillPreparedArg()'); return ''; } ###################################### # Reflection @@ -1592,9 +1539,9 @@ EOF; /** * 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 + * @param $table String: name of the table + * @param $field String: name of the column + * @param $fname String: function name for logging and profiling */ public function fieldExists( $table, $field, $fname = 'DatabaseIbm_db2::fieldExists' ) { $table = $this->tableName( $table ); @@ -1617,10 +1564,10 @@ SQL; /** * 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 + * @param $table String: table name + * @param $index String: index name + * @param $fname String: function name for logging and profiling + * @return Object query row in object form */ public function indexInfo( $table, $index, $fname = 'DatabaseIbm_db2::indexExists' ) { $table = $this->tableName( $table ); @@ -1631,17 +1578,17 @@ WHERE si.name='$index' AND si.tbname='$table' AND sc.tbcreator='$this->mSchema' SQL; $res = $this->query( $sql, $fname ); if ( !$res ) { - return NULL; + return null; } $row = $this->fetchObject( $res ); - if ($row != NULL) return $row; + 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 + * @param $table String: table name + * @param $field String: column name * @return IBM_DB2Field */ public function fieldInfo( $table, $field ) { @@ -1650,9 +1597,9 @@ SQL; /** * db2_field_type() wrapper - * @param Object $res Result of executed statement - * @param mixed $index number or name of the column - * @return string column type + * @param $res Object: result of executed statement + * @param $index Mixed: number or name of the column + * @return String column type */ public function fieldType( $res, $index ) { if ( $res instanceof ResultWrapper ) { @@ -1663,10 +1610,10 @@ SQL; /** * 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 + * @param $table String: table name + * @param $index String: index name + * @param $fname function name for profiling + * @return Bool */ public function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) { $table = $this->tableName( $table ); @@ -1689,9 +1636,9 @@ SQL; /** * 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 + * @param $table String: table name + * @param $field String: column name + * @return Integer: length or -1 for unlimited */ public function textFieldSize( $table, $field ) { $table = $this->tableName( $table ); @@ -1709,12 +1656,12 @@ SQL; /** * 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 + * @param $delTable String: deleting from this table + * @param $joinTable String: using data from this table + * @param $delVar String: variable in deleteable table + * @param $joinVar String: variable in data table + * @param $conds Array: conditionals for join table + * @param $fname String: function name for profiling */ public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseIbm_db2::deleteJoin" ) { if ( !$conds ) { @@ -1731,32 +1678,10 @@ 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 + * @param $b Mixed: data to be encoded * @return IBM_DB2Blob */ public function encodeBlob($b) { @@ -1765,7 +1690,7 @@ SQL; /** * Description is left as an exercise for the reader - * @param IBM_DB2Blob $b data to be decoded + * @param $b IBM_DB2Blob: data to be decoded * @return mixed */ public function decodeBlob($b) { @@ -1774,8 +1699,8 @@ SQL; /** * 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 + * @param $stringList Array: 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 @@ -1785,12 +1710,135 @@ SQL; /** * Generates the SQL required to convert a DB2 timestamp into a Unix epoch - * @param string $column name of timestamp column - * @return string SQL code + * @param $column String: name of timestamp column + * @return String: SQL code */ public function extractUnixEpoch( $column ) { // TODO // see SpecialAncientpages } + + ###################################### + # Prepared statements + ###################################### + + /** + * Intended to be compatible with the PEAR::DB wrapper functions. + * http://pear.php.net/manual/en/package.database.db.intro-execute.php + * + * ? = scalar value, quoted as necessary + * ! = raw SQL bit (a function for instance) + * & = filename; reads the file and inserts as a blob + * (we don't use this though...) + * @param $sql String: SQL statement with appropriate markers + * @param $func String: Name of the function, for profiling + * @return resource a prepared DB2 SQL statement + */ + public function prepare( $sql, $func = 'DB2::prepare' ) { + $stmt = db2_prepare($this->mConn, $sql, $this->mStmtOptions); + return $stmt; + } + + /** + * Frees resources associated with a prepared statement + * @return Boolean success or failure + */ + public function freePrepared( $prepared ) { + return db2_free_stmt($prepared); + } + + /** + * Execute a prepared query with the various arguments + * @param $prepared String: the prepared sql + * @param $args Mixed: either an array here, or put scalars as varargs + * @return Resource: results object + */ + public function execute( $prepared, $args = null ) { + if( !is_array( $args ) ) { + # Pull the var args + $args = func_get_args(); + array_shift( $args ); + } + $res = db2_execute($prepared, $args); + return $res; + } + + /** + * Prepare & execute an SQL statement, quoting and inserting arguments + * in the appropriate places. + * @param $query String + * @param $args ... + */ + public function safeQuery( $query, $args = null ) { + // copied verbatim from Database.php + $prepared = $this->prepare( $query, 'DB2::safeQuery' ); + if( !is_array( $args ) ) { + # Pull the var args + $args = func_get_args(); + array_shift( $args ); + } + $retval = $this->execute( $prepared, $args ); + $this->freePrepared( $prepared ); + return $retval; + } + + /** + * For faking prepared SQL statements on DBs that don't support + * it directly. + * @param $preparedQuery String: a 'preparable' SQL statement + * @param $args Array of arguments to fill it with + * @return String: executable statement + */ + public function fillPrepared( $preparedQuery, $args ) { + reset( $args ); + $this->preparedArgs =& $args; + + foreach ($args as $i => $arg) { + db2_bind_param($preparedQuery, $i+1, $args[$i]); + } + + return $preparedQuery; + } + + /** + * Switches module between regular and install modes + */ + public function setMode($mode) { + $old = $this->mMode; + $this->mMode = $mode; + return $old; + } + + /** + * Bitwise negation of a column or value in SQL + * Same as (~field) in C + * @param $field String + * @return String + */ + function bitNot($field) { + //expecting bit-fields smaller than 4bytes + return 'BITNOT('.$bitField.')'; + } + + /** + * Bitwise AND of two columns or values in SQL + * Same as (fieldLeft & fieldRight) in C + * @param $fieldLeft String + * @param $fieldRight String + * @return String + */ + function bitAnd($fieldLeft, $fieldRight) { + return 'BITAND('.$fieldLeft.', '.$fieldRight.')'; + } + + /** + * Bitwise OR of two columns or values in SQL + * Same as (fieldLeft | fieldRight) in C + * @param $fieldLeft String + * @param $fieldRight String + * @return String + */ + function bitOr($fieldLeft, $fieldRight) { + return 'BITOR('.$fieldLeft.', '.$fieldRight.')'; + } } -?>
\ No newline at end of file diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 28ccab2d..6b1206b0 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -10,7 +10,7 @@ /** * @ingroup Database */ -class DatabaseMssql extends Database { +class DatabaseMssql extends DatabaseBase { var $mAffectedRows; var $mLastResult; @@ -25,7 +25,7 @@ class DatabaseMssql extends Database { $failFunction = false, $flags = 0, $tablePrefix = 'get from global') { global $wgOut, $wgDBprefix, $wgCommandLineMode; - if (!isset($wgOut)) $wgOut = NULL; # Can't get a reference if it hasn't been set yet + if (!isset($wgOut)) $wgOut = null; # Can't get a reference if it hasn't been set yet $this->mOut =& $wgOut; $this->mFailFunction = $failFunction; $this->mFlags = $flags; @@ -45,6 +45,10 @@ class DatabaseMssql extends Database { } + function getType() { + return 'mssql'; + } + /** * todo: check if these should be true like parent class */ @@ -131,7 +135,7 @@ class DatabaseMssql extends Database { function close() { $this->mOpened = false; if ($this->mConn) { - if ($this->trxLevel()) $this->immediateCommit(); + if ($this->trxLevel()) $this->commit(); return mssql_close($this->mConn); } else return true; } @@ -446,22 +450,6 @@ class DatabaseMssql extends Database { } /** - * Estimate rows in dataset - * Returns estimated count, based on EXPLAIN output - * Takes same arguments as Database::select() - */ - function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { - $rows = 0; - $res = $this->select ($table, 'COUNT(*)', $conds, $fname, $options ); - if ($res) { - $row = $this->fetchObject($res); - $rows = $row[0]; - } - $this->freeResult($res); - return $rows; - } - - /** * Determines whether a field exists in a table * Usually aborts on failure * If errors are explicitly ignored, returns NULL on failure @@ -490,13 +478,13 @@ class DatabaseMssql extends Database { function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) { throw new DBUnexpectedError( $this, 'Database::indexInfo called which is not supported yet' ); - return NULL; + return null; $table = $this->tableName( $table ); $sql = 'SHOW INDEX FROM '.$table; $res = $this->query( $sql, $fname ); if ( !$res ) { - return NULL; + return null; } $result = array(); @@ -708,13 +696,6 @@ class DatabaseMssql extends Database { } /** - * USE INDEX clause - */ - function useIndexClause( $index ) { - return ""; - } - - /** * REPLACE query wrapper * PostgreSQL simulates this with a DELETE followed by INSERT * $row is the row to insert, an associative array @@ -858,18 +839,6 @@ class DatabaseMssql extends Database { } /** - * Returns an SQL expression for a simple conditional. - * - * @param $cond String: SQL expression which will result in a boolean value - * @param $trueVal String: SQL expression to return if true - * @param $falseVal String: SQL expression to return if false - * @return string SQL fragment - */ - function conditional( $cond, $trueVal, $falseVal ) { - return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; - } - - /** * Should determine if the last failure was due to a deadlock * @return bool */ @@ -878,22 +847,6 @@ class DatabaseMssql extends Database { } /** - * Begin a transaction, committing any previously open transaction - * @deprecated use begin() - */ - function immediateBegin( $fname = 'Database::immediateBegin' ) { - $this->begin(); - } - - /** - * Commit transaction, if one is open - * @deprecated use commit() - */ - function immediateCommit( $fname = 'Database::immediateCommit' ) { - $this->commit(); - } - - /** * Return MW-style timestamp used for MySQL schema */ function timestamp( $ts=0 ) { @@ -931,16 +884,6 @@ class DatabaseMssql extends Database { } /** - * not done - */ - public function setTimeout($timeout) { return; } - - function ping() { - wfDebug("Function ping() not written for MSSQL yet"); - return true; - } - - /** * How lagged is this slave? */ public function getLag() { @@ -1001,20 +944,9 @@ class DatabaseMssql extends Database { } } - /** - * No-op lock functions - */ - public function lock( $lockName, $method ) { - return true; - } - public function unlock( $lockName, $method ) { - return true; - } - public function getSearchEngine() { return "SearchEngineDummy"; } - } /** diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php new file mode 100644 index 00000000..ea7ef5b9 --- /dev/null +++ b/includes/db/DatabaseMysql.php @@ -0,0 +1,453 @@ +<?php +/** + * Database abstraction object for mySQL + * Inherit all methods and properties of Database::Database() + * + * @ingroup Database + * @see Database + */ +class DatabaseMysql extends DatabaseBase { + function getType() { + return 'mysql'; + } + + /*private*/ function doQuery( $sql ) { + if( $this->bufferResults() ) { + $ret = mysql_query( $sql, $this->mConn ); + } else { + $ret = mysql_unbuffered_query( $sql, $this->mConn ); + } + return $ret; + } + + function open( $server, $user, $password, $dbName ) { + global $wgAllDBsAreLocalhost; + wfProfileIn( __METHOD__ ); + + # Test for missing mysql.so + # First try to load it + if (!@extension_loaded('mysql')) { + @dl('mysql.so'); + } + + # Fail now + # Otherwise we get a suppressed fatal error, which is very hard to track down + if ( !function_exists( 'mysql_connect' ) ) { + throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" ); + } + + # Debugging hack -- fake cluster + if ( $wgAllDBsAreLocalhost ) { + $realServer = 'localhost'; + } else { + $realServer = $server; + } + $this->close(); + $this->mServer = $server; + $this->mUser = $user; + $this->mPassword = $password; + $this->mDBname = $dbName; + + $success = false; + + wfProfileIn("dbconnect-$server"); + + # The kernel's default SYN retransmission period is far too slow for us, + # so we use a short timeout plus a manual retry. Retrying means that a small + # but finite rate of SYN packet loss won't cause user-visible errors. + $this->mConn = false; + if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) { + $numAttempts = 2; + } else { + $numAttempts = 1; + } + $this->installErrorHandler(); + for ( $i = 0; $i < $numAttempts && !$this->mConn; $i++ ) { + if ( $i > 1 ) { + usleep( 1000 ); + } + if ( $this->mFlags & DBO_PERSISTENT ) { + $this->mConn = mysql_pconnect( $realServer, $user, $password ); + } else { + # Create a new connection... + $this->mConn = mysql_connect( $realServer, $user, $password, true ); + } + if ($this->mConn === false) { + #$iplus = $i + 1; + #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); + } + } + $phpError = $this->restoreErrorHandler(); + # Always log connection errors + if ( !$this->mConn ) { + $error = $this->lastError(); + if ( !$error ) { + $error = $phpError; + } + wfLogDBError( "Error connecting to {$this->mServer}: $error\n" ); + wfDebug( "DB connection error\n" ); + wfDebug( "Server: $server, User: $user, Password: " . + substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" ); + $success = false; + } + + wfProfileOut("dbconnect-$server"); + + if ( $dbName != '' && $this->mConn !== false ) { + $success = @/**/mysql_select_db( $dbName, $this->mConn ); + if ( !$success ) { + $error = "Error selecting database $dbName on server {$this->mServer} " . + "from client host " . wfHostname() . "\n"; + wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n"); + wfDebug( $error ); + } + } else { + # Delay USE query + $success = (bool)$this->mConn; + } + + if ( $success ) { + $version = $this->getServerVersion(); + if ( version_compare( $version, '4.1' ) >= 0 ) { + // Tell the server we're communicating with it in UTF-8. + // This may engage various charset conversions. + global $wgDBmysql5; + if( $wgDBmysql5 ) { + $this->query( 'SET NAMES utf8', __METHOD__ ); + } + // Turn off strict mode + $this->query( "SET sql_mode = ''", __METHOD__ ); + } + + // Turn off strict mode if it is on + } else { + $this->reportConnectionError( $phpError ); + } + + $this->mOpened = $success; + wfProfileOut( __METHOD__ ); + return $success; + } + + function close() { + $this->mOpened = false; + if ( $this->mConn ) { + if ( $this->trxLevel() ) { + $this->commit(); + } + return mysql_close( $this->mConn ); + } else { + return true; + } + } + + function freeResult( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + if ( !@/**/mysql_free_result( $res ) ) { + throw new DBUnexpectedError( $this, "Unable to free MySQL result" ); + } + } + + function fetchObject( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + @/**/$row = mysql_fetch_object( $res ); + if( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); + } + return $row; + } + + function fetchRow( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + @/**/$row = mysql_fetch_array( $res ); + if ( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); + } + return $row; + } + + function numRows( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + @/**/$n = mysql_num_rows( $res ); + if( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) ); + } + return $n; + } + + function numFields( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return mysql_num_fields( $res ); + } + + function fieldName( $res, $n ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return mysql_field_name( $res, $n ); + } + + function insertId() { return mysql_insert_id( $this->mConn ); } + + function dataSeek( $res, $row ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return mysql_data_seek( $res, $row ); + } + + function lastErrno() { + if ( $this->mConn ) { + return mysql_errno( $this->mConn ); + } else { + return mysql_errno(); + } + } + + function lastError() { + if ( $this->mConn ) { + # Even if it's non-zero, it can still be invalid + wfSuppressWarnings(); + $error = mysql_error( $this->mConn ); + if ( !$error ) { + $error = mysql_error(); + } + wfRestoreWarnings(); + } else { + $error = mysql_error(); + } + if( $error ) { + $error .= ' (' . $this->mServer . ')'; + } + return $error; + } + + function affectedRows() { return mysql_affected_rows( $this->mConn ); } + + /** + * Estimate rows in dataset + * Returns estimated count, based on EXPLAIN output + * Takes same arguments as Database::select() + */ + public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { + $options['EXPLAIN'] = true; + $res = $this->select( $table, $vars, $conds, $fname, $options ); + if ( $res === false ) + return false; + if ( !$this->numRows( $res ) ) { + $this->freeResult($res); + return 0; + } + + $rows = 1; + while( $plan = $this->fetchObject( $res ) ) { + $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero + } + + $this->freeResult($res); + return $rows; + } + + function fieldInfo( $table, $field ) { + $table = $this->tableName( $table ); + $res = $this->query( "SELECT * FROM $table LIMIT 1" ); + $n = mysql_num_fields( $res->result ); + for( $i = 0; $i < $n; $i++ ) { + $meta = mysql_fetch_field( $res->result, $i ); + if( $field == $meta->name ) { + return new MySQLField($meta); + } + } + return false; + } + + function selectDB( $db ) { + $this->mDBname = $db; + return mysql_select_db( $db, $this->mConn ); + } + + function strencode( $s ) { + return mysql_real_escape_string( $s, $this->mConn ); + } + + function ping() { + if( !function_exists( 'mysql_ping' ) ) { + wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" ); + return true; + } + $ping = mysql_ping( $this->mConn ); + if ( $ping ) { + return true; + } + + // Need to reconnect manually in MySQL client 5.0.13+ + if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) { + mysql_close( $this->mConn ); + $this->mOpened = false; + $this->mConn = false; + $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname ); + return true; + } + return false; + } + + function getServerVersion() { + return mysql_get_server_info( $this->mConn ); + } + + function useIndexClause( $index ) { + return "FORCE INDEX (" . $this->indexName( $index ) . ")"; + } + + function lowPriorityOption() { + return 'LOW_PRIORITY'; + } + + function getSoftwareLink() { + return '[http://www.mysql.com/ MySQL]'; + } + + function standardSelectDistinct() { + return false; + } + + public function setTimeout( $timeout ) { + $this->query( "SET net_read_timeout=$timeout" ); + $this->query( "SET net_write_timeout=$timeout" ); + } + + public function lock( $lockName, $method, $timeout = 5 ) { + $lockName = $this->addQuotes( $lockName ); + $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method ); + $row = $this->fetchObject( $result ); + $this->freeResult( $result ); + + if( $row->lockstatus == 1 ) { + return true; + } else { + wfDebug( __METHOD__." failed to acquire lock\n" ); + return false; + } + } + + public function unlock( $lockName, $method ) { + $lockName = $this->addQuotes( $lockName ); + $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method ); + $row = $this->fetchObject( $result ); + return $row->lockstatus; + } + + public function lockTables( $read, $write, $method, $lowPriority = true ) { + $items = array(); + + foreach( $write as $table ) { + $tbl = $this->tableName( $table ) . + ( $lowPriority ? ' LOW_PRIORITY' : '' ) . + ' WRITE'; + $items[] = $tbl; + } + foreach( $read as $table ) { + $items[] = $this->tableName( $table ) . ' READ'; + } + $sql = "LOCK TABLES " . implode( ',', $items ); + $this->query( $sql, $method ); + } + + public function unlockTables( $method ) { + $this->query( "UNLOCK TABLES", $method ); + } + + public function setBigSelects( $value = true ) { + if ( $value === 'default' ) { + if ( $this->mDefaultBigSelects === null ) { + # Function hasn't been called before so it must already be set to the default + return; + } else { + $value = $this->mDefaultBigSelects; + } + } elseif ( $this->mDefaultBigSelects === null ) { + $this->mDefaultBigSelects = (bool)$this->selectField( false, '@@sql_big_selects' ); + } + $encValue = $value ? '1' : '0'; + $this->query( "SET sql_big_selects=$encValue", __METHOD__ ); + } + + + /** + * Determines if the last failure was due to a deadlock + */ + function wasDeadlock() { + return $this->lastErrno() == 1213; + } + + /** + * Determines if the last query error was something that should be dealt + * with by pinging the connection and reissuing the query + */ + function wasErrorReissuable() { + return $this->lastErrno() == 2013 || $this->lastErrno() == 2006; + } + + /** + * Determines if the last failure was due to the database being read-only. + */ + function wasReadOnlyError() { + return $this->lastErrno() == 1223 || + ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false ); + } + + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseMysql::duplicateTableStructure' ) { + $tmp = $temporary ? 'TEMPORARY ' : ''; + if ( strcmp( $this->getServerVersion(), '4.1' ) < 0 ) { + # Hack for MySQL versions < 4.1, which don't support + # "CREATE TABLE ... LIKE". Note that + # "CREATE TEMPORARY TABLE ... SELECT * FROM ... LIMIT 0" + # would not create the indexes we need.... + # + # Note that we don't bother changing around the prefixes here be- + # cause we know we're using MySQL anyway. + + $res = $this->query( "SHOW CREATE TABLE $oldName" ); + $row = $this->fetchRow( $res ); + $oldQuery = $row[1]; + $query = preg_replace( '/CREATE TABLE `(.*?)`/', + "CREATE $tmp TABLE `$newName`", $oldQuery ); + if ($oldQuery === $query) { + # Couldn't do replacement + throw new MWException( "could not create temporary table $newName" ); + } + } else { + $query = "CREATE $tmp TABLE $newName (LIKE $oldName)"; + } + $this->query( $query, $fname ); + } + +} + +/** + * Legacy support: Database == DatabaseMysql + */ +class Database extends DatabaseMysql {} + +class MySQLMasterPos { + var $file, $pos; + + function __construct( $file, $pos ) { + $this->file = $file; + $this->pos = $pos; + } + + function __toString() { + return "{$this->file}/{$this->pos}"; + } +} diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 4c37a507..bd60bbf8 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -11,7 +11,7 @@ class ORABlob { var $mData; - function __construct($data) { + function __construct( $data ) { $this->mData = $data; } @@ -31,58 +31,80 @@ class ORAResult { private $cursor; private $stmt; private $nrows; - private $db; - function __construct(&$db, $stmt) { + private $unique; + private function array_unique_md( $array_in ) { + $array_out = array(); + $array_hashes = array(); + + foreach ( $array_in as $key => $item ) { + $hash = md5( serialize( $item ) ); + if ( !isset( $array_hashes[$hash] ) ) { + $array_hashes[$hash] = $hash; + $array_out[] = $item; + } + } + + return $array_out; + } + + function __construct( &$db, $stmt, $unique = false ) { $this->db =& $db; - if (($this->nrows = oci_fetch_all($stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM)) === false) { - $e = oci_error($stmt); - $db->reportQueryError($e['message'], $e['code'], '', __FUNCTION__); + + if ( ( $this->nrows = oci_fetch_all( $stmt, $this->rows, 0, - 1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM ) ) === false ) { + $e = oci_error( $stmt ); + $db->reportQueryError( $e['message'], $e['code'], '', __FUNCTION__ ); return; } + if ( $unique ) { + $this->rows = $this->array_unique_md( $this->rows ); + $this->nrows = count( $this->rows ); + } + $this->cursor = 0; $this->stmt = $stmt; } - function free() { - oci_free_statement($this->stmt); + public function free() { + oci_free_statement( $this->stmt ); } - function seek($row) { - $this->cursor = min($row, $this->nrows); + public function seek( $row ) { + $this->cursor = min( $row, $this->nrows ); } - function numRows() { + public function numRows() { return $this->nrows; } - function numFields() { - return oci_num_fields($this->stmt); + public function numFields() { + return oci_num_fields( $this->stmt ); } - function fetchObject() { - if ($this->cursor >= $this->nrows) + public function fetchObject() { + if ( $this->cursor >= $this->nrows ) { return false; - + } $row = $this->rows[$this->cursor++]; $ret = new stdClass(); - foreach ($row as $k => $v) { - $lc = strtolower(oci_field_name($this->stmt, $k + 1)); + foreach ( $row as $k => $v ) { + $lc = strtolower( oci_field_name( $this->stmt, $k + 1 ) ); $ret->$lc = $v; } return $ret; } - function fetchAssoc() { - if ($this->cursor >= $this->nrows) + public function fetchRow() { + if ( $this->cursor >= $this->nrows ) { return false; + } $row = $this->rows[$this->cursor++]; $ret = array(); - foreach ($row as $k => $v) { - $lc = strtolower(oci_field_name($this->stmt, $k + 1)); + foreach ( $row as $k => $v ) { + $lc = strtolower( oci_field_name( $this->stmt, $k + 1 ) ); $ret[$lc] = $v; $ret[$k] = $v; } @@ -91,30 +113,87 @@ class ORAResult { } /** + * Utility class. + * @ingroup Database + */ +class ORAField { + private $name, $tablename, $default, $max_length, $nullable, + $is_pk, $is_unique, $is_multiple, $is_key, $type; + + function __construct( $info ) { + $this->name = $info['column_name']; + $this->tablename = $info['table_name']; + $this->default = $info['data_default']; + $this->max_length = $info['data_length']; + $this->nullable = $info['not_null']; + $this->is_pk = isset( $info['prim'] ) && $info['prim'] == 1 ? 1 : 0; + $this->is_unique = isset( $info['uniq'] ) && $info['uniq'] == 1 ? 1 : 0; + $this->is_multiple = isset( $info['nonuniq'] ) && $info['nonuniq'] == 1 ? 1 : 0; + $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple ); + $this->type = $info['data_type']; + } + + function name() { + return $this->name; + } + + function tableName() { + return $this->tablename; + } + + function defaultValue() { + return $this->default; + } + + function maxLength() { + return $this->max_length; + } + + function nullable() { + return $this->nullable; + } + + function isKey() { + return $this->is_key; + } + + function isMultipleKey() { + return $this->is_multiple; + } + + function type() { + return $this->type; + } +} + +/** * @ingroup Database */ -class DatabaseOracle extends Database { - var $mInsertId = NULL; - var $mLastResult = NULL; - var $numeric_version = NULL; +class DatabaseOracle extends DatabaseBase { + var $mInsertId = null; + var $mLastResult = null; + var $numeric_version = null; var $lastResult = null; var $cursor = 0; var $mAffectedRows; - function DatabaseOracle($server = false, $user = false, $password = false, $dbName = false, - $failFunction = false, $flags = 0 ) - { + var $ignore_DUP_VAL_ON_INDEX = false; + var $sequenceData = null; - global $wgOut; - # 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 = $flags; - $this->open( $server, $user, $password, $dbName); + var $defaultCharset = 'AL32UTF8'; + + var $mFieldInfoCache = array(); + + function __construct( $server = false, $user = false, $password = false, $dbName = false, + $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) + { + $tablePrefix = $tablePrefix == 'get from global' ? $tablePrefix : strtoupper( $tablePrefix ); + parent::__construct( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix ); + wfRunHooks( 'DatabaseOraclePostInit', array( &$this ) ); + } + function getType() { + return 'oracle'; } function cascadingDeletes() { @@ -139,8 +218,7 @@ class DatabaseOracle extends Database { return true; } - static function newFromParams( $server = false, $user = false, $password = false, $dbName = false, - $failFunction = false, $flags = 0) + static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) { return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags ); } @@ -154,30 +232,35 @@ class DatabaseOracle extends Database { throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" ); } - # Needed for proper UTF-8 functionality - putenv("NLS_LANG=AMERICAN_AMERICA.AL32UTF8"); - $this->close(); $this->mServer = $server; $this->mUser = $user; $this->mPassword = $password; $this->mDBname = $dbName; - if (!strlen($user)) { ## e.g. the class is being loaded + if ( !strlen( $user ) ) { # e.g. the class is being loaded return; } - error_reporting( E_ALL ); - $this->mConn = oci_connect($user, $password, $dbName); + $session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT; + if ( $this->mFlags & DBO_DEFAULT ) { + $this->mConn = oci_new_connect( $user, $password, $dbName, $this->defaultCharset, $session_mode ); + } else { + $this->mConn = oci_connect( $user, $password, $dbName, $this->defaultCharset, $session_mode ); + } - 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"); + 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 false; } $this->mOpened = true; + + # removed putenv calls because they interfere with the system globaly + $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' ); + $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' ); return $this->mConn; } @@ -198,55 +281,101 @@ class DatabaseOracle extends Database { return $this->mTrxLevel ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS; } - function doQuery($sql) { - wfDebug("SQL: [$sql]\n"); - if (!mb_check_encoding($sql)) { - throw new MWException("SQL encoding is invalid"); + function doQuery( $sql ) { + wfDebug( "SQL: [$sql]\n" ); + if ( !mb_check_encoding( $sql ) ) { + throw new MWException( "SQL encoding is invalid\n$sql" ); } - if (($this->mLastResult = $stmt = oci_parse($this->mConn, $sql)) === false) { - $e = oci_error($this->mConn); - $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__); + // handle some oracle specifics + // remove AS column/table/subquery namings + if ( !defined( 'MEDIAWIKI_INSTALL' ) ) { + $sql = preg_replace( '/ as /i', ' ', $sql ); } + // Oracle has issues with UNION clause if the statement includes LOB fields + // So we do a UNION ALL and then filter the results array with array_unique + $union_unique = ( preg_match( '/\/\* UNION_UNIQUE \*\/ /', $sql ) != 0 ); + // EXPLAIN syntax in Oracle is EXPLAIN PLAN FOR and it return nothing + // you have to select data from plan table after explain + $explain_id = date( 'dmYHis' ); + + $sql = preg_replace( '/^EXPLAIN /', 'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR', $sql, 1, $explain_count ); + - if (oci_execute($stmt, $this->execFlags()) == false) { - $e = oci_error($stmt); - $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__); + wfSuppressWarnings(); + + if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) { + $e = oci_error( $this->mConn ); + $this->reportQueryError( $e['message'], $e['code'], $sql, __FUNCTION__ ); + return false; + } + + if ( oci_execute( $stmt, $this->execFlags() ) == false ) { + $e = oci_error( $stmt ); + if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) { + $this->reportQueryError( $e['message'], $e['code'], $sql, __FUNCTION__ ); + return false; + } } - if (oci_statement_type($stmt) == "SELECT") - return new ORAResult($this, $stmt); - else { - $this->mAffectedRows = oci_num_rows($stmt); + + wfRestoreWarnings(); + + if ( $explain_count > 0 ) { + return $this->doQuery( 'SELECT id, cardinality "ROWS" FROM plan_table WHERE statement_id = \'' . $explain_id . '\'' ); + } elseif ( oci_statement_type( $stmt ) == 'SELECT' ) { + return new ORAResult( $this, $stmt, $union_unique ); + } else { + $this->mAffectedRows = oci_num_rows( $stmt ); return true; } } - function queryIgnore($sql, $fname = '') { - return $this->query($sql, $fname, true); + function queryIgnore( $sql, $fname = '' ) { + return $this->query( $sql, $fname, true ); } - function freeResult($res) { - $res->free(); + function freeResult( $res ) { + if ( $res instanceof ORAResult ) { + $res->free(); + } else { + $res->result->free(); + } } - function fetchObject($res) { - return $res->fetchObject(); + function fetchObject( $res ) { + if ( $res instanceof ORAResult ) { + return $res->numRows(); + } else { + return $res->result->fetchObject(); + } } - function fetchRow($res) { - return $res->fetchAssoc(); + function fetchRow( $res ) { + if ( $res instanceof ORAResult ) { + return $res->fetchRow(); + } else { + return $res->result->fetchRow(); + } } - function numRows($res) { - return $res->numRows(); + function numRows( $res ) { + if ( $res instanceof ORAResult ) { + return $res->numRows(); + } else { + return $res->result->numRows(); + } } - function numFields($res) { - return $res->numFields(); + function numFields( $res ) { + if ( $res instanceof ORAResult ) { + return $res->numFields(); + } else { + return $res->result->numFields(); + } } - function fieldName($stmt, $n) { - return pg_field_name($stmt, $n); + function fieldName( $stmt, $n ) { + return oci_field_name( $stmt, $n ); } /** @@ -256,23 +385,29 @@ class DatabaseOracle extends Database { return $this->mInsertId; } - function dataSeek($res, $row) { - $res->seek($row); + function dataSeek( $res, $row ) { + if ( $res instanceof ORAResult ) { + $res->seek( $row ); + } else { + $res->result->seek( $row ); + } } function lastError() { - if ($this->mConn === false) + if ( $this->mConn === false ) { $e = oci_error(); - else - $e = oci_error($this->mConn); + } else { + $e = oci_error( $this->mConn ); + } return $e['message']; } function lastErrno() { - if ($this->mConn === false) + if ( $this->mConn === false ) { $e = oci_error(); - else - $e = oci_error($this->mConn); + } else { + $e = oci_error( $this->mConn ); + } return $e['code']; } @@ -284,122 +419,261 @@ class DatabaseOracle extends Database { * Returns information about an index * If errors are explicitly ignored, returns NULL on failure */ - function indexInfo( $table, $index, $fname = 'Database::indexExists' ) { + function indexInfo( $table, $index, $fname = 'DatabaseOracle::indexExists' ) { return false; } - function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) { + function indexUnique( $table, $index, $fname = 'DatabaseOracle::indexUnique' ) { return false; } - function insert( $table, $a, $fname = 'Database::insert', $options = array() ) { - if (!is_array($options)) - $options = array($options); + function insert( $table, $a, $fname = 'DatabaseOracle::insert', $options = array() ) { + if ( !count( $a ) ) { + return true; + } + + if ( !is_array( $options ) ) { + $options = array( $options ); + } - #if (in_array('IGNORE', $options)) - # $oldIgnore = $this->ignoreErrors(true); + if ( in_array( 'IGNORE', $options ) ) { + $this->ignore_DUP_VAL_ON_INDEX = true; + } - # IGNORE is performed using single-row inserts, ignoring errors in each - # FIXME: need some way to distiguish between key collision and other types of error - //$oldIgnore = $this->ignoreErrors(true); - if (!is_array(reset($a))) { - $a = array($a); + if ( !is_array( reset( $a ) ) ) { + $a = array( $a ); } - foreach ($a as $row) { - $this->insertOneRow($table, $row, $fname); + + foreach ( $a as &$row ) { + $this->insertOneRow( $table, $row, $fname ); } - //$this->ignoreErrors($oldIgnore); $retVal = true; - //if (in_array('IGNORE', $options)) - // $this->ignoreErrors($oldIgnore); + if ( in_array( 'IGNORE', $options ) ) { + $this->ignore_DUP_VAL_ON_INDEX = false; + } return $retVal; } - function insertOneRow($table, $row, $fname) { + private function insertOneRow( $table, $row, $fname ) { + global $wgLang; + + $table = $this->tableName( $table ); // "INSERT INTO tables (a, b, c)" - $sql = "INSERT INTO " . $this->tableName($table) . " (" . join(',', array_keys($row)) . ')'; + $sql = "INSERT INTO " . $table . " (" . join( ',', array_keys( $row ) ) . ')'; $sql .= " VALUES ("; // for each value, append ":key" $first = true; - $returning = ''; - foreach ($row as $col => $val) { - if (is_object($val)) { - $what = "EMPTY_BLOB()"; - assert($returning === ''); - $returning = " RETURNING $col INTO :bval"; - $blobcol = $col; - } else - $what = ":$col"; - - if ($first) - $sql .= "$what"; - else - $sql.= ", $what"; + foreach ( $row as $col => $val ) { + if ( $first ) { + $sql .= $val !== null ? ':' . $col : 'NULL'; + } else { + $sql .= $val !== null ? ', :' . $col : ', NULL'; + } + $first = false; } - $sql .= ") $returning"; + $sql .= ')'; - $stmt = oci_parse($this->mConn, $sql); - foreach ($row as $col => $val) { - if (!is_object($val)) { - if (oci_bind_by_name($stmt, ":$col", $row[$col]) === false) - $this->reportQueryError($this->lastErrno(), $this->lastError(), $sql, __METHOD__); + $stmt = oci_parse( $this->mConn, $sql ); + foreach ( $row as $col => &$val ) { + $col_info = $this->fieldInfoMulti( $table, $col ); + $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; + + if ( $val === null ) { + // do nothing ... null was inserted in statement creation + } elseif ( $col_type != 'BLOB' && $col_type != 'CLOB' ) { + if ( is_object( $val ) ) { + $val = $val->getData(); + } + + if ( preg_match( '/^timestamp.*/i', $col_type ) == 1 && strtolower( $val ) == 'infinity' ) { + $val = '31-12-2030 12:00:00.000000'; + } + + $val = ( $wgLang != null ) ? $wgLang->checkTitleEncoding( $val ) : $val; + if ( oci_bind_by_name( $stmt, ":$col", $val ) === false ) { + $this->reportQueryError( $this->lastErrno(), $this->lastError(), $sql, __METHOD__ ); + return false; + } + } else { + if ( ( $lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB ) ) === false ) { + $e = oci_error( $stmt ); + throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] ); + } + + if ( $col_type == 'BLOB' ) { // is_object($val)) { + $lob[$col]->writeTemporary( $val ); // ->getData()); + oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, SQLT_BLOB ); + } else { + $lob[$col]->writeTemporary( $val ); + oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, OCI_B_CLOB ); + } } } - if (($bval = oci_new_descriptor($this->mConn, OCI_D_LOB)) === false) { - $e = oci_error($stmt); - throw new DBUnexpectedError($this, "Cannot create LOB descriptor: " . $e['message']); + wfSuppressWarnings(); + + if ( oci_execute( $stmt, OCI_DEFAULT ) === false ) { + $e = oci_error( $stmt ); + + if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) { + $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; + } else { + $this->mAffectedRows = oci_num_rows( $stmt ); + } + } else { + $this->mAffectedRows = oci_num_rows( $stmt ); + } + + wfRestoreWarnings(); + + if ( isset( $lob ) ) { + foreach ( $lob as $lob_i => $lob_v ) { + $lob_v->free(); + } + } + + if ( !$this->mTrxLevel ) { + oci_commit( $this->mConn ); + } + + oci_free_statement( $stmt ); + } + + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseOracle::insertSelect', + $insertOptions = array(), $selectOptions = array() ) + { + $destTable = $this->tableName( $destTable ); + if ( !is_array( $selectOptions ) ) { + $selectOptions = array( $selectOptions ); + } + list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions ); + if ( is_array( $srcTable ) ) { + $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); + } else { + $srcTable = $this->tableName( $srcTable ); } - if (strlen($returning)) - oci_bind_by_name($stmt, ":bval", $bval, -1, SQLT_BLOB); + if ( ( $sequenceData = $this->getSequenceData( $destTable ) ) !== false && + !isset( $varMap[$sequenceData['column']] ) ) + $varMap[$sequenceData['column']] = 'GET_SEQUENCE_VALUE(\'' . $sequenceData['sequence'] . '\')'; - if (oci_execute($stmt, OCI_DEFAULT) === false) { - $e = oci_error($stmt); - $this->reportQueryError($e['message'], $e['code'], $sql, __METHOD__); + // count-alias subselect fields to avoid abigious definition errors + $i = 0; + foreach ( $varMap as $key => &$val ) { + $val = $val . ' field' . ( $i++ ); } - if (strlen($returning)) { - $bval->save($row[$blobcol]->getData()); - $bval->free(); + + $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . + " SELECT $startOpts " . implode( ',', $varMap ) . + " FROM $srcTable $useIndex "; + if ( $conds != '*' ) { + $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); + } + $sql .= " $tailOpts"; + + if ( in_array( 'IGNORE', $insertOptions ) ) { + $this->ignore_DUP_VAL_ON_INDEX = true; } - if (!$this->mTrxLevel) - oci_commit($this->mConn); - oci_free_statement($stmt); + $retval = $this->query( $sql, $fname ); + + if ( in_array( 'IGNORE', $insertOptions ) ) { + $this->ignore_DUP_VAL_ON_INDEX = false; + } + + return $retval; } function tableName( $name ) { - # Replace reserved words with better ones + global $wgSharedDB, $wgSharedPrefix, $wgSharedTables; + /* + Replace reserved words with better ones + Using uppercase because that's the only way Oracle can handle + quoted tablenames + */ switch( $name ) { case 'user': - return 'mwuser'; + $name = 'MWUSER'; + break; case 'text': - return 'pagecontent'; - default: - return $name; + $name = 'PAGECONTENT'; + break; + } + + /* + The rest of procedure is equal to generic Databse class + except for the quoting style + */ + if ( $name[0] == '"' && substr( $name, - 1, 1 ) == '"' ) { + return $name; + } + if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) { + return $name; + } + $dbDetails = array_reverse( explode( '.', $name, 2 ) ); + if ( isset( $dbDetails[1] ) ) { + @list( $table, $database ) = $dbDetails; + } else { + @list( $table ) = $dbDetails; + } + + $prefix = $this->mTablePrefix; + + if ( isset( $database ) ) { + $table = ( $table[0] == '`' ? $table : "`{$table}`" ); } + + if ( !isset( $database ) && isset( $wgSharedDB ) && $table[0] != '"' + && isset( $wgSharedTables ) + && is_array( $wgSharedTables ) + && in_array( $table, $wgSharedTables ) + ) { + $database = $wgSharedDB; + $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix; + } + + if ( isset( $database ) ) { + $database = ( $database[0] == '"' ? $database : "\"{$database}\"" ); + } + $table = ( $table[0] == '"' ? $table : "\"{$prefix}{$table}\"" ); + + $tableName = ( isset( $database ) ? "{$database}.{$table}" : "{$table}" ); + + return strtoupper( $tableName ); } /** * Return the next in a sequence, save the value for retrieval via insertId() */ - function nextSequenceValue($seqName) { - $res = $this->query("SELECT $seqName.nextval FROM dual"); - $row = $this->fetchRow($res); + function nextSequenceValue( $seqName ) { + $res = $this->query( "SELECT $seqName.nextval FROM dual" ); + $row = $this->fetchRow( $res ); $this->mInsertId = $row[0]; - $this->freeResult($res); + $this->freeResult( $res ); return $this->mInsertId; } /** - * Oracle does not have a "USE INDEX" clause, so return an empty string + * Return sequence_name if table has a sequence */ - function useIndexClause($index) { - return ''; + private function getSequenceData( $table ) { + if ( $this->sequenceData == null ) { + $result = $this->query( "SELECT lower(us.sequence_name), lower(utc.table_name), lower(utc.column_name) from user_sequences us, user_tab_columns utc where us.sequence_name = utc.table_name||'_'||utc.column_name||'_SEQ'" ); + + while ( ( $row = $result->fetchRow() ) !== false ) { + $this->sequenceData[$this->tableName( $row[1] )] = array( + 'sequence' => $row[0], + 'column' => $row[2] + ); + } + } + + return ( isset( $this->sequenceData[$table] ) ) ? $this->sequenceData[$table] : false; } # REPLACE query wrapper @@ -411,59 +685,44 @@ class DatabaseOracle extends Database { # It may be more efficient to leave off unique indexes which are unlikely to collide. # However if you do this, you run the risk of encountering errors which wouldn't have # occurred in MySQL - function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) { - $table = $this->tableName($table); + function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseOracle::replace' ) { + $table = $this->tableName( $table ); - if (count($rows)==0) { + if ( count( $rows ) == 0 ) { return; } # Single row case - if (!is_array(reset($rows))) { - $rows = array($rows); + if ( !is_array( reset( $rows ) ) ) { + $rows = array( $rows ); } - foreach( $rows as $row ) { + $sequenceData = $this->getSequenceData( $table ); + + 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] ); - } + $condsDelete = array(); + foreach ( $uniqueIndexes as $index ) + $condsDelete[$index] = $row[$index]; + if (count($condsDelete) > 0) { + $this->delete( $table, $condsDelete, $fname ); } - $sql .= ')'; - $this->query( $sql, $fname ); + } + + if ( $sequenceData !== false && !isset( $row[$sequenceData['column']] ) ) { + $row[$sequenceData['column']] = $this->nextSequenceValue( $sequenceData['sequence'] ); } # 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); + $this->insert( $table, $row, $fname ); } } # DELETE where the condition is a join - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) { + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseOracle::deleteJoin" ) { if ( !$conds ) { - throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' ); + throw new DBUnexpectedError( $this, 'DatabaseOracle::deleteJoin() called with empty $conds' ); } $delTable = $this->tableName( $delTable ); @@ -479,78 +738,80 @@ class DatabaseOracle extends Database { # Returns the size of a text field, or -1 for "unlimited" function textFieldSize( $table, $field ) { - $table = $this->tableName( $table ); - $sql = "SELECT t.typname as ftype,a.atttypmod as size - FROM pg_class c, pg_attribute a, pg_type t - WHERE relname='$table' AND a.attrelid=c.oid AND - a.atttypid=t.oid and a.attname='$field'"; - $res =$this->query($sql); - $row=$this->fetchObject($res); - if ($row->ftype=="varchar") { - $size=$row->size-4; + $fieldInfoData = $this->fieldInfo( $table, $field); + if ( $fieldInfoData->type == "varchar" ) { + $size = $row->size - 4; } else { - $size=$row->size; + $size = $row->size; } - $this->freeResult( $res ); return $size; } - function lowPriorityOption() { - return ''; - } - - function limitResult($sql, $limit, $offset) { - if ($offset === false) + function limitResult( $sql, $limit, $offset = false ) { + if ( $offset === false ) { $offset = 0; - return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < 1 + $limit + $offset"; + } + return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < (1 + $limit + $offset)"; } - /** - * Returns an SQL expression for a simple conditional. - * Uses CASE on Oracle - * - * @param $cond String: SQL expression which will result in a boolean value - * @param $trueVal String: SQL expression to return if true - * @param $falseVal String: SQL expression to return if false - * @return String: SQL fragment - */ - function conditional( $cond, $trueVal, $falseVal ) { - return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; + + function unionQueries( $sqls, $all ) { + $glue = ' UNION ALL '; + return 'SELECT * ' . ( $all ? '':'/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')' ; } function wasDeadlock() { return $this->lastErrno() == 'OCI-00060'; } - function timestamp($ts = 0) { - return wfTimestamp(TS_ORACLE, $ts); + + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseOracle::duplicateTableStructure' ) { + $temporary = $temporary ? 'TRUE' : 'FALSE'; + $oldName = trim(strtoupper($oldName), '"'); + $oldParts = explode('_', $oldName); + + $newName = trim(strtoupper($newName), '"'); + $newParts = explode('_', $newName); + + $oldPrefix = ''; + $newPrefix = ''; + for ($i = count($oldParts)-1; $i >= 0; $i--) { + if ($oldParts[$i] != $newParts[$i]) { + $oldPrefix = implode('_', $oldParts).'_'; + $newPrefix = implode('_', $newParts).'_'; + break; + } + unset($oldParts[$i]); + unset($newParts[$i]); + } + + $tabName = substr($oldName, strlen($oldPrefix)); + + return $this->query( 'BEGIN DUPLICATE_TABLE(\'' . $tabName . '\', \'' . $oldPrefix . '\', \''.$newPrefix.'\', ' . $temporary . '); END;', $fname ); + } + + function timestamp( $ts = 0 ) { + return wfTimestamp( TS_ORACLE, $ts ); } /** * Return aggregated value function call */ - function aggregateValue ($valuedata,$valuename='value') { + function aggregateValue ( $valuedata, $valuename = 'value' ) { return $valuedata; } - function reportQueryError($error, $errno, $sql, $fname, $tempIgnore = false) { + function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { # Ignore errors during error handling to avoid infinite # recursion - $ignore = $this->ignoreErrors(true); + $ignore = $this->ignoreErrors( true ); ++$this->mErrorCount; - if ($ignore || $tempIgnore) { -echo "error ignored! query = [$sql]\n"; - wfDebug("SQL ERROR (ignored): $error\n"); + if ( $ignore || $tempIgnore ) { + wfDebug( "SQL ERROR (ignored): $error\n" ); $this->ignoreErrors( $ignore ); - } - else { -echo "error!\n"; - $message = "A database error has occurred\n" . - "Query: $sql\n" . - "Function: $fname\n" . - "Error: $errno $error\n"; - throw new DBUnexpectedError($this, $message); + } else { + throw new DBQueryError( $this, $error, $errno, $sql, $fname ); } } @@ -558,80 +819,252 @@ echo "error!\n"; * @return string wikitext of a link to the server software's web site */ function getSoftwareLink() { - return "[http://www.oracle.com/ Oracle]"; + return '[http://www.oracle.com/ Oracle]'; } /** * @return string Version information from the database */ function getServerVersion() { - return oci_server_version($this->mConn); + return oci_server_version( $this->mConn ); } /** * Query whether a given table exists (in the given schema, or the default mw one if not given) */ - function tableExists($table) { - $etable= $this->addQuotes($table); - $SQL = "SELECT 1 FROM user_tables WHERE table_name='$etable'"; - $res = $this->query($SQL); - $count = $res ? oci_num_rows($res) : 0; - if ($res) - $this->freeResult($res); + function tableExists( $table ) { + $SQL = "SELECT 1 FROM user_tables WHERE table_name='$table'"; + $res = $this->doQuery( $SQL ); + if ( $res ) { + $count = $res->numRows(); + $res->free(); + } else { + $count = 0; + } return $count; } /** - * Query whether a given column exists in the mediawiki schema + * Function translates mysql_fetch_field() functionality on ORACLE. + * Caching is present for reducing query time. + * For internal calls. Use fieldInfo for normal usage. + * Returns false if the field doesn't exist + * + * @param Array $table + * @param String $field */ - function fieldExists( $table, $field ) { - return true; // XXX + private function fieldInfoMulti( $table, $field ) { + $tableWhere = ''; + $field = strtoupper($field); + if (is_array($table)) { + $table = array_map( array( &$this, 'tableName' ), $table ); + $tableWhere = 'IN ('; + foreach($table as &$singleTable) { + $singleTable = strtoupper(trim( $singleTable, '"' )); + if (isset($this->mFieldInfoCache["$singleTable.$field"])) { + return $this->mFieldInfoCache["$singleTable.$field"]; + } + $tableWhere .= '\''.$singleTable.'\','; + } + $tableWhere = rtrim($tableWhere, ',').')'; + } else { + $table = strtoupper(trim( $this->tableName($table), '"' )); + if (isset($this->mFieldInfoCache["$table.$field"])) { + return $this->mFieldInfoCache["$table.$field"]; + } + $tableWhere = '= \''.$table.'\''; + } + + $fieldInfoStmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name '.$tableWhere.' and column_name = \''.$field.'\'' ); + if ( oci_execute( $fieldInfoStmt, OCI_DEFAULT ) === false ) { + $e = oci_error( $fieldInfoStmt ); + $this->reportQueryError( $e['message'], $e['code'], 'fieldInfo QUERY', __METHOD__ ); + return false; + } + $res = new ORAResult( $this, $fieldInfoStmt ); + if ($res->numRows() == 0 ) { + if (is_array($table)) { + foreach($table as &$singleTable) { + $this->mFieldInfoCache["$singleTable.$field"] = false; + } + } else { + $this->mFieldInfoCache["$table.$field"] = false; + } + } else { + $fieldInfoTemp = new ORAField( $res->fetchRow() ); + $table = $fieldInfoTemp->tableName(); + $this->mFieldInfoCache["$table.$field"] = $fieldInfoTemp; + return $fieldInfoTemp; + } } function fieldInfo( $table, $field ) { - return false; // XXX + if ( is_array( $table ) ) { + throw new DBUnexpectedError( $this, 'Database::fieldInfo called with table array!' ); + } + return $this->fieldInfoMulti ($table, $field); + } + + function fieldExists( $table, $field, $fname = 'DatabaseOracle::fieldExists' ) { + return (bool)$this->fieldInfo( $table, $field, $fname ); } function begin( $fname = '' ) { $this->mTrxLevel = 1; } + function immediateCommit( $fname = '' ) { return true; } + function commit( $fname = '' ) { - oci_commit($this->mConn); + oci_commit( $this->mConn ); $this->mTrxLevel = 0; } /* Not even sure why this is used in the main codebase... */ - function limitResultForUpdate($sql, $num) { + function limitResultForUpdate( $sql, $num ) { return $sql; } - function strencode($s) { - return str_replace("'", "''", $s); + /* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */ + function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) { + $cmd = ''; + $done = false; + $dollarquote = false; + + $replacements = array(); + + while ( ! feof( $fp ) ) { + if ( $lineCallback ) { + call_user_func( $lineCallback ); + } + $line = trim( fgets( $fp, 1024 ) ); + $sl = strlen( $line ) - 1; + + if ( $sl < 0 ) { + continue; + } + if ( '-' == $line { 0 } && '-' == $line { 1 } ) { + continue; + } + + // Allow dollar quoting for function declarations + if ( substr( $line, 0, 8 ) == '/*$mw$*/' ) { + if ( $dollarquote ) { + $dollarquote = false; + $done = true; + } else { + $dollarquote = true; + } + } elseif ( !$dollarquote ) { + if ( ';' == $line { $sl } && ( $sl < 2 || ';' != $line { $sl - 1 } ) ) { + $done = true; + $line = substr( $line, 0, $sl ); + } + } + + if ( $cmd != '' ) { + $cmd .= ' '; + } + $cmd .= "$line\n"; + + if ( $done ) { + $cmd = str_replace( ';;', ";", $cmd ); + if ( strtolower( substr( $cmd, 0, 6 ) ) == 'define' ) { + if ( preg_match( '/^define\s*([^\s=]*)\s*=\s*\'\{\$([^\}]*)\}\'/', $cmd, $defines ) ) { + $replacements[$defines[2]] = $defines[1]; + } + } else { + foreach ( $replacements as $mwVar => $scVar ) { + $cmd = str_replace( '&' . $scVar . '.', '{$' . $mwVar . '}', $cmd ); + } + + $cmd = $this->replaceVars( $cmd ); + $res = $this->query( $cmd, __METHOD__ ); + if ( $resultCallback ) { + call_user_func( $resultCallback, $res, $this ); + } + + if ( false === $res ) { + $err = $this->lastError(); + return "Query \"{$cmd}\" failed with error code \"$err\".\n"; + } + } + + $cmd = ''; + $done = false; + } + } + return true; } - function encodeBlob($b) { - return new ORABlob($b); + function setup_database() { + global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser; + + $res = $this->sourceFile( "../maintenance/ora/tables.sql" ); + if ($res === true) { + print " done.</li>\n"; + } else { + print " <b>FAILED</b></li>\n"; + dieout( htmlspecialchars( $res ) ); + } + + // Avoid the non-standard "REPLACE INTO" syntax + echo "<li>Populating interwiki table</li>\n"; + $f = fopen( "../maintenance/interwiki.sql", 'r' ); + if ( $f == false ) { + dieout( "Could not find the interwiki.sql file" ); + } + + // do it like the postgres :D + $SQL = "INSERT INTO ".$this->tableName('interwiki')." (iw_prefix,iw_url,iw_local) VALUES "; + while ( !feof( $f ) ) { + $line = fgets( $f, 1024 ); + $matches = array(); + if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) { + continue; + } + $this->query( "$SQL $matches[1],$matches[2])" ); + } + + echo "<li>Table interwiki successfully populated</li>\n"; } - function decodeBlob($b) { - return $b; //return $b->load(); + + function strencode( $s ) { + return str_replace( "'", "''", $s ); } function addQuotes( $s ) { - global $wgLang; - $s = $wgLang->checkTitleEncoding($s); - return "'" . $this->strencode($s) . "'"; + global $wgLang; + if ( isset( $wgLang->mLoaded ) && $wgLang->mLoaded ) { + $s = $wgLang->checkTitleEncoding( $s ); + } + return "'" . $this->strencode( $s ) . "'"; } function quote_ident( $s ) { return $s; } - /* For now, does nothing */ - function selectDB( $db ) { - return true; + function selectRow( $table, $vars, $conds, $fname = 'DatabaseOracle::selectRow', $options = array(), $join_conds = array() ) { + global $wgLang; + + $conds2 = array(); + $conds = ($conds != null && !is_array($conds)) ? array($conds) : $conds; + foreach ( $conds as $col => $val ) { + $col_info = $this->fieldInfoMulti( $table, $col ); + $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; + if ( $col_type == 'CLOB' ) { + $conds2['TO_CHAR(' . $col . ')'] = $wgLang->checkTitleEncoding( $val ); + } elseif ( $col_type == 'VARCHAR2' && !mb_check_encoding( $val ) ) { + $conds2[$col] = $wgLang->checkTitleEncoding( $val ); + } else { + $conds2[$col] = $val; + } + } + + return parent::selectRow( $table, $vars, $conds2, $fname, $options, $join_conds ); } /** @@ -655,18 +1088,18 @@ echo "error!\n"; } } - if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; - if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; - - if (isset($options['LIMIT'])) { - // $tailOpts .= $this->limitResult('', $options['LIMIT'], - // isset($options['OFFSET']) ? $options['OFFSET'] - // : false); + if ( isset( $options['GROUP BY'] ) ) { + $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; + } + if ( isset( $options['ORDER BY'] ) ) { + $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; } - #if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE'; - #if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE'; - if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; + # if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE'; + # if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE'; + if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { + $startOpts .= 'DISTINCT'; + } if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { $useIndex = $this->useIndexClause( $options['USE INDEX'] ); @@ -677,13 +1110,46 @@ echo "error!\n"; return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); } - public function setTimeout( $timeout ) { - // @todo fixme no-op + public function delete( $table, $conds, $fname = 'DatabaseOracle::delete' ) { + global $wgLang; + + if ( $wgLang != null ) { + $conds2 = array(); + $conds = ($conds != null && !is_array($conds)) ? array($conds) : $conds; + foreach ( $conds as $col => $val ) { + $col_info = $this->fieldInfoMulti( $table, $col ); + $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; + if ( $col_type == 'CLOB' ) { + $conds2['TO_CHAR(' . $col . ')'] = $wgLang->checkTitleEncoding( $val ); + } else { + if ( is_array( $val ) ) { + $conds2[$col] = $val; + foreach ( $conds2[$col] as &$val2 ) { + $val2 = $wgLang->checkTitleEncoding( $val2 ); + } + } else { + $conds2[$col] = $wgLang->checkTitleEncoding( $val ); + } + } + } + + return parent::delete( $table, $conds2, $fname ); + } else { + return parent::delete( $table, $conds, $fname ); + } } - function ping() { - wfDebug( "Function ping() not written for DatabaseOracle.php yet"); - return true; + function bitNot( $field ) { + // expecting bit-fields smaller than 4bytes + return 'BITNOT(' . $bitField . ')'; + } + + function bitAnd( $fieldLeft, $fieldRight ) { + return 'BITAND(' . $fieldLeft . ', ' . $fieldRight . ')'; + } + + function bitOr( $fieldLeft, $fieldRight ) { + return 'BITOR(' . $fieldLeft . ', ' . $fieldRight . ')'; } /** @@ -696,8 +1162,8 @@ echo "error!\n"; return 0; } - function setFakeSlaveLag( $lag ) {} - function setFakeMaster( $enabled = true ) {} + function setFakeSlaveLag( $lag ) { } + function setFakeMaster( $enabled = true ) { } function getDBname() { return $this->mDBname; @@ -706,19 +1172,28 @@ echo "error!\n"; function getServer() { return $this->mServer; } - - /** - * No-op lock functions - */ - public function lock( $lockName, $method ) { - return true; - } - public function unlock( $lockName, $method ) { - return true; + + public function replaceVars( $ins ) { + $varnames = array( 'wgDBprefix' ); + if ( $this->mFlags & DBO_SYSDBA ) { + $varnames[] = 'wgDBOracleDefTS'; + $varnames[] = 'wgDBOracleTempTS'; + } + + // Ordinary variables + foreach ( $varnames as $var ) { + if ( isset( $GLOBALS[$var] ) ) { + $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check? + $ins = str_replace( '{$' . $var . '}', $val, $ins ); + $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins ); + $ins = str_replace( '/*$' . $var . '*/', $val, $ins ); + } + } + + return parent::replaceVars( $ins ); } - + public function getSearchEngine() { - return "SearchOracle"; + return 'SearchOracle'; } - } // end DatabaseOracle class diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index c940ad09..9072a5b2 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -11,7 +11,7 @@ class PostgresField { static function fromText($db, $table, $field) { global $wgDBmwschema; - $q = <<<END + $q = <<<SQL SELECT CASE WHEN typname = 'int2' THEN 'smallint' WHEN typname = 'int4' THEN 'integer' @@ -27,7 +27,7 @@ AND atttypid=pg_type.oid AND nspname=%s AND relname=%s AND attname=%s; -END; +SQL; $res = $db->query(sprintf($q, $db->addQuotes($wgDBmwschema), $db->addQuotes($table), @@ -68,11 +68,11 @@ END; /** * @ingroup Database */ -class DatabasePostgres extends Database { - var $mInsertId = NULL; - var $mLastResult = NULL; - var $numeric_version = NULL; - var $mAffectedRows = NULL; +class DatabasePostgres extends DatabaseBase { + var $mInsertId = null; + var $mLastResult = null; + var $numeric_version = null; + var $mAffectedRows = null; function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0 ) @@ -84,6 +84,10 @@ class DatabasePostgres extends Database { } + function getType() { + return 'postgres'; + } + function cascadingDeletes() { return true; } @@ -132,8 +136,8 @@ class DatabasePostgres extends Database { global $wgDBport; - if (!strlen($user)) { ## e.g. the class is being loaded - return; + if (!strlen($user)) { ## e.g. the class is being loaded + return; } $this->close(); $this->mServer = $server; @@ -152,7 +156,7 @@ class DatabasePostgres extends Database { if ($port!=false && $port!="") { $connectVars['port'] = $port; } - $connectString = $this->makeConnectionString( $connectVars ); + $connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW ); $this->installErrorHandler(); $this->mConn = pg_connect( $connectString ); @@ -578,7 +582,7 @@ class DatabasePostgres extends Database { $sql = mb_convert_encoding($sql,'UTF-8'); } $this->mLastResult = pg_query( $this->mConn, $sql); - $this->mAffectedRows = NULL; // use pg_affected_rows(mLastResult) + $this->mAffectedRows = null; // use pg_affected_rows(mLastResult) return $this->mLastResult; } @@ -713,7 +717,7 @@ class DatabasePostgres extends Database { $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'"; $res = $this->query( $sql, $fname ); if ( !$res ) { - return NULL; + return null; } while ( $row = $this->fetchObject( $res ) ) { if ( $row->indexname == $this->indexName( $index ) ) { @@ -730,7 +734,7 @@ class DatabasePostgres extends Database { ")'"; $res = $this->query( $sql, $fname ); if ( !$res ) - return NULL; + return null; while ($row = $this->fetchObject( $res )) return true; return false; @@ -873,6 +877,81 @@ class DatabasePostgres extends Database { } + /** + * INSERT SELECT wrapper + * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...) + * Source items may be literals rather then field names, but strings should be quoted with Database::addQuotes() + * $conds may be "*" to copy the whole table + * srcTable may be an array of tables. + * @todo FIXME: implement this a little better (seperate select/insert)? + */ + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect', + $insertOptions = array(), $selectOptions = array() ) + { + $destTable = $this->tableName( $destTable ); + + // If IGNORE is set, we use savepoints to emulate mysql's behavior + $ignore = in_array( 'IGNORE', $insertOptions ) ? 'mw' : ''; + + if( is_array( $insertOptions ) ) { + $insertOptions = implode( ' ', $insertOptions ); + } + if( !is_array( $selectOptions ) ) { + $selectOptions = array( $selectOptions ); + } + list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions ); + if( is_array( $srcTable ) ) { + $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); + } else { + $srcTable = $this->tableName( $srcTable ); + } + + // If we are not in a transaction, we need to be for savepoint trickery + $didbegin = 0; + if ( $ignore ) { + if( !$this->mTrxLevel ) { + $this->begin(); + $didbegin = 1; + } + $olde = error_reporting( 0 ); + $numrowsinserted = 0; + pg_query( $this->mConn, "SAVEPOINT $ignore"); + } + + $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . + " SELECT $startOpts " . implode( ',', $varMap ) . + " FROM $srcTable $useIndex"; + + if ( $conds != '*') { + $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); + } + + $sql .= " $tailOpts"; + + $res = (bool)$this->query( $sql, $fname, $ignore ); + if( $ignore ) { + $bar = pg_last_error(); + if( $bar != false ) { + pg_query( $this->mConn, "ROLLBACK TO $ignore" ); + } else { + pg_query( $this->mConn, "RELEASE $ignore" ); + $numrowsinserted++; + } + $olde = error_reporting( $olde ); + if( $didbegin ) { + $this->commit(); + } + + // Set the affected row count for the whole operation + $this->mAffectedRows = $numrowsinserted; + + // IGNORE always returns true + return true; + } + + return $res; + } + function tableName( $name ) { # Replace reserved words with better ones switch( $name ) { @@ -898,7 +977,7 @@ class DatabasePostgres extends Database { } /** - * Return the current value of a sequence. Assumes it has ben nextval'ed in this session. + * Return the current value of a sequence. Assumes it has been nextval'ed in this session. */ function currentSequenceValue( $seqName ) { $safeseq = preg_replace( "/'/", "''", $seqName ); @@ -909,13 +988,6 @@ class DatabasePostgres extends Database { return $currval; } - /** - * Postgres does not have a "USE INDEX" clause, so return an empty string - */ - function useIndexClause( $index ) { - return ''; - } - # REPLACE query wrapper # Postgres simulates this with a DELETE followed by INSERT # $row is the row to insert, an associative array @@ -1009,31 +1081,18 @@ class DatabasePostgres extends Database { return $size; } - function lowPriorityOption() { - return ''; - } - function limitResult($sql, $limit, $offset=false) { return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":""); } - /** - * Returns an SQL expression for a simple conditional. - * Uses CASE on Postgres - * - * @param $cond String: SQL expression which will result in a boolean value - * @param $trueVal String: SQL expression to return if true - * @param $falseVal String: SQL expression to return if false - * @return String: SQL fragment - */ - function conditional( $cond, $trueVal, $falseVal ) { - return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; - } - function wasDeadlock() { return $this->lastErrno() == '40P01'; } + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabasePostgres::duplicateTableStructure' ) { + return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname ); + } + function timestamp( $ts=0 ) { return wfTimestamp(TS_POSTGRES,$ts); } @@ -1126,18 +1185,18 @@ class DatabasePostgres extends Database { function triggerExists( $table, $trigger ) { global $wgDBmwschema; - $q = <<<END + $q = <<<SQL SELECT 1 FROM pg_class, pg_namespace, pg_trigger WHERE relnamespace=pg_namespace.oid AND relkind='r' AND tgrelid=pg_class.oid AND nspname=%s AND relname=%s AND tgname=%s -END; +SQL; $res = $this->query(sprintf($q, $this->addQuotes($wgDBmwschema), $this->addQuotes($table), $this->addQuotes($trigger))); if (!$res) - return NULL; + return null; $rows = $res->numRows(); $this->freeResult( $res ); return $rows; @@ -1161,7 +1220,7 @@ END; $this->addQuotes($constraint)); $res = $this->query($SQL); if (!$res) - return NULL; + return null; $rows = $res->numRows(); $this->freeResult($res); return $rows; @@ -1252,11 +1311,17 @@ END; if (!$res) { print "<b>FAILED</b>. Make sure that the user \"" . htmlspecialchars( $wgDBuser ) . "\" can write to the schema \"" . htmlspecialchars( $wgDBmwschema ) . "\"</li>\n"; - dieout("</ul>"); + dieout(""); # Will close the main list <ul> and finish the page. } $this->doQuery("DROP TABLE $safeschema.$ctest"); - $res = dbsource( "../maintenance/postgres/tables.sql", $this); + $res = $this->sourceFile( "../maintenance/postgres/tables.sql" ); + if ($res === true) { + print " done.</li>\n"; + } else { + print " <b>FAILED</b></li>\n"; + dieout( htmlspecialchars( $res ) ); + } ## Update version information $mwv = $this->addQuotes($wgVersion); @@ -1274,10 +1339,13 @@ END; "WHERE type = 'Creation'"; $this->query($SQL); + echo "<li>Populating interwiki table... "; + ## Avoid the non-standard "REPLACE INTO" syntax $f = fopen( "../maintenance/interwiki.sql", 'r' ); if ($f == false ) { - dieout( "<li>Could not find the interwiki.sql file"); + print "<b>FAILED</b></li>"; + dieout( "Could not find the interwiki.sql file" ); } ## We simply assume it is already empty as we have just created it $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES "; @@ -1289,7 +1357,7 @@ END; } $this->query("$SQL $matches[1],$matches[2])"); } - print " (table interwiki successfully populated)...\n"; + print " successfully populated.</li>\n"; $this->doQuery("COMMIT"); } @@ -1324,11 +1392,6 @@ END; return '"' . preg_replace( '/"/', '""', $s) . '"'; } - /* For now, does nothing */ - function selectDB( $db ) { - return true; - } - /** * Postgres specific version of replaceVars. * Calls the parent version in Database.php @@ -1392,15 +1455,6 @@ END; return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); } - public function setTimeout( $timeout ) { - // @todo fixme no-op - } - - function ping() { - wfDebug( "Function ping() not written for DatabasePostgres.php yet"); - return true; - } - /** * How lagged is this slave? * @@ -1425,17 +1479,7 @@ END; return implode( ' || ', $stringList ); } - /* These are not used yet, but we know we don't want the default version */ - - public function lock( $lockName, $method ) { - return true; - } - public function unlock( $lockName, $method ) { - return true; - } - public function getSearchEngine() { return "SearchPostgres"; } - } // end DatabasePostgres class diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index 455c0b48..c149cf04 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -10,7 +10,7 @@ /** * @ingroup Database */ -class DatabaseSqlite extends Database { +class DatabaseSqlite extends DatabaseBase { var $mAffectedRows; var $mLastResult; @@ -18,110 +18,162 @@ class DatabaseSqlite extends Database { var $mName; /** - * Constructor + * Constructor. + * Parameters $server, $user and $password are not used. */ - function __construct($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0) { - global $wgOut,$wgSQLiteDataDir, $wgSQLiteDataDirMode; - if ("$wgSQLiteDataDir" == '') $wgSQLiteDataDir = dirname($_SERVER['DOCUMENT_ROOT']).'/data'; - if (!is_dir($wgSQLiteDataDir)) wfMkdirParents( $wgSQLiteDataDir, $wgSQLiteDataDirMode ); + function __construct( $server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0 ) { $this->mFailFunction = $failFunction; $this->mFlags = $flags; - $this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite"; $this->mName = $dbName; - $this->open($server, $user, $password, $dbName); + $this->open( $server, $user, $password, $dbName ); + } + + function getType() { + return 'sqlite'; } /** - * todo: check if these should be true like parent class + * @todo: check if it should be true like parent class */ function implicitGroupby() { return false; } - function implicitOrderby() { return false; } - static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) { - return new DatabaseSqlite($server, $user, $password, $dbName, $failFunction, $flags); + static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) { + return new DatabaseSqlite( $server, $user, $password, $dbName, $failFunction, $flags ); } /** Open an SQLite database and return a resource handle to it * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases */ - function open($server,$user,$pass,$dbName) { - $this->mConn = false; - if ($dbName) { - $file = $this->mDatabaseFile; - try { - if ( $this->mFlags & DBO_PERSISTENT ) { - $this->mConn = new PDO( "sqlite:$file", $user, $pass, - array( PDO::ATTR_PERSISTENT => true ) ); - } else { - $this->mConn = new PDO( "sqlite:$file", $user, $pass ); - } - } catch ( PDOException $e ) { - $err = $e->getMessage(); - } - if ( $this->mConn === false ) { - wfDebug( "DB connection error: $err\n" ); - if ( !$this->mFailFunction ) { - throw new DBConnectionError( $this, $err ); - } else { - return false; - } + function open( $server, $user, $pass, $dbName ) { + global $wgSQLiteDataDir; - } - $this->mOpened = $this->mConn; - # set error codes only, don't raise exceptions - $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); + $fileName = self::generateFileName( $wgSQLiteDataDir, $dbName ); + if ( !is_readable( $fileName ) ) { + throw new DBConnectionError( $this, "SQLite database not accessible" ); $this->mConn = false; } + $this->openFile( $fileName ); return $this->mConn; } /** + * Opens a database file + * @return SQL connection or false if failed + */ + function openFile( $fileName ) { + $this->mDatabaseFile = $fileName; + try { + if ( $this->mFlags & DBO_PERSISTENT ) { + $this->mConn = new PDO( "sqlite:$fileName", '', '', + array( PDO::ATTR_PERSISTENT => true ) ); + } else { + $this->mConn = new PDO( "sqlite:$fileName", '', '' ); + } + } catch ( PDOException $e ) { + $err = $e->getMessage(); + } + if ( $this->mConn === false ) { + wfDebug( "DB connection error: $err\n" ); + if ( !$this->mFailFunction ) { + throw new DBConnectionError( $this, $err ); + } else { + return false; + } + + } + $this->mOpened = !!$this->mConn; + # set error codes only, don't raise exceptions + if ( $this->mOpened ) { + $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); + return true; + } + } + + /** * Close an SQLite database */ function close() { $this->mOpened = false; - if (is_object($this->mConn)) { - if ($this->trxLevel()) $this->immediateCommit(); + if ( is_object( $this->mConn ) ) { + if ( $this->trxLevel() ) $this->commit(); $this->mConn = null; } return true; } /** + * Generates a database file name. Explicitly public for installer. + * @param $dir String: Directory where database resides + * @param $dbName String: Database name + * @return String + */ + public static function generateFileName( $dir, $dbName ) { + return "$dir/$dbName.sqlite"; + } + + /** + * Returns version of currently supported SQLite fulltext search module or false if none present. + * @return String + */ + function getFulltextSearchModule() { + $table = 'dummy_search_test'; + $this->query( "DROP TABLE IF EXISTS $table", __METHOD__ ); + if ( $this->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) { + $this->query( "DROP TABLE IF EXISTS $table", __METHOD__ ); + return 'FTS3'; + } + return false; + } + + /** * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result */ - function doQuery($sql) { - $res = $this->mConn->query($sql); - if ($res === false) { + function doQuery( $sql ) { + $res = $this->mConn->query( $sql ); + if ( $res === false ) { return false; } else { $r = $res instanceof ResultWrapper ? $res->result : $res; $this->mAffectedRows = $r->rowCount(); - $res = new ResultWrapper($this,$r->fetchAll()); + $res = new ResultWrapper( $this, $r->fetchAll() ); } return $res; } - function freeResult($res) { - if ($res instanceof ResultWrapper) $res->result = NULL; else $res = NULL; + function freeResult( $res ) { + if ( $res instanceof ResultWrapper ) + $res->result = null; + else + $res = null; } - function fetchObject($res) { - if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res; - $cur = current($r); - if (is_array($cur)) { - next($r); + function fetchObject( $res ) { + if ( $res instanceof ResultWrapper ) + $r =& $res->result; + else + $r =& $res; + + $cur = current( $r ); + if ( is_array( $cur ) ) { + next( $r ); $obj = new stdClass; - foreach ($cur as $k => $v) if (!is_numeric($k)) $obj->$k = $v; + foreach ( $cur as $k => $v ) + if ( !is_numeric( $k ) ) + $obj->$k = $v; + return $obj; } return false; } - function fetchRow($res) { - if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res; - $cur = current($r); - if (is_array($cur)) { - next($r); + function fetchRow( $res ) { + if ( $res instanceof ResultWrapper ) + $r =& $res->result; + else + $r =& $res; + + $cur = current( $r ); + if ( is_array( $cur ) ) { + next( $r ); return $cur; } return false; @@ -130,20 +182,20 @@ class DatabaseSqlite extends Database { /** * The PDO::Statement class implements the array interface so count() will work */ - function numRows($res) { + function numRows( $res ) { $r = $res instanceof ResultWrapper ? $res->result : $res; - return count($r); + return count( $r ); } - function numFields($res) { + function numFields( $res ) { $r = $res instanceof ResultWrapper ? $res->result : $res; - return is_array($r) ? count($r[0]) : 0; + return is_array( $r ) ? count( $r[0] ) : 0; } - function fieldName($res,$n) { + function fieldName( $res, $n ) { $r = $res instanceof ResultWrapper ? $res->result : $res; - if (is_array($r)) { - $keys = array_keys($r[0]); + if ( is_array( $r ) ) { + $keys = array_keys( $r[0] ); return $keys[$n]; } return false; @@ -152,8 +204,8 @@ class DatabaseSqlite extends Database { /** * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks */ - function tableName($name) { - return str_replace('`','',parent::tableName($name)); + function tableName( $name ) { + return str_replace( '`', '', parent::tableName( $name ) ); } /** @@ -170,20 +222,26 @@ class DatabaseSqlite extends Database { return $this->mConn->lastInsertId(); } - function dataSeek($res,$row) { - if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res; - reset($r); - if ($row > 0) for ($i = 0; $i < $row; $i++) next($r); + function dataSeek( $res, $row ) { + if ( $res instanceof ResultWrapper ) + $r =& $res->result; + else + $r =& $res; + reset( $r ); + if ( $row > 0 ) + for ( $i = 0; $i < $row; $i++ ) + next( $r ); } function lastError() { - if (!is_object($this->mConn)) return "Cannot return last error, no db connection"; + if ( !is_object( $this->mConn ) ) + return "Cannot return last error, no db connection"; $e = $this->mConn->errorInfo(); - return isset($e[2]) ? $e[2] : ''; + return isset( $e[2] ) ? $e[2] : ''; } function lastErrno() { - if (!is_object($this->mConn)) { + if ( !is_object( $this->mConn ) ) { return "Cannot return last error, no db connection"; } else { $info = $this->mConn->errorInfo(); @@ -200,7 +258,7 @@ class DatabaseSqlite extends Database { * Returns false if the index does not exist * - if errors are explicitly ignored, returns NULL on failure */ - function indexInfo($table, $index, $fname = 'Database::indexExists') { + function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) { $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')'; $res = $this->query( $sql, $fname ); if ( !$res ) { @@ -216,8 +274,8 @@ class DatabaseSqlite extends Database { return $info; } - function indexUnique($table, $index, $fname = 'Database::indexUnique') { - $row = $this->selectRow( 'sqlite_master', '*', + function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) { + $row = $this->selectRow( 'sqlite_master', '*', array( 'type' => 'index', 'name' => $this->indexName( $index ), @@ -239,67 +297,81 @@ class DatabaseSqlite extends Database { /** * Filter the options used in SELECT statements */ - function makeSelectOptions($options) { - foreach ($options as $k => $v) if (is_numeric($k) && $v == 'FOR UPDATE') $options[$k] = ''; - return parent::makeSelectOptions($options); + function makeSelectOptions( $options ) { + foreach ( $options as $k => $v ) + if ( is_numeric( $k ) && $v == 'FOR UPDATE' ) + $options[$k] = ''; + return parent::makeSelectOptions( $options ); } /** - * Based on MySQL method (parent) with some prior SQLite-sepcific adjustments + * Based on generic method (parent) with some prior SQLite-sepcific adjustments */ - function insert($table, $a, $fname = 'DatabaseSqlite::insert', $options = array()) { - if (!count($a)) return true; - if (!is_array($options)) $options = array($options); + function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) { + if ( !count( $a ) ) return true; + if ( !is_array( $options ) ) $options = array( $options ); # SQLite uses OR IGNORE not just IGNORE - foreach ($options as $k => $v) if ($v == 'IGNORE') $options[$k] = 'OR IGNORE'; + foreach ( $options as $k => $v ) + if ( $v == 'IGNORE' ) + $options[$k] = 'OR IGNORE'; # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts - if (isset($a[0]) && is_array($a[0])) { + if ( isset( $a[0] ) && is_array( $a[0] ) ) { $ret = true; - foreach ($a as $k => $v) if (!parent::insert($table,$v,"$fname/multi-row",$options)) $ret = false; + foreach ( $a as $k => $v ) + if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) + $ret = false; + } else { + $ret = parent::insert( $table, $a, "$fname/single-row", $options ); } - else $ret = parent::insert($table,$a,"$fname/single-row",$options); return $ret; } - /** - * SQLite does not have a "USE INDEX" clause, so return an empty string - */ - function useIndexClause($index) { - return ''; + function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) { + if ( !count( $rows ) ) return true; + + # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries + if ( isset( $rows[0] ) && is_array( $rows[0] ) ) { + $ret = true; + foreach ( $rows as $k => $v ) + if ( !parent::replace( $table, $uniqueIndexes, $v, "$fname/multi-row" ) ) + $ret = false; + } else { + $ret = parent::replace( $table, $uniqueIndexes, $rows, "$fname/single-row" ); + } + + return $ret; } /** * Returns the size of a text field, or -1 for "unlimited" * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though. */ - function textFieldSize($table, $field) { - return -1; + function textFieldSize( $table, $field ) { + return - 1; } - /** - * No low priority option in SQLite - */ - function lowPriorityOption() { - return ''; + function unionSupportsOrderAndLimit() { + return false; } - /** - * Returns an SQL expression for a simple conditional. - * - uses CASE on SQLite - */ - function conditional($cond, $trueVal, $falseVal) { - return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; + function unionQueries( $sqls, $all ) { + $glue = $all ? ' UNION ALL ' : ' UNION '; + return implode( $glue, $sqls ); } function wasDeadlock() { - return $this->lastErrno() == SQLITE_BUSY; + return $this->lastErrno() == 5; // SQLITE_BUSY } function wasErrorReissuable() { - return $this->lastErrno() == SQLITE_SCHEMA; + return $this->lastErrno() == 17; // SQLITE_SCHEMA; + } + + function wasReadOnlyError() { + return $this->lastErrno() == 8; // SQLITE_READONLY; } /** @@ -313,15 +385,14 @@ class DatabaseSqlite extends Database { * @return string Version information from the database */ function getServerVersion() { - global $wgContLang; - $ver = $this->mConn->getAttribute(PDO::ATTR_SERVER_VERSION); + $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION ); return $ver; } /** * Query whether a given column exists in the mediawiki schema */ - function fieldExists($table, $field, $fname = '') { + function fieldExists( $table, $field, $fname = '' ) { $info = $this->fieldInfo( $table, $field ); return (bool)$info; } @@ -330,7 +401,7 @@ class DatabaseSqlite extends Database { * Get information about a given field * Returns false if the field does not exist. */ - function fieldInfo($table, $field) { + function fieldInfo( $table, $field ) { $tableName = $this->tableName( $table ); $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')'; $res = $this->query( $sql, __METHOD__ ); @@ -343,73 +414,60 @@ class DatabaseSqlite extends Database { } function begin( $fname = '' ) { - if ($this->mTrxLevel == 1) $this->commit(); + if ( $this->mTrxLevel == 1 ) $this->commit(); $this->mConn->beginTransaction(); $this->mTrxLevel = 1; } function commit( $fname = '' ) { - if ($this->mTrxLevel == 0) return; + if ( $this->mTrxLevel == 0 ) return; $this->mConn->commit(); $this->mTrxLevel = 0; } function rollback( $fname = '' ) { - if ($this->mTrxLevel == 0) return; + if ( $this->mTrxLevel == 0 ) return; $this->mConn->rollBack(); $this->mTrxLevel = 0; } - function limitResultForUpdate($sql, $num) { + function limitResultForUpdate( $sql, $num ) { return $this->limitResult( $sql, $num ); } - function strencode($s) { - return substr($this->addQuotes($s),1,-1); + function strencode( $s ) { + return substr( $this->addQuotes( $s ), 1, - 1 ); } - function encodeBlob($b) { + function encodeBlob( $b ) { return new Blob( $b ); } - function decodeBlob($b) { - if ($b instanceof Blob) { + function decodeBlob( $b ) { + if ( $b instanceof Blob ) { $b = $b->fetch(); } return $b; } - function addQuotes($s) { + function addQuotes( $s ) { if ( $s instanceof Blob ) { return "x'" . bin2hex( $s->fetch() ) . "'"; } else { - return $this->mConn->quote($s); + return $this->mConn->quote( $s ); } } - function quote_ident($s) { return $s; } - - /** - * Not possible in SQLite - * We have ATTACH_DATABASE but that requires database selectors before the - * table names and in any case is really a different concept to MySQL's USE - */ - function selectDB($db) { - if ( $db != $this->mName ) { - throw new MWException( 'selectDB is not implemented in SQLite' ); - } + function quote_ident( $s ) { + return $s; } - /** - * not done - */ - public function setTimeout($timeout) { return; } - - /** - * No-op for a non-networked database - */ - function ping() { - return true; + function buildLike() { + $params = func_get_args(); + if ( count( $params ) > 0 && is_array( $params[0] ) ) { + $params = $params[0]; + } + return parent::buildLike( $params ) . "ESCAPE '\' "; } /** @@ -424,40 +482,33 @@ class DatabaseSqlite extends Database { * - this is the same way PostgreSQL works, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile) */ public function setup_database() { - global $IP,$wgSQLiteDataDir,$wgDBTableOptions; - $wgDBTableOptions = ''; + global $IP; # Process common MySQL/SQLite table definitions $err = $this->sourceFile( "$IP/maintenance/tables.sql" ); - if ($err !== true) { - $this->reportQueryError($err,0,$sql,__FUNCTION__); - exit( 1 ); + if ( $err !== true ) { + echo " <b>FAILED</b></li>"; + dieout( htmlspecialchars( $err ) ); } + echo " done.</li>"; # Use DatabasePostgres's code to populate interwiki from MySQL template - $f = fopen("$IP/maintenance/interwiki.sql",'r'); - if ($f == false) dieout("<li>Could not find the interwiki.sql file"); + $f = fopen( "$IP/maintenance/interwiki.sql", 'r' ); + if ( $f == false ) { + dieout( "Could not find the interwiki.sql file." ); + } + $sql = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES "; - while (!feof($f)) { - $line = fgets($f,1024); + while ( !feof( $f ) ) { + $line = fgets( $f, 1024 ); $matches = array(); - if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue; - $this->query("$sql $matches[1],$matches[2])"); + if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) continue; + $this->query( "$sql $matches[1],$matches[2])" ); } } - /** - * No-op lock functions - */ - public function lock( $lockName, $method ) { - return true; - } - public function unlock( $lockName, $method ) { - return true; - } - public function getSearchEngine() { - return "SearchEngineDummy"; + return "SearchSqlite"; } /** @@ -471,23 +522,33 @@ class DatabaseSqlite extends Database { protected function replaceVars( $s ) { $s = parent::replaceVars( $s ); - if ( preg_match( '/^\s*CREATE TABLE/i', $s ) ) { + if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) { // CREATE TABLE hacks to allow schema file sharing with MySQL - + // binary/varbinary column type -> blob - $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'blob\1', $s ); + $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s ); // no such thing as unsigned - $s = preg_replace( '/\bunsigned\b/i', '', $s ); - // INT -> INTEGER for primary keys - $s = preg_replacE( '/\bint\b/i', 'integer', $s ); + $s = preg_replace( '/\b(un)?signed\b/i', '', $s ); + // INT -> INTEGER + $s = preg_replace( '/\b(tiny|small|medium|big|)int(\([\s\d]*\)|\b)/i', 'INTEGER', $s ); + // varchar -> TEXT + $s = preg_replace( '/\bvarchar\(\d+\)/i', 'TEXT', $s ); + // TEXT normalization + $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s ); + // BLOB normalization + $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s ); + // BOOL -> INTEGER + $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s ); + // DATETIME -> TEXT + $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s ); // No ENUM type - $s = preg_replace( '/enum\([^)]*\)/i', 'blob', $s ); + $s = preg_replace( '/enum\([^)]*\)/i', 'BLOB', $s ); // binary collation type -> nothing $s = preg_replace( '/\bbinary\b/i', '', $s ); // auto_increment -> autoincrement - $s = preg_replace( '/\bauto_increment\b/i', 'autoincrement', $s ); + $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s ); // No explicit options - $s = preg_replace( '/\)[^)]*$/', ')', $s ); + $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s ); } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) { // No truncated indexes $s = preg_replace( '/\(\d+\)/', '', $s ); @@ -504,9 +565,31 @@ class DatabaseSqlite extends Database { return '(' . implode( ') || (', $stringList ) . ')'; } + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) { + $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name='$oldName' AND type='table'", $fname ); + $obj = $this->fetchObject( $res ); + if ( !$obj ) { + throw new MWException( "Couldn't retrieve structure for table $oldName" ); + } + $sql = $obj->sql; + $sql = preg_replace( '/\b' . preg_quote( $oldName ) . '\b/', $newName, $sql, 1 ); + return $this->query( $sql, $fname ); + } + } // end DatabaseSqlite class /** + * This class allows simple acccess to a SQLite database independently from main database settings + * @ingroup Database + */ +class DatabaseSqliteStandalone extends DatabaseSqlite { + public function __construct( $fileName, $flags = 0 ) { + $this->mFlags = $flags; + $this->openFile( $fileName ); + } +} + +/** * @ingroup Database */ class SQLiteField { @@ -545,10 +628,9 @@ class SQLiteField { # isKey(), isMultipleKey() not implemented, MySQL-specific concept. # Suggest removal from base class [TS] - + function type() { return $this->info->type; } } // end SQLiteField - diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php index 3876d71f..10c87133 100644 --- a/includes/db/LBFactory.php +++ b/includes/db/LBFactory.php @@ -25,7 +25,7 @@ abstract class LBFactory { /** * Shut down, close connections and destroy the cached instance. - * + * */ static function destroyInstance() { if ( self::$instance ) { @@ -41,7 +41,7 @@ abstract class LBFactory { abstract function __construct( $conf ); /** - * Create a new load balancer object. The resulting object will be untracked, + * Create a new load balancer object. The resulting object will be untracked, * not chronology-protected, and the caller is responsible for cleaning it up. * * @param string $wiki Wiki ID, or false for the current wiki @@ -58,8 +58,8 @@ abstract class LBFactory { abstract function getMainLB( $wiki = false ); /* - * Create a new load balancer for external storage. The resulting object will be - * untracked, not chronology-protected, and the caller is responsible for + * Create a new load balancer for external storage. The resulting object will be + * untracked, not chronology-protected, and the caller is responsible for * cleaning it up. * * @param string $cluster External storage cluster, or false for core @@ -142,8 +142,8 @@ class LBFactory_Simple extends LBFactory { } return new LoadBalancer( array( - 'servers' => $servers, - 'masterWaitTimeout' => $wgMasterWaitTimeout + 'servers' => $servers, + 'masterWaitTimeout' => $wgMasterWaitTimeout )); } @@ -162,7 +162,7 @@ class LBFactory_Simple extends LBFactory { throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" ); } return new LoadBalancer( array( - 'servers' => $wgExternalServers[$cluster] + 'servers' => $wgExternalServers[$cluster] )); } diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index 0b8ef05a..083b70b3 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -809,7 +809,7 @@ class LoadBalancer { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { foreach ( $conns3 as $conn ) { - $conn->immediateCommit(); + $conn->commit(); } } } @@ -831,7 +831,7 @@ class LoadBalancer { } } - function waitTimeout( $value = NULL ) { + function waitTimeout( $value = null ) { return wfSetVar( $this->mWaitTimeout, $value ); } @@ -878,14 +878,18 @@ class LoadBalancer { * Get the hostname and lag time of the most-lagged slave. * This is useful for maintenance scripts that need to throttle their updates. * May attempt to open connections to slaves on the default DB. + * @param $wiki string Wiki ID, or false for the default database */ - function getMaxLag() { + function getMaxLag( $wiki = false ) { $maxLag = -1; $host = ''; foreach ( $this->mServers as $i => $conn ) { - $conn = $this->getAnyOpenConnection( $i ); + $conn = false; + if ( $wiki === false ) { + $conn = $this->getAnyOpenConnection( $i ); + } if ( !$conn ) { - $conn = $this->openConnection( $i ); + $conn = $this->openConnection( $i, $wiki ); } if ( !$conn ) { continue; @@ -912,4 +916,11 @@ class LoadBalancer { $this->mLagTimes = $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki ); return $this->mLagTimes; } + + /** + * Clear the cache for getLagTimes + */ + function clearLagTimeCache() { + $this->mLagTimes = null; + } } |