Completed
Branch master (174b3a)
by
unknown
26:51
created

DatabasePostgres   F

Complexity

Total Complexity 199

Size/Duplication

Total Lines 1408
Duplicated Lines 14.28 %

Coupling/Cohesion

Components 3
Dependencies 11

Importance

Changes 0
Metric Value
dl 201
loc 1408
rs 3.9999
c 0
b 0
f 0
wmc 199
lcom 3
cbo 11

77 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A getType() 0 3 1
A implicitGroupby() 0 3 1
A implicitOrderby() 0 3 1
A hasConstraint() 0 9 1
C open() 0 76 11
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 29 8
F insert() 33 107 21
C nativeInsertSelect() 5 62 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 9 2
A listTables() 7 16 4
A timestamp() 0 5 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 30 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 buildStringCast() 0 3 1
A streamStatementEnd() 0 12 3
A lockIsFree() 8 8 1
A lock() 0 18 2
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
/**
25
 * @ingroup Database
26
 */
27
class DatabasePostgres extends DatabaseBase {
28
	/** @var int|bool */
29
	protected $port;
30
31
	/** @var resource */
32
	protected $mLastResult = null;
33
	/** @var int The number of rows affected as an integer */
34
	protected $mAffectedRows = null;
35
36
	/** @var int */
37
	private $mInsertId = null;
38
	/** @var float|string */
39
	private $numericVersion = null;
40
	/** @var string Connect string to open a PostgreSQL connection */
41
	private $connectString;
42
	/** @var string */
43
	private $mCoreSchema;
44
45
	public function __construct( array $params ) {
46
		$this->port = isset( $params['port'] ) ? $params['port'] : false;
47
		parent::__construct( $params );
48
	}
49
50
	function getType() {
51
		return 'postgres';
52
	}
53
54
	function implicitGroupby() {
55
		return false;
56
	}
57
58
	function implicitOrderby() {
59
		return false;
60
	}
61
62
	function hasConstraint( $name ) {
63
		$sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
64
			"WHERE c.connamespace = n.oid AND conname = '" .
65
			pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" .
66
			pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
67
		$res = $this->doQuery( $sql );
68
69
		return $this->numRows( $res );
70
	}
71
72
	/**
73
	 * Usually aborts on failure
74
	 * @param string $server
75
	 * @param string $user
76
	 * @param string $password
77
	 * @param string $dbName
78
	 * @throws DBConnectionError|Exception
79
	 * @return resource|bool|null
80
	 */
81
	function open( $server, $user, $password, $dbName ) {
82
		# Test for Postgres support, to avoid suppressed fatal error
83
		if ( !function_exists( 'pg_connect' ) ) {
84
			throw new DBConnectionError(
85
				$this,
86
				"Postgres functions missing, have you compiled PHP with the --with-pgsql\n" .
87
				"option? (Note: if you recently installed PHP, you may need to restart your\n" .
88
				"webserver and database)\n"
89
			);
90
		}
91
92
		if ( !strlen( $user ) ) { # e.g. the class is being loaded
93
			return null;
94
		}
95
96
		$this->mServer = $server;
97
		$this->mUser = $user;
98
		$this->mPassword = $password;
99
		$this->mDBname = $dbName;
100
101
		$connectVars = [
102
			'dbname' => $dbName,
103
			'user' => $user,
104
			'password' => $password
105
		];
106
		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...
107
			$connectVars['host'] = $server;
108
		}
109
		if ( (int)$this->port > 0 ) {
110
			$connectVars['port'] = (int)$this->port;
111
		}
112
		if ( $this->mFlags & DBO_SSL ) {
113
			$connectVars['sslmode'] = 1;
114
		}
115
116
		$this->connectString = $this->makeConnectionString( $connectVars );
117
		$this->close();
118
		$this->installErrorHandler();
119
120
		try {
121
			$this->mConn = pg_connect( $this->connectString );
122
		} catch ( Exception $ex ) {
123
			$this->restoreErrorHandler();
124
			throw $ex;
125
		}
126
127
		$phpError = $this->restoreErrorHandler();
128
129
		if ( !$this->mConn ) {
130
			$this->queryLogger->debug( "DB connection error\n" );
131
			$this->queryLogger->debug(
132
				"Server: $server, Database: $dbName, User: $user, Password: " .
133
				substr( $password, 0, 3 ) . "...\n" );
134
			$this->queryLogger->debug( $this->lastError() . "\n" );
135
			throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
136
		}
137
138
		$this->mOpened = true;
139
140
		# If called from the command-line (e.g. importDump), only show errors
141
		if ( $this->cliMode ) {
142
			$this->doQuery( "SET client_min_messages = 'ERROR'" );
143
		}
144
145
		$this->query( "SET client_encoding='UTF8'", __METHOD__ );
146
		$this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
147
		$this->query( "SET timezone = 'GMT'", __METHOD__ );
148
		$this->query( "SET standard_conforming_strings = on", __METHOD__ );
149
		if ( $this->getServerVersion() >= 9.0 ) {
150
			$this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
151
		}
152
153
		$this->determineCoreSchema( $this->mSchema );
154
155
		return $this->mConn;
156
	}
157
158
	/**
159
	 * Postgres doesn't support selectDB in the same way MySQL does. So if the
160
	 * DB name doesn't match the open connection, open a new one
161
	 * @param string $db
162
	 * @return bool
163
	 */
164
	function selectDB( $db ) {
165
		if ( $this->mDBname !== $db ) {
166
			return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
167
		} else {
168
			return true;
169
		}
170
	}
171
172
	function makeConnectionString( $vars ) {
173
		$s = '';
174
		foreach ( $vars as $name => $value ) {
175
			$s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
176
		}
177
178
		return $s;
179
	}
