Completed
Branch master (7e350b)
by
unknown
30:36
created

DatabaseSqlite::wasErrorReissuable()   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 0
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
35
	/** @var string File name for SQLite database file */
36
	protected $dbPath;
37
38
	/** @var string Transaction mode */
39
	protected $trxMode;
40
41
	/** @var int The number of rows affected as an integer */
42
	protected $mAffectedRows;
43
44
	/** @var resource */
45
	protected $mLastResult;
46
47
	/** @var PDO */
48
	protected $mConn;
49
50
	/** @var FSLockManager (hopefully on the same server as the DB) */
51
	protected $lockMgr;
52
53
	/**
54
	 * Additional params include:
55
	 *   - dbDirectory : directory containing the DB and the lock file directory
56
	 *                   [defaults to $wgSQLiteDataDir]
57
	 *   - dbFilePath  : use this to force the path of the DB file
58
	 *   - trxMode     : one of (deferred, immediate, exclusive)
59
	 * @param array $p
60
	 */
61
	function __construct( array $p ) {
62
		global $wgSharedDB, $wgSQLiteDataDir;
63
64
		$this->dbDir = isset( $p['dbDirectory'] ) ? $p['dbDirectory'] : $wgSQLiteDataDir;
65
66
		if ( isset( $p['dbFilePath'] ) ) {
67
			parent::__construct( $p );
68
			// Standalone .sqlite file mode.
69
			// Super doesn't open when $user is false, but we can work with $dbName,
70
			// which is derived from the file path in this case.
71
			$this->openFile( $p['dbFilePath'] );
72
		} else {
73
			$this->mDBname = $p['dbname'];
74
			// Stock wiki mode using standard file names per DB.
75
			parent::__construct( $p );
76
			// Super doesn't open when $user is false, but we can work with $dbName
77
			if ( $p['dbname'] && !$this->isOpen() ) {
78
				if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) {
79
					if ( $wgSharedDB ) {
80
						$this->attachDatabase( $wgSharedDB );
81
					}
82
				}
83
			}
84
		}
85
86
		$this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
87
		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...
88
			!in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] )
89
		) {
90
			$this->trxMode = null;
91
			wfWarn( "Invalid SQLite transaction mode provided." );
92
		}
93
94
		$this->lockMgr = new FSLockManager( [ 'lockDirectory' => "{$this->dbDir}/locks" ] );
95
	}
96
97
	/**
98
	 * @param string $filename
99
	 * @param array $p Options map; supports:
100
	 *   - flags       : (same as __construct counterpart)
101
	 *   - trxMode     : (same as __construct counterpart)
102
	 *   - dbDirectory : (same as __construct counterpart)
103
	 * @return DatabaseSqlite
104
	 * @since 1.25
105
	 */
106
	public static function newStandaloneInstance( $filename, array $p = [] ) {
107
		$p['dbFilePath'] = $filename;
108
		$p['schema'] = false;
109
		$p['tablePrefix'] = '';
110
111
		return DatabaseBase::factory( 'sqlite', $p );
112
	}
113
114
	/**
115
	 * @return string
116
	 */
117
	function getType() {
118
		return 'sqlite';
119
	}
120
121
	/**
122
	 * @todo Check if it should be true like parent class
123
	 *
124
	 * @return bool
125
	 */
126
	function implicitGroupby() {
127
		return false;
128
	}
129
130
	/** Open an SQLite database and return a resource handle to it
131
	 *  NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
132
	 *
133
	 * @param string $server
134
	 * @param string $user
135
	 * @param string $pass
136
	 * @param string $dbName
137
	 *
138
	 * @throws DBConnectionError
139
	 * @return PDO
140
	 */
141
	function open( $server, $user, $pass, $dbName ) {
142
		$this->close();
143
		$fileName = self::generateFileName( $this->dbDir, $dbName );
144
		if ( !is_readable( $fileName ) ) {
145
			$this->mConn = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<PDO> of property $mConn.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
146
			throw new DBConnectionError( $this, "SQLite database not accessible" );
147
		}
148
		$this->openFile( $fileName );
149
150
		return $this->mConn;
151
	}
152
153
	/**
154
	 * Opens a database file
155
	 *
156
	 * @param string $fileName
157
	 * @throws DBConnectionError
158
	 * @return PDO|bool SQL connection or false if failed
159
	 */
