Completed
Branch master (8d5465)
by
unknown
31:25
created

DatabasePostgres::nativeInsertSelect()   C

Complexity

Conditions 8
Paths 96

Size

Total Lines 62
Code Lines 38

Duplication

Lines 11
Ratio 17.74 %

Importance

Changes 0
Metric Value
cc 8
eloc 38
nc 96
nop 7
dl 11
loc 62
rs 6.943
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This is the Postgres database abstraction layer.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Database
22
 */
23
24
class PostgresField implements Field {
25
	private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname,
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...
26
		$has_default, $default;
27
28
	/**
29
	 * @param DatabaseBase $db
30
	 * @param string $table
31
	 * @param string $field
32
	 * @return null|PostgresField
33
	 */
34
	static function fromText( $db, $table, $field ) {
35
		$q = <<<SQL
36
SELECT
37
 attnotnull, attlen, conname AS conname,
38
 atthasdef,
39
 adsrc,
40
 COALESCE(condeferred, 'f') AS deferred,
41
 COALESCE(condeferrable, 'f') AS deferrable,
42
 CASE WHEN typname = 'int2' THEN 'smallint'
43
  WHEN typname = 'int4' THEN 'integer'
44
  WHEN typname = 'int8' THEN 'bigint'
45
  WHEN typname = 'bpchar' THEN 'char'
46
 ELSE typname END AS typname
47
FROM pg_class c
48
JOIN pg_namespace n ON (n.oid = c.relnamespace)
49
JOIN pg_attribute a ON (a.attrelid = c.oid)
50
JOIN pg_type t ON (t.oid = a.atttypid)
51
LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f')
52
LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum
53
WHERE relkind = 'r'
54
AND nspname=%s
55
AND relname=%s
56
AND attname=%s;
57
SQL;
58
59
		$table = $db->tableName( $table, 'raw' );
60
		$res = $db->query(
61
			sprintf( $q,
62
				$db->addQuotes( $db->getCoreSchema() ),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class DatabaseBase as the method getCoreSchema() does only exist in the following sub-classes of DatabaseBase: DatabasePostgres. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
63
				$db->addQuotes( $table ),
64
				$db->addQuotes( $field )
65
			)
66
		);
67
		$row = $db->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $db->query(sprintf($q, $...db->addQuotes($field))) on line 60 can also be of type boolean; however, IDatabase::fetchObject() does only seem to accept object<ResultWrapper>|object<stdClass>, 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...
68
		if ( !$row ) {
69
			return null;
70
		}
71
		$n = new PostgresField;
72
		$n->type = $row->typname;
73
		$n->nullable = ( $row->attnotnull == 'f' );
74
		$n->name = $field;
75
		$n->tablename = $table;
76
		$n->max_length = $row->attlen;
77
		$n->deferrable = ( $row->deferrable == 't' );
78
		$n->deferred = ( $row->deferred == 't' );
79
		$n->conname = $row->conname;
80
		$n->has_default = ( $row->atthasdef === 't' );
81
		$n->default = $row->adsrc;
82
83
		return $n;
84
	}
85
86
	function name() {
87
		return $this->name;
88
	}
89
90
	function tableName() {
91
		return $this->tablename;
92
	}
93
94
	function type() {
95
		return $this->type;
96
	}
97
98
	function isNullable() {
99
		return $this->nullable;
100
	}
101
102
	function maxLength() {
103
		return $this->max_length;
104
	}
105
106
	function is_deferrable() {
107
		return $this->deferrable;
108
	}
109
110
	function is_deferred() {
111
		return $this->deferred;
112
	}
113
114
	function conname() {
115
		return $this->conname;
116
	}
117
118
	/**
119
	 * @since 1.19
120
	 * @return bool|mixed
121
	 */
122
	function defaultValue() {
123
		if ( $this->has_default ) {
124
			return $this->default;
125
		} else {
126
			return false;
127
		}
128
	}
129
}
130
131
/**
132
 * Manage savepoints within a transaction
133
 * @ingroup Database
134
 * @since 1.19
135
 */
136
class SavepointPostgres {
137
	/** @var DatabasePostgres Establish a savepoint within a transaction */
138
	protected $dbw;
139
	protected $id;
140
	protected $didbegin;
141
142
	/**
143
	 * @param DatabaseBase $dbw
144
	 * @param int $id
145
	 */
146
	public function __construct( $dbw, $id ) {
147
		$this->dbw = $dbw;
0 ignored issues
show
Documentation Bug introduced by
$dbw is of type object<DatabaseBase>, but the property $dbw was declared to be of type object<DatabasePostgres>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
148
		$this->id = $id;
149
		$this->didbegin = false;
150
		/* If we are not in a transaction, we need to be for savepoint trickery */
151
		if ( !$dbw->trxLevel() ) {
152
			$dbw->begin( "FOR SAVEPOINT", DatabasePostgres::TRANSACTION_INTERNAL );
153
			$this->didbegin = true;
154
		}
155
	}
156
157
	public function __destruct() {
158
		if ( $this->didbegin ) {
159
			$this->dbw->rollback();
160
			$this->didbegin = false;
161
		}
162
	}
163
164
	public function commit() {
165
		if ( $this->didbegin ) {
166
			$this->dbw->commit();
167
			$this->didbegin = false;
168
		}
169
	}
170
171
	protected function query( $keyword, $msg_ok, $msg_failed ) {
172
		if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
173
		} else {
174
			wfDebug( sprintf( $msg_failed, $this->id ) );
175
		}
176
	}
177
178
	public function savepoint() {
179
		$this->query( "SAVEPOINT",
180
			"Transaction state: savepoint \"%s\" established.\n",
181
			"Transaction state: establishment of savepoint \"%s\" FAILED.\n"
182
		);
183
	}
184
185
	public function release() {
186
		$this->query( "RELEASE",
187
			"Transaction state: savepoint \"%s\" released.\n",
188
			"Transaction state: release of savepoint \"%s\" FAILED.\n"
189
		);
190
	}
191
192
	public function rollback() {
193
		$this->query( "ROLLBACK TO",
194
			"Transaction state: savepoint \"%s\" rolled back.\n",
195
			"Transaction state: rollback of savepoint \"%s\" FAILED.\n"
196
		);
197
	}
198
199
	public function __toString() {
200
		return (string)$this->id;
201
	}
202
}
203
204
/**
205
 * @ingroup Database
206
 */