180
181
	/**
182
	 * Closes a database connection, if it is open
183
	 * Returns success, true if already closed
184
	 * @return bool
185
	 */
186
	protected function closeConnection() {
187
		return pg_close( $this->mConn );
188
	}
189
190
	public function doQuery( $sql ) {
191
		$sql = mb_convert_encoding( $sql, 'UTF-8' );
192
		// Clear previously left over PQresult
193
		while ( $res = pg_get_result( $this->mConn ) ) {
194
			pg_free_result( $res );
195
		}
196
		if ( pg_send_query( $this->mConn, $sql ) === false ) {
197
			throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
198
		}
199
		$this->mLastResult = pg_get_result( $this->mConn );
200
		$this->mAffectedRows = null;
201
		if ( pg_result_error( $this->mLastResult ) ) {
202
			return false;
203
		}
204
205
		return $this->mLastResult;
206
	}
207
208
	protected function dumpError() {
209
		$diags = [
210
			PGSQL_DIAG_SEVERITY,
211
			PGSQL_DIAG_SQLSTATE,
212
			PGSQL_DIAG_MESSAGE_PRIMARY,
213
			PGSQL_DIAG_MESSAGE_DETAIL,
214
			PGSQL_DIAG_MESSAGE_HINT,
215
			PGSQL_DIAG_STATEMENT_POSITION,
216
			PGSQL_DIAG_INTERNAL_POSITION,
217
			PGSQL_DIAG_INTERNAL_QUERY,
218
			PGSQL_DIAG_CONTEXT,
219
			PGSQL_DIAG_SOURCE_FILE,
220
			PGSQL_DIAG_SOURCE_LINE,
221
			PGSQL_DIAG_SOURCE_FUNCTION
222
		];
223
		foreach ( $diags as $d ) {
224
			$this->queryLogger->debug( sprintf( "PgSQL ERROR(%d): %s\n",
225
				$d, pg_result_error_field( $this->mLastResult, $d ) ) );
226
		}
227
	}
228
229
	function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
230
		if ( $tempIgnore ) {
231
			/* Check for constraint violation */
232
			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...
233
				parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
234
235
				return;
236
			}
237
		}
238
		/* Transaction stays in the ERROR state until rolled back */
239
		if ( $this->mTrxLevel ) {
240
			$ignore = $this->ignoreErrors( true );
241
			$this->rollback( __METHOD__ );
242
			$this->ignoreErrors( $ignore );
243
		}
244
		parent::reportQueryError( $error, $errno, $sql, $fname, false );
245
	}
246
247
	function queryIgnore( $sql, $fname = __METHOD__ ) {
248
		return $this->query( $sql, $fname, true );
249
	}
250
251
	/**
252
	 * @param stdClass|ResultWrapper $res
253
	 * @throws DBUnexpectedError
254
	 */
255 View Code Duplication
	function freeResult( $res ) {
256
		if ( $res instanceof ResultWrapper ) {
257
			$res = $res->result;
258
		}
259
		MediaWiki\suppressWarnings();
260
		$ok = pg_free_result( $res );
261
		MediaWiki\restoreWarnings();
262
		if ( !$ok ) {
263
			throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
264
		}
265
	}
266
267
	/**
268
	 * @param ResultWrapper|stdClass $res
269
	 * @return stdClass
270
	 * @throws DBUnexpectedError
271
	 */
272 View Code Duplication
	function fetchObject( $res ) {
273
		if ( $res instanceof ResultWrapper ) {
274
			$res = $res->result;
275
		}
276
		MediaWiki\suppressWarnings();
277
		$row = pg_fetch_object( $res );
278
		MediaWiki\restoreWarnings();
279
		# @todo FIXME: HACK HACK HACK HACK debug
280
281
		# @todo hashar: not sure if the following test really trigger if the object
282
		#          fetching failed.
283
		if ( pg_last_error( $this->mConn ) ) {
284
			throw new DBUnexpectedError(
285
				$this,
286
				'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
287
			);
288
		}
289
290
		return $row;
291
	}
292
293 View Code Duplication
	function fetchRow( $res ) {
294
		if ( $res instanceof ResultWrapper ) {
295
			$res = $res->result;
296
		}
297
		MediaWiki\suppressWarnings();
298
		$row = pg_fetch_array( $res );
299
		MediaWiki\restoreWarnings();
300
		if ( pg_last_error( $this->mConn ) ) {
301
			throw new DBUnexpectedError(
302
				$this,
303
				'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
304
			);
305
		}
306
307
		return $row;
308
	}
309
310 View Code Duplication
	function numRows( $res ) {
311
		if ( $res instanceof ResultWrapper ) {
312
			$res = $res->result;
313
		}
314
		MediaWiki\suppressWarnings();
315
		$n = pg_num_rows( $res );
316
		MediaWiki\restoreWarnings();
317
		if ( pg_last_error( $this->mConn ) ) {
318
			throw new DBUnexpectedError(
319
				$this,
320
				'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
321
			);
322
		}
323
324
		return $n;
325
	}
326
327
	function numFields( $res ) {
328
		if ( $res instanceof ResultWrapper ) {
329
			$res = $res->result;
330
		}
331
332
		return pg_num_fields( $res );
333
	}
334
335
	function fieldName( $res, $n ) {
336
		if ( $res instanceof ResultWrapper ) {
337
			$res = $res->result;
338
		}
339
340
		return pg_field_name( $res, $n );
341
	}
342
343
	/**
344
	 * Return the result of the last call to nextSequenceValue();
345
	 * This must be called after nextSequenceValue().
346
	 *
347
	 * @return int|null
348
	 */
349
	function insertId() {
350
		return $this->mInsertId;
351
	}
352
353
	/**
354
	 * @param mixed $res
355
	 * @param int $row
356
	 * @return bool
357
	 */