160
	protected function openFile( $fileName ) {
161
		$err = false;
162
163
		$this->dbPath = $fileName;
164
		try {
165
			if ( $this->mFlags & DBO_PERSISTENT ) {
166
				$this->mConn = new PDO( "sqlite:$fileName", '', '',
167
					[ PDO::ATTR_PERSISTENT => true ] );
168
			} else {
169
				$this->mConn = new PDO( "sqlite:$fileName", '', '' );
170
			}
171
		} catch ( PDOException $e ) {
172
			$err = $e->getMessage();
173
		}
174
175
		if ( !$this->mConn ) {
176
			wfDebug( "DB connection error: $err\n" );
177
			throw new DBConnectionError( $this, $err );
0 ignored issues
show
Security Bug introduced by
It seems like $err defined by false on line 161 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...
178
		}
179
180
		$this->mOpened = !!$this->mConn;
181
		if ( $this->mOpened ) {
182
			# Set error codes only, don't raise exceptions
183
			$this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
184
			# Enforce LIKE to be case sensitive, just like MySQL
185
			$this->query( 'PRAGMA case_sensitive_like = 1' );
186
187
			return $this->mConn;
188
		}
189
190
		return false;
191
	}
192
193
	/**
194
	 * @return string SQLite DB file path
195
	 * @since 1.25
196
	 */
197
	public function getDbFilePath() {
198
		return $this->dbPath;
199
	}
200
201
	/**
202
	 * Does not actually close the connection, just destroys the reference for GC to do its work
203
	 * @return bool
204
	 */
205
	protected function closeConnection() {
206
		$this->mConn = null;
207
208
		return true;
209
	}
210
211
	/**
212
	 * Generates a database file name. Explicitly public for installer.
213
	 * @param string $dir Directory where database resides
214
	 * @param string $dbName Database name
215
	 * @return string
216
	 */
217
	public static function generateFileName( $dir, $dbName ) {
218
		return "$dir/$dbName.sqlite";
219
	}
220
221
	/**
222
	 * Check if the searchindext table is FTS enabled.
223
	 * @return bool False if not enabled.
224
	 */
225
	function checkForEnabledSearch() {
226
		if ( self::$fulltextEnabled === null ) {
227
			self::$fulltextEnabled = false;
228
			$table = $this->tableName( 'searchindex' );
229
			$res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
230
			if ( $res ) {
231
				$row = $res->fetchRow();
232
				self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
233
			}
234
		}
235
236
		return self::$fulltextEnabled;
237
	}
238
239
	/**
240
	 * Returns version of currently supported SQLite fulltext search module or false if none present.
241
	 * @return string
242
	 */
243
	static function getFulltextSearchModule() {
244
		static $cachedResult = null;
245
		if ( $cachedResult !== null ) {
246
			return $cachedResult;
247
		}
248
		$cachedResult = false;
249
		$table = 'dummy_search_test';
250
251
		$db = self::newStandaloneInstance( ':memory:' );
252
		if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
253
			$cachedResult = 'FTS3';
254
		}
255
		$db->close();
256
257
		return $cachedResult;
258
	}
259
260
	/**
261
	 * Attaches external database to our connection, see http://sqlite.org/lang_attach.html
262
	 * for details.
263
	 *
264
	 * @param string $name Database name to be used in queries like
265
	 *   SELECT foo FROM dbname.table
266
	 * @param bool|string $file Database file name. If omitted, will be generated
267
	 *   using $name and configured data directory
268
	 * @param string $fname Calling function name
269
	 * @return ResultWrapper
270
	 */
271
	function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
272
		if ( !$file ) {
273
			$file = self::generateFileName( $this->dbDir, $name );
274
		}
275
		$file = $this->addQuotes( $file );
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->addQuotes($file) on line 275 can also be of type boolean; however, DatabaseSqlite::addQuotes() does only seem to accept object<Blob>|string, 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...
276
277
		return $this->query( "ATTACH DATABASE $file AS $name", $fname );
278
	}
279
280
	/**
281
	 * @see DatabaseBase::isWriteQuery()
282
	 *
283
	 * @param string $sql
284
	 * @return bool
285
	 */
286
	function isWriteQuery( $sql ) {
287
		return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
288
	}
289
290
	/**
291
	 * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
292
	 *
293
	 * @param string $sql
294
	 * @return bool|ResultWrapper
295
	 */