207
class DatabasePostgres extends Database {
208
	/** @var resource */
209
	protected $mLastResult = null;
210
211
	/** @var int The number of rows affected as an integer */
212
	protected $mAffectedRows = null;
213
214
	/** @var int */
215
	private $mInsertId = null;
216
217
	/** @var float|string */
218
	private $numericVersion = null;
219
220
	/** @var string Connect string to open a PostgreSQL connection */
221
	private $connectString;
222
223
	/** @var string */
224
	private $mCoreSchema;
225
226
	function getType() {
227
		return 'postgres';
228
	}
229
230
	function cascadingDeletes() {
231
		return true;
232
	}
233
234
	function cleanupTriggers() {
235
		return true;
236
	}
237
238
	function strictIPs() {
239
		return true;
240
	}
241
242
	function realTimestamps() {
243
		return true;
244
	}
245
246
	function implicitGroupby() {
247
		return false;
248
	}
249
250
	function implicitOrderby() {
251
		return false;
252
	}
253
254
	function searchableIPs() {
255
		return true;
256
	}
257
258
	function functionalIndexes() {
259
		return true;
260
	}
261
262
	function hasConstraint( $name ) {
263
		$sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
264
			"WHERE c.connamespace = n.oid AND conname = '" .
265
			pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" .
266
			pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
267
		$res = $this->doQuery( $sql );
268
269
		return $this->numRows( $res );
270
	}
271
272
	/**
273
	 * Usually aborts on failure
274
	 * @param string $server
275
	 * @param string $user
276
	 * @param string $password
277
	 * @param string $dbName
278
	 * @throws DBConnectionError|Exception
279
	 * @return DatabaseBase|null
280
	 */
281
	function open( $server, $user, $password, $dbName ) {
282
		# Test for Postgres support, to avoid suppressed fatal error
283
		if ( !function_exists( 'pg_connect' ) ) {
284
			throw new DBConnectionError(
285
				$this,
286
				"Postgres functions missing, have you compiled PHP with the --with-pgsql\n" .
287
				"option? (Note: if you recently installed PHP, you may need to restart your\n" .
288
				"webserver and database)\n"
289
			);
290
		}
291
292
		global $wgDBport;
293
294
		if ( !strlen( $user ) ) { # e.g. the class is being loaded
295
			return null;
296
		}
297
298
		$this->mServer = $server;
299
		$port = $wgDBport;
300
		$this->mUser = $user;
301
		$this->mPassword = $password;
302
		$this->mDBname = $dbName;
303
304
		$connectVars = [
305
			'dbname' => $dbName,
306
			'user' => $user,
307
			'password' => $password
308
		];
309
		if ( $server != false && $server != '' ) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $server of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
310
			$connectVars['host'] = $server;
311
		}
312
		if ( $port != false && $port != '' ) {
313
			$connectVars['port'] = $port;
314
		}
315
		if ( $this->mFlags & DBO_SSL ) {
316
			$connectVars['sslmode'] = 1;
317
		}
318
319
		$this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW );
320
		$this->close();
321
		$this->installErrorHandler();
322
323
		try {
324
			$this->mConn = pg_connect( $this->connectString );
325
		} catch ( Exception $ex ) {
326
			$this->restoreErrorHandler();
327
			throw $ex;
328
		}
329
330
		$phpError = $this->restoreErrorHandler();
331
332
		if ( !$this->mConn ) {
333
			wfDebug( "DB connection error\n" );
334
			wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " .
335
				substr( $password, 0, 3 ) . "...\n" );
336
			wfDebug( $this->lastError() . "\n" );
337
			throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
338
		}
339
340
		$this->mOpened = true;
341
342
		global $wgCommandLineMode;
343
		# If called from the command-line (e.g. importDump), only show errors
344
		if ( $wgCommandLineMode ) {
345
			$this->doQuery( "SET client_min_messages = 'ERROR'" );
346
		}
347
348
		$this->query( "SET client_encoding='UTF8'", __METHOD__ );
349
		$this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
350
		$this->query( "SET timezone = 'GMT'", __METHOD__ );
351
		$this->query( "SET standard_conforming_strings = on", __METHOD__ );
352
		if ( $this->getServerVersion() >= 9.0 ) {
353
			$this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
354
		}
355
356
		global $wgDBmwschema;
357
		$this->determineCoreSchema( $wgDBmwschema );
358
359
		return $this->mConn;
360
	}
361
362
	/**
363
	 * Postgres doesn't support selectDB in the same way MySQL does. So if the
364
	 * DB name doesn't match the open connection, open a new one
365
	 * @param string $db
366
	 * @return bool
367
	 */
368
	function selectDB( $db ) {
369
		if ( $this->mDBname !== $db ) {
370
			return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
371
		} else {
372
			return true;
373
		}
374
	}
375
376
	function makeConnectionString( $vars ) {
377
		$s = '';
378
		foreach ( $vars as $name => $value ) {
379
			$s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
380
		}
381
382
		return $s;
383
	}
384
385
	/**
386
	 * Closes a database connection, if it is open
387
	 * Returns success, true if already closed
388
	 * @return bool
389
	 */
390
	protected function closeConnection() {
391
		return pg_close( $this->mConn );
392
	}
393
394
	public function doQuery( $sql ) {
395
		$sql = mb_convert_encoding( $sql, 'UTF-8' );
396
		// Clear previously left over PQresult
397
		while ( $res = pg_get_result( $this->mConn ) ) {
398
			pg_free_result( $res );
399
		}
400
		if ( pg_send_query( $this->mConn, $sql ) === false ) {
401
			throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
402
		}
403
		$this->mLastResult = pg_get_result( $this->mConn );
404
		$this->mAffectedRows = null;
405
		if ( pg_result_error( $this->mLastResult ) ) {
406
			return false;
407
		}
408
409
		return $this->mLastResult;
410
	}
411
412
	protected function dumpError() {
413
		$diags = [
414
			PGSQL_DIAG_SEVERITY,
415
			PGSQL_DIAG_SQLSTATE,
416
			PGSQL_DIAG_MESSAGE_PRIMARY,
417
			PGSQL_DIAG_MESSAGE_DETAIL,
418
			PGSQL_DIAG_MESSAGE_HINT,
419
			PGSQL_DIAG_STATEMENT_POSITION,
420
			PGSQL_DIAG_INTERNAL_POSITION,
421
			PGSQL_DIAG_INTERNAL_QUERY,
422
			PGSQL_DIAG_CONTEXT,
423
			PGSQL_DIAG_SOURCE_FILE,
424
			PGSQL_DIAG_SOURCE_LINE,
425
			PGSQL_DIAG_SOURCE_FUNCTION
426
		];
427
		foreach ( $diags as $d ) {
428
			wfDebug( sprintf( "PgSQL ERROR(%d): %s\n",
429
				$d, pg_result_error_field( $this->mLastResult, $d ) ) );
430
		}
431
	}
432
433
	function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
434
		if ( $tempIgnore ) {
435
			/* Check for constraint violation */
436
			if ( $errno === '23505' ) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $errno (integer) and '23505' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
437
				parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
438
439
				return;
440
			}
441
		}
442
		/* Transaction stays in the ERROR state until rolled back */
443
		if ( $this->mTrxLevel ) {
444
			$ignore = $this->ignoreErrors( true );
445
			$this->rollback( __METHOD__ );
446
			$this->ignoreErrors( $ignore );
447
		}
448
		parent::reportQueryError( $error, $errno, $sql, $fname, false );
449
	}
450
451
	function queryIgnore( $sql, $fname = __METHOD__ ) {
452
		return $this->query( $sql, $fname, true );
453
	}