358
	function dataSeek( $res, $row ) {
359
		if ( $res instanceof ResultWrapper ) {
360
			$res = $res->result;
361
		}
362
363
		return pg_result_seek( $res, $row );
364
	}
365
366
	function lastError() {
367
		if ( $this->mConn ) {
368
			if ( $this->mLastResult ) {
369
				return pg_result_error( $this->mLastResult );
370
			} else {
371
				return pg_last_error();
372
			}
373
		} else {
374
			return 'No database connection';
375
		}
376
	}
377
378
	function lastErrno() {
379
		if ( $this->mLastResult ) {
380
			return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
381
		} else {
382
			return false;
383
		}
384
	}
385
386
	function affectedRows() {
387
		if ( !is_null( $this->mAffectedRows ) ) {
388
			// Forced result for simulated queries
389
			return $this->mAffectedRows;
390
		}
391
		if ( empty( $this->mLastResult ) ) {
392
			return 0;
393
		}
394
395
		return pg_affected_rows( $this->mLastResult );
396
	}
397
398
	/**
399
	 * Estimate rows in dataset
400
	 * Returns estimated count, based on EXPLAIN output
401
	 * This is not necessarily an accurate estimate, so use sparingly
402
	 * Returns -1 if count cannot be found
403
	 * Takes same arguments as Database::select()
404
	 *
405
	 * @param string $table
406
	 * @param string $vars
407
	 * @param string $conds
408
	 * @param string $fname
409
	 * @param array $options
410
	 * @return int
411
	 */
412 View Code Duplication
	function estimateRowCount( $table, $vars = '*', $conds = '',
413
		$fname = __METHOD__, $options = []
414
	) {
415
		$options['EXPLAIN'] = true;
416
		$res = $this->select( $table, $vars, $conds, $fname, $options );
417
		$rows = -1;
418
		if ( $res ) {
419
			$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 416 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...
420
			$count = [];
421
			if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
422
				$rows = (int)$count[1];
423
			}
424
		}
425
426
		return $rows;
427
	}
428
429
	/**
430
	 * Returns information about an index
431
	 * If errors are explicitly ignored, returns NULL on failure
432
	 *
433
	 * @param string $table
434
	 * @param string $index
435
	 * @param string $fname
436
	 * @return bool|null
437
	 */
438
	function indexInfo( $table, $index, $fname = __METHOD__ ) {
439
		$sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
440
		$res = $this->query( $sql, $fname );
441
		if ( !$res ) {
442
			return null;
443
		}
444
		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...
445
			if ( $row->indexname == $this->indexName( $index ) ) {
446
				return $row;
447
			}
448
		}
449
450
		return false;
451
	}
452
453
	/**
454
	 * Returns is of attributes used in index
455
	 *
456
	 * @since 1.19
457
	 * @param string $index
458
	 * @param bool|string $schema
459
	 * @return array
460
	 */
461
	function indexAttributes( $index, $schema = false ) {
462
		if ( $schema === false ) {
463
			$schema = $this->getCoreSchema();
464
		}
465
		/*
466
		 * A subquery would be not needed if we didn't care about the order
467
		 * of attributes, but we do
468
		 */
469
		$sql = <<<__INDEXATTR__
470
471
			SELECT opcname,
472
				attname,
473
				i.indoption[s.g] as option,
474
				pg_am.amname
475
			FROM
476
				(SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
477
					FROM
478
						pg_index isub
479
					JOIN pg_class cis
480
						ON cis.oid=isub.indexrelid
481
					JOIN pg_namespace ns
482
						ON cis.relnamespace = ns.oid
483
					WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
484
				pg_attribute,
485
				pg_opclass opcls,
486
				pg_am,
487
				pg_class ci
488
				JOIN pg_index i
489
					ON ci.oid=i.indexrelid
490
				JOIN pg_class ct
491
					ON ct.oid = i.indrelid
492
				JOIN pg_namespace n
493
					ON ci.relnamespace = n.oid
494
				WHERE
495
					ci.relname='$index' AND n.nspname='$schema'
496
					AND	attrelid = ct.oid
497
					AND	i.indkey[s.g] = attnum
498
					AND	i.indclass[s.g] = opcls.oid
499
					AND	pg_am.oid = opcls.opcmethod
500
__INDEXATTR__;
501
		$res = $this->query( $sql, __METHOD__ );
502
		$a = [];
503
		if ( $res ) {
504
			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...
505
				$a[] = [
506
					$row->attname,
507
					$row->opcname,
508
					$row->amname,
509
					$row->option ];
510
			}
511
		} else {
512
			return null;
513
		}
514
515
		return $a;
516
	}
517
518
	function indexUnique( $table, $index, $fname = __METHOD__ ) {
519
		$sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
520
			" AND indexdef LIKE 'CREATE UNIQUE%(" .
521
			$this->strencode( $this->indexName( $index ) ) .
522
			")'";
523
		$res = $this->query( $sql, $fname );
524
		if ( !$res ) {
525
			return null;
526
		}
527
528
		return $res->numRows() > 0;
529
	}
530
531
	function selectSQLText(
532
		$table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
533
	) {
534
		// Change the FOR UPDATE option as necessary based on the join conditions. Then pass
535
		// to the parent function to get the actual SQL text.
536
		// In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
537
		// can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to
538
		// do so causes a DB error. This wrapper checks which tables can be locked and adjusts it
539
		// accordingly.
540
		// MySQL uses "ORDER BY NULL" as an optimization hint, but that is illegal in PostgreSQL.
541
		if ( is_array( $options ) ) {
542
			$forUpdateKey = array_search( 'FOR UPDATE', $options, true );
543
			if ( $forUpdateKey !== false && $join_conds ) {
544
				unset( $options[$forUpdateKey] );
545
546
				foreach ( $join_conds as $table_cond => $join_cond ) {
547
					if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
548
						$options['FOR UPDATE'][] = $table_cond;
549
					}
550
				}
551
			}
552
553
			if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
554
				unset( $options['ORDER BY'] );
555
			}
