MediaWiki
REL1_21
|
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