454
455
	/**
456
	 * @param stdClass|ResultWrapper $res
457
	 * @throws DBUnexpectedError
458
	 */
459 View Code Duplication
	function freeResult( $res ) {
460
		if ( $res instanceof ResultWrapper ) {
461
			$res = $res->result;
462
		}
463
		MediaWiki\suppressWarnings();
464
		$ok = pg_free_result( $res );
465
		MediaWiki\restoreWarnings();
466
		if ( !$ok ) {
467
			throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
468
		}
469
	}
470
471
	/**
472
	 * @param ResultWrapper|stdClass $res
473
	 * @return stdClass
474
	 * @throws DBUnexpectedError
475
	 */
476 View Code Duplication
	function fetchObject( $res ) {
477
		if ( $res instanceof ResultWrapper ) {
478
			$res = $res->result;
479
		}
480
		MediaWiki\suppressWarnings();
481
		$row = pg_fetch_object( $res );
482
		MediaWiki\restoreWarnings();
483
		# @todo FIXME: HACK HACK HACK HACK debug
484
485
		# @todo hashar: not sure if the following test really trigger if the object
486
		#          fetching failed.
487
		if ( pg_last_error( $this->mConn ) ) {
488
			throw new DBUnexpectedError(
489
				$this,
490
				'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
491
			);
492
		}
493
494
		return $row;
495
	}
496
497 View Code Duplication
	function fetchRow( $res ) {
498
		if ( $res instanceof ResultWrapper ) {
499
			$res = $res->result;
500
		}
501
		MediaWiki\suppressWarnings();
502
		$row = pg_fetch_array( $res );
503
		MediaWiki\restoreWarnings();
504
		if ( pg_last_error( $this->mConn ) ) {
505
			throw new DBUnexpectedError(
506
				$this,
507
				'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
508
			);
509
		}
510
511
		return $row;
512
	}
513
514 View Code Duplication
	function numRows( $res ) {
515
		if ( $res instanceof ResultWrapper ) {
516
			$res = $res->result;
517
		}
518
		MediaWiki\suppressWarnings();
519
		$n = pg_num_rows( $res );
520
		MediaWiki\restoreWarnings();
521
		if ( pg_last_error( $this->mConn ) ) {
522
			throw new DBUnexpectedError(
523
				$this,
524
				'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
525
			);
526
		}
527
528
		return $n;
529
	}
530
531
	function numFields( $res ) {
532
		if ( $res instanceof ResultWrapper ) {
533
			$res = $res->result;
534
		}
535
536
		return pg_num_fields( $res );
537
	}
538
539
	function fieldName( $res, $n ) {
540
		if ( $res instanceof ResultWrapper ) {
541
			$res = $res->result;
542
		}
543
544
		return pg_field_name( $res, $n );
545
	}
546
547
	/**
548
	 * Return the result of the last call to nextSequenceValue();
549
	 * This must be called after nextSequenceValue().
550
	 *
551
	 * @return int|null
552
	 */
553
	function insertId() {
554
		return $this->mInsertId;
555
	}
556
557
	/**
558
	 * @param mixed $res
559
	 * @param int $row
560
	 * @return bool
561
	 */
562
	function dataSeek( $res, $row ) {
563
		if ( $res instanceof ResultWrapper ) {
564
			$res = $res->result;
565
		}
566
567
		return pg_result_seek( $res, $row );
568
	}
569
570
	function lastError() {
571
		if ( $this->mConn ) {
572
			if ( $this->mLastResult ) {
573
				return pg_result_error( $this->mLastResult );
574
			} else {
575
				return pg_last_error();
576
			}
577
		} else {
578
			return 'No database connection';
579
		}
580
	}
581
582
	function lastErrno() {
583
		if ( $this->mLastResult ) {
584
			return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
585
		} else {
586
			return false;
587
		}
588
	}
589
590
	function affectedRows() {
591
		if ( !is_null( $this->mAffectedRows ) ) {
592
			// Forced result for simulated queries
593
			return $this->mAffectedRows;
594
		}
595
		if ( empty( $this->mLastResult ) ) {
596
			return 0;
597
		}
598
599
		return pg_affected_rows( $this->mLastResult );
600
	}
601
602
	/**
603
	 * Estimate rows in dataset
604
	 * Returns estimated count, based on EXPLAIN output
605
	 * This is not necessarily an accurate estimate, so use sparingly
606
	 * Returns -1 if count cannot be found
607
	 * Takes same arguments as Database::select()
608
	 *
609
	 * @param string $table
610
	 * @param string $vars
611
	 * @param string $conds
612
	 * @param string $fname
613
	 * @param array $options
614
	 * @return int
615
	 */
616 View Code Duplication
	function estimateRowCount( $table, $vars = '*', $conds = '',
617
		$fname = __METHOD__, $options = []
618
	) {
619
		$options['EXPLAIN'] = true;
620
		$res = $this->select( $table, $vars, $conds, $fname, $options );
621
		$rows = -1;
622
		if ( $res ) {
623
			$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->select($table, $v...onds, $fname, $options) on line 620 can also be of type boolean; however, DatabasePostgres::fetchRow() does only seem to accept object<ResultWrapper>, 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...
624
			$count = [];
625
			if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
626
				$rows = (int)$count[1];
627
			}
628
		}
629
630
		return $rows;
631
	}
632
633
	/**
634
	 * Returns information about an index
635
	 * If errors are explicitly ignored, returns NULL on failure
636
	 *
637
	 * @param string $table
638
	 * @param string $index
639
	 * @param string $fname
640
	 * @return bool|null
641
	 */
642
	function indexInfo( $table, $index, $fname = __METHOD__ ) {
643
		$sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
644
		$res = $this->query( $sql, $fname );
645
		if ( !$res ) {
646
			return null;
647
		}
648
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res 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...
649
			if ( $row->indexname == $this->indexName( $index ) ) {
650
				return $row;
651
			}
652
		}
653
654
		return false;
655
	}
656
657
	/**
658
	 * Returns is of attributes used in index
659
	 *
660
	 * @since 1.19
661
	 * @param string $index
662
	 * @param bool|string $schema
663
	 * @return array
664
	 */