556
		}
557
558
		return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
559
	}
560
561
	/**
562
	 * INSERT wrapper, inserts an array into a table
563
	 *
564
	 * $args may be a single associative array, or an array of these with numeric keys,
565
	 * for multi-row insert (Postgres version 8.2 and above only).
566
	 *
567
	 * @param string $table Name of the table to insert to.
568
	 * @param array $args Items to insert into the table.
569
	 * @param string $fname Name of the function, for profiling
570
	 * @param array|string $options String or array. Valid options: IGNORE
571
	 * @return bool Success of insert operation. IGNORE always returns true.
572
	 */
573
	function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
574
		if ( !count( $args ) ) {
575
			return true;
576
		}
577
578
		$table = $this->tableName( $table );
579
		if ( !isset( $this->numericVersion ) ) {
580
			$this->getServerVersion();
581
		}
582
583
		if ( !is_array( $options ) ) {
584
			$options = [ $options ];
585
		}
586
587 View Code Duplication
		if ( isset( $args[0] ) && is_array( $args[0] ) ) {
588
			$multi = true;
589
			$keys = array_keys( $args[0] );
590
		} else {
591
			$multi = false;
592
			$keys = array_keys( $args );
593
		}
594
595
		// If IGNORE is set, we use savepoints to emulate mysql's behavior
596
		$savepoint = $olde = null;
597
		$numrowsinserted = 0;
598
		if ( in_array( 'IGNORE', $options ) ) {
599
			$savepoint = new SavepointPostgres( $this, 'mw', $this->queryLogger );
600
			$olde = error_reporting( 0 );
601
			// For future use, we may want to track the number of actual inserts
602
			// Right now, insert (all writes) simply return true/false
603
		}
604
605
		$sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
606
607
		if ( $multi ) {
608
			if ( $this->numericVersion >= 8.2 && !$savepoint ) {
609
				$first = true;
610 View Code Duplication
				foreach ( $args as $row ) {
611
					if ( $first ) {
612
						$first = false;
613
					} else {
614
						$sql .= ',';
615
					}
616
					$sql .= '(' . $this->makeList( $row ) . ')';
617
				}
618
				$res = (bool)$this->query( $sql, $fname, $savepoint );
619
			} else {
620
				$res = true;
621
				$origsql = $sql;
622
				foreach ( $args as $row ) {
623
					$tempsql = $origsql;
624
					$tempsql .= '(' . $this->makeList( $row ) . ')';
625
626
					if ( $savepoint ) {
627
						$savepoint->savepoint();
628
					}
629
630
					$tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
631
632 View Code Duplication
					if ( $savepoint ) {
633
						$bar = pg_result_error( $this->mLastResult );
634
						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...
635
							$savepoint->rollback();
636
						} else {
637
							$savepoint->release();
638
							$numrowsinserted++;
639
						}
640
					}
641
642
					// If any of them fail, we fail overall for this function call
643
					// Note that this will be ignored if IGNORE is set
644
					if ( !$tempres ) {
645
						$res = false;
646
					}
647
				}
648
			}
649
		} else {
650
			// Not multi, just a lone insert
651
			if ( $savepoint ) {
652
				$savepoint->savepoint();
653
			}
654
655
			$sql .= '(' . $this->makeList( $args ) . ')';
656
			$res = (bool)$this->query( $sql, $fname, $savepoint );
657 View Code Duplication
			if ( $savepoint ) {
658
				$bar = pg_result_error( $this->mLastResult );
659
				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...
660
					$savepoint->rollback();
661
				} else {
662
					$savepoint->release();
663
					$numrowsinserted++;
664
				}
665
			}
666
		}
667
		if ( $savepoint ) {
668
			error_reporting( $olde );
669
			$savepoint->commit();
670
671
			// Set the affected row count for the whole operation
672
			$this->mAffectedRows = $numrowsinserted;
673
674
			// IGNORE always returns true
675
			return true;
676
		}
677
678
		return $res;
679
	}
680
681
	/**
682
	 * INSERT SELECT wrapper
683
	 * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ]
684
	 * Source items may be literals rather then field names, but strings should
685
	 * be quoted with Database::addQuotes()
686
	 * $conds may be "*" to copy the whole table
687
	 * srcTable may be an array of tables.
688
	 * @todo FIXME: Implement this a little better (seperate select/insert)?
689
	 *
690
	 * @param string $destTable
691
	 * @param array|string $srcTable
692
	 * @param array $varMap
693
	 * @param array $conds
694
	 * @param string $fname
695
	 * @param array $insertOptions
696
	 * @param array $selectOptions
697
	 * @return bool
698
	 */
699
	function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
700
		$insertOptions = [], $selectOptions = [] ) {
701
		$destTable = $this->tableName( $destTable );
702
703
		if ( !is_array( $insertOptions ) ) {
704
			$insertOptions = [ $insertOptions ];
705
		}
706
707
		/*
708
		 * If IGNORE is set, we use savepoints to emulate mysql's behavior
709
		 * Ignore LOW PRIORITY option, since it is MySQL-specific
710
		 */
711
		$savepoint = $olde = null;
712
		$numrowsinserted = 0;
713
		if ( in_array( 'IGNORE', $insertOptions ) ) {
714
			$savepoint = new SavepointPostgres( $this, 'mw', $this->queryLogger );
715
			$olde = error_reporting( 0 );
716
			$savepoint->savepoint();
717
		}
718
719
		if ( !is_array( $selectOptions ) ) {
720
			$selectOptions = [ $selectOptions ];
721
		}
722
		list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
723
			$this->makeSelectOptions( $selectOptions );
724 View Code Duplication
		if ( is_array( $srcTable ) ) {
725
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
726
		} else {
727
			$srcTable = $this->tableName( $srcTable );
728
		}
729
730
		$sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
731
			" SELECT $startOpts " . implode( ',', $varMap ) .
732
			" FROM $srcTable $useIndex $ignoreIndex ";
733
734
		if ( $conds != '*' ) {
735
			$sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
736
		}
737
738
		$sql .= " $tailOpts";
739
740
		$res = (bool)$this->query( $sql, $fname, $savepoint );
741
		if ( $savepoint ) {
742
			$bar = pg_result_error( $this->mLastResult );
743
			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...
744
				$savepoint->rollback();
745
			} else {
746
				$savepoint->release();
747
				$numrowsinserted++;
748
			}
749
			error_reporting( $olde );
750
			$savepoint->commit();
751
752
			// Set the affected row count for the whole operation
753
			$this->mAffectedRows = $numrowsinserted;
754
755
			// IGNORE always returns true
756
			return true;
757
		}