296
	protected function doQuery( $sql ) {
297
		$res = $this->mConn->query( $sql );
298
		if ( $res === false ) {
299
			return false;
300
		} else {
301
			$r = $res instanceof ResultWrapper ? $res->result : $res;
302
			$this->mAffectedRows = $r->rowCount();
303
			$res = new ResultWrapper( $this, $r->fetchAll() );
304
		}
305
306
		return $res;
307
	}
308
309
	/**
310
	 * @param ResultWrapper|mixed $res
311
	 */
312
	function freeResult( $res ) {
313
		if ( $res instanceof ResultWrapper ) {
314
			$res->result = null;
315
		} else {
316
			$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...
317
		}
318
	}
319
320
	/**
321
	 * @param ResultWrapper|array $res
322
	 * @return stdClass|bool
323
	 */
324
	function fetchObject( $res ) {
325
		if ( $res instanceof ResultWrapper ) {
326
			$r =& $res->result;
327
		} else {
328
			$r =& $res;
329
		}
330
331
		$cur = current( $r );
332
		if ( is_array( $cur ) ) {
333
			next( $r );
334
			$obj = new stdClass;
335
			foreach ( $cur as $k => $v ) {
336
				if ( !is_numeric( $k ) ) {
337
					$obj->$k = $v;
338
				}
339
			}
340
341
			return $obj;
342
		}
343
344
		return false;
345
	}
346
347
	/**
348
	 * @param ResultWrapper|mixed $res
349
	 * @return array|bool
350
	 */
351
	function fetchRow( $res ) {
352
		if ( $res instanceof ResultWrapper ) {
353
			$r =& $res->result;
354
		} else {
355
			$r =& $res;
356
		}
357
		$cur = current( $r );
358
		if ( is_array( $cur ) ) {
359
			next( $r );
360
361
			return $cur;
362
		}
363
364
		return false;
365
	}
366
367
	/**
368
	 * The PDO::Statement class implements the array interface so count() will work
369
	 *
370
	 * @param ResultWrapper|array $res
371
	 * @return int
372
	 */
373
	function numRows( $res ) {
374
		$r = $res instanceof ResultWrapper ? $res->result : $res;
375
376
		return count( $r );
377
	}
378
379
	/**
380
	 * @param ResultWrapper $res
381
	 * @return int
382
	 */
383
	function numFields( $res ) {
384
		$r = $res instanceof ResultWrapper ? $res->result : $res;
385
		if ( is_array( $r ) && count( $r ) > 0 ) {
386
			// The size of the result array is twice the number of fields. (Bug: 65578)
387
			return count( $r[0] ) / 2;
388
		} else {
389
			// If the result is empty return 0
390
			return 0;
391
		}
392
	}
393
394
	/**
395
	 * @param ResultWrapper $res
396
	 * @param int $n
397
	 * @return bool
398
	 */
399
	function fieldName( $res, $n ) {
400
		$r = $res instanceof ResultWrapper ? $res->result : $res;
401
		if ( is_array( $r ) ) {
402
			$keys = array_keys( $r[0] );
403
404
			return $keys[$n];
405
		}
406
407
		return false;
408
	}
409
410
	/**
411
	 * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
412
	 *
413
	 * @param string $name
414
	 * @param string $format
415
	 * @return string
416
	 */
417
	function tableName( $name, $format = 'quoted' ) {
418
		// table names starting with sqlite_ are reserved
419
		if ( strpos( $name, 'sqlite_' ) === 0 ) {
420
			return $name;
421
		}
422
423
		return str_replace( '"', '', parent::tableName( $name, $format ) );
424
	}
425
426
	/**
427
	 * Index names have DB scope
428
	 *
429
	 * @param string $index
430
	 * @return string
431
	 */
432
	protected function indexName( $index ) {
433
		return $index;
434
	}
435
436
	/**
437
	 * This must be called after nextSequenceVal
438
	 *
439
	 * @return int
440
	 */
441
	function insertId() {
442
		// PDO::lastInsertId yields a string :(
443
		return intval( $this->mConn->lastInsertId() );
444
	}
445
446
	/**
447
	 * @param ResultWrapper|array $res
448
	 * @param int $row
449
	 */
450
	function dataSeek( $res, $row ) {
451
		if ( $res instanceof ResultWrapper ) {
452
			$r =& $res->result;
453
		} else {
454
			$r =& $res;
455
		}
456
		reset( $r );
457
		if ( $row > 0 ) {
458
			for ( $i = 0; $i < $row; $i++ ) {
459
				next( $r );
460
			}
461
		}
462
	}