665
	function indexAttributes( $index, $schema = false ) {
666
		if ( $schema === false ) {
667
			$schema = $this->getCoreSchema();
668
		}
669
		/*
670
		 * A subquery would be not needed if we didn't care about the order
671
		 * of attributes, but we do
672
		 */
673
		$sql = <<<__INDEXATTR__
674
675
			SELECT opcname,
676
				attname,
677
				i.indoption[s.g] as option,
678
				pg_am.amname
679
			FROM
680
				(SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
681
					FROM
682
						pg_index isub
683
					JOIN pg_class cis
684
						ON cis.oid=isub.indexrelid
685
					JOIN pg_namespace ns
686
						ON cis.relnamespace = ns.oid
687
					WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
688
				pg_attribute,
689
				pg_opclass opcls,
690
				pg_am,
691
				pg_class ci
692
				JOIN pg_index i
693
					ON ci.oid=i.indexrelid
694
				JOIN pg_class ct
695
					ON ct.oid = i.indrelid
696
				JOIN pg_namespace n
697
					ON ci.relnamespace = n.oid
698
				WHERE
699
					ci.relname='$index' AND n.nspname='$schema'
700
					AND	attrelid = ct.oid
701
					AND	i.indkey[s.g] = attnum
702
					AND	i.indclass[s.g] = opcls.oid
703
					AND	pg_am.oid = opcls.opcmethod
704
__INDEXATTR__;
705
		$res = $this->query( $sql, __METHOD__ );
706
		$a = [];
707
		if ( $res ) {
708
			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...
709
				$a[] = [
710
					$row->attname,
711
					$row->opcname,
712
					$row->amname,
713
					$row->option ];
714
			}
715
		} else {
716
			return null;
717
		}
718
719
		return $a;
720
	}
721
722
	function indexUnique( $table, $index, $fname = __METHOD__ ) {
723
		$sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
724
			" AND indexdef LIKE 'CREATE UNIQUE%(" .
725
			$this->strencode( $this->indexName( $index ) ) .
726
			")'";
727
		$res = $this->query( $sql, $fname );
728
		if ( !$res ) {
729
			return null;
730
		}
731
732
		return $res->numRows() > 0;
733
	}
734
735
	/**
736
	 * Change the FOR UPDATE option as necessary based on the join conditions. Then pass
737
	 * to the parent function to get the actual SQL text.
738
	 *
739
	 * In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
740
	 * can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to do
741
	 * so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly.
742
	 *
743
	 * MySQL uses "ORDER BY NULL" as an optimization hint, but that syntax is illegal in PostgreSQL.
744
	 * @see DatabaseBase::selectSQLText
745
	 */
746
	function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
747
		$options = [], $join_conds = []
748
	) {
749
		if ( is_array( $options ) ) {
750
			$forUpdateKey = array_search( 'FOR UPDATE', $options, true );
751
			if ( $forUpdateKey !== false && $join_conds ) {
752
				unset( $options[$forUpdateKey] );
753
754
				foreach ( $join_conds as $table_cond => $join_cond ) {
755
					if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
756
						$options['FOR UPDATE'][] = $table_cond;
757
					}
758
				}
759
			}
760
761
			if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
762
				unset( $options['ORDER BY'] );
763
			}
764
		}
765
766
		return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
767
	}
768
769
	/**
770
	 * INSERT wrapper, inserts an array into a table
771
	 *
772
	 * $args may be a single associative array, or an array of these with numeric keys,
773
	 * for multi-row insert (Postgres version 8.2 and above only).
774
	 *
775
	 * @param string $table Name of the table to insert to.
776
	 * @param array $args Items to insert into the table.
777
	 * @param string $fname Name of the function, for profiling
778
	 * @param array|string $options String or array. Valid options: IGNORE
779
	 * @return bool Success of insert operation. IGNORE always returns true.
780
	 */
781
	function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
782
		if ( !count( $args ) ) {
783
			return true;
784
		}
785
786
		$table = $this->tableName( $table );
787
		if ( !isset( $this->numericVersion ) ) {
788
			$this->getServerVersion();
789
		}
790
791
		if ( !is_array( $options ) ) {
792
			$options = [ $options ];
793
		}
794
795 View Code Duplication
		if ( isset( $args[0] ) && is_array( $args[0] ) ) {
796
			$multi = true;
797
			$keys = array_keys( $args[0] );
798
		} else {
799
			$multi = false;
800
			$keys = array_keys( $args );
801
		}
802
803
		// If IGNORE is set, we use savepoints to emulate mysql's behavior
804
		$savepoint = null;
805 View Code Duplication
		if ( in_array( 'IGNORE', $options ) ) {
806
			$savepoint = new SavepointPostgres( $this, 'mw' );
807
			$olde = error_reporting( 0 );
808
			// For future use, we may want to track the number of actual inserts
809
			// Right now, insert (all writes) simply return true/false
810
			$numrowsinserted = 0;
811
		}
812
813
		$sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
814
815
		if ( $multi ) {
816
			if ( $this->numericVersion >= 8.2 && !$savepoint ) {
817
				$first = true;
818 View Code Duplication
				foreach ( $args as $row ) {
819
					if ( $first ) {
820
						$first = false;
821
					} else {
822
						$sql .= ',';
823
					}
824
					$sql .= '(' . $this->makeList( $row ) . ')';
825
				}
826
				$res = (bool)$this->query( $sql, $fname, $savepoint );
827
			} else {
828
				$res = true;
829
				$origsql = $sql;
830
				foreach ( $args as $row ) {
831
					$tempsql = $origsql;
832
					$tempsql .= '(' . $this->makeList( $row ) . ')';
833
834
					if ( $savepoint ) {
835
						$savepoint->savepoint();
836
					}
837
838
					$tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
839
840 View Code Duplication
					if ( $savepoint ) {
841
						$bar = pg_result_error( $this->mLastResult );
842
						if ( $bar != false ) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $bar of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
843
							$savepoint->rollback();
844
						} else {
845
							$savepoint->release();
846
							$numrowsinserted++;
0 ignored issues
show
Bug introduced by
The variable $numrowsinserted does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
847
						}
848
					}
849
850
					// If any of them fail, we fail overall for this function call
851
					// Note that this will be ignored if IGNORE is set
852
					if ( !$tempres ) {
853
						$res = false;
854
					}
855
				}
856
			}
857
		} else {
858
			// Not multi, just a lone insert
859
			if ( $savepoint ) {
860
				$savepoint->savepoint();
861
			}
862
863
			$sql .= '(' . $this->makeList( $args ) . ')';
864
			$res = (bool)$this->query( $sql, $fname, $savepoint );
865 View Code Duplication
			if ( $savepoint ) {
866
				$bar = pg_result_error( $this->mLastResult );
867
				if ( $bar != false ) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $bar of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
868
					$savepoint->rollback();
869
				} else {
870
					$savepoint->release();
871
					$numrowsinserted++;
872
				}
873
			}
874
		}
875
		if ( $savepoint ) {
876
			error_reporting( $olde );
0 ignored issues
show
Bug introduced by
The variable $olde does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
877
			$savepoint->commit();
878
879
			// Set the affected row count for the whole operation
880
			$this->mAffectedRows = $numrowsinserted;
881
882
			// IGNORE always returns true
883
			return true;
884
		}
885
886
		return $res;
887
	}
888
889
	/**
890
	 * INSERT SELECT wrapper
891
	 * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ]
892
	 * Source items may be literals rather then field names, but strings should
893
	 * be quoted with Database::addQuotes()
894
	 * $conds may be "*" to copy the whole table
895
	 * srcTable may be an array of tables.
896
	 * @todo FIXME: Implement this a little better (seperate select/insert)?
897
	 *
898
	 * @param string $destTable
899
	 * @param array|string $srcTable
900
	 * @param array $varMap
901
	 * @param array $conds
902
	 * @param string $fname
903
	 * @param array $insertOptions
904
	 * @param array $selectOptions
905
	 * @return bool
906
	 */