758
759
		return $res;
760
	}
761
762 View Code Duplication
	function tableName( $name, $format = 'quoted' ) {
763
		# Replace reserved words with better ones
764
		switch ( $name ) {
765
			case 'user':
766
				return $this->realTableName( 'mwuser', $format );
767
			case 'text':
768
				return $this->realTableName( 'pagecontent', $format );
769
			default:
770
				return $this->realTableName( $name, $format );
771
		}
772
	}
773
774
	/* Don't cheat on installer */
775
	function realTableName( $name, $format = 'quoted' ) {
776
		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...
777
	}
778
779
	/**
780
	 * Return the next in a sequence, save the value for retrieval via insertId()
781
	 *
782
	 * @param string $seqName
783
	 * @return int|null
784
	 */
785
	function nextSequenceValue( $seqName ) {
786
		$safeseq = str_replace( "'", "''", $seqName );
787
		$res = $this->query( "SELECT nextval('$safeseq')" );
788
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query("SELECT nextval('{$safeseq}')") on line 787 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...
789
		$this->mInsertId = $row[0];
790
791
		return $this->mInsertId;
792
	}
793
794
	/**
795
	 * Return the current value of a sequence. Assumes it has been nextval'ed in this session.
796
	 *
797
	 * @param string $seqName
798
	 * @return int
799
	 */
800
	function currentSequenceValue( $seqName ) {
801
		$safeseq = str_replace( "'", "''", $seqName );
802
		$res = $this->query( "SELECT currval('$safeseq')" );
803
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query("SELECT currval('{$safeseq}')") on line 802 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...
804
		$currval = $row[0];
805
806
		return $currval;
807
	}
808
809
	# Returns the size of a text field, or -1 for "unlimited"
810
	function textFieldSize( $table, $field ) {
811
		$table = $this->tableName( $table );
812
		$sql = "SELECT t.typname as ftype,a.atttypmod as size
813
			FROM pg_class c, pg_attribute a, pg_type t
814
			WHERE relname='$table' AND a.attrelid=c.oid AND
815
				a.atttypid=t.oid and a.attname='$field'";
816
		$res = $this->query( $sql );
817
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql) on line 816 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...
818
		if ( $row->ftype == 'varchar' ) {
819
			$size = $row->size - 4;
820
		} else {
821
			$size = $row->size;
822
		}
823
824
		return $size;
825
	}
826
827
	function limitResult( $sql, $limit, $offset = false ) {
828
		return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
829
	}
830
831
	function wasDeadlock() {
832
		return $this->lastErrno() == '40P01';
833
	}
834
835
	function duplicateTableStructure(
836
		$oldName, $newName, $temporary = false, $fname = __METHOD__
837
	) {
838
		$newName = $this->addIdentifierQuotes( $newName );
839
		$oldName = $this->addIdentifierQuotes( $oldName );
840
841
		return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " .
842
			"(LIKE $oldName INCLUDING DEFAULTS)", $fname );
843
	}
844
845
	function listTables( $prefix = null, $fname = __METHOD__ ) {
846
		$eschema = $this->addQuotes( $this->getCoreSchema() );
847
		$result = $this->query(
848
			"SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
849
		$endArray = [];
850
851 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...
852
			$vars = get_object_vars( $table );
853
			$table = array_pop( $vars );
854
			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...
855
				$endArray[] = $table;
856
			}
857
		}
858
859
		return $endArray;
860
	}
861
862
	function timestamp( $ts = 0 ) {
863
		$ct = new ConvertableTimestamp( $ts );
864
865
		return $ct->getTimestamp( TS_POSTGRES );
866
	}
867
868
	/**
869
	 * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
870
	 * to http://www.php.net/manual/en/ref.pgsql.php
871
	 *
872
	 * Parsing a postgres array can be a tricky problem, he's my
873
	 * take on this, it handles multi-dimensional arrays plus
874
	 * escaping using a nasty regexp to determine the limits of each
875
	 * data-item.
876
	 *
877
	 * This should really be handled by PHP PostgreSQL module
878
	 *
879
	 * @since 1.19
880
	 * @param string $text Postgreql array returned in a text form like {a,b}
881
	 * @param string $output
882
	 * @param int|bool $limit
883
	 * @param int $offset
884
	 * @return string
885
	 */
886
	function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
887
		if ( false === $limit ) {
888
			$limit = strlen( $text ) - 1;
889
			$output = [];
890
		}
891
		if ( '{}' == $text ) {
892
			return $output;
893
		}
894
		do {
895
			if ( '{' != $text[$offset] ) {
896
				preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
897
					$text, $match, 0, $offset );
898
				$offset += strlen( $match[0] );
899
				$output[] = ( '"' != $match[1][0]
900
					? $match[1]
901
					: stripcslashes( substr( $match[1], 1, -1 ) ) );
902
				if ( '},' == $match[3] ) {
903
					return $output;
904
				}
905
			} else {
906
				$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 889 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...
907
			}
