Passed
Pull Request — development (#3445)
by Emanuele
06:45
created

Query   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Test Coverage

Coverage 39.11%

Importance

Changes 11
Bugs 1 Features 1
Metric Value
eloc 109
dl 0
loc 395
ccs 97
cts 248
cp 0.3911
rs 8.8798
c 11
b 1
f 1
wmc 44

22 Methods

Rating   Name   Duplication   Size   Complexity  
A _replaceIdentifier() 0 8 2
A error() 0 19 2
B replace() 0 59 11
A server_version() 0 5 1
A initialChecks() 0 18 4
A last_error() 0 8 2
A list_tables() 0 5 1
A title() 0 3 1
A executeQuery() 0 5 1
A transaction() 0 22 4
A insert() 0 31 3
A lastResult() 0 3 1
A case_sensitive() 0 3 1
A support_ignore() 0 3 1
A select_db() 0 3 1
A insert_id() 0 21 2
A fix_prefix() 0 3 1
A unescape_string() 0 3 1
A escape_string() 0 3 1
A validConnection() 0 3 1
A client_version() 0 5 1
A server_info() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Query 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.

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 Query, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to the Postgre database.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Database\Postgresql;
18
19
use ElkArte\Database\AbstractQuery;
20
use ElkArte\Errors\Errors;
21
use ElkArte\ValuesContainer;
22
23
/**
24
 * PostgreSQL database class, implements database class to control mysql functions
25
 */
26
class Query extends AbstractQuery
27
{
28
	/**
29
	 * {@inheritDoc}
30
	 */
31
	const ESCAPE_CHAR = '\'\'';
32
33
	/**
34
	 * Since PostgreSQL doesn't support INSERT REPLACE we are using this to remember
35
	 * the rows affected by the delete
36
	 *
37
	 * @var int
38
	 */
39
	private $_in_transaction = false;
40
41
	/**
42
	 * {@inheritDoc}
43
	 */
44
	protected $ilike = ' ILIKE ';
45
46
	/**
47
	 * {@inheritDoc}
48
	 */
49
	protected $not_ilike = ' NOT ILIKE ';
50
51
	/**
52
	 * {@inheritDoc}
53
	 */
54
	protected $rlike = ' ~* ';
55
56
	/**
57
	 * {@inheritDoc}
58
	 */
59
	protected $not_rlike = ' !~* ';
60
61
	/**
62
	 * {@inheritDoc}
63
	 */
64
	public function fix_prefix($db_prefix, $db_name)
65
	{
66
		return $db_prefix;
67
	}
68
69
	/**
70
	 * {@inheritDoc}
71
	 */
72
	public function last_error()
73
	{
74
		if (is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
75
		{
76 37
			return pg_last_error($this->connection);
77
		}
78
79 37
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by ElkArte\Database\QueryInterface::last_error() of string.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
80
	}
81
82
	/**
83
	 * {@inheritDoc}
84
	 */
85 37
	public function insert($method, $table, $columns, $data, $keys, $disable_trans = false)
86
	{
87 27
		// Compatibility check meant to support the old way of doing REPLACE's
88
		if ($method === 'replace')
89
		{
90
			return $this->replace($table, $columns, $data, $keys, $disable_trans);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->replace($t... $keys, $disable_trans) returns the type ElkArte\Database\Postgresql\Result which is incompatible with the return type mandated by ElkArte\Database\QueryInterface::insert() of boolean|resource.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
91 37
		}
92
93 37
		list($table, $indexed_columns, $insertRows) = $this->prepareInsert($table, $columns, $data);
94 37
95
		// Do the insert.
96 33
		$this->result = $this->query('', '
97 33
			INSERT INTO ' . $table . '("' . implode('", "', $indexed_columns) . '")
98
			VALUES
99
			' . implode(',
100
			', $insertRows),
101 37
			array(
102
				'security_override' => true,
103 28
			)
104 28
		);
105 28
		$inserted_results = !is_resource($this->_db_last_result) ? 0 : pg_affected_rows($this->_db_last_result);
106 28
107
		$last_inserted_id = $this->insert_id($table);
108
109 28
		$this->result->updateDetails([
110
			'insert_id' => $last_inserted_id,
111 20
			'insertedResults' => $inserted_results,
112
			'lastResult' => $this->_db_last_result,
113
		]);
114
115 17
		return $this->result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->result returns the type ElkArte\Database\Postgresql\Result which is incompatible with the return type mandated by ElkArte\Database\QueryInterface::insert() of boolean|resource.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
116
	}
117
118
	/**
119 28
	 * {@inheritDoc}
120
	 */
121 28
	public function replace($table, $columns, $data, $keys, $disable_trans = false)
122
	{
123 28
		$local_transaction = false;
124
		if (!$this->_in_transaction && !$disable_trans)
125
		{
126
			$this->transaction('begin');
127 28
			$local_transaction = true;
128
		}
129 28
130
		// PostgreSQL doesn't support replace: we implement a MySQL-compatible behavior instead
131 28
		$count = 0;
132 28
		$where = '';
133 28
		$db_replace_result = 0;
134
		foreach ($columns as $columnName => $type)
135
		{
136 28
			// Are we restricting the length?
137
			if (strpos($type, 'string-') !== false)
138
			{
139
				$actualType = sprintf($columnName . ' = SUBSTRING({string:%1$s}, 1, ' . substr($type, 7) . '), ', $count);
140
			}
141 37
			else
142
			{
143
				$actualType = sprintf($columnName . ' = {%1$s:%2$s}, ', $type, $count);
144 37
			}
145 37
146
			// A key? That's what we were looking for.
147
			if (in_array($columnName, $keys))
148 37
			{
149
				$where .= (empty($where) ? '' : ' AND ') . substr($actualType, 0, -2);
150 25
			}
151
			$count++;
152
		}
153
154 34
		// Make it so.
155
		if (!empty($where))
156
		{
157 37
			foreach ($data as $k => $entry)
158
			{
159
				$this->query('', '
160 37
					DELETE FROM ' . $table .
161
					' WHERE ' . $where,
162
					$entry
163 37
				);
164 37
				$db_replace_result += (!is_resource($this->_db_last_result) ? 0 : pg_affected_rows($this->_db_last_result));
165
			}
166 37
		}
167
168
		$this->insert('', $table, $columns, $data, $keys, $disable_trans);
169 37
170 37
		$this->result->updateDetails([
171 37
			'replaceResults' => $db_replace_result + $this->result->getDetail('insertedResults')
172
		]);
173
174 37
		if ($local_transaction)
175 37
		{
176
			$this->transaction('commit');
177 37
		}
178
179
		return $this->result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->result returns the type ElkArte\Database\Postgresql\Result which is incompatible with the return type mandated by ElkArte\Database\QueryInterface::replace() of boolean|resource.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
180 37
	}
181
182
	/**
183 37
	 * {@inheritDoc}
184
	 */
185 37
	public function transaction($type = 'commit')
186
	{
187 28
		if ($type === 'begin')
188
		{
189
			$this->_in_transaction = true;
0 ignored issues
show
Documentation Bug introduced by
The property $_in_transaction was declared of type integer, but true is of type true. 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...
190
191 37
			return @pg_query($this->connection, 'BEGIN');
0 ignored issues
show
Bug introduced by
$this->connection of type ElkArte\Database\ConnectionInterface is incompatible with the type resource expected by parameter $connection of pg_query(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

191
			return @pg_query(/** @scrutinizer ignore-type */ $this->connection, 'BEGIN');
Loading history...
192
		}
193 33
194
		if ($type === 'rollback')
195
		{
196 37
			return @pg_query($this->connection, 'ROLLBACK');
197
		}
198 37
199
		if ($type === 'commit')
200
		{
201 37
			$this->_in_transaction = false;
202 37
203 37
			return @pg_query($this->connection, 'COMMIT');
204 37
		}
205 37
206 37
		return false;
207
	}
208
209
	/**
210 37
	 * {@inheritDoc}
211
	 */
212
	protected function initialChecks($db_string, $db_values, $identifier = '')
213
	{
214
		// Special queries that need processing.
215
		$replacements = array(
216 33
			'pm_conversation_list' => array(
217
				'~ORDER\\s+BY\\s+\\{raw:sort\\}~' => 'ORDER BY ' . (isset($db_values['sort']) ? ($db_values['sort'] === 'pm.id_pm' ? 'MAX(pm.id_pm)' : $db_values['sort']) : ''),
218 33
			),
219
		);
220 33
221
		if (isset($replacements[$identifier]))
222 33
		{
223
			$db_string = preg_replace(array_keys($replacements[$identifier]), array_values($replacements[$identifier]), $db_string);
224
		}
225 33
226
		// Limits need to be a little different, left in place for non conformance addons
227
		$db_string = preg_replace('~\sLIMIT\s(\d+|{int:.+}),\s*(\d+|{int:.+})(.*)~i', ' LIMIT $2 OFFSET $1 $3', $db_string);
228
229
		return $db_string;
230 33
	}
231
232 33
	/**
233
	 * {@inheritDoc}
234 33
	 */
235
	protected function executeQuery($db_string)
236
	{
237
		$this->_db_last_result = @pg_query($this->connection, $db_string);
0 ignored issues
show
Bug introduced by
$this->connection of type ElkArte\Database\ConnectionInterface is incompatible with the type resource expected by parameter $connection of pg_query(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

237
		$this->_db_last_result = @pg_query(/** @scrutinizer ignore-type */ $this->connection, $db_string);
Loading history...
Documentation Bug introduced by
It seems like @pg_query($this->connection, $db_string) can also be of type false. However, the property $_db_last_result is declared as type resource. Maybe add an additional type 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 mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
238
239
		$this->result = new Result($this->_db_last_result);
240
	}
241
242
	/**
243 148
	 * {@inheritDoc}
244
	 */
245
	public function error($db_string)
246
	{
247
		global $txt, $modSettings;
248 148
249
		// We'll try recovering the file and line number the original db query was called from.
250
		list ($file, $line) = $this->backtrace_message();
251
252
		// Just in case nothing can be found from debug_backtrace
253
		$file = $file ?? __FILE__;
254
		$line = $line ?? __LINE__;
255
256
		// Decide which connection to use
257
		// This is the error message...
258
		$query_error = @pg_last_error($this->connection);
0 ignored issues
show
Bug introduced by
$this->connection of type ElkArte\Database\ConnectionInterface is incompatible with the type resource expected by parameter $connection of pg_last_error(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

258
		$query_error = @pg_last_error(/** @scrutinizer ignore-type */ $this->connection);
Loading history...
259
260
		// Log the error.
261
		Errors::instance()->log_error($txt['database_error'] . ': ' . $query_error . (!empty($modSettings['enableErrorQueryLogging']) ? "\n\n" . $db_string : ''), 'database', $file, $line);
262
263
		$this->throwError($db_string, $query_error, $file, $line);
264
	}
265
266
	/**
267
	 * Last inserted id.
268
	 *
269
	 * @param string $table
270
	 *
271 148
	 * @return bool|int
272
	 * @throws \ElkArte\Exceptions\Exception
273
	 */
274
	public function insert_id($table)
275
	{
276
		$table = str_replace('{db_prefix}', $this->_db_prefix, $table);
277
278 148
		$this->skip_next_error();
279
280
		// Try get the last ID for the auto increment field.
281
		$request = $this->query('', '
282
			SELECT CURRVAL(\'' . $table . '_seq\') AS insertID',
283
			array('security_override' => true)
284 148
		);
285
286 148
		if (!$request)
0 ignored issues
show
introduced by
$request is of type ElkArte\Database\Postgresql\Result, thus it always evaluated to true.
Loading history...
287
		{
288
			return false;
289
		}
290
291
		list ($lastID) = $request->fetch_row();
292 148
		$request->free_result();
293 148
294
		return $lastID;
295 148
	}
296
297
	/**
298 148
	 * Unescape an escaped string!
299
	 *
300 148
	 * @param string $string
301
	 *
302 148
	 * @return string
303
	 */
304 148
	public function unescape_string($string)
305
	{
306
		return strtr($string, array('\'\'' => '\''));
307
	}
308
309
	/**
310 148
	 * {@inheritDoc}
311
	 */
312 37
	public function support_ignore()
313
	{
314
		return false;
315
	}
316 148
317
	/**
318 148
	 * {@inheritDoc}
319
	 */
320
	public function server_version()
321
	{
322 148
		$version = pg_version();
323
324 34
		return $version['server'];
325
	}
326
327 148
	/**
328
	 * {@inheritDoc}
329
	 */
330
	public function title()
331
	{
332
		return 'PostgreSQL';
333
	}
334
335
	/**
336
	 * {@inheritDoc}
337
	 */
338
	public function case_sensitive()
339
	{
340
		return true;
341
	}
342
343
	/**
344
	 * {@inheritDoc}
345
	 */
346
	public function escape_string($string)
347
	{
348
		return pg_escape_string($string);
0 ignored issues
show
Bug introduced by
The call to pg_escape_string() has too few arguments starting with data. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

348
		return /** @scrutinizer ignore-call */ pg_escape_string($string);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
349
	}
350
351
	/**
352
	 * {@inheritDoc}
353
	 */
354
	public function server_info()
355
	{
356
		// give info on client! we use it in install and upgrade and such things.
357
		$version = pg_version();
358
359
		return $version['client'];
360
	}
361
362
	/**
363
	 * {@inheritDoc}
364
	 */
365
	public function client_version()
366
	{
367
		$version = pg_version();
368
369
		return $version['client'];
370
	}
371
372
	/**
373
	 * Dummy function really. Doesn't do anything in PostgreSQL.
374
	 *
375
	 * {@inheritDoc}
376
	 */
377
	public function select_db($db_name = null)
378
	{
379
		return true;
380
	}
381
382
	/**
383
	 * Returns the result resouce of the last query executed
384 37
	 *
385
	 * @return resource
386 37
	 */
387
	public function lastResult()
388 37
	{
389
		return $this->_db_last_result;
390
	}
391 37
392 37
	/**
393 37
	 * {@inheritDoc}
394
	 */
395
	public function validConnection()
396 37
	{
397
		return is_resource($this->connection);
398 34
	}
399
400
	/**
401 19
	 * {@inheritDoc}
402 19
	 */
403
	public function list_tables($db_name_str = false, $filter = false)
404 19
	{
405
		$dump = new Dump($this);
406
407
		return $dump->list_tables($db_name_str, $filter);
408
	}
409
410
	/**
411
	 * {@inheritDoc}
412
	 */
413
	protected function _replaceIdentifier($replacement)
414
	{
415
		if (preg_match('~[a-z_][0-9a-zA-Z$,_]{0,60}~', $replacement) !== 1)
416
		{
417
			$this->error_backtrace('Wrong value type sent to the database. Invalid identifier used. (' . $replacement . ')', '', E_USER_ERROR, __FILE__, __LINE__);
418
		}
419
420
		return '"' . $replacement . '"';
421
	}
422
}
423