907
	function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
908
		$insertOptions = [], $selectOptions = [] ) {
909
		$destTable = $this->tableName( $destTable );
910
911
		if ( !is_array( $insertOptions ) ) {
912
			$insertOptions = [ $insertOptions ];
913
		}
914
915
		/*
916
		 * If IGNORE is set, we use savepoints to emulate mysql's behavior
917
		 * Ignore LOW PRIORITY option, since it is MySQL-specific
918
		 */
919
		$savepoint = null;
920 View Code Duplication
		if ( in_array( 'IGNORE', $insertOptions ) ) {
921
			$savepoint = new SavepointPostgres( $this, 'mw' );
922
			$olde = error_reporting( 0 );
923
			$numrowsinserted = 0;
924
			$savepoint->savepoint();
925
		}
926
927
		if ( !is_array( $selectOptions ) ) {
928
			$selectOptions = [ $selectOptions ];
929
		}
930
		list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
931
			$this->makeSelectOptions( $selectOptions );
932 View Code Duplication
		if ( is_array( $srcTable ) ) {
933
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
934
		} else {
935
			$srcTable = $this->tableName( $srcTable );
936
		}
937
938
		$sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
939
			" SELECT $startOpts " . implode( ',', $varMap ) .
940
			" FROM $srcTable $useIndex $ignoreIndex ";
941
942
		if ( $conds != '*' ) {
943
			$sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
944
		}
945
946
		$sql .= " $tailOpts";
947
948
		$res = (bool)$this->query( $sql, $fname, $savepoint );
949
		if ( $savepoint ) {
950
			$bar = pg_result_error( $this->mLastResult );
951
			if ( $bar != false ) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $bar of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
952
				$savepoint->rollback();
953
			} else {
954
				$savepoint->release();
955
				$numrowsinserted++;
0 ignored issues
show
Bug introduced by
The variable $numrowsinserted does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
956
			}
957
			error_reporting( $olde );
0 ignored issues
show
Bug introduced by
The variable $olde does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
958
			$savepoint->commit();
959
960
			// Set the affected row count for the whole operation
961
			$this->mAffectedRows = $numrowsinserted;
962
963
			// IGNORE always returns true
964
			return true;
965
		}
966
967
		return $res;
968
	}
969
970 View Code Duplication
	function tableName( $name, $format = 'quoted' ) {
971
		# Replace reserved words with better ones
972
		switch ( $name ) {
973
			case 'user':
974
				return $this->realTableName( 'mwuser', $format );
975
			case 'text':
976
				return $this->realTableName( 'pagecontent', $format );
977
			default:
978
				return $this->realTableName( $name, $format );
979
		}
980
	}
981
982
	/* Don't cheat on installer */
983
	function realTableName( $name, $format = 'quoted' ) {
984
		return parent::tableName( $name, $format );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (tableName() instead of realTableName()). Are you sure this is correct? If so, you might want to change this to $this->tableName().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
985
	}
986
987
	/**
988
	 * Return the next in a sequence, save the value for retrieval via insertId()
989
	 *
990
	 * @param string $seqName
991
	 * @return int|null
992
	 */
993
	function nextSequenceValue( $seqName ) {
994
		$safeseq = str_replace( "'", "''", $seqName );
995
		$res = $this->query( "SELECT nextval('$safeseq')" );
996
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query("SELECT nextval('{$safeseq}')") on line 995 can also be of type boolean; however, DatabasePostgres::fetchRow() does only seem to accept object<ResultWrapper>, 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...
997
		$this->mInsertId = $row[0];
998
999
		return $this->mInsertId;
1000
	}
1001
1002
	/**
1003
	 * Return the current value of a sequence. Assumes it has been nextval'ed in this session.
1004
	 *
1005
	 * @param string $seqName
1006
	 * @return int
1007
	 */
1008
	function currentSequenceValue( $seqName ) {
1009
		$safeseq = str_replace( "'", "''", $seqName );
1010
		$res = $this->query( "SELECT currval('$safeseq')" );
1011
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query("SELECT currval('{$safeseq}')") on line 1010 can also be of type boolean; however, DatabasePostgres::fetchRow() does only seem to accept object<ResultWrapper>, 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...
1012
		$currval = $row[0];
1013
1014
		return $currval;
1015
	}
1016
1017
	# Returns the size of a text field, or -1 for "unlimited"
1018
	function textFieldSize( $table, $field ) {
1019
		$table = $this->tableName( $table );
1020
		$sql = "SELECT t.typname as ftype,a.atttypmod as size
1021
			FROM pg_class c, pg_attribute a, pg_type t
1022
			WHERE relname='$table' AND a.attrelid=c.oid AND
1023
				a.atttypid=t.oid and a.attname='$field'";
1024
		$res = $this->query( $sql );
1025
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql) on line 1024 can also be of type boolean; however, DatabasePostgres::fetchObject() does only seem to accept object<ResultWrapper>|object<stdClass>, 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...
1026
		if ( $row->ftype == 'varchar' ) {
1027
			$size = $row->size - 4;
1028
		} else {
1029
			$size = $row->size;
1030
		}
1031
1032
		return $size;
1033
	}
1034
1035
	function limitResult( $sql, $limit, $offset = false ) {
1036
		return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
1037
	}
1038
1039
	function wasDeadlock() {
1040
		return $this->lastErrno() == '40P01';
1041
	}
1042
1043
	function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
1044
		$newName = $this->addIdentifierQuotes( $newName );
1045
		$oldName = $this->addIdentifierQuotes( $oldName );
1046
1047
		return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " .
1048
			"(LIKE $oldName INCLUDING DEFAULTS)", $fname );
1049
	}
1050
1051
	function listTables( $prefix = null, $fname = __METHOD__ ) {
1052
		$eschema = $this->addQuotes( $this->getCoreSchema() );
1053
		$result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
1054
		$endArray = [];
1055
1056 View Code Duplication
		foreach ( $result as $table ) {
0 ignored issues
show
Bug introduced by
The expression $result 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...
1057
			$vars = get_object_vars( $table );
1058
			$table = array_pop( $vars );
1059
			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...
1060
				$endArray[] = $table;
1061
			}
1062
		}
1063
1064
		return $endArray;
1065
	}
1066
1067
	function timestamp( $ts = 0 ) {
1068
		return wfTimestamp( TS_POSTGRES, $ts );
1069
	}
1070
1071
	/**
1072
	 * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
1073
	 * to http://www.php.net/manual/en/ref.pgsql.php
1074
	 *
1075
	 * Parsing a postgres array can be a tricky problem, he's my
1076
	 * take on this, it handles multi-dimensional arrays plus
1077
	 * escaping using a nasty regexp to determine the limits of each
1078
	 * data-item.
1079
	 *
1080
	 * This should really be handled by PHP PostgreSQL module
1081
	 *
1082
	 * @since 1.19
1083
	 * @param string $text Postgreql array returned in a text form like {a,b}
1084
	 * @param string $output
1085
	 * @param int $limit
1086
	 * @param int $offset
1087
	 * @return string
1088
	 */
