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