463
464
	/**
465
	 * @return string
466
	 */
467
	function lastError() {
468
		if ( !is_object( $this->mConn ) ) {
469
			return "Cannot return last error, no db connection";
470
		}
471
		$e = $this->mConn->errorInfo();
472
473
		return isset( $e[2] ) ? $e[2] : '';
474
	}
475
476
	/**
477
	 * @return string
478
	 */
479
	function lastErrno() {
480
		if ( !is_object( $this->mConn ) ) {
481
			return "Cannot return last error, no db connection";
482
		} else {
483
			$info = $this->mConn->errorInfo();
484
485
			return $info[1];
486
		}
487
	}
488
489
	/**
490
	 * @return int
491
	 */
492
	function affectedRows() {
493
		return $this->mAffectedRows;
494
	}
495
496
	/**
497
	 * Returns information about an index
498
	 * Returns false if the index does not exist
499
	 * - if errors are explicitly ignored, returns NULL on failure
500
	 *
501
	 * @param string $table
502
	 * @param string $index
503
	 * @param string $fname
504
	 * @return array
505
	 */
506
	function indexInfo( $table, $index, $fname = __METHOD__ ) {
507
		$sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
508
		$res = $this->query( $sql, $fname );
509
		if ( !$res ) {
510
			return null;
511
		}
512
		if ( $res->numRows() == 0 ) {
513
			return false;
514
		}
515
		$info = [];
516
		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...
517
			$info[] = $row->name;
518
		}
519
520
		return $info;
521
	}
522
523
	/**
524
	 * @param string $table
525
	 * @param string $index
526
	 * @param string $fname
527
	 * @return bool|null
528
	 */
529
	function indexUnique( $table, $index, $fname = __METHOD__ ) {
530
		$row = $this->selectRow( 'sqlite_master', '*',
531
			[
532
				'type' => 'index',
533
				'name' => $this->indexName( $index ),
534
			], $fname );
535
		if ( !$row || !isset( $row->sql ) ) {
536
			return null;
537
		}
538
539
		// $row->sql will be of the form CREATE [UNIQUE] INDEX ...
540
		$indexPos = strpos( $row->sql, 'INDEX' );
541
		if ( $indexPos === false ) {
542
			return null;
543
		}
544
		$firstPart = substr( $row->sql, 0, $indexPos );
545
		$options = explode( ' ', $firstPart );
546
547
		return in_array( 'UNIQUE', $options );
548
	}
549
550
	/**
551
	 * Filter the options used in SELECT statements
552
	 *
553
	 * @param array $options
554
	 * @return array
555
	 */
556
	function makeSelectOptions( $options ) {
557
		foreach ( $options as $k => $v ) {
558
			if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
559
				$options[$k] = '';
560
			}
561
		}
562
563
		return parent::makeSelectOptions( $options );
564
	}
565
566
	/**
567
	 * @param array $options
568
	 * @return string
569
	 */
570
	protected function makeUpdateOptionsArray( $options ) {
571
		$options = parent::makeUpdateOptionsArray( $options );
572
		$options = self::fixIgnore( $options );
573
574
		return $options;
575
	}
576
577
	/**
578
	 * @param array $options
579
	 * @return array
580
	 */
581
	static function fixIgnore( $options ) {
582
		# SQLite uses OR IGNORE not just IGNORE
583
		foreach ( $options as $k => $v ) {
584
			if ( $v == 'IGNORE' ) {
585
				$options[$k] = 'OR IGNORE';
586
			}
587
		}
588
589
		return $options;
590
	}
591
592
	/**
593
	 * @param array $options
594
	 * @return string
595
	 */
596
	function makeInsertOptions( $options ) {
597
		$options = self::fixIgnore( $options );
598
599
		return parent::makeInsertOptions( $options );
600
	}
601
602
	/**
603
	 * Based on generic method (parent) with some prior SQLite-sepcific adjustments
604
	 * @param string $table
605
	 * @param array $a
606
	 * @param string $fname
607
	 * @param array $options
608
	 * @return bool
609
	 */
610
	function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
611
		if ( !count( $a ) ) {
612
			return true;
613
		}
614
615
		# SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
616
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
617
			$ret = true;
618
			foreach ( $a as $v ) {
619
				if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
620
					$ret = false;
621
				}
622
			}
623
		} else {
624
			$ret = parent::insert( $table, $a, "$fname/single-row", $options );
625
		}