1089
	function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
1090
		if ( false === $limit ) {
1091
			$limit = strlen( $text ) - 1;
1092
			$output = [];
1093
		}
1094
		if ( '{}' == $text ) {
1095
			return $output;
1096
		}
1097
		do {
1098
			if ( '{' != $text[$offset] ) {
1099
				preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
1100
					$text, $match, 0, $offset );
1101
				$offset += strlen( $match[0] );
1102
				$output[] = ( '"' != $match[1][0]
1103
					? $match[1]
1104
					: stripcslashes( substr( $match[1], 1, -1 ) ) );
1105
				if ( '},' == $match[3] ) {
1106
					return $output;
1107
				}
1108
			} else {
1109
				$offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 );
0 ignored issues
show
Bug introduced by
It seems like $output defined by array() on line 1092 can also be of type array; however, DatabasePostgres::pg_array_parse() does only seem to accept 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...
1110
			}
1111
		} while ( $limit > $offset );
1112
1113
		return $output;
1114
	}
1115
1116
	/**
1117
	 * Return aggregated value function call
1118
	 * @param array $valuedata
1119
	 * @param string $valuename
1120
	 * @return array
1121
	 */
1122
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1123
		return $valuedata;
1124
	}
1125
1126
	/**
1127
	 * @return string Wikitext of a link to the server software's web site
1128
	 */
1129
	public function getSoftwareLink() {
1130
		return '[{{int:version-db-postgres-url}} PostgreSQL]';
1131
	}
1132
1133
	/**
1134
	 * Return current schema (executes SELECT current_schema())
1135
	 * Needs transaction
1136
	 *
1137
	 * @since 1.19
1138
	 * @return string Default schema for the current session
1139
	 */
1140
	function getCurrentSchema() {
1141
		$res = $this->query( "SELECT current_schema()", __METHOD__ );
1142
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query('SELECT cur..._schema()', __METHOD__) on line 1141 can also be of type boolean; however, DatabasePostgres::fetchRow() does only seem to accept object<ResultWrapper>, 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...
1143
1144
		return $row[0];
1145
	}
1146
1147
	/**
1148
	 * Return list of schemas which are accessible without schema name
1149
	 * This is list does not contain magic keywords like "$user"
1150
	 * Needs transaction
1151
	 *
1152
	 * @see getSearchPath()
1153
	 * @see setSearchPath()
1154
	 * @since 1.19
1155
	 * @return array List of actual schemas for the current sesson
1156
	 */
1157
	function getSchemas() {
1158
		$res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
1159
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query('SELECT cur...as(false)', __METHOD__) on line 1158 can also be of type boolean; however, DatabasePostgres::fetchRow() does only seem to accept object<ResultWrapper>, 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...
1160
		$schemas = [];
1161
1162
		/* PHP pgsql support does not support array type, "{a,b}" string is returned */
1163
1164
		return $this->pg_array_parse( $row[0], $schemas );
1165
	}
1166
1167
	/**
1168
	 * Return search patch for schemas
1169
	 * This is different from getSchemas() since it contain magic keywords
1170
	 * (like "$user").
1171
	 * Needs transaction
1172
	 *
1173
	 * @since 1.19
1174
	 * @return array How to search for table names schemas for the current user
1175
	 */
1176
	function getSearchPath() {
1177
		$res = $this->query( "SHOW search_path", __METHOD__ );
1178
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query('SHOW search_path', __METHOD__) on line 1177 can also be of type boolean; however, DatabasePostgres::fetchRow() does only seem to accept object<ResultWrapper>, 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...
1179
1180
		/* PostgreSQL returns SHOW values as strings */
1181
1182
		return explode( ",", $row[0] );
1183
	}
1184
1185
	/**
1186
	 * Update search_path, values should already be sanitized
1187
	 * Values may contain magic keywords like "$user"
1188
	 * @since 1.19
1189
	 *
1190
	 * @param array $search_path List of schemas to be searched by default
1191
	 */
1192
	function setSearchPath( $search_path ) {
1193
		$this->query( "SET search_path = " . implode( ", ", $search_path ) );
1194
	}
1195
1196
	/**
1197
	 * Determine default schema for MediaWiki core
1198
	 * Adjust this session schema search path if desired schema exists
1199
	 * and is not alread there.
1200
	 *
1201
	 * We need to have name of the core schema stored to be able
1202
	 * to query database metadata.
1203
	 *
1204
	 * This will be also called by the installer after the schema is created
1205
	 *
1206
	 * @since 1.19
1207
	 *
1208
	 * @param string $desiredSchema
1209
	 */
1210
	function determineCoreSchema( $desiredSchema ) {
1211
		$this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
1212
		if ( $this->schemaExists( $desiredSchema ) ) {
1213
			if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
1214
				$this->mCoreSchema = $desiredSchema;
1215
				wfDebug( "Schema \"" . $desiredSchema . "\" already in the search path\n" );
1216
			} else {
1217
				/**
1218
				 * Prepend our schema (e.g. 'mediawiki') in front
1219
				 * of the search path
1220
				 * Fixes bug 15816
1221
				 */
1222
				$search_path = $this->getSearchPath();
1223
				array_unshift( $search_path,
1224
					$this->addIdentifierQuotes( $desiredSchema ) );
1225
				$this->setSearchPath( $search_path );
1226
				$this->mCoreSchema = $desiredSchema;
1227
				wfDebug( "Schema \"" . $desiredSchema . "\" added to the search path\n" );
1228
			}
1229
		} else {
1230
			$this->mCoreSchema = $this->getCurrentSchema();
1231
			wfDebug( "Schema \"" . $desiredSchema . "\" not found, using current \"" .
1232
				$this->mCoreSchema . "\"\n" );
1233
		}
1234
		/* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
1235
		$this->commit( __METHOD__ );
1236
	}
1237
1238
	/**
1239
	 * Return schema name fore core MediaWiki tables
1240
	 *
1241
	 * @since 1.19
1242
	 * @return string Core schema name
1243
	 */
1244
	function getCoreSchema() {
1245
		return $this->mCoreSchema;
1246
	}
1247
1248
	/**
1249
	 * @return string Version information from the database
1250
	 */
1251
	function getServerVersion() {
1252
		if ( !isset( $this->numericVersion ) ) {
1253
			$versionInfo = pg_version( $this->mConn );
1254
			if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
1255
				// Old client, abort install
1256
				$this->numericVersion = '7.3 or earlier';
1257
			} elseif ( isset( $versionInfo['server'] ) ) {
1258
				// Normal client
1259
				$this->numericVersion = $versionInfo['server'];
1260
			} else {
1261
				// Bug 16937: broken pgsql extension from PHP<5.3
1262
				$this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' );
1263
			}
1264
		}
