Completed
Branch master (939199)
by
unknown
39:35
created

includes/libs/rdbms/database/DatabaseSqlite.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
938
		if ( !$obj ) {
939
			throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
940
		}
941
		$sql = $obj->sql;
942
		$sql = preg_replace(
943
			'/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/',
944
			$this->addIdentifierQuotes( $newName ),
945
			$sql,
946
			1
947
		);
948
		if ( $temporary ) {
949
			if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
950
				$this->queryLogger->debug(
951
					"Table $oldName is virtual, can't create a temporary duplicate.\n" );
952
			} else {
953
				$sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
954
			}
955
		}
956
957
		$res = $this->query( $sql, $fname );
958
959
		// Take over indexes
960
		$indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
961
		foreach ( $indexList as $index ) {
962
			if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
963
				continue;
964
			}
965
966
			if ( $index->unique ) {
967
				$sql = 'CREATE UNIQUE INDEX';
968
			} else {
969
				$sql = 'CREATE INDEX';
970
			}
971
			// Try to come up with a new index name, given indexes have database scope in SQLite
972
			$indexName = $newName . '_' . $index->name;
973
			$sql .= ' ' . $indexName . ' ON ' . $newName;
974
975
			$indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' );
976
			$fields = [];
977
			foreach ( $indexInfo as $indexInfoRow ) {
978
				$fields[$indexInfoRow->seqno] = $indexInfoRow->name;
979
			}
980
981
			$sql .= '(' . implode( ',', $fields ) . ')';
982
983
			$this->query( $sql );
984
		}
985
986
		return $res;
987
	}
988
989
	/**
990
	 * List all tables on the database
991
	 *
992
	 * @param string $prefix Only show tables with this prefix, e.g. mw_
993
	 * @param string $fname Calling function name
994
	 *
995
	 * @return array
996
	 */
997
	function listTables( $prefix = null, $fname = __METHOD__ ) {
998
		$result = $this->select(
999
			'sqlite_master',
1000
			'name',
1001
			"type='table'"
1002
		);
1003
1004
		$endArray = [];
1005
1006 View Code Duplication
		foreach ( $result as $table ) {
1007
			$vars = get_object_vars( $table );
1008
			$table = array_pop( $vars );
1009
1010
			if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
1011
				if ( strpos( $table, 'sqlite_' ) !== 0 ) {
1012
					$endArray[] = $table;
1013
				}
1014
			}
1015
		}
1016
1017
		return $endArray;
1018
	}
1019
1020
	/**
1021
	 * Override due to no CASCADE support
1022
	 *
1023
	 * @param string $tableName
1024
	 * @param string $fName
1025
	 * @return bool|ResultWrapper
1026
	 * @throws DBReadOnlyError
1027
	 */
1028 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
1029
		if ( !$this->tableExists( $tableName, $fName ) ) {
1030
			return false;
1031
		}
1032
		$sql = "DROP TABLE " . $this->tableName( $tableName );
1033
1034
		return $this->query( $sql, $fName );
1035
	}
1036
1037
	protected function requiresDatabaseUser() {
1038
		return false; // just a file
1039
	}
1040
1041
	/**
1042
	 * @return string
1043
	 */
1044
	public function __toString() {
1045
		return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
1046
	}
1047
}
1048