626
627
		return $ret;
628
	}
629
630
	/**
631
	 * @param string $table
632
	 * @param array $uniqueIndexes Unused
633
	 * @param string|array $rows
634
	 * @param string $fname
635
	 * @return bool|ResultWrapper
636
	 */
637
	function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
638
		if ( !count( $rows ) ) {
639
			return true;
640
		}
641
642
		# SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
643
		if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
644
			$ret = true;
645
			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...
646
				if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
647
					$ret = false;
648
				}
649
			}
650
		} else {
651
			$ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
652
		}
653
654
		return $ret;
655
	}
656
657
	/**
658
	 * Returns the size of a text field, or -1 for "unlimited"
659
	 * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though.
660
	 *
661
	 * @param string $table
662
	 * @param string $field
663
	 * @return int
664
	 */
665
	function textFieldSize( $table, $field ) {
666
		return -1;
667
	}
668
669
	/**
670
	 * @return bool
671
	 */
672
	function unionSupportsOrderAndLimit() {
673
		return false;
674
	}
675
676
	/**
677
	 * @param string $sqls
678
	 * @param bool $all Whether to "UNION ALL" or not
679
	 * @return string
680
	 */
681
	function unionQueries( $sqls, $all ) {
682
		$glue = $all ? ' UNION ALL ' : ' UNION ';
683
684
		return implode( $glue, $sqls );
685
	}
686
687
	/**
688
	 * @return bool
689
	 */
690
	function wasDeadlock() {
691
		return $this->lastErrno() == 5; // SQLITE_BUSY
692
	}
693
694
	/**
695
	 * @return bool
696
	 */
697
	function wasErrorReissuable() {
698
		return $this->lastErrno() == 17; // SQLITE_SCHEMA;
699
	}
700
701
	/**
702
	 * @return bool
703
	 */
704
	function wasReadOnlyError() {
705
		return $this->lastErrno() == 8; // SQLITE_READONLY;
706
	}
707
708
	/**
709
	 * @return string Wikitext of a link to the server software's web site
710
	 */
711
	public function getSoftwareLink() {
712
		return "[{{int:version-db-sqlite-url}} SQLite]";
713
	}
714
715
	/**
716
	 * @return string Version information from the database
717
	 */
718
	function getServerVersion() {
719
		$ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
720
721
		return $ver;
722
	}
723
724
	/**
725
	 * @return string User-friendly database information
726
	 */
727
	public function getServerInfo() {
728
		return wfMessage( self::getFulltextSearchModule()
729
			? 'sqlite-has-fts'
730
			: 'sqlite-no-fts', $this->getServerVersion() )->text();
731
	}
732
733
	/**
734
	 * Get information about a given field
735
	 * Returns false if the field does not exist.
736
	 *
737
	 * @param string $table
738
	 * @param string $field
739
	 * @return SQLiteField|bool False on failure
740
	 */
741
	function fieldInfo( $table, $field ) {
742
		$tableName = $this->tableName( $table );
743
		$sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
744
		$res = $this->query( $sql, __METHOD__ );
745
		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...
746
			if ( $row->name == $field ) {
747
				return new SQLiteField( $row, $tableName );
748
			}
749
		}
750
751
		return false;
752
	}
753
754
	protected function doBegin( $fname = '' ) {
755
		if ( $this->trxMode ) {
756
			$this->query( "BEGIN {$this->trxMode}", $fname );
757
		} else {
758
			$this->query( 'BEGIN', $fname );
759
		}
760
		$this->mTrxLevel = 1;
761
	}
762
763
	/**
764
	 * @param string $s
765
	 * @return string
766
	 */
767
	function strencode( $s ) {
768
		return substr( $this->addQuotes( $s ), 1, -1 );
769
	}
770
771
	/**
772
	 * @param string $b
773
	 * @return Blob
774
	 */
775
	function encodeBlob( $b ) {
776
		return new Blob( $b );
777
	}
778
779
	/**
780
	 * @param Blob|string $b
781
	 * @return string
782
	 */
783
	function decodeBlob( $b ) {
784
		if ( $b instanceof Blob ) {
785
			$b = $b->fetch();
786
		}
787
788
		return $b;
789
	}
790
791
	/**
792
	 * @param Blob|string $s
793
	 * @return string
794
	 */