1265
1266
		return $this->numericVersion;
1267
	}
1268
1269
	/**
1270
	 * Query whether a given relation exists (in the given schema, or the
1271
	 * default mw one if not given)
1272
	 * @param string $table
1273
	 * @param array|string $types
1274
	 * @param bool|string $schema
1275
	 * @return bool
1276
	 */
1277
	function relationExists( $table, $types, $schema = false ) {
1278
		if ( !is_array( $types ) ) {
1279
			$types = [ $types ];
1280
		}
1281
		if ( !$schema ) {
1282
			$schema = $this->getCoreSchema();
1283
		}
1284
		$table = $this->realTableName( $table, 'raw' );
1285
		$etable = $this->addQuotes( $table );
1286
		$eschema = $this->addQuotes( $schema );
0 ignored issues
show
Bug introduced by
It seems like $schema can also be of type string; however, DatabasePostgres::addQuotes() does only seem to accept null|boolean|object<Blob>, 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...
1287
		$sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
1288
			. "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
1289
			. "AND c.relkind IN ('" . implode( "','", $types ) . "')";
1290
		$res = $this->query( $sql );
1291
		$count = $res ? $res->numRows() : 0;
1292
1293
		return (bool)$count;
1294
	}
1295
1296
	/**
1297
	 * For backward compatibility, this function checks both tables and
1298
	 * views.
1299
	 * @param string $table
1300
	 * @param string $fname
1301
	 * @param bool|string $schema
1302
	 * @return bool
1303
	 */
1304
	function tableExists( $table, $fname = __METHOD__, $schema = false ) {
1305
		return $this->relationExists( $table, [ 'r', 'v' ], $schema );
1306
	}
1307
1308
	function sequenceExists( $sequence, $schema = false ) {
1309
		return $this->relationExists( $sequence, 'S', $schema );
1310
	}
1311
1312 View Code Duplication
	function triggerExists( $table, $trigger ) {
1313
		$q = <<<SQL
1314
	SELECT 1 FROM pg_class, pg_namespace, pg_trigger
1315
		WHERE relnamespace=pg_namespace.oid AND relkind='r'
1316
			  AND tgrelid=pg_class.oid
1317
			  AND nspname=%s AND relname=%s AND tgname=%s
1318
SQL;
1319
		$res = $this->query(
1320
			sprintf(
1321
				$q,
1322
				$this->addQuotes( $this->getCoreSchema() ),
1323
				$this->addQuotes( $table ),
1324
				$this->addQuotes( $trigger )
1325
			)
1326
		);
1327
		if ( !$res ) {
1328
			return null;
1329
		}
1330
		$rows = $res->numRows();
1331
1332
		return $rows;
1333
	}
1334
1335
	function ruleExists( $table, $rule ) {
1336
		$exists = $this->selectField( 'pg_rules', 'rulename',
1337
			[
1338
				'rulename' => $rule,
1339
				'tablename' => $table,
1340
				'schemaname' => $this->getCoreSchema()
1341
			]
1342
		);
1343
1344
		return $exists === $rule;
1345
	}
1346
1347 View Code Duplication
	function constraintExists( $table, $constraint ) {
1348
		$sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
1349
			"WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
1350
			$this->addQuotes( $this->getCoreSchema() ),
1351
			$this->addQuotes( $table ),
1352
			$this->addQuotes( $constraint )
1353
		);
1354
		$res = $this->query( $sql );
1355
		if ( !$res ) {
1356
			return null;
1357
		}
1358
		$rows = $res->numRows();
1359
1360
		return $rows;
1361
	}
1362
1363
	/**
1364
	 * Query whether a given schema exists. Returns true if it does, false if it doesn't.
1365
	 * @param string $schema
1366
	 * @return bool
1367
	 */
1368
	function schemaExists( $schema ) {
1369
		$exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
1370
			[ 'nspname' => $schema ], __METHOD__ );
1371
1372
		return (bool)$exists;
1373
	}
1374
1375
	/**
1376
	 * Returns true if a given role (i.e. user) exists, false otherwise.
1377
	 * @param string $roleName
1378
	 * @return bool
1379
	 */
1380
	function roleExists( $roleName ) {
1381
		$exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
1382
			[ 'rolname' => $roleName ], __METHOD__ );
1383
1384
		return (bool)$exists;
1385
	}
1386
1387
	/**
1388
	 * @var string $table
1389
	 * @var string $field
1390
	 * @return PostgresField|null
1391
	 */
1392
	function fieldInfo( $table, $field ) {
1393
		return PostgresField::fromText( $this, $table, $field );
1394
	}
1395
1396
	/**
1397
	 * pg_field_type() wrapper
1398
	 * @param ResultWrapper|resource $res ResultWrapper or PostgreSQL query result resource
1399
	 * @param int $index Field number, starting from 0
1400
	 * @return string
1401
	 */
1402
	function fieldType( $res, $index ) {
1403
		if ( $res instanceof ResultWrapper ) {
1404
			$res = $res->result;
1405
		}
1406
1407
		return pg_field_type( $res, $index );
1408
	}
1409
1410
	/**
1411
	 * @param string $b
1412
	 * @return Blob
1413
	 */
1414
	function encodeBlob( $b ) {
1415
		return new PostgresBlob( pg_escape_bytea( $b ) );
1416
	}
1417
1418
	function decodeBlob( $b ) {
1419
		if ( $b instanceof PostgresBlob ) {
1420
			$b = $b->fetch();
1421
		} elseif ( $b instanceof Blob ) {
1422
			return $b->fetch();
1423
		}
1424
1425
		return pg_unescape_bytea( $b );
1426
	}
1427
1428
	function strencode( $s ) {
1429
		// Should not be called by us
1430
1431
		return pg_escape_string( $this->mConn, $s );
1432
	}
1433
1434
	/**
1435
	 * @param null|bool|Blob $s
1436
	 * @return int|string
1437
	 */
1438
	function addQuotes( $s ) {
1439
		if ( is_null( $s ) ) {
1440
			return 'NULL';
1441
		} elseif ( is_bool( $s ) ) {
1442
			return intval( $s );
1443
		} elseif ( $s instanceof Blob ) {
1444
			if ( $s instanceof PostgresBlob ) {
1445
				$s = $s->fetch();
1446
			} else {
1447
				$s = pg_escape_bytea( $this->mConn, $s->fetch() );
1448
			}
1449
			return "'$s'";
1450
		}
1451
1452
		return "'" . pg_escape_string( $this->mConn, $s ) . "'";
1453
	}
1454
1455
	/**
1456
	 * Postgres specific version of replaceVars.
1457
	 * Calls the parent version in Database.php
1458
	 *
1459
	 * @param string $ins SQL string, read from a stream (usually tables.sql)
1460
	 * @return string SQL string
1461
	 */
