Completed
Branch master (d58858)
by
unknown
28:23
created

DatabasePostgres   D

Complexity

Total Complexity 205

Size/Duplication

Total Lines 1417
Duplicated Lines 15.1 %

Coupling/Cohesion

Components 3
Dependencies 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 205
c 1
b 0
f 0
lcom 3
cbo 8
dl 214
loc 1417
rs 4.4102

82 Methods

Rating   Name   Duplication   Size   Complexity  
A getType() 0 3 1
A cascadingDeletes() 0 3 1
A cleanupTriggers() 0 3 1
A strictIPs() 0 3 1
A realTimestamps() 0 3 1
A implicitGroupby() 0 3 1
A implicitOrderby() 0 3 1
A searchableIPs() 0 3 1
A functionalIndexes() 0 3 1
A hasConstraint() 0 9 1
C open() 0 80 12
A selectDB() 0 7 2
A makeConnectionString() 0 8 2
A closeConnection() 0 3 1
A doQuery() 0 17 4
A dumpError() 0 20 2
A reportQueryError() 0 17 4
A queryIgnore() 0 3 1
A freeResult() 11 11 3
A fetchObject() 20 20 3
A fetchRow() 16 16 3
A numRows() 16 16 3
A numFields() 0 7 2
A fieldName() 0 7 2
A insertId() 0 3 1
A dataSeek() 0 7 2
A lastError() 0 11 3
A lastErrno() 0 7 2
A affectedRows() 0 11 3
A estimateRowCount() 16 16 3
A indexInfo() 0 14 4
A indexAttributes() 0 56 4
A indexUnique() 0 12 2
C selectSQLText() 0 22 8
F insert() 40 107 21
B insertSelect() 11 61 8
A tableName() 11 11 3
A realTableName() 0 3 1
A nextSequenceValue() 0 8 1
A currentSequenceValue() 0 8 1
A textFieldSize() 0 16 2
A limitResult() 0 3 2
A wasDeadlock() 0 3 1
A duplicateTableStructure() 0 7 2
A listTables() 7 15 4
A timestamp() 0 3 1
C pg_array_parse() 0 26 7
A aggregateValue() 0 3 1
A getSoftwareLink() 0 3 1
A getCurrentSchema() 0 6 1
A getSchemas() 0 9 1
A getSearchPath() 0 8 1
A setSearchPath() 0 3 1
B determineCoreSchema() 0 27 3
A getCoreSchema() 0 3 1
A getServerVersion() 0 17 4
A relationExists() 0 18 4
A tableExists() 0 3 1
A sequenceExists() 0 3 1
A triggerExists() 22 22 2
A ruleExists() 0 11 1
A constraintExists() 15 15 2
A schemaExists() 0 6 1
A roleExists() 0 6 1
A fieldInfo() 0 3 1
A fieldType() 0 7 2
A encodeBlob() 0 3 1
A decodeBlob() 0 9 3
A strencode() 0 5 1
B addQuotes() 0 16 5
A replaceVars() 0 14 3
C makeSelectOptions() 0 34 7
A getDBname() 0 3 1
A getServer() 0 3 1
A buildConcat() 0 3 1
A buildGroupConcatField() 7 7 1
A getSearchEngine() 0 3 1
A streamStatementEnd() 0 12 3
A lockIsFree() 8 8 1
A lock() 0 18 3
A unlock() 14 14 2
A bigintFromLockName() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DatabasePostgres often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DatabasePostgres, and based on these observations, apply Extract Interface, too.

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" );
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $join_conds of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

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