Factored MySQL-specific munging out of Language::stripForSearch() to DatabaseMysql...
[lhc/web/wiklou.git] / includes / db / DatabaseMysql.php
index 31de630..63f267c 100644 (file)
@@ -7,6 +7,12 @@
  * @see Database
  */
 class DatabaseMysql extends DatabaseBase {
+       static $mMinSearchLength;
+
+       function getType() {
+               return 'mysql';
+       }
+
        /*private*/ function doQuery( $sql ) {
                if( $this->bufferResults() ) {
                        $ret = mysql_query( $sql, $this->mConn );
@@ -129,7 +135,7 @@ class DatabaseMysql extends DatabaseBase {
                $this->mOpened = false;
                if ( $this->mConn ) {
                        if ( $this->trxLevel() ) {
-                               $this->immediateCommit();
+                               $this->commit();
                        }
                        return mysql_close( $this->mConn );
                } else {
@@ -229,6 +235,30 @@ class DatabaseMysql extends DatabaseBase {
        }
 
        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 );
@@ -289,11 +319,8 @@ class DatabaseMysql extends DatabaseBase {
                return '[http://www.mysql.com/ MySQL]';
        }
 
-       /**
-        * @return String: Database type for use in messages
-       */
-       function getDBtypeForMsg() {
-               return 'MySQL';
+       function standardSelectDistinct() {
+               return false;
        }
 
        public function setTimeout( $timeout ) {
@@ -322,23 +349,104 @@ class DatabaseMysql extends DatabaseBase {
                return $row->lockstatus;
        }
 
-       public function lockTables( $read, $write, $method ) {
+       public function lockTables( $read, $write, $method, $lowPriority = true ) {
                $items = array();
 
                foreach( $write as $table ) {
-                       $items[] = $this->tableName( $table ) . ' LOW_PRIORITY WRITE';
+                       $tbl = $this->tableName( $table ) . 
+                                       ( $lowPriority ? ' LOW_PRIORITY' : '' ) . 
+                                       ' WRITE';
+                       $items[] = $tbl;
                }
                foreach( $read as $table ) {
                        $items[] = $this->tableName( $table ) . ' READ';
                }
                $sql = "LOCK TABLES " . implode( ',', $items );
-               $db->query( $sql, $method );
+               $this->query( $sql, $method );
        }
 
        public function unlockTables( $method ) {
                $this->query( "UNLOCK TABLES", $method );
        }
        
+       /**
+        * Converts some characters for MySQL's indexing to grok it correctly,
+        * and pads short words to overcome limitations.
+        */
+       function stripForSearch( $string ) {
+               global $wgContLang;
+
+               wfProfileIn( __METHOD__ );
+
+               // MySQL fulltext index doesn't grok utf-8, so we
+               // need to fold cases and convert to hex
+               $out = preg_replace_callback(
+                       "/([\\xc0-\\xff][\\x80-\\xbf]*)/",
+                       array( $this, 'stripForSearchCallback' ),
+                       $wgContLang->lc( $string ) );
+
+               // And to add insult to injury, the default indexing
+               // ignores short words... Pad them so we can pass them
+               // through without reconfiguring the server...
+               $minLength = $this->minSearchLength();
+               if( $minLength > 1 ) {
+                       $n = $minLength - 1;
+                       $out = preg_replace(
+                               "/\b(\w{1,$n})\b/",
+                               "$1u800",
+                               $out );
+               }
+
+               // Periods within things like hostnames and IP addresses
+               // are also important -- we want a search for "example.com"
+               // or "192.168.1.1" to work sanely.
+               //
+               // MySQL's search seems to ignore them, so you'd match on
+               // "example.wikipedia.com" and "192.168.83.1" as well.
+               $out = preg_replace(
+                       "/(\w)\.(\w|\*)/u",
+                       "$1u82e$2",
+                       $out );
+
+               wfProfileOut( __METHOD__ );
+               
+               return $out;
+       }
+
+       /**
+        * Armor a case-folded UTF-8 string to get through MySQL's
+        * fulltext search without being mucked up by funny charset
+        * settings or anything else of the sort.
+        */
+       protected function stripForSearchCallback( $matches ) {
+               return 'u8' . bin2hex( $matches[1] );
+       }
+
+       /**
+        * Check MySQL server's ft_min_word_len setting so we know
+        * if we need to pad short words...
+        * 
+        * @return int
+        */
+       protected function minSearchLength() {
+               if( is_null( self::$mMinSearchLength ) ) {
+                       $sql = "show global variables like 'ft\\_min\\_word\\_len'";
+
+                       // Even though this query is pretty fast, let's not overload the master
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $result = $dbr->query( $sql );
+                       $row = $result->fetchObject();
+                       $result->free();
+
+                       if( $row && $row->Variable_name == 'ft_min_word_len' ) {
+                               self::$mMinSearchLength = intval( $row->Value );
+                       } else {
+                               self::$mMinSearchLength = 0;
+                       }
+               }
+               return self::$mMinSearchLength;
+       }
+
        public function setBigSelects( $value = true ) {
                if ( $value === 'default' ) {
                        if ( $this->mDefaultBigSelects === null ) {
@@ -353,6 +461,57 @@ class DatabaseMysql extends DatabaseBase {
                $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 );
+       }
+
 }
 
 /**