908
		} while ( $limit > $offset );
909
910
		return $output;
911
	}
912
913
	/**
914
	 * Return aggregated value function call
915
	 * @param array $valuedata
916
	 * @param string $valuename
917
	 * @return array
918
	 */
919
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
920
		return $valuedata;
921
	}
922
923
	/**
924
	 * @return string Wikitext of a link to the server software's web site
925
	 */
926
	public function getSoftwareLink() {
927
		return '[{{int:version-db-postgres-url}} PostgreSQL]';
928
	}
929
930
	/**
931
	 * Return current schema (executes SELECT current_schema())
932
	 * Needs transaction
933
	 *
934
	 * @since 1.19
935
	 * @return string Default schema for the current session
936
	 */
937
	function getCurrentSchema() {
938
		$res = $this->query( "SELECT current_schema()", __METHOD__ );
939
		$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 938 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...
940
941
		return $row[0];
942
	}
943
944
	/**
945
	 * Return list of schemas which are accessible without schema name
946
	 * This is list does not contain magic keywords like "$user"
947
	 * Needs transaction
948
	 *
949
	 * @see getSearchPath()
950
	 * @see setSearchPath()
951
	 * @since 1.19
952
	 * @return array List of actual schemas for the current sesson
953
	 */
954
	function getSchemas() {
955
		$res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
956
		$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 955 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...
957
		$schemas = [];
958
959
		/* PHP pgsql support does not support array type, "{a,b}" string is returned */
960
961
		return $this->pg_array_parse( $row[0], $schemas );
962
	}
963
964
	/**
965
	 * Return search patch for schemas
966
	 * This is different from getSchemas() since it contain magic keywords
967
	 * (like "$user").
968
	 * Needs transaction
969
	 *
970
	 * @since 1.19
971
	 * @return array How to search for table names schemas for the current user
972
	 */
973
	function getSearchPath() {
974
		$res = $this->query( "SHOW search_path", __METHOD__ );
975
		$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 974 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...
976
977
		/* PostgreSQL returns SHOW values as strings */
978
979
		return explode( ",", $row[0] );
980
	}
981
982
	/**
983
	 * Update search_path, values should already be sanitized
984
	 * Values may contain magic keywords like "$user"
985
	 * @since 1.19
986
	 *
987
	 * @param array $search_path List of schemas to be searched by default
988
	 */
989
	function setSearchPath( $search_path ) {
990
		$this->query( "SET search_path = " . implode( ", ", $search_path ) );
991
	}
992
993
	/**
994
	 * Determine default schema for MediaWiki core
995
	 * Adjust this session schema search path if desired schema exists
996
	 * and is not alread there.
997
	 *
998
	 * We need to have name of the core schema stored to be able
999
	 * to query database metadata.
1000
	 *
1001
	 * This will be also called by the installer after the schema is created
1002
	 *
1003
	 * @since 1.19
1004
	 *
1005
	 * @param string $desiredSchema
1006
	 */
1007
	function determineCoreSchema( $desiredSchema ) {
1008
		$this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
1009
		if ( $this->schemaExists( $desiredSchema ) ) {
1010
			if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
1011
				$this->mCoreSchema = $desiredSchema;
1012
				$this->queryLogger->debug(
1013
					"Schema \"" . $desiredSchema . "\" already in the search path\n" );
1014
			} else {
1015
				/**
1016
				 * Prepend our schema (e.g. 'mediawiki') in front
1017
				 * of the search path
1018
				 * Fixes bug 15816
1019
				 */
1020
				$search_path = $this->getSearchPath();
1021
				array_unshift( $search_path,
1022
					$this->addIdentifierQuotes( $desiredSchema ) );
1023
				$this->setSearchPath( $search_path );
1024
				$this->mCoreSchema = $desiredSchema;
1025
				$this->queryLogger->debug(
1026
					"Schema \"" . $desiredSchema . "\" added to the search path\n" );
1027
			}
1028
		} else {
1029
			$this->mCoreSchema = $this->getCurrentSchema();
1030
			$this->queryLogger->debug(
1031
				"Schema \"" . $desiredSchema . "\" not found, using current \"" .
1032
				$this->mCoreSchema . "\"\n" );
1033
		}
1034
		/* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
1035
		$this->commit( __METHOD__ );
1036
	}
1037
1038
	/**
1039
	 * Return schema name fore core MediaWiki tables
1040
	 *
1041
	 * @since 1.19
1042
	 * @return string Core schema name
1043
	 */
1044
	function getCoreSchema() {
1045
		return $this->mCoreSchema;
1046
	}
1047
1048
	/**
1049
	 * @return string Version information from the database
1050
	 */
1051
	function getServerVersion() {
1052
		if ( !isset( $this->numericVersion ) ) {
1053
			$versionInfo = pg_version( $this->mConn );
1054
			if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
1055
				// Old client, abort install
1056
				$this->numericVersion = '7.3 or earlier';
1057
			} elseif ( isset( $versionInfo['server'] ) ) {
1058
				// Normal client
1059
				$this->numericVersion = $versionInfo['server'];
1060
			} else {
1061
				// Bug 16937: broken pgsql extension from PHP<5.3
1062
				$this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' );
1063
			}
1064
		}
1065
1066
		return $this->numericVersion;
1067
	}
1068
1069
	/**
1070
	 * Query whether a given relation exists (in the given schema, or the
1071
	 * default mw one if not given)
1072
	 * @param string $table
1073
	 * @param array|string $types
1074
	 * @param bool|string $schema
1075
	 * @return bool
1076
	 */
