Break long lines and formatting updates for includes/db/
[lhc/web/wiklou.git] / includes / db / DatabaseSqlite.php
1 <?php
2 /**
3 * This is the SQLite database abstraction layer.
4 * See maintenance/sqlite/README for development notes and other specific information
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 * @ingroup Database
23 */
24
25 /**
26 * @ingroup Database
27 */
28 class DatabaseSqlite extends DatabaseBase {
29 private static $fulltextEnabled = null;
30
31 var $mAffectedRows;
32 var $mLastResult;
33 var $mDatabaseFile;
34 var $mName;
35
36 /**
37 * @var PDO
38 */
39 protected $mConn;
40
41 function __construct( $p = null ) {
42 global $wgSharedDB;
43
44 if ( !is_array( $p ) ) { // legacy calling pattern
45 wfDeprecated( __METHOD__ . " method called without parameter array.", "1.22" );
46 $args = func_get_args();
47 $p = array(
48 'host' => isset( $args[0] ) ? $args[0] : false,
49 'user' => isset( $args[1] ) ? $args[1] : false,
50 'password' => isset( $args[2] ) ? $args[2] : false,
51 'dbname' => isset( $args[3] ) ? $args[3] : false,
52 'flags' => isset( $args[4] ) ? $args[4] : 0,
53 'tablePrefix' => isset( $args[5] ) ? $args[5] : 'get from global',
54 'foreign' => isset( $args[6] ) ? $args[6] : false
55 );
56 }
57 $this->mName = $p['dbname'];
58 parent::__construct( $p );
59 // parent doesn't open when $user is false, but we can work with $dbName
60 if ( $p['dbname'] && !$this->isOpen() ) {
61 if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) {
62 if ( $wgSharedDB ) {
63 $this->attachDatabase( $wgSharedDB );
64 }
65 }
66 }
67 }
68
69 /**
70 * @return string
71 */
72 function getType() {
73 return 'sqlite';
74 }
75
76 /**
77 * @todo Check if it should be true like parent class
78 *
79 * @return bool
80 */
81 function implicitGroupby() {
82 return false;
83 }
84
85 /** Open an SQLite database and return a resource handle to it
86 * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
87 *
88 * @param string $server
89 * @param string $user
90 * @param string $pass
91 * @param string $dbName
92 *
93 * @throws DBConnectionError
94 * @return PDO
95 */
96 function open( $server, $user, $pass, $dbName ) {
97 global $wgSQLiteDataDir;
98
99 $this->close();
100 $fileName = self::generateFileName( $wgSQLiteDataDir, $dbName );
101 if ( !is_readable( $fileName ) ) {
102 $this->mConn = false;
103 throw new DBConnectionError( $this, "SQLite database not accessible" );
104 }
105 $this->openFile( $fileName );
106
107 return $this->mConn;
108 }
109
110 /**
111 * Opens a database file
112 *
113 * @param $fileName string
114 *
115 * @throws DBConnectionError
116 * @return PDO|bool SQL connection or false if failed
117 */
118 function openFile( $fileName ) {
119 $this->mDatabaseFile = $fileName;
120 try {
121 if ( $this->mFlags & DBO_PERSISTENT ) {
122 $this->mConn = new PDO( "sqlite:$fileName", '', '',
123 array( PDO::ATTR_PERSISTENT => true ) );
124 } else {
125 $this->mConn = new PDO( "sqlite:$fileName", '', '' );
126 }
127 } catch ( PDOException $e ) {
128 $err = $e->getMessage();
129 }
130 if ( !$this->mConn ) {
131 wfDebug( "DB connection error: $err\n" );
132 throw new DBConnectionError( $this, $err );
133 }
134 $this->mOpened = !!$this->mConn;
135 # set error codes only, don't raise exceptions
136 if ( $this->mOpened ) {
137 $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
138 # Enforce LIKE to be case sensitive, just like MySQL
139 $this->query( 'PRAGMA case_sensitive_like = 1' );
140
141 return true;
142 }
143 }
144
145 /**
146 * Does not actually close the connection, just destroys the reference for GC to do its work
147 * @return bool
148 */
149 protected function closeConnection() {
150 $this->mConn = null;
151
152 return true;
153 }
154
155 /**
156 * Generates a database file name. Explicitly public for installer.
157 * @param string $dir Directory where database resides
158 * @param string $dbName Database name
159 * @return String
160 */
161 public static function generateFileName( $dir, $dbName ) {
162 return "$dir/$dbName.sqlite";
163 }
164
165 /**
166 * Check if the searchindext table is FTS enabled.
167 * @return bool False if not enabled.
168 */
169 function checkForEnabledSearch() {
170 if ( self::$fulltextEnabled === null ) {
171 self::$fulltextEnabled = false;
172 $table = $this->tableName( 'searchindex' );
173 $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
174 if ( $res ) {
175 $row = $res->fetchRow();
176 self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
177 }
178 }
179
180 return self::$fulltextEnabled;
181 }
182
183 /**
184 * Returns version of currently supported SQLite fulltext search module or false if none present.
185 * @return String
186 */
187 static function getFulltextSearchModule() {
188 static $cachedResult = null;
189 if ( $cachedResult !== null ) {
190 return $cachedResult;
191 }
192 $cachedResult = false;
193 $table = 'dummy_search_test';
194
195 $db = new DatabaseSqliteStandalone( ':memory:' );
196
197 if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
198 $cachedResult = 'FTS3';
199 }
200 $db->close();
201
202 return $cachedResult;
203 }
204
205 /**
206 * Attaches external database to our connection, see http://sqlite.org/lang_attach.html
207 * for details.
208 *
209 * @param string $name database name to be used in queries like
210 * SELECT foo FROM dbname.table
211 * @param string $file database file name. If omitted, will be generated
212 * using $name and $wgSQLiteDataDir
213 * @param string $fname calling function name
214 *
215 * @return ResultWrapper
216 */
217 function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
218 global $wgSQLiteDataDir;
219 if ( !$file ) {
220 $file = self::generateFileName( $wgSQLiteDataDir, $name );
221 }
222 $file = $this->addQuotes( $file );
223
224 return $this->query( "ATTACH DATABASE $file AS $name", $fname );
225 }
226
227 /**
228 * @see DatabaseBase::isWriteQuery()
229 *
230 * @param $sql string
231 *
232 * @return bool
233 */
234 function isWriteQuery( $sql ) {
235 return parent::isWriteQuery( $sql ) && !preg_match( '/^ATTACH\b/i', $sql );
236 }
237
238 /**
239 * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
240 *
241 * @param $sql string
242 *
243 * @return ResultWrapper
244 */
245 protected function doQuery( $sql ) {
246 $res = $this->mConn->query( $sql );
247 if ( $res === false ) {
248 return false;
249 } else {
250 $r = $res instanceof ResultWrapper ? $res->result : $res;
251 $this->mAffectedRows = $r->rowCount();
252 $res = new ResultWrapper( $this, $r->fetchAll() );
253 }
254
255 return $res;
256 }
257
258 /**
259 * @param $res ResultWrapper
260 */
261 function freeResult( $res ) {
262 if ( $res instanceof ResultWrapper ) {
263 $res->result = null;
264 } else {
265 $res = null;
266 }
267 }
268
269 /**
270 * @param $res ResultWrapper
271 * @return object|bool
272 */
273 function fetchObject( $res ) {
274 if ( $res instanceof ResultWrapper ) {
275 $r =& $res->result;
276 } else {
277 $r =& $res;
278 }
279
280 $cur = current( $r );
281 if ( is_array( $cur ) ) {
282 next( $r );
283 $obj = new stdClass;
284 foreach ( $cur as $k => $v ) {
285 if ( !is_numeric( $k ) ) {
286 $obj->$k = $v;
287 }
288 }
289
290 return $obj;
291 }
292
293 return false;
294 }
295
296 /**
297 * @param $res ResultWrapper
298 * @return array|bool
299 */
300 function fetchRow( $res ) {
301 if ( $res instanceof ResultWrapper ) {
302 $r =& $res->result;
303 } else {
304 $r =& $res;
305 }
306 $cur = current( $r );
307 if ( is_array( $cur ) ) {
308 next( $r );
309
310 return $cur;
311 }
312
313 return false;
314 }
315
316 /**
317 * The PDO::Statement class implements the array interface so count() will work
318 *
319 * @param $res ResultWrapper
320 *
321 * @return int
322 */
323 function numRows( $res ) {
324 $r = $res instanceof ResultWrapper ? $res->result : $res;
325
326 return count( $r );
327 }
328
329 /**
330 * @param $res ResultWrapper
331 * @return int
332 */
333 function numFields( $res ) {
334 $r = $res instanceof ResultWrapper ? $res->result : $res;
335
336 return is_array( $r ) ? count( $r[0] ) : 0;
337 }
338
339 /**
340 * @param $res ResultWrapper
341 * @param $n
342 * @return bool
343 */
344 function fieldName( $res, $n ) {
345 $r = $res instanceof ResultWrapper ? $res->result : $res;
346 if ( is_array( $r ) ) {
347 $keys = array_keys( $r[0] );
348
349 return $keys[$n];
350 }
351
352 return false;
353 }
354
355 /**
356 * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
357 *
358 * @param $name
359 * @param $format String
360 * @return string
361 */
362 function tableName( $name, $format = 'quoted' ) {
363 // table names starting with sqlite_ are reserved
364 if ( strpos( $name, 'sqlite_' ) === 0 ) {
365 return $name;
366 }
367
368 return str_replace( '"', '', parent::tableName( $name, $format ) );
369 }
370
371 /**
372 * Index names have DB scope
373 *
374 * @param $index string
375 *
376 * @return string
377 */
378 function indexName( $index ) {
379 return $index;
380 }
381
382 /**
383 * This must be called after nextSequenceVal
384 *
385 * @return int
386 */
387 function insertId() {
388 // PDO::lastInsertId yields a string :(
389 return intval( $this->mConn->lastInsertId() );
390 }
391
392 /**
393 * @param $res ResultWrapper
394 * @param $row
395 */
396 function dataSeek( $res, $row ) {
397 if ( $res instanceof ResultWrapper ) {
398 $r =& $res->result;
399 } else {
400 $r =& $res;
401 }
402 reset( $r );
403 if ( $row > 0 ) {
404 for ( $i = 0; $i < $row; $i++ ) {
405 next( $r );
406 }
407 }
408 }
409
410 /**
411 * @return string
412 */
413 function lastError() {
414 if ( !is_object( $this->mConn ) ) {
415 return "Cannot return last error, no db connection";
416 }
417 $e = $this->mConn->errorInfo();
418
419 return isset( $e[2] ) ? $e[2] : '';
420 }
421
422 /**
423 * @return string
424 */
425 function lastErrno() {
426 if ( !is_object( $this->mConn ) ) {
427 return "Cannot return last error, no db connection";
428 } else {
429 $info = $this->mConn->errorInfo();
430
431 return $info[1];
432 }
433 }
434
435 /**
436 * @return int
437 */
438 function affectedRows() {
439 return $this->mAffectedRows;
440 }
441
442 /**
443 * Returns information about an index
444 * Returns false if the index does not exist
445 * - if errors are explicitly ignored, returns NULL on failure
446 *
447 * @return array
448 */
449 function indexInfo( $table, $index, $fname = __METHOD__ ) {
450 $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
451 $res = $this->query( $sql, $fname );
452 if ( !$res ) {
453 return null;
454 }
455 if ( $res->numRows() == 0 ) {
456 return false;
457 }
458 $info = array();
459 foreach ( $res as $row ) {
460 $info[] = $row->name;
461 }
462
463 return $info;
464 }
465
466 /**
467 * @param $table
468 * @param $index
469 * @param $fname string
470 * @return bool|null
471 */
472 function indexUnique( $table, $index, $fname = __METHOD__ ) {
473 $row = $this->selectRow( 'sqlite_master', '*',
474 array(
475 'type' => 'index',
476 'name' => $this->indexName( $index ),
477 ), $fname );
478 if ( !$row || !isset( $row->sql ) ) {
479 return null;
480 }
481
482 // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
483 $indexPos = strpos( $row->sql, 'INDEX' );
484 if ( $indexPos === false ) {
485 return null;
486 }
487 $firstPart = substr( $row->sql, 0, $indexPos );
488 $options = explode( ' ', $firstPart );
489
490 return in_array( 'UNIQUE', $options );
491 }
492
493 /**
494 * Filter the options used in SELECT statements
495 *
496 * @param $options array
497 *
498 * @return array
499 */
500 function makeSelectOptions( $options ) {
501 foreach ( $options as $k => $v ) {
502 if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
503 $options[$k] = '';
504 }
505 }
506
507 return parent::makeSelectOptions( $options );
508 }
509
510 /**
511 * @param $options array
512 * @return string
513 */
514 function makeUpdateOptions( $options ) {
515 $options = self::fixIgnore( $options );
516
517 return parent::makeUpdateOptions( $options );
518 }
519
520 /**
521 * @param $options array
522 * @return array
523 */
524 static function fixIgnore( $options ) {
525 # SQLite uses OR IGNORE not just IGNORE
526 foreach ( $options as $k => $v ) {
527 if ( $v == 'IGNORE' ) {
528 $options[$k] = 'OR IGNORE';
529 }
530 }
531
532 return $options;
533 }
534
535 /**
536 * @param $options array
537 * @return string
538 */
539 function makeInsertOptions( $options ) {
540 $options = self::fixIgnore( $options );
541
542 return parent::makeInsertOptions( $options );
543 }
544
545 /**
546 * Based on generic method (parent) with some prior SQLite-sepcific adjustments
547 * @return bool
548 */
549 function insert( $table, $a, $fname = __METHOD__, $options = array() ) {
550 if ( !count( $a ) ) {
551 return true;
552 }
553
554 # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
555 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
556 $ret = true;
557 foreach ( $a as $v ) {
558 if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
559 $ret = false;
560 }
561 }
562 } else {
563 $ret = parent::insert( $table, $a, "$fname/single-row", $options );
564 }
565
566 return $ret;
567 }
568
569 /**
570 * @param $table
571 * @param $uniqueIndexes
572 * @param $rows
573 * @param $fname string
574 * @return bool|ResultWrapper
575 */
576 function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
577 if ( !count( $rows ) ) {
578 return true;
579 }
580
581 # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
582 if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
583 $ret = true;
584 foreach ( $rows as $v ) {
585 if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
586 $ret = false;
587 }
588 }
589 } else {
590 $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
591 }
592
593 return $ret;
594 }
595
596 /**
597 * Returns the size of a text field, or -1 for "unlimited"
598 * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though.
599 *
600 * @return int
601 */
602 function textFieldSize( $table, $field ) {
603 return -1;
604 }
605
606 /**
607 * @return bool
608 */
609 function unionSupportsOrderAndLimit() {
610 return false;
611 }
612
613 /**
614 * @param $sqls
615 * @param $all
616 * @return string
617 */
618 function unionQueries( $sqls, $all ) {
619 $glue = $all ? ' UNION ALL ' : ' UNION ';
620
621 return implode( $glue, $sqls );
622 }
623
624 /**
625 * @return bool
626 */
627 function wasDeadlock() {
628 return $this->lastErrno() == 5; // SQLITE_BUSY
629 }
630
631 /**
632 * @return bool
633 */
634 function wasErrorReissuable() {
635 return $this->lastErrno() == 17; // SQLITE_SCHEMA;
636 }
637
638 /**
639 * @return bool
640 */
641 function wasReadOnlyError() {
642 return $this->lastErrno() == 8; // SQLITE_READONLY;
643 }
644
645 /**
646 * @return string wikitext of a link to the server software's web site
647 */
648 public function getSoftwareLink() {
649 return "[http://sqlite.org/ SQLite]";
650 }
651
652 /**
653 * @return string Version information from the database
654 */
655 function getServerVersion() {
656 $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
657
658 return $ver;
659 }
660
661 /**
662 * @return string User-friendly database information
663 */
664 public function getServerInfo() {
665 return wfMessage( self::getFulltextSearchModule()
666 ? 'sqlite-has-fts'
667 : 'sqlite-no-fts', $this->getServerVersion() )->text();
668 }
669
670 /**
671 * Get information about a given field
672 * Returns false if the field does not exist.
673 *
674 * @param $table string
675 * @param $field string
676 * @return SQLiteField|bool False on failure
677 */
678 function fieldInfo( $table, $field ) {
679 $tableName = $this->tableName( $table );
680 $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
681 $res = $this->query( $sql, __METHOD__ );
682 foreach ( $res as $row ) {
683 if ( $row->name == $field ) {
684 return new SQLiteField( $row, $tableName );
685 }
686 }
687
688 return false;
689 }
690
691 protected function doBegin( $fname = '' ) {
692 if ( $this->mTrxLevel == 1 ) {
693 $this->commit( __METHOD__ );
694 }
695 try {
696 $this->mConn->beginTransaction();
697 } catch ( PDOException $e ) {
698 throw new DBUnexpectedError( $this, 'Error in BEGIN query: ' . $e->getMessage() );
699 }
700 $this->mTrxLevel = 1;
701 }
702
703 protected function doCommit( $fname = '' ) {
704 if ( $this->mTrxLevel == 0 ) {
705 return;
706 }
707 try {
708 $this->mConn->commit();
709 } catch ( PDOException $e ) {
710 throw new DBUnexpectedError( $this, 'Error in COMMIT query: ' . $e->getMessage() );
711 }
712 $this->mTrxLevel = 0;
713 }
714
715 protected function doRollback( $fname = '' ) {
716 if ( $this->mTrxLevel == 0 ) {
717 return;
718 }
719 $this->mConn->rollBack();
720 $this->mTrxLevel = 0;
721 }
722
723 /**
724 * @param $s string
725 * @return string
726 */
727 function strencode( $s ) {
728 return substr( $this->addQuotes( $s ), 1, -1 );
729 }
730
731 /**
732 * @param $b
733 * @return Blob
734 */
735 function encodeBlob( $b ) {
736 return new Blob( $b );
737 }
738
739 /**
740 * @param $b Blob|string
741 * @return string
742 */
743 function decodeBlob( $b ) {
744 if ( $b instanceof Blob ) {
745 $b = $b->fetch();
746 }
747
748 return $b;
749 }
750
751 /**
752 * @param $s Blob|string
753 * @return string
754 */
755 function addQuotes( $s ) {
756 if ( $s instanceof Blob ) {
757 return "x'" . bin2hex( $s->fetch() ) . "'";
758 } elseif ( is_bool( $s ) ) {
759 return (int)$s;
760 } elseif ( strpos( $s, "\0" ) !== false ) {
761 // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
762 // This is a known limitation of SQLite's mprintf function which PDO should work around,
763 // but doesn't. I have reported this to php.net as bug #63419:
764 // https://bugs.php.net/bug.php?id=63419
765 // There was already a similar report for SQLite3::escapeString, bug #62361:
766 // https://bugs.php.net/bug.php?id=62361
767 return "x'" . bin2hex( $s ) . "'";
768 } else {
769 return $this->mConn->quote( $s );
770 }
771 }
772
773 /**
774 * @return string
775 */
776 function buildLike() {
777 $params = func_get_args();
778 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
779 $params = $params[0];
780 }
781
782 return parent::buildLike( $params ) . "ESCAPE '\' ";
783 }
784
785 /**
786 * @return string
787 */
788 public function getSearchEngine() {
789 return "SearchSqlite";
790 }
791
792 /**
793 * No-op version of deadlockLoop
794 * @return mixed
795 */
796 public function deadlockLoop( /*...*/ ) {
797 $args = func_get_args();
798 $function = array_shift( $args );
799
800 return call_user_func_array( $function, $args );
801 }
802
803 /**
804 * @param $s string
805 * @return string
806 */
807 protected function replaceVars( $s ) {
808 $s = parent::replaceVars( $s );
809 if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
810 // CREATE TABLE hacks to allow schema file sharing with MySQL
811
812 // binary/varbinary column type -> blob
813 $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
814 // no such thing as unsigned
815 $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
816 // INT -> INTEGER
817 $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
818 // floating point types -> REAL
819 $s = preg_replace(
820 '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i',
821 'REAL',
822 $s
823 );
824 // varchar -> TEXT
825 $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
826 // TEXT normalization
827 $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
828 // BLOB normalization
829 $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
830 // BOOL -> INTEGER
831 $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
832 // DATETIME -> TEXT
833 $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
834 // No ENUM type
835 $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
836 // binary collation type -> nothing
837 $s = preg_replace( '/\bbinary\b/i', '', $s );
838 // auto_increment -> autoincrement
839 $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
840 // No explicit options
841 $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
842 // AUTOINCREMENT should immedidately follow PRIMARY KEY
843 $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
844 } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
845 // No truncated indexes
846 $s = preg_replace( '/\(\d+\)/', '', $s );
847 // No FULLTEXT
848 $s = preg_replace( '/\bfulltext\b/i', '', $s );
849 } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
850 // DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
851 $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
852 }
853
854 return $s;
855 }
856
857 /**
858 * Build a concatenation list to feed into a SQL query
859 *
860 * @param $stringList array
861 *
862 * @return string
863 */
864 function buildConcat( $stringList ) {
865 return '(' . implode( ') || (', $stringList ) . ')';
866 }
867
868 public function buildGroupConcatField(
869 $delim, $table, $field, $conds = '', $join_conds = array()
870 ) {
871 $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
872
873 return '(' . $this->selectSQLText( $table, $fld, $conds, null, array(), $join_conds ) . ')';
874 }
875
876 /**
877 * @throws MWException
878 * @param $oldName
879 * @param $newName
880 * @param $temporary bool
881 * @param $fname string
882 * @return bool|ResultWrapper
883 */
884 function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
885 $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" .
886 $this->addQuotes( $oldName ) . " AND type='table'", $fname );
887 $obj = $this->fetchObject( $res );
888 if ( !$obj ) {
889 throw new MWException( "Couldn't retrieve structure for table $oldName" );
890 }
891 $sql = $obj->sql;
892 $sql = preg_replace(
893 '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/',
894 $this->addIdentifierQuotes( $newName ),
895 $sql,
896 1
897 );
898 if ( $temporary ) {
899 if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
900 wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" );
901 } else {
902 $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
903 }
904 }
905
906 return $this->query( $sql, $fname );
907 }
908
909 /**
910 * List all tables on the database
911 *
912 * @param string $prefix Only show tables with this prefix, e.g. mw_
913 * @param string $fname calling function name
914 *
915 * @return array
916 */
917 function listTables( $prefix = null, $fname = __METHOD__ ) {
918 $result = $this->select(
919 'sqlite_master',
920 'name',
921 "type='table'"
922 );
923
924 $endArray = array();
925
926 foreach ( $result as $table ) {
927 $vars = get_object_vars( $table );
928 $table = array_pop( $vars );
929
930 if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
931 if ( strpos( $table, 'sqlite_' ) !== 0 ) {
932 $endArray[] = $table;
933 }
934 }
935 }
936
937 return $endArray;
938 }
939 } // end DatabaseSqlite class
940
941 /**
942 * This class allows simple acccess to a SQLite database independently from main database settings
943 * @ingroup Database
944 */
945 class DatabaseSqliteStandalone extends DatabaseSqlite {
946 public function __construct( $fileName, $flags = 0 ) {
947 $this->mFlags = $flags;
948 $this->tablePrefix( null );
949 $this->openFile( $fileName );
950 }
951 }
952
953 /**
954 * @ingroup Database
955 */
956 class SQLiteField implements Field {
957 private $info, $tableName;
958
959 function __construct( $info, $tableName ) {
960 $this->info = $info;
961 $this->tableName = $tableName;
962 }
963
964 function name() {
965 return $this->info->name;
966 }
967
968 function tableName() {
969 return $this->tableName;
970 }
971
972 function defaultValue() {
973 if ( is_string( $this->info->dflt_value ) ) {
974 // Typically quoted
975 if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) {
976 return str_replace( "''", "'", $this->info->dflt_value );
977 }
978 }
979
980 return $this->info->dflt_value;
981 }
982
983 /**
984 * @return bool
985 */
986 function isNullable() {
987 return !$this->info->notnull;
988 }
989
990 function type() {
991 return $this->info->type;
992 }
993 } // end SQLiteField