1462
	protected function replaceVars( $ins ) {
1463
		$ins = parent::replaceVars( $ins );
1464
1465
		if ( $this->numericVersion >= 8.3 ) {
1466
			// Thanks for not providing backwards-compatibility, 8.3
1467
			$ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
1468
		}
1469
1470
		if ( $this->numericVersion <= 8.1 ) { // Our minimum version
1471
			$ins = str_replace( 'USING gin', 'USING gist', $ins );
1472
		}
1473
1474
		return $ins;
1475
	}
1476
1477
	/**
1478
	 * Various select options
1479
	 *
1480
	 * @param array $options An associative array of options to be turned into
1481
	 *   an SQL query, valid keys are listed in the function.
1482
	 * @return array
1483
	 */
1484
	function makeSelectOptions( $options ) {
1485
		$preLimitTail = $postLimitTail = '';
1486
		$startOpts = $useIndex = $ignoreIndex = '';
1487
1488
		$noKeyOptions = [];
1489
		foreach ( $options as $key => $option ) {
1490
			if ( is_numeric( $key ) ) {
1491
				$noKeyOptions[$option] = true;
1492
			}
1493
		}
1494
1495
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1496
1497
		$preLimitTail .= $this->makeOrderBy( $options );
1498
1499
		// if ( isset( $options['LIMIT'] ) ) {
1500
		// 	$tailOpts .= $this->limitResult( '', $options['LIMIT'],
1501
		// 		isset( $options['OFFSET'] ) ? $options['OFFSET']
1502
		// 		: false );
1503
		// }
1504
1505
		if ( isset( $options['FOR UPDATE'] ) ) {
1506
			$postLimitTail .= ' FOR UPDATE OF ' .
1507
				implode( ', ', array_map( [ &$this, 'tableName' ], $options['FOR UPDATE'] ) );
1508
		} elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1509
			$postLimitTail .= ' FOR UPDATE';
1510
		}
1511
1512
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1513
			$startOpts .= 'DISTINCT';
1514
		}
1515
1516
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1517
	}
1518
1519
	function getDBname() {
1520
		return $this->mDBname;
1521
	}
1522
1523
	function getServer() {
1524
		return $this->mServer;
1525
	}
1526
1527
	function buildConcat( $stringList ) {
1528
		return implode( ' || ', $stringList );
1529
	}
1530
1531 View Code Duplication
	public function buildGroupConcatField(
1532
		$delimiter, $table, $field, $conds = '', $options = [], $join_conds = []
1533
	) {
1534
		$fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')';
1535
1536
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1537
	}
1538
1539
	/**
1540
	 * @param string $field Field or column to cast
1541
	 * @return string
1542
	 * @since 1.28
1543
	 */
1544
	public function buildStringCast( $field ) {
1545
		return $field . '::text';
1546
	}
1547
1548
	public function getSearchEngine() {
1549
		return 'SearchPostgres';
1550
	}
1551
1552
	public function streamStatementEnd( &$sql, &$newLine ) {
1553
		# Allow dollar quoting for function declarations
1554
		if ( substr( $newLine, 0, 4 ) == '$mw$' ) {
1555
			if ( $this->delimiter ) {
1556
				$this->delimiter = false;
0 ignored issues
show
Documentation Bug introduced by
The property $delimiter was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1557
			} else {
1558
				$this->delimiter = ';';
1559
			}
1560
		}
1561
1562
		return parent::streamStatementEnd( $sql, $newLine );
1563
	}
1564
1565
	/**
1566
	 * Check to see if a named lock is available. This is non-blocking.
1567
	 * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1568
	 *
1569
	 * @param string $lockName Name of lock to poll
1570
	 * @param string $method Name of method calling us
1571
	 * @return bool
1572
	 * @since 1.20
1573
	 */
1574 View Code Duplication
	public function lockIsFree( $lockName, $method ) {
1575
		$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
0 ignored issues
show
Bug introduced by
It seems like $this->bigintFromLockName($lockName) targeting DatabasePostgres::bigintFromLockName() can also be of type string; however, DatabasePostgres::addQuotes() does only seem to accept null|boolean|object<Blob>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1576
		$result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
1577
			WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
1578
		$row = $this->fetchObject( $result );
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query("SELECT (CA...S lockstatus", $method) on line 1576 can also be of type boolean; however, DatabasePostgres::fetchObject() does only seem to accept object<ResultWrapper>|object<stdClass>, 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...
1579
1580
		return ( $row->lockstatus === 't' );
1581
	}
1582
1583
	/**
1584
	 * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1585
	 * @param string $lockName
1586
	 * @param string $method
1587
	 * @param int $timeout
1588
	 * @return bool
1589
	 */
1590
	public function lock( $lockName, $method, $timeout = 5 ) {
1591
		$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
0 ignored issues
show
Bug introduced by
It seems like $this->bigintFromLockName($lockName) targeting DatabasePostgres::bigintFromLockName() can also be of type string; however, DatabasePostgres::addQuotes() does only seem to accept null|boolean|object<Blob>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1592
		$loop = new WaitConditionLoop(
1593
			function () use ( $lockName, $key, $timeout, $method ) {
1594
				$res = $this->query( "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
1595
				$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query("SELECT pg_...S lockstatus", $method) on line 1594 can also be of type boolean; however, DatabasePostgres::fetchObject() does only seem to accept object<ResultWrapper>|object<stdClass>, 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...
1596
				if ( $row->lockstatus === 't' ) {
1597
					parent::lock( $lockName, $method, $timeout ); // record
1598
					return true;
1599
				}
1600
1601
				return WaitConditionLoop::CONDITION_CONTINUE;
1602
			},
1603
			$timeout
1604
		);
1605
1606
		return ( $loop->invoke() === $loop::CONDITION_REACHED );
1607
	}
1608
1609
	/**
1610
	 * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM
1611
	 * PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1612
	 * @param string $lockName
1613
	 * @param string $method
1614
	 * @return bool
1615
	 */
1616 View Code Duplication
	public function unlock( $lockName, $method ) {
1617
		$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
0 ignored issues
show
Bug introduced by
It seems like $this->bigintFromLockName($lockName) targeting DatabasePostgres::bigintFromLockName() can also be of type string; however, DatabasePostgres::addQuotes() does only seem to accept null|boolean|object<Blob>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1618
		$result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
1619
		$row = $this->fetchObject( $result );
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query("SELECT pg_...s lockstatus", $method) on line 1618 can also be of type boolean; however, DatabasePostgres::fetchObject() does only seem to accept object<ResultWrapper>|object<stdClass>, 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...
1620
1621
		if ( $row->lockstatus === 't' ) {
1622
			parent::unlock( $lockName, $method ); // record
1623
			return true;
1624
		}
1625
1626
		wfDebug( __METHOD__ . " failed to release lock\n" );
1627
1628
		return false;
1629
	}
1630
1631
	/**
1632
	 * @param string $lockName
1633
	 * @return string Integer
1634
	 */
1635
	private function bigintFromLockName( $lockName ) {
1636
		return Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
1637
	}
1638
} // end DatabasePostgres class
1639
1640
class PostgresBlob extends Blob {
1641
}
1642