1077
	function relationExists( $table, $types, $schema = false ) {
1078
		if ( !is_array( $types ) ) {
1079
			$types = [ $types ];
1080
		}
1081
		if ( !$schema ) {
1082
			$schema = $this->getCoreSchema();
1083
		}
1084
		$table = $this->realTableName( $table, 'raw' );
1085
		$etable = $this->addQuotes( $table );
1086
		$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...
1087
		$sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
1088
			. "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
1089
			. "AND c.relkind IN ('" . implode( "','", $types ) . "')";
1090
		$res = $this->query( $sql );
1091
		$count = $res ? $res->numRows() : 0;
1092
1093
		return (bool)$count;
1094
	}
1095
1096
	/**
1097
	 * For backward compatibility, this function checks both tables and
1098
	 * views.
1099
	 * @param string $table
1100
	 * @param string $fname
1101
	 * @param bool|string $schema
1102
	 * @return bool
1103
	 */
1104
	function tableExists( $table, $fname = __METHOD__, $schema = false ) {
1105
		return $this->relationExists( $table, [ 'r', 'v' ], $schema );
1106
	}
1107
1108
	function sequenceExists( $sequence, $schema = false ) {
1109
		return $this->relationExists( $sequence, 'S', $schema );
1110
	}
1111
1112 View Code Duplication
	function triggerExists( $table, $trigger ) {
1113
		$q = <<<SQL
1114
	SELECT 1 FROM pg_class, pg_namespace, pg_trigger
1115
		WHERE relnamespace=pg_namespace.oid AND relkind='r'
1116
			  AND tgrelid=pg_class.oid
1117
			  AND nspname=%s AND relname=%s AND tgname=%s
1118
SQL;
1119
		$res = $this->query(
1120
			sprintf(
1121
				$q,
1122
				$this->addQuotes( $this->getCoreSchema() ),
1123
				$this->addQuotes( $table ),
1124
				$this->addQuotes( $trigger )
1125
			)
1126
		);
1127
		if ( !$res ) {
1128
			return null;
1129
		}
1130
		$rows = $res->numRows();
1131
1132
		return $rows;
1133
	}
1134
1135
	function ruleExists( $table, $rule ) {
1136
		$exists = $this->selectField( 'pg_rules', 'rulename',
1137
			[
1138
				'rulename' => $rule,
1139
				'tablename' => $table,
1140
				'schemaname' => $this->getCoreSchema()
1141
			]
1142
		);
1143
1144
		return $exists === $rule;
1145
	}
1146
1147 View Code Duplication
	function constraintExists( $table, $constraint ) {
1148
		$sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
1149
			"WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
1150
			$this->addQuotes( $this->getCoreSchema() ),
1151
			$this->addQuotes( $table ),
1152
			$this->addQuotes( $constraint )
1153
		);
1154
		$res = $this->query( $sql );
1155
		if ( !$res ) {
1156
			return null;
1157
		}
1158
		$rows = $res->numRows();
1159
1160
		return $rows;
1161
	}
1162
1163
	/**
1164
	 * Query whether a given schema exists. Returns true if it does, false if it doesn't.
1165
	 * @param string $schema
1166
	 * @return bool
1167
	 */
1168
	function schemaExists( $schema ) {
1169
		$exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
1170
			[ 'nspname' => $schema ], __METHOD__ );
1171
1172
		return (bool)$exists;
1173
	}
1174
1175
	/**
1176
	 * Returns true if a given role (i.e. user) exists, false otherwise.
1177
	 * @param string $roleName
1178
	 * @return bool
1179
	 */
1180
	function roleExists( $roleName ) {
1181
		$exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
1182
			[ 'rolname' => $roleName ], __METHOD__ );
1183
1184
		return (bool)$exists;
1185
	}
1186
1187
	/**
1188
	 * @var string $table
1189
	 * @var string $field
1190
	 * @return PostgresField|null
1191
	 */
1192
	function fieldInfo( $table, $field ) {
1193
		return PostgresField::fromText( $this, $table, $field );
1194
	}
1195
1196
	/**
1197
	 * pg_field_type() wrapper
1198
	 * @param ResultWrapper|resource $res ResultWrapper or PostgreSQL query result resource
1199
	 * @param int $index Field number, starting from 0
1200
	 * @return string
1201
	 */
1202
	function fieldType( $res, $index ) {
1203
		if ( $res instanceof ResultWrapper ) {
1204
			$res = $res->result;
1205
		}
1206
1207
		return pg_field_type( $res, $index );
1208
	}
1209
1210
	/**
1211
	 * @param string $b
1212
	 * @return Blob
1213
	 */
1214
	function encodeBlob( $b ) {
1215
		return new PostgresBlob( pg_escape_bytea( $b ) );
1216
	}
1217
1218
	function decodeBlob( $b ) {
1219
		if ( $b instanceof PostgresBlob ) {
1220
			$b = $b->fetch();
1221
		} elseif ( $b instanceof Blob ) {
1222
			return $b->fetch();
1223
		}
1224
1225
		return pg_unescape_bytea( $b );
1226
	}
1227
1228
	function strencode( $s ) {
1229
		// Should not be called by us
1230
1231
		return pg_escape_string( $this->mConn, $s );
1232
	}
1233
1234
	/**
1235
	 * @param null|bool|Blob $s
1236
	 * @return int|string
1237
	 */
1238
	function addQuotes( $s ) {
1239
		if ( is_null( $s ) ) {
1240
			return 'NULL';
1241
		} elseif ( is_bool( $s ) ) {
1242
			return intval( $s );
1243
		} elseif ( $s instanceof Blob ) {
1244
			if ( $s instanceof PostgresBlob ) {
1245
				$s = $s->fetch();
1246
			} else {
1247
				$s = pg_escape_bytea( $this->mConn, $s->fetch() );
1248
			}
1249
			return "'$s'";
1250
		}
1251
1252
		return "'" . pg_escape_string( $this->mConn, $s ) . "'";
1253
	}
