Completed
Branch master (f441c6)
by
unknown
52:29
created

DatabaseSqlite::selectDB()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->trxMode of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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 );
0 ignored issues
show
Security Bug introduced by
It seems like $err defined by false on line 167 can also be of type false; however, DBConnectionError::__construct() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
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;
0 ignored issues
show
Unused Code introduced by
$res is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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 ) {
0 ignored issues
show
Bug introduced by
The expression $rows of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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 ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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
Bug introduced by
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 ) {
0 ignored issues
show
Bug introduced by
The expression $indexList of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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 ) {
0 ignored issues
show
Bug introduced by
The expression $indexInfo of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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 ) {
0 ignored issues
show
Bug introduced by
The expression $result of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1007
			$vars = get_object_vars( $table );
1008
			$table = array_pop( $vars );
1009
1010
			if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $prefix of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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