795
	function addQuotes( $s ) {
796
		if ( $s instanceof Blob ) {
797
			return "x'" . bin2hex( $s->fetch() ) . "'";
798
		} elseif ( is_bool( $s ) ) {
799
			return (int)$s;
800
		} elseif ( strpos( $s, "\0" ) !== false ) {
801
			// SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
802
			// This is a known limitation of SQLite's mprintf function which PDO
803
			// should work around, but doesn't. I have reported this to php.net as bug #63419:
804
			// https://bugs.php.net/bug.php?id=63419
805
			// There was already a similar report for SQLite3::escapeString, bug #62361:
806
			// https://bugs.php.net/bug.php?id=62361
807
			// There is an additional bug regarding sorting this data after insert
808
			// on older versions of sqlite shipped with ubuntu 12.04
809
			// https://phabricator.wikimedia.org/T74367
810
			wfDebugLog(
811
				__CLASS__,
812
				__FUNCTION__ .
813
					': Quoting value containing null byte. ' .
814
					'For consistency all binary data should have been ' .
815
					'first processed with self::encodeBlob()'
816
			);
817
			return "x'" . bin2hex( $s ) . "'";
818
		} else {
819
			return $this->mConn->quote( $s );
820
		}
821
	}
822
823
	/**
824
	 * @return string
825
	 */
826 View Code Duplication
	function buildLike() {
827
		$params = func_get_args();
828
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
829
			$params = $params[0];
830
		}
831
832
		return parent::buildLike( $params ) . "ESCAPE '\' ";
833
	}
834
835
	/**
836
	 * @param string $field Field or column to cast
837
	 * @return string
838
	 * @since 1.28
839
	 */
840
	public function buildStringCast( $field ) {
841
		return 'CAST ( ' . $field . ' AS TEXT )';
842
	}
843
844
	/**
845
	 * @return string
846
	 */
847
	public function getSearchEngine() {
848
		return "SearchSqlite";
849
	}
850
851
	/**
852
	 * No-op version of deadlockLoop
853
	 *
854
	 * @return mixed
855
	 */
856
	public function deadlockLoop( /*...*/ ) {
857
		$args = func_get_args();
858
		$function = array_shift( $args );
859
860
		return call_user_func_array( $function, $args );
861
	}
862
863
	/**
864
	 * @param string $s
865
	 * @return string
866
	 */
867
	protected function replaceVars( $s ) {
868
		$s = parent::replaceVars( $s );
869
		if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
870
			// CREATE TABLE hacks to allow schema file sharing with MySQL
871
872
			// binary/varbinary column type -> blob
873
			$s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
874
			// no such thing as unsigned
875
			$s = preg_replace( '/\b(un)?signed\b/i', '', $s );
876
			// INT -> INTEGER
877
			$s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
878
			// floating point types -> REAL
879
			$s = preg_replace(
880
				'/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i',
881
				'REAL',
882
				$s
883
			);
884
			// varchar -> TEXT
885
			$s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
886
			// TEXT normalization
887
			$s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
888
			// BLOB normalization
889
			$s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
890
			// BOOL -> INTEGER
891
			$s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
892
			// DATETIME -> TEXT
893
			$s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
894
			// No ENUM type
895
			$s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
896
			// binary collation type -> nothing
897
			$s = preg_replace( '/\bbinary\b/i', '', $s );
898
			// auto_increment -> autoincrement
899
			$s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
900
			// No explicit options
901
			$s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
902
			// AUTOINCREMENT should immedidately follow PRIMARY KEY
903
			$s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
904
		} elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
905
			// No truncated indexes
906
			$s = preg_replace( '/\(\d+\)/', '', $s );
907
			// No FULLTEXT
908
			$s = preg_replace( '/\bfulltext\b/i', '', $s );
909
		} elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
910
			// DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
911
			$s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
912
		} elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
913
			// INSERT IGNORE --> INSERT OR IGNORE
914
			$s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
915
		}
916
917
		return $s;
918
	}
919
920
	public function lock( $lockName, $method, $timeout = 5 ) {
921
		if ( !is_dir( "{$this->dbDir}/locks" ) ) { // create dir as needed
922
			if ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) ) {
923
				throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." );
924
			}
925
		}
926
927
		return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK();
928
	}
929
930
	public function unlock( $lockName, $method ) {
931
		return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isOK();
932
	}
933
934
	/**
935
	 * Build a concatenation list to feed into a SQL query
936
	 *
937
	 * @param string[] $stringList
938
	 * @return string
939
	 */