1254
1255
	/**
1256
	 * Postgres specific version of replaceVars.
1257
	 * Calls the parent version in Database.php
1258
	 *
1259
	 * @param string $ins SQL string, read from a stream (usually tables.sql)
1260
	 * @return string SQL string
1261
	 */
1262
	protected function replaceVars( $ins ) {
1263
		$ins = parent::replaceVars( $ins );
1264
1265
		if ( $this->numericVersion >= 8.3 ) {
1266
			// Thanks for not providing backwards-compatibility, 8.3
1267
			$ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
1268
		}
1269
1270
		if ( $this->numericVersion <= 8.1 ) { // Our minimum version
1271
			$ins = str_replace( 'USING gin', 'USING gist', $ins );
1272
		}
1273
1274
		return $ins;
1275
	}
1276
1277
	/**
1278
	 * Various select options
1279
	 *
1280
	 * @param array $options An associative array of options to be turned into
1281
	 *   an SQL query, valid keys are listed in the function.
1282
	 * @return array
1283
	 */
1284
	function makeSelectOptions( $options ) {
1285
		$preLimitTail = $postLimitTail = '';
1286
		$startOpts = $useIndex = $ignoreIndex = '';
1287
1288
		$noKeyOptions = [];
1289
		foreach ( $options as $key => $option ) {
1290
			if ( is_numeric( $key ) ) {
1291
				$noKeyOptions[$option] = true;
1292
			}
1293
		}
1294
1295
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1296
1297
		$preLimitTail .= $this->makeOrderBy( $options );
1298
1299
		// if ( isset( $options['LIMIT'] ) ) {
1300
		// 	$tailOpts .= $this->limitResult( '', $options['LIMIT'],
1301
		// 		isset( $options['OFFSET'] ) ? $options['OFFSET']
1302
		// 		: false );
1303
		// }
1304
1305
		if ( isset( $options['FOR UPDATE'] ) ) {
1306
			$postLimitTail .= ' FOR UPDATE OF ' .
1307
				implode( ', ', array_map( [ &$this, 'tableName' ], $options['FOR UPDATE'] ) );
1308
		} elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1309
			$postLimitTail .= ' FOR UPDATE';
1310
		}
1311
1312
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1313
			$startOpts .= 'DISTINCT';
1314
		}
1315
1316
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1317
	}
1318
1319
	function getDBname() {
1320
		return $this->mDBname;
1321
	}
1322
1323
	function getServer() {
1324
		return $this->mServer;
1325
	}
1326
1327
	function buildConcat( $stringList ) {
1328
		return implode( ' || ', $stringList );
1329
	}
1330
1331 View Code Duplication
	public function buildGroupConcatField(
1332
		$delimiter, $table, $field, $conds = '', $options = [], $join_conds = []
1333
	) {
1334
		$fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')';
1335
1336
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1337
	}
1338
1339
	/**
1340
	 * @param string $field Field or column to cast
1341
	 * @return string
1342
	 * @since 1.28
1343
	 */
1344
	public function buildStringCast( $field ) {
1345
		return $field . '::text';
1346
	}
1347
1348
	public function streamStatementEnd( &$sql, &$newLine ) {
1349
		# Allow dollar quoting for function declarations
1350
		if ( substr( $newLine, 0, 4 ) == '$mw$' ) {
1351
			if ( $this->delimiter ) {
1352
				$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...
1353
			} else {
1354
				$this->delimiter = ';';
1355
			}
1356
		}
1357
1358
		return parent::streamStatementEnd( $sql, $newLine );
1359
	}
1360
1361
	/**
1362
	 * Check to see if a named lock is available. This is non-blocking.
1363
	 * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1364
	 *
1365
	 * @param string $lockName Name of lock to poll
1366
	 * @param string $method Name of method calling us
1367
	 * @return bool
1368
	 * @since 1.20
1369
	 */
1370 View Code Duplication
	public function lockIsFree( $lockName, $method ) {
1371
		$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...
1372
		$result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
1373
			WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
1374
		$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 1372 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...
1375
1376
		return ( $row->lockstatus === 't' );
1377
	}
1378
1379
	/**
1380
	 * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1381
	 * @param string $lockName
1382
	 * @param string $method
1383
	 * @param int $timeout
1384
	 * @return bool
1385
	 */
1386
	public function lock( $lockName, $method, $timeout = 5 ) {
1387
		$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...
1388
		$loop = new WaitConditionLoop(
1389
			function () use ( $lockName, $key, $timeout, $method ) {
1390
				$res = $this->query( "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
1391
				$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 1390 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...
1392
				if ( $row->lockstatus === 't' ) {
1393
					parent::lock( $lockName, $method, $timeout ); // record
1394
					return true;
1395
				}
1396
1397
				return WaitConditionLoop::CONDITION_CONTINUE;
1398
			},
1399
			$timeout
1400
		);
1401
1402
		return ( $loop->invoke() === $loop::CONDITION_REACHED );
1403
	}
1404
1405
	/**
1406
	 * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM
1407
	 * PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1408
	 * @param string $lockName
1409
	 * @param string $method
1410
	 * @return bool
1411
	 */
1412 View Code Duplication
	public function unlock( $lockName, $method ) {
1413
		$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...
1414
		$result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
1415
		$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 1414 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...
1416
1417
		if ( $row->lockstatus === 't' ) {
1418
			parent::unlock( $lockName, $method ); // record
1419
			return true;
1420
		}
1421
1422
		$this->queryLogger->debug( __METHOD__ . " failed to release lock\n" );
1423
1424
		return false;
1425
	}
1426
1427
	/**
1428
	 * @param string $lockName
1429
	 * @return string Integer
1430
	 */
1431
	private function bigintFromLockName( $lockName ) {
1432
		return Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
1433
	}
1434
}
1435