MediaWiki  REL1_21
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                         # Enforce LIKE to be case sensitive, just like MySQL
00131                         $this->query( 'PRAGMA case_sensitive_like = 1' );
00132                         return true;
00133                 }
00134         }
00135 
00140         protected function closeConnection() {
00141                 $this->mConn = null;
00142                 return true;
00143         }
00144 
00151         public static function generateFileName( $dir, $dbName ) {
00152                 return "$dir/$dbName.sqlite";
00153         }
00154 
00159         function checkForEnabledSearch() {
00160                 if ( self::$fulltextEnabled === null ) {
00161                         self::$fulltextEnabled = false;
00162                         $table = $this->tableName( 'searchindex' );
00163                         $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
00164                         if ( $res ) {
00165                                 $row = $res->fetchRow();
00166                                 self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
00167                         }
00168                 }
00169                 return self::$fulltextEnabled;
00170         }
00171 
00176         static function getFulltextSearchModule() {
00177                 static $cachedResult = null;
00178                 if ( $cachedResult !== null ) {
00179                         return $cachedResult;
00180                 }
00181                 $cachedResult = false;
00182                 $table = 'dummy_search_test';
00183 
00184                 $db = new DatabaseSqliteStandalone( ':memory:' );
00185 
00186                 if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
00187                         $cachedResult = 'FTS3';
00188                 }
00189                 $db->close();
00190                 return $cachedResult;
00191         }
00192 
00203         function attachDatabase( $name, $file = false, $fname = 'DatabaseSqlite::attachDatabase' ) {
00204                 global $wgSQLiteDataDir;
00205                 if ( !$file ) {
00206                         $file = self::generateFileName( $wgSQLiteDataDir, $name );
00207                 }
00208                 $file = $this->addQuotes( $file );
00209                 return $this->query( "ATTACH DATABASE $file AS $name", $fname );
00210         }
00211 
00219         function isWriteQuery( $sql ) {
00220                 return parent::isWriteQuery( $sql ) && !preg_match( '/^ATTACH\b/i', $sql );
00221         }
00222 
00230         protected function doQuery( $sql ) {
00231                 $res = $this->mConn->query( $sql );
00232                 if ( $res === false ) {
00233                         return false;
00234                 } else {
00235                         $r = $res instanceof ResultWrapper ? $res->result : $res;
00236                         $this->mAffectedRows = $r->rowCount();
00237                         $res = new ResultWrapper( $this, $r->fetchAll() );
00238                 }
00239                 return $res;
00240         }
00241 
00245         function freeResult( $res ) {
00246                 if ( $res instanceof ResultWrapper ) {
00247                         $res->result = null;
00248                 } else {
00249                         $res = null;
00250                 }
00251         }
00252 
00257         function fetchObject( $res ) {
00258                 if ( $res instanceof ResultWrapper ) {
00259                         $r =& $res->result;
00260                 } else {
00261                         $r =& $res;
00262                 }
00263 
00264                 $cur = current( $r );
00265                 if ( is_array( $cur ) ) {
00266                         next( $r );
00267                         $obj = new stdClass;
00268                         foreach ( $cur as $k => $v ) {
00269                                 if ( !is_numeric( $k ) ) {
00270                                         $obj->$k = $v;
00271                                 }
00272                         }
00273 
00274                         return $obj;
00275                 }
00276                 return false;
00277         }
00278 
00283         function fetchRow( $res ) {
00284                 if ( $res instanceof ResultWrapper ) {
00285                         $r =& $res->result;
00286                 } else {
00287                         $r =& $res;
00288                 }
00289                 $cur = current( $r );
00290                 if ( is_array( $cur ) ) {
00291                         next( $r );
00292                         return $cur;
00293                 }
00294                 return false;
00295         }
00296 
00304         function numRows( $res ) {
00305                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00306                 return count( $r );
00307         }
00308 
00313         function numFields( $res ) {
00314                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00315                 return is_array( $r ) ? count( $r[0] ) : 0;
00316         }
00317 
00323         function fieldName( $res, $n ) {
00324                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00325                 if ( is_array( $r ) ) {
00326                         $keys = array_keys( $r[0] );
00327                         return $keys[$n];
00328                 }
00329                 return false;
00330         }
00331 
00339         function tableName( $name, $format = 'quoted' ) {
00340                 // table names starting with sqlite_ are reserved
00341                 if ( strpos( $name, 'sqlite_' ) === 0 ) {
00342                         return $name;
00343                 }
00344                 return str_replace( '"', '', parent::tableName( $name, $format ) );
00345         }
00346 
00354         function indexName( $index ) {
00355                 return $index;
00356         }
00357 
00363         function insertId() {
00364                 // PDO::lastInsertId yields a string :(
00365                 return intval( $this->mConn->lastInsertId() );
00366         }
00367 
00372         function dataSeek( $res, $row ) {
00373                 if ( $res instanceof ResultWrapper ) {
00374                         $r =& $res->result;
00375                 } else {
00376                         $r =& $res;
00377                 }
00378                 reset( $r );
00379                 if ( $row > 0 ) {
00380                         for ( $i = 0; $i < $row; $i++ ) {
00381                                 next( $r );
00382                         }
00383                 }
00384         }
00385 
00389         function lastError() {
00390                 if ( !is_object( $this->mConn ) ) {
00391                         return "Cannot return last error, no db connection";
00392                 }
00393                 $e = $this->mConn->errorInfo();
00394                 return isset( $e[2] ) ? $e[2] : '';
00395         }
00396 
00400         function lastErrno() {
00401                 if ( !is_object( $this->mConn ) ) {
00402                         return "Cannot return last error, no db connection";
00403                 } else {
00404                         $info = $this->mConn->errorInfo();
00405                         return $info[1];
00406                 }
00407         }
00408 
00412         function affectedRows() {
00413                 return $this->mAffectedRows;
00414         }
00415 
00423         function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) {
00424                 $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
00425                 $res = $this->query( $sql, $fname );
00426                 if ( !$res ) {
00427                         return null;
00428                 }
00429                 if ( $res->numRows() == 0 ) {
00430                         return false;
00431                 }
00432                 $info = array();
00433                 foreach ( $res as $row ) {
00434                         $info[] = $row->name;
00435                 }
00436                 return $info;
00437         }
00438 
00445         function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) {
00446                 $row = $this->selectRow( 'sqlite_master', '*',
00447                         array(
00448                                 'type' => 'index',
00449                                 'name' => $this->indexName( $index ),
00450                         ), $fname );
00451                 if ( !$row || !isset( $row->sql ) ) {
00452                         return null;
00453                 }
00454 
00455                 // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
00456                 $indexPos = strpos( $row->sql, 'INDEX' );
00457                 if ( $indexPos === false ) {
00458                         return null;
00459                 }
00460                 $firstPart = substr( $row->sql, 0, $indexPos );
00461                 $options = explode( ' ', $firstPart );
00462                 return in_array( 'UNIQUE', $options );
00463         }
00464 
00472         function makeSelectOptions( $options ) {
00473                 foreach ( $options as $k => $v ) {
00474                         if ( is_numeric( $k ) && ($v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE') ) {
00475                                 $options[$k] = '';
00476                         }
00477                 }
00478                 return parent::makeSelectOptions( $options );
00479         }
00480 
00485         function makeUpdateOptions( $options ) {
00486                 $options = self::fixIgnore( $options );
00487                 return parent::makeUpdateOptions( $options );
00488         }
00489 
00494         static function fixIgnore( $options ) {
00495                 # SQLite uses OR IGNORE not just IGNORE
00496                 foreach ( $options as $k => $v ) {
00497                         if ( $v == 'IGNORE' ) {
00498                                 $options[$k] = 'OR IGNORE';
00499                         }
00500                 }
00501                 return $options;
00502         }
00503 
00508         function makeInsertOptions( $options ) {
00509                 $options = self::fixIgnore( $options );
00510                 return parent::makeInsertOptions( $options );
00511         }
00512 
00517         function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) {
00518                 if ( !count( $a ) ) {
00519                         return true;
00520                 }
00521 
00522                 # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
00523                 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
00524                         $ret = true;
00525                         foreach ( $a as $v ) {
00526                                 if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
00527                                         $ret = false;
00528                                 }
00529                         }
00530                 } else {
00531                         $ret = parent::insert( $table, $a, "$fname/single-row", $options );
00532                 }
00533 
00534                 return $ret;
00535         }
00536 
00544         function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) {
00545                 if ( !count( $rows ) ) return true;
00546 
00547                 # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
00548                 if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
00549                         $ret = true;
00550                         foreach ( $rows as $v ) {
00551                                 if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
00552                                         $ret = false;
00553                                 }
00554                         }
00555                 } else {
00556                         $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
00557                 }
00558 
00559                 return $ret;
00560         }
00561 
00568         function textFieldSize( $table, $field ) {
00569                 return -1;
00570         }
00571 
00575         function unionSupportsOrderAndLimit() {
00576                 return false;
00577         }
00578 
00584         function unionQueries( $sqls, $all ) {
00585                 $glue = $all ? ' UNION ALL ' : ' UNION ';
00586                 return implode( $glue, $sqls );
00587         }
00588 
00592         function wasDeadlock() {
00593                 return $this->lastErrno() == 5; // SQLITE_BUSY
00594         }
00595 
00599         function wasErrorReissuable() {
00600                 return $this->lastErrno() == 17; // SQLITE_SCHEMA;
00601         }
00602 
00606         function wasReadOnlyError() {
00607                 return $this->lastErrno() == 8; // SQLITE_READONLY;
00608         }
00609 
00613         public static function getSoftwareLink() {
00614                 return "[http://sqlite.org/ SQLite]";
00615         }
00616 
00620         function getServerVersion() {
00621                 $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
00622                 return $ver;
00623         }
00624 
00628         public function getServerInfo() {
00629                 return wfMessage( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() )->text();
00630         }
00631 
00640         function fieldInfo( $table, $field ) {
00641                 $tableName = $this->tableName( $table );
00642                 $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
00643                 $res = $this->query( $sql, __METHOD__ );
00644                 foreach ( $res as $row ) {
00645                         if ( $row->name == $field ) {
00646                                 return new SQLiteField( $row, $tableName );
00647                         }
00648                 }
00649                 return false;
00650         }
00651 
00652         protected function doBegin( $fname = '' ) {
00653                 if ( $this->mTrxLevel == 1 ) {
00654                         $this->commit( __METHOD__ );
00655                 }
00656                 $this->mConn->beginTransaction();
00657                 $this->mTrxLevel = 1;
00658         }
00659 
00660         protected function doCommit( $fname = '' ) {
00661                 if ( $this->mTrxLevel == 0 ) {
00662                         return;
00663                 }
00664                 $this->mConn->commit();
00665                 $this->mTrxLevel = 0;
00666         }
00667 
00668         protected function doRollback( $fname = '' ) {
00669                 if ( $this->mTrxLevel == 0 ) {
00670                         return;
00671                 }
00672                 $this->mConn->rollBack();
00673                 $this->mTrxLevel = 0;
00674         }
00675 
00680         function strencode( $s ) {
00681                 return substr( $this->addQuotes( $s ), 1, - 1 );
00682         }
00683 
00688         function encodeBlob( $b ) {
00689                 return new Blob( $b );
00690         }
00691 
00696         function decodeBlob( $b ) {
00697                 if ( $b instanceof Blob ) {
00698                         $b = $b->fetch();
00699                 }
00700                 return $b;
00701         }
00702 
00707         function addQuotes( $s ) {
00708                 if ( $s instanceof Blob ) {
00709                         return "x'" . bin2hex( $s->fetch() ) . "'";
00710                 } else if ( strpos( $s, "\0" ) !== false ) {
00711                         // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
00712                         // This is a known limitation of SQLite's mprintf function which PDO should work around,
00713                         // but doesn't. I have reported this to php.net as bug #63419:
00714                         // https://bugs.php.net/bug.php?id=63419
00715                         // There was already a similar report for SQLite3::escapeString, bug #62361:
00716                         // https://bugs.php.net/bug.php?id=62361
00717                         return "x'" . bin2hex( $s ) . "'";
00718                 } else {
00719                         return $this->mConn->quote( $s );
00720                 }
00721         }
00722 
00726         function buildLike() {
00727                 $params = func_get_args();
00728                 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
00729                         $params = $params[0];
00730                 }
00731                 return parent::buildLike( $params ) . "ESCAPE '\' ";
00732         }
00733 
00737         public function getSearchEngine() {
00738                 return "SearchSqlite";
00739         }
00740 
00745         public function deadlockLoop( /*...*/ ) {
00746                 $args = func_get_args();
00747                 $function = array_shift( $args );
00748                 return call_user_func_array( $function, $args );
00749         }
00750 
00755         protected function replaceVars( $s ) {
00756                 $s = parent::replaceVars( $s );
00757                 if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
00758                         // CREATE TABLE hacks to allow schema file sharing with MySQL
00759 
00760                         // binary/varbinary column type -> blob
00761                         $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
00762                         // no such thing as unsigned
00763                         $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
00764                         // INT -> INTEGER
00765                         $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
00766                         // floating point types -> REAL
00767                         $s = preg_replace( '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i', 'REAL', $s );
00768                         // varchar -> TEXT
00769                         $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
00770                         // TEXT normalization
00771                         $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
00772                         // BLOB normalization
00773                         $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
00774                         // BOOL -> INTEGER
00775                         $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
00776                         // DATETIME -> TEXT
00777                         $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
00778                         // No ENUM type
00779                         $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
00780                         // binary collation type -> nothing
00781                         $s = preg_replace( '/\bbinary\b/i', '', $s );
00782                         // auto_increment -> autoincrement
00783                         $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
00784                         // No explicit options
00785                         $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
00786                         // AUTOINCREMENT should immedidately follow PRIMARY KEY
00787                         $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
00788                 } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
00789                         // No truncated indexes
00790                         $s = preg_replace( '/\(\d+\)/', '', $s );
00791                         // No FULLTEXT
00792                         $s = preg_replace( '/\bfulltext\b/i', '', $s );
00793                 }
00794                 return $s;
00795         }
00796 
00804         function buildConcat( $stringList ) {
00805                 return '(' . implode( ') || (', $stringList ) . ')';
00806         }
00807 
00816         function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) {
00817                 $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . $this->addQuotes( $oldName ) . " AND type='table'", $fname );
00818                 $obj = $this->fetchObject( $res );
00819                 if ( !$obj ) {
00820                         throw new MWException( "Couldn't retrieve structure for table $oldName" );
00821                 }
00822                 $sql = $obj->sql;
00823                 $sql = preg_replace( '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/', $this->addIdentifierQuotes( $newName ), $sql, 1 );
00824                 if ( $temporary ) {
00825                         if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
00826                                 wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" );
00827                         } else {
00828                                 $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
00829                         }
00830                 }
00831                 return $this->query( $sql, $fname );
00832         }
00833 
00842         function listTables( $prefix = null, $fname = 'DatabaseSqlite::listTables' ) {
00843                 $result = $this->select(
00844                         'sqlite_master',
00845                         'name',
00846                         "type='table'"
00847                 );
00848 
00849                 $endArray = array();
00850 
00851                 foreach( $result as $table ) {
00852                         $vars = get_object_vars( $table );
00853                         $table = array_pop( $vars );
00854 
00855                         if( !$prefix || strpos( $table, $prefix ) === 0 ) {
00856                                 if ( strpos( $table, 'sqlite_' ) !== 0 ) {
00857                                         $endArray[] = $table;
00858                                 }
00859 
00860                         }
00861                 }
00862 
00863                 return $endArray;
00864         }
00865 
00866 } // end DatabaseSqlite class
00867 
00872 class DatabaseSqliteStandalone extends DatabaseSqlite {
00873         public function __construct( $fileName, $flags = 0 ) {
00874                 $this->mFlags = $flags;
00875                 $this->tablePrefix( null );
00876                 $this->openFile( $fileName );
00877         }
00878 }
00879 
00883 class SQLiteField implements Field {
00884         private $info, $tableName;
00885         function __construct( $info, $tableName ) {
00886                 $this->info = $info;
00887                 $this->tableName = $tableName;
00888         }
00889 
00890         function name() {
00891                 return $this->info->name;
00892         }
00893 
00894         function tableName() {
00895                 return $this->tableName;
00896         }
00897 
00898         function defaultValue() {
00899                 if ( is_string( $this->info->dflt_value ) ) {
00900                         // Typically quoted
00901                         if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) {
00902                                 return str_replace( "''", "'", $this->info->dflt_value );
00903                         }
00904                 }
00905                 return $this->info->dflt_value;
00906         }
00907 
00911         function isNullable() {
00912                 return !$this->info->notnull;
00913         }
00914 
00915         function type() {
00916                 return $this->info->type;
00917         }
00918 
00919 } // end SQLiteField