MediaWiki  REL1_19
DatabaseSqlite.php
Go to the documentation of this file.
00001 <?php
00013 class DatabaseSqlite extends DatabaseBase {
00014 
00015         private static $fulltextEnabled = null;
00016 
00017         var $mAffectedRows;
00018         var $mLastResult;
00019         var $mDatabaseFile;
00020         var $mName;
00021 
00025         protected $mConn;
00026 
00036         function __construct( $server = false, $user = false, $password = false, $dbName = false, $flags = 0 ) {
00037                 $this->mName = $dbName;
00038                 parent::__construct( $server, $user, $password, $dbName, $flags );
00039                 // parent doesn't open when $user is false, but we can work with $dbName
00040                 if( $dbName ) {
00041                         global $wgSharedDB;
00042                         if( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) {
00043                                 $this->attachDatabase( $wgSharedDB );
00044                         }
00045                 }
00046         }
00047 
00051         function getType() {
00052                 return 'sqlite';
00053         }
00054 
00060         function implicitGroupby() {
00061                 return false;
00062         }
00063 
00074         function open( $server, $user, $pass, $dbName ) {
00075                 global $wgSQLiteDataDir;
00076 
00077                 $fileName = self::generateFileName( $wgSQLiteDataDir, $dbName );
00078                 if ( !is_readable( $fileName ) ) {
00079                         $this->mConn = false;
00080                         throw new DBConnectionError( $this, "SQLite database not accessible" );
00081                 }
00082                 $this->openFile( $fileName );
00083                 return $this->mConn;
00084         }
00085 
00093         function openFile( $fileName ) {
00094                 $this->mDatabaseFile = $fileName;
00095                 try {
00096                         if ( $this->mFlags & DBO_PERSISTENT ) {
00097                                 $this->mConn = new PDO( "sqlite:$fileName", '', '',
00098                                         array( PDO::ATTR_PERSISTENT => true ) );
00099                         } else {
00100                                 $this->mConn = new PDO( "sqlite:$fileName", '', '' );
00101                         }
00102                 } catch ( PDOException $e ) {
00103                         $err = $e->getMessage();
00104                 }
00105                 if ( !$this->mConn ) {
00106                         wfDebug( "DB connection error: $err\n" );
00107                         throw new DBConnectionError( $this, $err );
00108                 }
00109                 $this->mOpened = !!$this->mConn;
00110                 # set error codes only, don't raise exceptions
00111                 if ( $this->mOpened ) {
00112                         $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
00113                         return true;
00114                 }
00115         }
00116 
00122         function close() {
00123                 $this->mOpened = false;
00124                 if ( is_object( $this->mConn ) ) {
00125                         if ( $this->trxLevel() ) $this->commit();
00126                         $this->mConn = null;
00127                 }
00128                 return true;
00129         }
00130 
00137         public static function generateFileName( $dir, $dbName ) {
00138                 return "$dir/$dbName.sqlite";
00139         }
00140 
00145         function checkForEnabledSearch() {
00146                 if ( self::$fulltextEnabled === null ) {
00147                         self::$fulltextEnabled = false;
00148                         $table = $this->tableName( 'searchindex' );
00149                         $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
00150                         if ( $res ) {
00151                                 $row = $res->fetchRow();
00152                                 self::$fulltextEnabled = stristr($row['sql'], 'fts' ) !== false;
00153                         }
00154                 }
00155                 return self::$fulltextEnabled;
00156         }
00157 
00162         static function getFulltextSearchModule() {
00163                 static $cachedResult = null;
00164                 if ( $cachedResult !== null ) {
00165                         return $cachedResult;
00166                 }
00167                 $cachedResult = false;
00168                 $table = 'dummy_search_test';
00169                 
00170                 $db = new DatabaseSqliteStandalone( ':memory:' );
00171 
00172                 if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
00173                         $cachedResult = 'FTS3';
00174                 }
00175                 $db->close();
00176                 return $cachedResult;
00177         }
00178 
00189         function attachDatabase( $name, $file = false, $fname = 'DatabaseSqlite::attachDatabase' ) {
00190                 global $wgSQLiteDataDir;
00191                 if ( !$file ) {
00192                         $file = self::generateFileName( $wgSQLiteDataDir, $name );
00193                 }
00194                 $file = $this->addQuotes( $file );
00195                 return $this->query( "ATTACH DATABASE $file AS $name", $fname );
00196         }
00197 
00205         function isWriteQuery( $sql ) {
00206                 return parent::isWriteQuery( $sql ) && !preg_match( '/^ATTACH\b/i', $sql );
00207         }
00208 
00216         protected function doQuery( $sql ) {
00217                 $res = $this->mConn->query( $sql );
00218                 if ( $res === false ) {
00219                         return false;
00220                 } else {
00221                         $r = $res instanceof ResultWrapper ? $res->result : $res;
00222                         $this->mAffectedRows = $r->rowCount();
00223                         $res = new ResultWrapper( $this, $r->fetchAll() );
00224                 }
00225                 return $res;
00226         }
00227 
00231         function freeResult( $res ) {
00232                 if ( $res instanceof ResultWrapper ) {
00233                         $res->result = null;
00234                 } else {
00235                         $res = null;
00236                 }
00237         }
00238 
00243         function fetchObject( $res ) {
00244                 if ( $res instanceof ResultWrapper ) {
00245                         $r =& $res->result;
00246                 } else {
00247                         $r =& $res;
00248                 }
00249 
00250                 $cur = current( $r );
00251                 if ( is_array( $cur ) ) {
00252                         next( $r );
00253                         $obj = new stdClass;
00254                         foreach ( $cur as $k => $v ) {
00255                                 if ( !is_numeric( $k ) ) {
00256                                         $obj->$k = $v;
00257                                 }
00258                         }
00259 
00260                         return $obj;
00261                 }
00262                 return false;
00263         }
00264 
00269         function fetchRow( $res ) {
00270                 if ( $res instanceof ResultWrapper ) {
00271                         $r =& $res->result;
00272                 } else {
00273                         $r =& $res;
00274                 }
00275                 $cur = current( $r );
00276                 if ( is_array( $cur ) ) {
00277                         next( $r );
00278                         return $cur;
00279                 }
00280                 return false;
00281         }
00282 
00290         function numRows( $res ) {
00291                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00292                 return count( $r );
00293         }
00294 
00299         function numFields( $res ) {
00300                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00301                 return is_array( $r ) ? count( $r[0] ) : 0;
00302         }
00303 
00309         function fieldName( $res, $n ) {
00310                 $r = $res instanceof ResultWrapper ? $res->result : $res;
00311                 if ( is_array( $r ) ) {
00312                         $keys = array_keys( $r[0] );
00313                         return $keys[$n];
00314                 }
00315                 return false;
00316         }
00317 
00325         function tableName( $name, $format = 'quoted' ) {
00326                 // table names starting with sqlite_ are reserved
00327                 if ( strpos( $name, 'sqlite_' ) === 0 ) {
00328                         return $name;
00329                 }
00330                 return str_replace( '"', '', parent::tableName( $name, $format ) );
00331         }
00332 
00340         function indexName( $index ) {
00341                 return $index;
00342         }
00343 
00349         function insertId() {
00350                 return $this->mConn->lastInsertId();
00351         }
00352 
00357         function dataSeek( $res, $row ) {
00358                 if ( $res instanceof ResultWrapper ) {
00359                         $r =& $res->result;
00360                 } else {
00361                         $r =& $res;
00362                 }
00363                 reset( $r );
00364                 if ( $row > 0 ) {
00365                         for ( $i = 0; $i < $row; $i++ ) {
00366                                 next( $r );
00367                         }
00368                 }
00369         }
00370 
00374         function lastError() {
00375                 if ( !is_object( $this->mConn ) ) {
00376                         return "Cannot return last error, no db connection";
00377                 }
00378                 $e = $this->mConn->errorInfo();
00379                 return isset( $e[2] ) ? $e[2] : '';
00380         }
00381 
00385         function lastErrno() {
00386                 if ( !is_object( $this->mConn ) ) {
00387                         return "Cannot return last error, no db connection";
00388                 } else {
00389                         $info = $this->mConn->errorInfo();
00390                         return $info[1];
00391                 }
00392         }
00393 
00397         function affectedRows() {
00398                 return $this->mAffectedRows;
00399         }
00400 
00408         function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) {
00409                 $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
00410                 $res = $this->query( $sql, $fname );
00411                 if ( !$res ) {
00412                         return null;
00413                 }
00414                 if ( $res->numRows() == 0 ) {
00415                         return false;
00416                 }
00417                 $info = array();
00418                 foreach ( $res as $row ) {
00419                         $info[] = $row->name;
00420                 }
00421                 return $info;
00422         }
00423 
00430         function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) {
00431                 $row = $this->selectRow( 'sqlite_master', '*',
00432                         array(
00433                                 'type' => 'index',
00434                                 'name' => $this->indexName( $index ),
00435                         ), $fname );
00436                 if ( !$row || !isset( $row->sql ) ) {
00437                         return null;
00438                 }
00439 
00440                 // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
00441                 $indexPos = strpos( $row->sql, 'INDEX' );
00442                 if ( $indexPos === false ) {
00443                         return null;
00444                 }
00445                 $firstPart = substr( $row->sql, 0, $indexPos );
00446                 $options = explode( ' ', $firstPart );
00447                 return in_array( 'UNIQUE', $options );
00448         }
00449 
00457         function makeSelectOptions( $options ) {
00458                 foreach ( $options as $k => $v ) {
00459                         if ( is_numeric( $k ) && $v == 'FOR UPDATE' ) {
00460                                 $options[$k] = '';
00461                         }
00462                 }
00463                 return parent::makeSelectOptions( $options );
00464         }
00465 
00470         function makeUpdateOptions( $options ) {
00471                 $options = self::fixIgnore( $options );
00472                 return parent::makeUpdateOptions( $options );
00473         }
00474 
00479         static function fixIgnore( $options ) {
00480                 # SQLite uses OR IGNORE not just IGNORE
00481                 foreach ( $options as $k => $v ) {
00482                         if ( $v == 'IGNORE' ) {
00483                                 $options[$k] = 'OR IGNORE';
00484                         }
00485                 }
00486                 return $options;
00487         }
00488 
00493         function makeInsertOptions( $options ) {
00494                 $options = self::fixIgnore( $options );
00495                 return parent::makeInsertOptions( $options );
00496         }
00497 
00501         function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) {
00502                 if ( !count( $a ) ) {
00503                         return true;
00504                 }
00505 
00506                 # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
00507                 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
00508                         $ret = true;
00509                         foreach ( $a as $v ) {
00510                                 if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
00511                                         $ret = false;
00512                                 }
00513                         }
00514                 } else {
00515                         $ret = parent::insert( $table, $a, "$fname/single-row", $options );
00516                 }
00517 
00518                 return $ret;
00519         }
00520 
00528         function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) {
00529                 if ( !count( $rows ) ) return true;
00530 
00531                 # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
00532                 if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
00533                         $ret = true;
00534                         foreach ( $rows as $v ) {
00535                                 if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
00536                                         $ret = false;
00537                                 }
00538                         }
00539                 } else {
00540                         $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
00541                 }
00542 
00543                 return $ret;
00544         }
00545 
00552         function textFieldSize( $table, $field ) {
00553                 return -1;
00554         }
00555 
00559         function unionSupportsOrderAndLimit() {
00560                 return false;
00561         }
00562 
00568         function unionQueries( $sqls, $all ) {
00569                 $glue = $all ? ' UNION ALL ' : ' UNION ';
00570                 return implode( $glue, $sqls );
00571         }
00572 
00576         function wasDeadlock() {
00577                 return $this->lastErrno() == 5; // SQLITE_BUSY
00578         }
00579 
00583         function wasErrorReissuable() {
00584                 return $this->lastErrno() ==  17; // SQLITE_SCHEMA;
00585         }
00586 
00590         function wasReadOnlyError() {
00591                 return $this->lastErrno() == 8; // SQLITE_READONLY;
00592         }
00593 
00597         public static function getSoftwareLink() {
00598                 return "[http://sqlite.org/ SQLite]";
00599         }
00600 
00604         function getServerVersion() {
00605                 $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
00606                 return $ver;
00607         }
00608 
00612         public function getServerInfo() {
00613                 return wfMsg( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() );
00614         }
00615 
00622         function fieldInfo( $table, $field ) {
00623                 $tableName = $this->tableName( $table );
00624                 $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
00625                 $res = $this->query( $sql, __METHOD__ );
00626                 foreach ( $res as $row ) {
00627                         if ( $row->name == $field ) {
00628                                 return new SQLiteField( $row, $tableName );
00629                         }
00630                 }
00631                 return false;
00632         }
00633 
00634         function begin( $fname = '' ) {
00635                 if ( $this->mTrxLevel == 1 ) {
00636                         $this->commit();
00637                 }
00638                 $this->mConn->beginTransaction();
00639                 $this->mTrxLevel = 1;
00640         }
00641 
00642         function commit( $fname = '' ) {
00643                 if ( $this->mTrxLevel == 0 ) {
00644                         return;
00645                 }
00646                 $this->mConn->commit();
00647                 $this->mTrxLevel = 0;
00648         }
00649 
00650         function rollback( $fname = '' ) {
00651                 if ( $this->mTrxLevel == 0 ) {
00652                         return;
00653                 }
00654                 $this->mConn->rollBack();
00655                 $this->mTrxLevel = 0;
00656         }
00657 
00663         function limitResultForUpdate( $sql, $num ) {
00664                 return $this->limitResult( $sql, $num );
00665         }
00666 
00671         function strencode( $s ) {
00672                 return substr( $this->addQuotes( $s ), 1, - 1 );
00673         }
00674 
00679         function encodeBlob( $b ) {
00680                 return new Blob( $b );
00681         }
00682 
00687         function decodeBlob( $b ) {
00688                 if ( $b instanceof Blob ) {
00689                         $b = $b->fetch();
00690                 }
00691                 return $b;
00692         }
00693 
00698         function addQuotes( $s ) {
00699                 if ( $s instanceof Blob ) {
00700                         return "x'" . bin2hex( $s->fetch() ) . "'";
00701                 } else {
00702                         return $this->mConn->quote( $s );
00703                 }
00704         }
00705 
00709         function buildLike() {
00710                 $params = func_get_args();
00711                 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
00712                         $params = $params[0];
00713                 }
00714                 return parent::buildLike( $params ) . "ESCAPE '\' ";
00715         }
00716 
00720         public function getSearchEngine() {
00721                 return "SearchSqlite";
00722         }
00723 
00727         public function deadlockLoop( /*...*/ ) {
00728                 $args = func_get_args();
00729                 $function = array_shift( $args );
00730                 return call_user_func_array( $function, $args );
00731         }
00732 
00737         protected function replaceVars( $s ) {
00738                 $s = parent::replaceVars( $s );
00739                 if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
00740                         // CREATE TABLE hacks to allow schema file sharing with MySQL
00741 
00742                         // binary/varbinary column type -> blob
00743                         $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
00744                         // no such thing as unsigned
00745                         $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
00746                         // INT -> INTEGER
00747                         $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
00748                         // floating point types -> REAL
00749                         $s = preg_replace( '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i', 'REAL', $s );
00750                         // varchar -> TEXT
00751                         $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
00752                         // TEXT normalization
00753                         $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
00754                         // BLOB normalization
00755                         $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
00756                         // BOOL -> INTEGER
00757                         $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
00758                         // DATETIME -> TEXT
00759                         $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
00760                         // No ENUM type
00761                         $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
00762                         // binary collation type -> nothing
00763                         $s = preg_replace( '/\bbinary\b/i', '', $s );
00764                         // auto_increment -> autoincrement
00765                         $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
00766                         // No explicit options
00767                         $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
00768                         // AUTOINCREMENT should immedidately follow PRIMARY KEY
00769                         $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
00770                 } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
00771                         // No truncated indexes
00772                         $s = preg_replace( '/\(\d+\)/', '', $s );
00773                         // No FULLTEXT
00774                         $s = preg_replace( '/\bfulltext\b/i', '', $s );
00775                 }
00776                 return $s;
00777         }
00778 
00786         function buildConcat( $stringList ) {
00787                 return '(' . implode( ') || (', $stringList ) . ')';
00788         }
00789 
00798         function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) {
00799                 $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . $this->addQuotes( $oldName ) . " AND type='table'", $fname );
00800                 $obj = $this->fetchObject( $res );
00801                 if ( !$obj ) {
00802                         throw new MWException( "Couldn't retrieve structure for table $oldName" );
00803                 }
00804                 $sql = $obj->sql;
00805                 $sql = preg_replace( '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/', $this->addIdentifierQuotes( $newName ), $sql, 1 );
00806                 if ( $temporary ) {
00807                         if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
00808                                 wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" );
00809                         } else {
00810                                 $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
00811                         }
00812                 }
00813                 return $this->query( $sql, $fname );
00814         }
00815         
00816         
00825         function listTables( $prefix = null, $fname = 'DatabaseSqlite::listTables' ) {
00826                 $result = $this->select(
00827                         'sqlite_master',
00828                         'name',
00829                         "type='table'"
00830                 );
00831                 
00832                 $endArray = array();
00833                 
00834                 foreach( $result as $table ) {  
00835                         $vars = get_object_vars($table);
00836                         $table = array_pop( $vars );
00837                         
00838                         if( !$prefix || strpos( $table, $prefix ) === 0 ) {
00839                                 if ( strpos( $table, 'sqlite_' ) !== 0 ) {
00840                                         $endArray[] = $table;
00841                                 }
00842                                 
00843                         }
00844                 }
00845                 
00846                 return $endArray;
00847         }
00848 
00849 } // end DatabaseSqlite class
00850 
00855 class DatabaseSqliteStandalone extends DatabaseSqlite {
00856         public function __construct( $fileName, $flags = 0 ) {
00857                 $this->mFlags = $flags;
00858                 $this->tablePrefix( null );
00859                 $this->openFile( $fileName );
00860         }
00861 }
00862 
00866 class SQLiteField implements Field {
00867         private $info, $tableName;
00868         function __construct( $info, $tableName ) {
00869                 $this->info = $info;
00870                 $this->tableName = $tableName;
00871         }
00872 
00873         function name() {
00874                 return $this->info->name;
00875         }
00876 
00877         function tableName() {
00878                 return $this->tableName;
00879         }
00880 
00881         function defaultValue() {
00882                 if ( is_string( $this->info->dflt_value ) ) {
00883                         // Typically quoted
00884                         if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) {
00885                                 return str_replace( "''", "'", $this->info->dflt_value );
00886                         }
00887                 }
00888                 return $this->info->dflt_value;
00889         }
00890 
00894         function isNullable() {
00895                 return !$this->info->notnull;
00896         }
00897 
00898         function type() {
00899                 return $this->info->type;
00900         }
00901 
00902 } // end SQLiteField