MediaWiki  master
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 
00090         function open( $server, $user, $pass, $dbName ) {
00091                 global $wgSQLiteDataDir;
00092 
00093                 $fileName = self::generateFileName( $wgSQLiteDataDir, $dbName );
00094                 if ( !is_readable( $fileName ) ) {
00095                         $this->mConn = false;
00096                         throw new DBConnectionError( $this, "SQLite database not accessible" );
00097                 }
00098                 $this->openFile( $fileName );
00099                 return $this->mConn;
00100         }
00101 
00110         function openFile( $fileName ) {
00111                 $this->mDatabaseFile = $fileName;
00112                 try {
00113                         if ( $this->mFlags & DBO_PERSISTENT ) {
00114                                 $this->mConn = new PDO( "sqlite:$fileName", '', '',
00115                                         array( PDO::ATTR_PERSISTENT => true ) );
00116                         } else {
00117                                 $this->mConn = new PDO( "sqlite:$fileName", '', '' );
00118                         }
00119                 } catch ( PDOException $e ) {
00120                         $err = $e->getMessage();
00121                 }
00122                 if ( !$this->mConn ) {
00123                         wfDebug( "DB connection error: $err\n" );
00124                         throw new DBConnectionError( $this, $err );
00125                 }
00126                 $this->mOpened = !!$this->mConn;
00127                 # set error codes only, don't raise exceptions
00128                 if ( $this->mOpened ) {
00129                         $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
00130                         return true;
00131                 }
00132         }
00133 
00138         protected function closeConnection() {
00139                 $this->mConn = null;
00140                 return true;
00141         }
00142 
00149         public static function generateFileName( $dir, $dbName ) {
00150                 return "$dir/$dbName.sqlite";
00151         }
00152 
00157         function checkForEnabledSearch() {
00158                 if ( self::$fulltextEnabled === null ) {
00159                         self::$fulltextEnabled = false;
00160                         $table = $this->tableName( 'searchindex' );
00161                         $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
00162                         if ( $res ) {
00163                                 $row = $res->fetchRow();
00164                                 self::$fulltextEnabled = stristr($row['sql'], 'fts' ) !== false;
00165                         }
00166                 }
00167                 return self::$fulltextEnabled;
00168         }
00169 
00174         static function getFulltextSearchModule() {
00175                 static $cachedResult = null;
00176                 if ( $cachedResult !== null ) {
00177                         return $cachedResult;
00178                 }
00179                 $cachedResult = false;
00180                 $table = 'dummy_search_test';
00181 
00182                 $db = new DatabaseSqliteStandalone( ':memory:' );
00183 
00184                 if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
00185                         $cachedResult = 'FTS3';
00186                 }
00187                 $db->close();
00188                 return $cachedResult;
00189         }
00190 
00201         function attachDatabase( $name, $file = false, $fname = 'DatabaseSqlite::attachDatabase' ) {
00202                 global $wgSQLiteDataDir;
00203                 if ( !$file ) {
00204                         $file = self::generateFileName( $wgSQLiteDataDir, $name );
00205                 }
00206                 $file = $this->addQuotes( $file );
00207                 return $this->query( "ATTACH DATABASE $file AS $name", $fname );
00208         }
00209 
00217         function isWriteQuery( $sql ) {
00218                 return parent::isWriteQuery( $sql ) && !preg_match( '/^ATTACH\b/i', $sql );
00219         }
00220 
00228         protected function doQuery( $sql ) {
00229                 $res = $this->mConn->query( $sql );
00230                 if ( $res === false ) {
00231                         return false;
00232                 } else {
00233                         $r = $res instanceof ResultWrapper ? $res->result : $res;
00234                         $this->mAffectedRows = $r->rowCount();
00235                         $res = new ResultWrapper( $this, $r->fetchAll() );
00236                 }
00237                 return $res;
00238         }
00239 
00243         function freeResult( $res ) {
00244                 if ( $res instanceof ResultWrapper ) {
00245                         $res->result = null;
00246                 } else {
00247                         $res = null;
00248                 }
00249         }
00250 
00255         function fetchObject( $res ) {
00256                 if ( $res instanceof ResultWrapper ) {
00257                         $r =& $res->result;
00258                 } else {
00259                         $r =& $res;
00260                 }
00261 
00262                 $cur = current( $r );
00263                 if ( is_array( $cur ) ) {
00264                         next( $r );
00265                         $obj = new stdClass;
00266                         foreach ( $cur as $k => $v ) {
00267                                 if ( !is_numeric( $k ) ) {
00268                                         $obj->$k = $v;
00269                                 }
00270                         }
00271 
00272                         return $obj;
00273                 }
00274                 return false;
00275         }
00276 
00281         function fetchRow( $res ) {
00282                 if ( $res instanceof ResultWrapper ) {
00283                         $r =& $res->result;
00284                 } else {
00285                         $r =& $res;
00286                 }
00287                 $cur = current( $r );
00288                 if ( is_array( $cur ) ) {
00289                         next( $r );
00290                         return $cur;
00291                 }
00292                 return false;
00293         }
00294 
00302         function numRows( $res ) {
00303                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00304                 return count( $r );
00305         }
00306 
00311         function numFields( $res ) {
00312                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00313                 return is_array( $r ) ? count( $r[0] ) : 0;
00314         }
00315 
00321         function fieldName( $res, $n ) {
00322                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00323                 if ( is_array( $r ) ) {
00324                         $keys = array_keys( $r[0] );
00325                         return $keys[$n];
00326                 }
00327                 return false;
00328         }
00329 
00337         function tableName( $name, $format = 'quoted' ) {
00338                 // table names starting with sqlite_ are reserved
00339                 if ( strpos( $name, 'sqlite_' ) === 0 ) {
00340                         return $name;
00341                 }
00342                 return str_replace( '"', '', parent::tableName( $name, $format ) );
00343         }
00344 
00352         function indexName( $index ) {
00353                 return $index;
00354         }
00355 
00361         function insertId() {
00362                 // PDO::lastInsertId yields a string :(
00363                 return intval( $this->mConn->lastInsertId() );
00364         }
00365 
00370         function dataSeek( $res, $row ) {
00371                 if ( $res instanceof ResultWrapper ) {
00372                         $r =& $res->result;
00373                 } else {
00374                         $r =& $res;
00375                 }
00376                 reset( $r );
00377                 if ( $row > 0 ) {
00378                         for ( $i = 0; $i < $row; $i++ ) {
00379                                 next( $r );
00380                         }
00381                 }
00382         }
00383 
00387         function lastError() {
00388                 if ( !is_object( $this->mConn ) ) {
00389                         return "Cannot return last error, no db connection";
00390                 }
00391                 $e = $this->mConn->errorInfo();
00392                 return isset( $e[2] ) ? $e[2] : '';
00393         }
00394 
00398         function lastErrno() {
00399                 if ( !is_object( $this->mConn ) ) {
00400                         return "Cannot return last error, no db connection";
00401                 } else {
00402                         $info = $this->mConn->errorInfo();
00403                         return $info[1];
00404                 }
00405         }
00406 
00410         function affectedRows() {
00411                 return $this->mAffectedRows;
00412         }
00413 
00421         function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) {
00422                 $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
00423                 $res = $this->query( $sql, $fname );
00424                 if ( !$res ) {
00425                         return null;
00426                 }
00427                 if ( $res->numRows() == 0 ) {
00428                         return false;
00429                 }
00430                 $info = array();
00431                 foreach ( $res as $row ) {
00432                         $info[] = $row->name;
00433                 }
00434                 return $info;
00435         }
00436 
00443         function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) {
00444                 $row = $this->selectRow( 'sqlite_master', '*',
00445                         array(
00446                                 'type' => 'index',
00447                                 'name' => $this->indexName( $index ),
00448                         ), $fname );
00449                 if ( !$row || !isset( $row->sql ) ) {
00450                         return null;
00451                 }
00452 
00453                 // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
00454                 $indexPos = strpos( $row->sql, 'INDEX' );
00455                 if ( $indexPos === false ) {
00456                         return null;
00457                 }
00458                 $firstPart = substr( $row->sql, 0, $indexPos );
00459                 $options = explode( ' ', $firstPart );
00460                 return in_array( 'UNIQUE', $options );
00461         }
00462 
00470         function makeSelectOptions( $options ) {
00471                 foreach ( $options as $k => $v ) {
00472                         if ( is_numeric( $k ) && $v == 'FOR UPDATE' ) {
00473                                 $options[$k] = '';
00474                         }
00475                 }
00476                 return parent::makeSelectOptions( $options );
00477         }
00478 
00483         function makeUpdateOptions( $options ) {
00484                 $options = self::fixIgnore( $options );
00485                 return parent::makeUpdateOptions( $options );
00486         }
00487 
00492         static function fixIgnore( $options ) {
00493                 # SQLite uses OR IGNORE not just IGNORE
00494                 foreach ( $options as $k => $v ) {
00495                         if ( $v == 'IGNORE' ) {
00496                                 $options[$k] = 'OR IGNORE';
00497                         }
00498                 }
00499                 return $options;
00500         }
00501 
00506         function makeInsertOptions( $options ) {
00507                 $options = self::fixIgnore( $options );
00508                 return parent::makeInsertOptions( $options );
00509         }
00510 
00515         function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) {
00516                 if ( !count( $a ) ) {
00517                         return true;
00518                 }
00519 
00520                 # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
00521                 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
00522                         $ret = true;
00523                         foreach ( $a as $v ) {
00524                                 if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
00525                                         $ret = false;
00526                                 }
00527                         }
00528                 } else {
00529                         $ret = parent::insert( $table, $a, "$fname/single-row", $options );
00530                 }
00531 
00532                 return $ret;
00533         }
00534 
00542         function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) {
00543                 if ( !count( $rows ) ) return true;
00544 
00545                 # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
00546                 if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
00547                         $ret = true;
00548                         foreach ( $rows as $v ) {
00549                                 if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
00550                                         $ret = false;
00551                                 }
00552                         }
00553                 } else {
00554                         $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
00555                 }
00556 
00557                 return $ret;
00558         }
00559 
00566         function textFieldSize( $table, $field ) {
00567                 return -1;
00568         }
00569 
00573         function unionSupportsOrderAndLimit() {
00574                 return false;
00575         }
00576 
00582         function unionQueries( $sqls, $all ) {
00583                 $glue = $all ? ' UNION ALL ' : ' UNION ';
00584                 return implode( $glue, $sqls );
00585         }
00586 
00590         function wasDeadlock() {
00591                 return $this->lastErrno() == 5; // SQLITE_BUSY
00592         }
00593 
00597         function wasErrorReissuable() {
00598                 return $this->lastErrno() ==  17; // SQLITE_SCHEMA;
00599         }
00600 
00604         function wasReadOnlyError() {
00605                 return $this->lastErrno() == 8; // SQLITE_READONLY;
00606         }
00607 
00611         public static function getSoftwareLink() {
00612                 return "[http://sqlite.org/ SQLite]";
00613         }
00614 
00618         function getServerVersion() {
00619                 $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
00620                 return $ver;
00621         }
00622 
00626         public function getServerInfo() {
00627                 return wfMessage( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() )->text();
00628         }
00629 
00638         function fieldInfo( $table, $field ) {
00639                 $tableName = $this->tableName( $table );
00640                 $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
00641                 $res = $this->query( $sql, __METHOD__ );
00642                 foreach ( $res as $row ) {
00643                         if ( $row->name == $field ) {
00644                                 return new SQLiteField( $row, $tableName );
00645                         }
00646                 }
00647                 return false;
00648         }
00649 
00650         protected function doBegin( $fname = '' ) {
00651                 if ( $this->mTrxLevel == 1 ) {
00652                         $this->commit( __METHOD__ );
00653                 }
00654                 $this->mConn->beginTransaction();
00655                 $this->mTrxLevel = 1;
00656         }
00657 
00658         protected function doCommit( $fname = '' ) {
00659                 if ( $this->mTrxLevel == 0 ) {
00660                         return;
00661                 }
00662                 $this->mConn->commit();
00663                 $this->mTrxLevel = 0;
00664         }
00665 
00666         protected function doRollback( $fname = '' ) {
00667                 if ( $this->mTrxLevel == 0 ) {
00668                         return;
00669                 }
00670                 $this->mConn->rollBack();
00671                 $this->mTrxLevel = 0;
00672         }
00673 
00678         function strencode( $s ) {
00679                 return substr( $this->addQuotes( $s ), 1, - 1 );
00680         }
00681 
00686         function encodeBlob( $b ) {
00687                 return new Blob( $b );
00688         }
00689 
00694         function decodeBlob( $b ) {
00695                 if ( $b instanceof Blob ) {
00696                         $b = $b->fetch();
00697                 }
00698                 return $b;
00699         }
00700 
00705         function addQuotes( $s ) {
00706                 if ( $s instanceof Blob ) {
00707                         return "x'" . bin2hex( $s->fetch() ) . "'";
00708                 } else if ( strpos( $s, "\0" ) !== false ) {
00709                         // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
00710                         // This is a known limitation of SQLite's mprintf function which PDO should work around,
00711                         // but doesn't. I have reported this to php.net as bug #63419:
00712                         // https://bugs.php.net/bug.php?id=63419
00713                         // There was already a similar report for SQLite3::escapeString, bug #62361:
00714                         // https://bugs.php.net/bug.php?id=62361
00715                         return "x'" . bin2hex( $s ) . "'";
00716                 } else {
00717                         return $this->mConn->quote( $s );
00718                 }
00719         }
00720 
00724         function buildLike() {
00725                 $params = func_get_args();
00726                 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
00727                         $params = $params[0];
00728                 }
00729                 return parent::buildLike( $params ) . "ESCAPE '\' ";
00730         }
00731 
00735         public function getSearchEngine() {
00736                 return "SearchSqlite";
00737         }
00738 
00743         public function deadlockLoop( /*...*/ ) {
00744                 $args = func_get_args();
00745                 $function = array_shift( $args );
00746                 return call_user_func_array( $function, $args );
00747         }
00748 
00753         protected function replaceVars( $s ) {
00754                 $s = parent::replaceVars( $s );
00755                 if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
00756                         // CREATE TABLE hacks to allow schema file sharing with MySQL
00757 
00758                         // binary/varbinary column type -> blob
00759                         $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
00760                         // no such thing as unsigned
00761                         $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
00762                         // INT -> INTEGER
00763                         $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
00764                         // floating point types -> REAL
00765                         $s = preg_replace( '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i', 'REAL', $s );
00766                         // varchar -> TEXT
00767                         $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
00768                         // TEXT normalization
00769                         $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
00770                         // BLOB normalization
00771                         $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
00772                         // BOOL -> INTEGER
00773                         $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
00774                         // DATETIME -> TEXT
00775                         $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
00776                         // No ENUM type
00777                         $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
00778                         // binary collation type -> nothing
00779                         $s = preg_replace( '/\bbinary\b/i', '', $s );
00780                         // auto_increment -> autoincrement
00781                         $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
00782                         // No explicit options
00783                         $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
00784                         // AUTOINCREMENT should immedidately follow PRIMARY KEY
00785                         $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
00786                 } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
00787                         // No truncated indexes
00788                         $s = preg_replace( '/\(\d+\)/', '', $s );
00789                         // No FULLTEXT
00790                         $s = preg_replace( '/\bfulltext\b/i', '', $s );
00791                 }
00792                 return $s;
00793         }
00794 
00802         function buildConcat( $stringList ) {
00803                 return '(' . implode( ') || (', $stringList ) . ')';
00804         }
00805 
00814         function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) {
00815                 $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . $this->addQuotes( $oldName ) . " AND type='table'", $fname );
00816                 $obj = $this->fetchObject( $res );
00817                 if ( !$obj ) {
00818                         throw new MWException( "Couldn't retrieve structure for table $oldName" );
00819                 }
00820                 $sql = $obj->sql;
00821                 $sql = preg_replace( '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/', $this->addIdentifierQuotes( $newName ), $sql, 1 );
00822                 if ( $temporary ) {
00823                         if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
00824                                 wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" );
00825                         } else {
00826                                 $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
00827                         }
00828                 }
00829                 return $this->query( $sql, $fname );
00830         }
00831 
00832 
00841         function listTables( $prefix = null, $fname = 'DatabaseSqlite::listTables' ) {
00842                 $result = $this->select(
00843                         'sqlite_master',
00844                         'name',
00845                         "type='table'"
00846                 );
00847 
00848                 $endArray = array();
00849 
00850                 foreach( $result as $table ) {
00851                         $vars = get_object_vars($table);
00852                         $table = array_pop( $vars );
00853 
00854                         if( !$prefix || strpos( $table, $prefix ) === 0 ) {
00855                                 if ( strpos( $table, 'sqlite_' ) !== 0 ) {
00856                                         $endArray[] = $table;
00857                                 }
00858 
00859                         }
00860                 }
00861 
00862                 return $endArray;
00863         }
00864 
00865 } // end DatabaseSqlite class
00866 
00871 class DatabaseSqliteStandalone extends DatabaseSqlite {
00872         public function __construct( $fileName, $flags = 0 ) {
00873                 $this->mFlags = $flags;
00874                 $this->tablePrefix( null );
00875                 $this->openFile( $fileName );
00876         }
00877 }
00878 
00882 class SQLiteField implements Field {
00883         private $info, $tableName;
00884         function __construct( $info, $tableName ) {
00885                 $this->info = $info;
00886                 $this->tableName = $tableName;
00887         }
00888 
00889         function name() {
00890                 return $this->info->name;
00891         }
00892 
00893         function tableName() {
00894                 return $this->tableName;
00895         }
00896 
00897         function defaultValue() {
00898                 if ( is_string( $this->info->dflt_value ) ) {
00899                         // Typically quoted
00900                         if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) {
00901                                 return str_replace( "''", "'", $this->info->dflt_value );
00902                         }
00903                 }
00904                 return $this->info->dflt_value;
00905         }
00906 
00910         function isNullable() {
00911                 return !$this->info->notnull;
00912         }
00913 
00914         function type() {
00915                 return $this->info->type;
00916         }
00917 
00918 } // end SQLiteField