940
	function buildConcat( $stringList ) {
941
		return '(' . implode( ') || (', $stringList ) . ')';
942
	}
943
944 View Code Duplication
	public function buildGroupConcatField(
945
		$delim, $table, $field, $conds = '', $join_conds = []
946
	) {
947
		$fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
948
949
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
950
	}
951
952
	/**
953
	 * @throws MWException
954
	 * @param string $oldName
955
	 * @param string $newName
956
	 * @param bool $temporary
957
	 * @param string $fname
958
	 * @return bool|ResultWrapper
959
	 */
960
	function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
961
		$res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" .
962
			$this->addQuotes( $oldName ) . " AND type='table'", $fname );
963
		$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 961 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...
964
		if ( !$obj ) {
965
			throw new MWException( "Couldn't retrieve structure for table $oldName" );
966
		}
967
		$sql = $obj->sql;
968
		$sql = preg_replace(
969
			'/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/',
970
			$this->addIdentifierQuotes( $newName ),
971
			$sql,
972
			1
973
		);
974
		if ( $temporary ) {
975
			if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
976
				wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" );
977
			} else {
978
				$sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
979
			}
980
		}
981
982
		$res = $this->query( $sql, $fname );
983
984
		// Take over indexes
985
		$indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
986
		foreach ( $indexList as $index ) {
0 ignored issues
show
Bug introduced by
The expression $indexList 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...
987
			if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
988
				continue;
989
			}
990
991
			if ( $index->unique ) {
992
				$sql = 'CREATE UNIQUE INDEX';
993
			} else {
994
				$sql = 'CREATE INDEX';
995
			}
996
			// Try to come up with a new index name, given indexes have database scope in SQLite
997
			$indexName = $newName . '_' . $index->name;
998
			$sql .= ' ' . $indexName . ' ON ' . $newName;
999
1000
			$indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' );
1001
			$fields = [];
1002
			foreach ( $indexInfo as $indexInfoRow ) {
0 ignored issues
show
Bug introduced by
The expression $indexInfo 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...
1003
				$fields[$indexInfoRow->seqno] = $indexInfoRow->name;
1004
			}
1005
1006
			$sql .= '(' . implode( ',', $fields ) . ')';
1007
1008
			$this->query( $sql );
1009
		}
1010
1011
		return $res;
1012
	}
1013
1014
	/**
1015
	 * List all tables on the database
1016
	 *
1017
	 * @param string $prefix Only show tables with this prefix, e.g. mw_
1018
	 * @param string $fname Calling function name
1019
	 *
1020
	 * @return array
1021
	 */
1022
	function listTables( $prefix = null, $fname = __METHOD__ ) {
1023
		$result = $this->select(
1024
			'sqlite_master',
1025
			'name',
1026
			"type='table'"
1027
		);
1028
1029
		$endArray = [];
1030
1031 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...
1032
			$vars = get_object_vars( $table );
1033
			$table = array_pop( $vars );
1034
1035
			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...
1036
				if ( strpos( $table, 'sqlite_' ) !== 0 ) {
1037
					$endArray[] = $table;
1038
				}
1039
			}
1040
		}
1041
1042
		return $endArray;
1043
	}
1044
1045
	/**
1046
	 * @return string
1047
	 */
1048
	public function __toString() {
1049
		return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
1050
	}
1051
1052
} // end DatabaseSqlite class
1053
1054
/**
1055
 * @ingroup Database
1056
 */
1057
class SQLiteField implements Field {
1058
	private $info, $tableName;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
1059
1060
	function __construct( $info, $tableName ) {
1061
		$this->info = $info;
1062
		$this->tableName = $tableName;
1063
	}
1064
1065
	function name() {
1066
		return $this->info->name;
1067
	}
1068
1069
	function tableName() {
1070
		return $this->tableName;
1071
	}
1072
1073
	function defaultValue() {
1074
		if ( is_string( $this->info->dflt_value ) ) {
1075
			// Typically quoted
1076
			if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) {
1077
				return str_replace( "''", "'", $this->info->dflt_value );
1078
			}
1079
		}
1080
1081
		return $this->info->dflt_value;
1082
	}
1083
1084
	/**
1085
	 * @return bool
1086
	 */
1087
	function isNullable() {
1088
		return !$this->info->notnull;
1089
	}
1090
1091
	function type() {
1092
		return $this->info->type;
1093
	}
1094
} // end SQLiteField
1095