MediaWiki  REL1_20
DatabaseSqlite.php
Go to the documentation of this file.
00001 <?php
00028 class DatabaseSqlite extends DatabaseBase {
00029 
00030         private static $fulltextEnabled = null;
00031 
00032         var $mAffectedRows;
00033         var $mLastResult;
00034         var $mDatabaseFile;
00035         var $mName;
00036 
00040         protected $mConn;
00041 
00051         function __construct( $server = false, $user = false, $password = false, $dbName = false, $flags = 0 ) {
00052                 $this->mName = $dbName;
00053                 parent::__construct( $server, $user, $password, $dbName, $flags );
00054                 // parent doesn't open when $user is false, but we can work with $dbName
00055                 if( $dbName ) {
00056                         global $wgSharedDB;
00057                         if( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) {
00058                                 $this->attachDatabase( $wgSharedDB );
00059                         }
00060                 }
00061         }
00062 
00066         function getType() {
00067                 return 'sqlite';
00068         }
00069 
00075         function implicitGroupby() {
00076                 return false;
00077         }
00078 
00089         function open( $server, $user, $pass, $dbName ) {
00090                 global $wgSQLiteDataDir;
00091 
00092                 $fileName = self::generateFileName( $wgSQLiteDataDir, $dbName );
00093                 if ( !is_readable( $fileName ) ) {
00094                         $this->mConn = false;
00095                         throw new DBConnectionError( $this, "SQLite database not accessible" );
00096                 }
00097                 $this->openFile( $fileName );
00098                 return $this->mConn;
00099         }
00100 
00108         function openFile( $fileName ) {
00109                 $this->mDatabaseFile = $fileName;
00110                 try {
00111                         if ( $this->mFlags & DBO_PERSISTENT ) {
00112                                 $this->mConn = new PDO( "sqlite:$fileName", '', '',
00113                                         array( PDO::ATTR_PERSISTENT => true ) );
00114                         } else {
00115                                 $this->mConn = new PDO( "sqlite:$fileName", '', '' );
00116                         }
00117                 } catch ( PDOException $e ) {
00118                         $err = $e->getMessage();
00119                 }
00120                 if ( !$this->mConn ) {
00121                         wfDebug( "DB connection error: $err\n" );
00122                         throw new DBConnectionError( $this, $err );
00123                 }
00124                 $this->mOpened = !!$this->mConn;
00125                 # set error codes only, don't raise exceptions
00126                 if ( $this->mOpened ) {
00127                         $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
00128                         return true;
00129                 }
00130         }
00131 
00136         protected function closeConnection() {
00137                 $this->mConn = null;
00138                 return true;
00139         }
00140 
00147         public static function generateFileName( $dir, $dbName ) {
00148                 return "$dir/$dbName.sqlite";
00149         }
00150 
00155         function checkForEnabledSearch() {
00156                 if ( self::$fulltextEnabled === null ) {
00157                         self::$fulltextEnabled = false;
00158                         $table = $this->tableName( 'searchindex' );
00159                         $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
00160                         if ( $res ) {
00161                                 $row = $res->fetchRow();
00162                                 self::$fulltextEnabled = stristr($row['sql'], 'fts' ) !== false;
00163                         }
00164                 }
00165                 return self::$fulltextEnabled;
00166         }
00167 
00172         static function getFulltextSearchModule() {
00173                 static $cachedResult = null;
00174                 if ( $cachedResult !== null ) {
00175                         return $cachedResult;
00176                 }
00177                 $cachedResult = false;
00178                 $table = 'dummy_search_test';
00179 
00180                 $db = new DatabaseSqliteStandalone( ':memory:' );
00181 
00182                 if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
00183                         $cachedResult = 'FTS3';
00184                 }
00185                 $db->close();
00186                 return $cachedResult;
00187         }
00188 
00199         function attachDatabase( $name, $file = false, $fname = 'DatabaseSqlite::attachDatabase' ) {
00200                 global $wgSQLiteDataDir;
00201                 if ( !$file ) {
00202                         $file = self::generateFileName( $wgSQLiteDataDir, $name );
00203                 }
00204                 $file = $this->addQuotes( $file );
00205                 return $this->query( "ATTACH DATABASE $file AS $name", $fname );
00206         }
00207 
00215         function isWriteQuery( $sql ) {
00216                 return parent::isWriteQuery( $sql ) && !preg_match( '/^ATTACH\b/i', $sql );
00217         }
00218 
00226         protected function doQuery( $sql ) {
00227                 $res = $this->mConn->query( $sql );
00228                 if ( $res === false ) {
00229                         return false;
00230                 } else {
00231                         $r = $res instanceof ResultWrapper ? $res->result : $res;
00232                         $this->mAffectedRows = $r->rowCount();
00233                         $res = new ResultWrapper( $this, $r->fetchAll() );
00234                 }
00235                 return $res;
00236         }
00237 
00241         function freeResult( $res ) {
00242                 if ( $res instanceof ResultWrapper ) {
00243                         $res->result = null;
00244                 } else {
00245                         $res = null;
00246                 }
00247         }
00248 
00253         function fetchObject( $res ) {
00254                 if ( $res instanceof ResultWrapper ) {
00255                         $r =& $res->result;
00256                 } else {
00257                         $r =& $res;
00258                 }
00259 
00260                 $cur = current( $r );
00261                 if ( is_array( $cur ) ) {
00262                         next( $r );
00263                         $obj = new stdClass;
00264                         foreach ( $cur as $k => $v ) {
00265                                 if ( !is_numeric( $k ) ) {
00266                                         $obj->$k = $v;
00267                                 }
00268                         }
00269 
00270                         return $obj;
00271                 }
00272                 return false;
00273         }
00274 
00279         function fetchRow( $res ) {
00280                 if ( $res instanceof ResultWrapper ) {
00281                         $r =& $res->result;
00282                 } else {
00283                         $r =& $res;
00284                 }
00285                 $cur = current( $r );
00286                 if ( is_array( $cur ) ) {
00287                         next( $r );
00288                         return $cur;
00289                 }
00290                 return false;
00291         }
00292 
00300         function numRows( $res ) {
00301                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00302                 return count( $r );
00303         }
00304 
00309         function numFields( $res ) {
00310                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00311                 return is_array( $r ) ? count( $r[0] ) : 0;
00312         }
00313 
00319         function fieldName( $res, $n ) {
00320                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00321                 if ( is_array( $r ) ) {
00322                         $keys = array_keys( $r[0] );
00323                         return $keys[$n];
00324                 }
00325                 return false;
00326         }
00327 
00335         function tableName( $name, $format = 'quoted' ) {
00336                 // table names starting with sqlite_ are reserved
00337                 if ( strpos( $name, 'sqlite_' ) === 0 ) {
00338                         return $name;
00339                 }
00340                 return str_replace( '"', '', parent::tableName( $name, $format ) );
00341         }
00342 
00350         function indexName( $index ) {
00351                 return $index;
00352         }
00353 
00359         function insertId() {
00360                 // PDO::lastInsertId yields a string :(
00361                 return intval( $this->mConn->lastInsertId() );
00362         }
00363 
00368         function dataSeek( $res, $row ) {
00369                 if ( $res instanceof ResultWrapper ) {
00370                         $r =& $res->result;
00371                 } else {
00372                         $r =& $res;
00373                 }
00374                 reset( $r );
00375                 if ( $row > 0 ) {
00376                         for ( $i = 0; $i < $row; $i++ ) {
00377                                 next( $r );
00378                         }
00379                 }
00380         }
00381 
00385         function lastError() {
00386                 if ( !is_object( $this->mConn ) ) {
00387                         return "Cannot return last error, no db connection";
00388                 }
00389                 $e = $this->mConn->errorInfo();
00390                 return isset( $e[2] ) ? $e[2] : '';
00391         }
00392 
00396         function lastErrno() {
00397                 if ( !is_object( $this->mConn ) ) {
00398                         return "Cannot return last error, no db connection";
00399                 } else {
00400                         $info = $this->mConn->errorInfo();
00401                         return $info[1];
00402                 }
00403         }
00404 
00408         function affectedRows() {
00409                 return $this->mAffectedRows;
00410         }
00411 
00419         function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) {
00420                 $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
00421                 $res = $this->query( $sql, $fname );
00422                 if ( !$res ) {
00423                         return null;
00424                 }
00425                 if ( $res->numRows() == 0 ) {
00426                         return false;
00427                 }
00428                 $info = array();
00429                 foreach ( $res as $row ) {
00430                         $info[] = $row->name;
00431                 }
00432                 return $info;
00433         }
00434 
00441         function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) {
00442                 $row = $this->selectRow( 'sqlite_master', '*',
00443                         array(
00444                                 'type' => 'index',
00445                                 'name' => $this->indexName( $index ),
00446                         ), $fname );
00447                 if ( !$row || !isset( $row->sql ) ) {
00448                         return null;
00449                 }
00450 
00451                 // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
00452                 $indexPos = strpos( $row->sql, 'INDEX' );
00453                 if ( $indexPos === false ) {
00454                         return null;
00455                 }
00456                 $firstPart = substr( $row->sql, 0, $indexPos );
00457                 $options = explode( ' ', $firstPart );
00458                 return in_array( 'UNIQUE', $options );
00459         }
00460 
00468         function makeSelectOptions( $options ) {
00469                 foreach ( $options as $k => $v ) {
00470                         if ( is_numeric( $k ) && $v == 'FOR UPDATE' ) {
00471                                 $options[$k] = '';
00472                         }
00473                 }
00474                 return parent::makeSelectOptions( $options );
00475         }
00476 
00481         function makeUpdateOptions( $options ) {
00482                 $options = self::fixIgnore( $options );
00483                 return parent::makeUpdateOptions( $options );
00484         }
00485 
00490         static function fixIgnore( $options ) {
00491                 # SQLite uses OR IGNORE not just IGNORE
00492                 foreach ( $options as $k => $v ) {
00493                         if ( $v == 'IGNORE' ) {
00494                                 $options[$k] = 'OR IGNORE';
00495                         }
00496                 }
00497                 return $options;
00498         }
00499 
00504         function makeInsertOptions( $options ) {
00505                 $options = self::fixIgnore( $options );
00506                 return parent::makeInsertOptions( $options );
00507         }
00508 
00513         function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) {
00514                 if ( !count( $a ) ) {
00515                         return true;
00516                 }
00517 
00518                 # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
00519                 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
00520                         $ret = true;
00521                         foreach ( $a as $v ) {
00522                                 if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
00523                                         $ret = false;
00524                                 }
00525                         }
00526                 } else {
00527                         $ret = parent::insert( $table, $a, "$fname/single-row", $options );
00528                 }
00529 
00530                 return $ret;
00531         }
00532 
00540         function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) {
00541                 if ( !count( $rows ) ) return true;
00542 
00543                 # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
00544                 if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
00545                         $ret = true;
00546                         foreach ( $rows as $v ) {
00547                                 if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
00548                                         $ret = false;
00549                                 }
00550                         }
00551                 } else {
00552                         $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
00553                 }
00554 
00555                 return $ret;
00556         }
00557 
00564         function textFieldSize( $table, $field ) {
00565                 return -1;
00566         }
00567 
00571         function unionSupportsOrderAndLimit() {
00572                 return false;
00573         }
00574 
00580         function unionQueries( $sqls, $all ) {
00581                 $glue = $all ? ' UNION ALL ' : ' UNION ';
00582                 return implode( $glue, $sqls );
00583         }
00584 
00588         function wasDeadlock() {
00589                 return $this->lastErrno() == 5; // SQLITE_BUSY
00590         }
00591 
00595         function wasErrorReissuable() {
00596                 return $this->lastErrno() ==  17; // SQLITE_SCHEMA;
00597         }
00598 
00602         function wasReadOnlyError() {
00603                 return $this->lastErrno() == 8; // SQLITE_READONLY;
00604         }
00605 
00609         public static function getSoftwareLink() {
00610                 return "[http://sqlite.org/ SQLite]";
00611         }
00612 
00616         function getServerVersion() {
00617                 $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
00618                 return $ver;
00619         }
00620 
00624         public function getServerInfo() {
00625                 return wfMessage( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() )->text();
00626         }
00627 
00636         function fieldInfo( $table, $field ) {
00637                 $tableName = $this->tableName( $table );
00638                 $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
00639                 $res = $this->query( $sql, __METHOD__ );
00640                 foreach ( $res as $row ) {
00641                         if ( $row->name == $field ) {
00642                                 return new SQLiteField( $row, $tableName );
00643                         }
00644                 }
00645                 return false;
00646         }
00647 
00648         protected function doBegin( $fname = '' ) {
00649                 if ( $this->mTrxLevel == 1 ) {
00650                         $this->commit( __METHOD__ );
00651                 }
00652                 $this->mConn->beginTransaction();
00653                 $this->mTrxLevel = 1;
00654         }
00655 
00656         protected function doCommit( $fname = '' ) {
00657                 if ( $this->mTrxLevel == 0 ) {
00658                         return;
00659                 }
00660                 $this->mConn->commit();
00661                 $this->mTrxLevel = 0;
00662         }
00663 
00664         protected function doRollback( $fname = '' ) {
00665                 if ( $this->mTrxLevel == 0 ) {
00666                         return;
00667                 }
00668                 $this->mConn->rollBack();
00669                 $this->mTrxLevel = 0;
00670         }
00671 
00676         function strencode( $s ) {
00677                 return substr( $this->addQuotes( $s ), 1, - 1 );
00678         }
00679 
00684         function encodeBlob( $b ) {
00685                 return new Blob( $b );
00686         }
00687 
00692         function decodeBlob( $b ) {
00693                 if ( $b instanceof Blob ) {
00694                         $b = $b->fetch();
00695                 }
00696                 return $b;
00697         }
00698 
00703         function addQuotes( $s ) {
00704                 if ( $s instanceof Blob ) {
00705                         return "x'" . bin2hex( $s->fetch() ) . "'";
00706                 } else {
00707                         return $this->mConn->quote( $s );
00708                 }
00709         }
00710 
00714         function buildLike() {
00715                 $params = func_get_args();
00716                 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
00717                         $params = $params[0];
00718                 }
00719                 return parent::buildLike( $params ) . "ESCAPE '\' ";
00720         }
00721 
00725         public function getSearchEngine() {
00726                 return "SearchSqlite";
00727         }
00728 
00733         public function deadlockLoop( /*...*/ ) {
00734                 $args = func_get_args();
00735                 $function = array_shift( $args );
00736                 return call_user_func_array( $function, $args );
00737         }
00738 
00743         protected function replaceVars( $s ) {
00744                 $s = parent::replaceVars( $s );
00745                 if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
00746                         // CREATE TABLE hacks to allow schema file sharing with MySQL
00747 
00748                         // binary/varbinary column type -> blob
00749                         $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
00750                         // no such thing as unsigned
00751                         $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
00752                         // INT -> INTEGER
00753                         $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
00754                         // floating point types -> REAL
00755                         $s = preg_replace( '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i', 'REAL', $s );
00756                         // varchar -> TEXT
00757                         $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
00758                         // TEXT normalization
00759                         $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
00760                         // BLOB normalization
00761                         $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
00762                         // BOOL -> INTEGER
00763                         $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
00764                         // DATETIME -> TEXT
00765                         $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
00766                         // No ENUM type
00767                         $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
00768                         // binary collation type -> nothing
00769                         $s = preg_replace( '/\bbinary\b/i', '', $s );
00770                         // auto_increment -> autoincrement
00771                         $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
00772                         // No explicit options
00773                         $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
00774                         // AUTOINCREMENT should immedidately follow PRIMARY KEY
00775                         $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
00776                 } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
00777                         // No truncated indexes
00778                         $s = preg_replace( '/\(\d+\)/', '', $s );
00779                         // No FULLTEXT
00780                         $s = preg_replace( '/\bfulltext\b/i', '', $s );
00781                 }
00782                 return $s;
00783         }
00784 
00792         function buildConcat( $stringList ) {
00793                 return '(' . implode( ') || (', $stringList ) . ')';
00794         }
00795 
00804         function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) {
00805                 $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . $this->addQuotes( $oldName ) . " AND type='table'", $fname );
00806                 $obj = $this->fetchObject( $res );
00807                 if ( !$obj ) {
00808                         throw new MWException( "Couldn't retrieve structure for table $oldName" );
00809                 }
00810                 $sql = $obj->sql;
00811                 $sql = preg_replace( '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/', $this->addIdentifierQuotes( $newName ), $sql, 1 );
00812                 if ( $temporary ) {
00813                         if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
00814                                 wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" );
00815                         } else {
00816                                 $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
00817                         }
00818                 }
00819                 return $this->query( $sql, $fname );
00820         }
00821 
00822 
00831         function listTables( $prefix = null, $fname = 'DatabaseSqlite::listTables' ) {
00832                 $result = $this->select(
00833                         'sqlite_master',
00834                         'name',
00835                         "type='table'"
00836                 );
00837 
00838                 $endArray = array();
00839 
00840                 foreach( $result as $table ) {
00841                         $vars = get_object_vars($table);
00842                         $table = array_pop( $vars );
00843 
00844                         if( !$prefix || strpos( $table, $prefix ) === 0 ) {
00845                                 if ( strpos( $table, 'sqlite_' ) !== 0 ) {
00846                                         $endArray[] = $table;
00847                                 }
00848 
00849                         }
00850                 }
00851 
00852                 return $endArray;
00853         }
00854 
00855 } // end DatabaseSqlite class
00856 
00861 class DatabaseSqliteStandalone extends DatabaseSqlite {
00862         public function __construct( $fileName, $flags = 0 ) {
00863                 $this->mFlags = $flags;
00864                 $this->tablePrefix( null );
00865                 $this->openFile( $fileName );
00866         }
00867 }
00868 
00872 class SQLiteField implements Field {
00873         private $info, $tableName;
00874         function __construct( $info, $tableName ) {
00875                 $this->info = $info;
00876                 $this->tableName = $tableName;
00877         }
00878 
00879         function name() {
00880                 return $this->info->name;
00881         }
00882 
00883         function tableName() {
00884                 return $this->tableName;
00885         }
00886 
00887         function defaultValue() {
00888                 if ( is_string( $this->info->dflt_value ) ) {
00889                         // Typically quoted
00890                         if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) {
00891                                 return str_replace( "''", "'", $this->info->dflt_value );
00892                         }
00893                 }
00894                 return $this->info->dflt_value;
00895         }
00896 
00900         function isNullable() {
00901                 return !$this->info->notnull;
00902         }
00903 
00904         function type() {
00905                 return $this->info->type;
00906         }
00907 
00908 } // end SQLiteField