Passed
Pull Request — development (#3537)
by Martyn
06:41
created

Query::insert()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 47
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 8.9103

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 23
nc 9
nop 6
dl 0
loc 47
ccs 21
cts 37
cp 0.5676
crap 8.9103
rs 8.9297
c 1
b 0
f 0
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
22
/**
23
 * PostgreSQL database class, implements database class to control mysql functions
24
 */
25
class Query extends AbstractQuery
26
{
27
	/**
28
	 * {@inheritDoc}
29
	 */
30
	const ESCAPE_CHAR = '\'\'';
31
32
	/**
33
	 * Since PostgreSQL doesn't support INSERT REPLACE we are using this to remember
34
	 * the rows affected by the delete
35
	 *
36
	 * @var int
37
	 */
38
	private $_in_transaction = false;
39
40
	/**
41
	 * {@inheritDoc}
42
	 */
43
	protected $ilike = ' ILIKE ';
44
45
	/**
46
	 * {@inheritDoc}
47
	 */
48
	protected $not_ilike = ' NOT ILIKE ';
49
50
	/**
51
	 * {@inheritDoc}
52
	 */
53
	protected $rlike = ' ~* ';
54
55
	/**
56
	 * {@inheritDoc}
57
	 */
58
	protected $not_rlike = ' !~* ';
59
60
	/**
61
	 * Used by insert to deal with conflicts (mostly for replace)
62
	 */
63
	private $on_conflict = '';
64
65
	/**
66
	 * {@inheritDoc}
67
	 */
68
	public function fix_prefix($db_prefix, $db_name)
69
	{
70
		return $db_prefix;
71
	}
72
73
	/**
74
	 * {@inheritDoc}
75
	 */
76 37
	public function last_error()
77
	{
78
		if (is_resource($this->connection) || $this->connection instanceof \PgSql\Connection)
0 ignored issues
show
Bug introduced by
The type PgSql\Connection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
79 37
		{
80
			return pg_last_error($this->connection);
0 ignored issues
show
Bug introduced by
It seems like $this->connection can also be of type PgSql\Connection; however, parameter $connection of pg_last_error() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

80
			return pg_last_error(/** @scrutinizer ignore-type */ $this->connection);
Loading history...
81
		}
82
83
		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...
84
	}
85 37
86
	/**
87 27
	 * {@inheritDoc}
88
	 */
89
	public function insert($method, $table, $columns, $data, $keys, $disable_trans = false)
90
	{
91 37
		// Compatibility check meant to support the old way of doing REPLACE's
92
		if ($method === 'replace')
93 37
		{
94 37
			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...
95
		}
96 33
97 33
		if ($method === 'ignore')
98
		{
99
			$this->on_conflict = ' ON CONFLICT (' . implode(', ', $keys) . ') DO NOTHING';
100
		}
101 37
102
		list($table, $indexed_columns, $insertRows) = $this->prepareInsert($table, $columns, $data);
103 28
104 28
		// Do the insert.
105 28
		$this->result = $this->query('', '
106 28
			INSERT INTO ' . $table . '("' . implode('", "', $indexed_columns) . '")
107
			VALUES
108
			' . implode(',
109 28
			', $insertRows) . $this->on_conflict,
110
			array(
111 20
				'security_override' => true,
112
			)
113
		);
114
		if ($method === 'ignore')
115 17
		{
116
			$this->on_conflict = '';
117
		}
118
119 28
		if(is_resource($this->_db_last_result) || $this->_db_last_result instanceof \PgSQL\Result)
0 ignored issues
show
Bug introduced by
The type PgSQL\Result was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
120
		{
121 28
			$inserted_results = pg_affected_rows($this->_db_last_result);
0 ignored issues
show
Bug introduced by
It seems like $this->_db_last_result can also be of type PgSQL\Result; however, parameter $result of pg_affected_rows() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

121
			$inserted_results = pg_affected_rows(/** @scrutinizer ignore-type */ $this->_db_last_result);
Loading history...
122
		}
123 28
		else
124
		{
125
			$inserted_results = 0;
126
		}
127 28
		$last_inserted_id = $this->insert_id($table);
128
129 28
		$this->result->updateDetails([
130
			'insert_id' => $last_inserted_id,
131 28
			'insertedResults' => $inserted_results,
132 28
			'lastResult' => $this->_db_last_result,
133 28
		]);
134
135
		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...
136 28
	}
137
138
	/**
139
	 * {@inheritDoc}
140
	 */
141 37
	public function replace($table, $columns, $data, $keys, $disable_trans = false)
142
	{
143
		$sets = [];
144 37
		foreach ($columns as $columnName => $type)
145 37
		{
146
			$sets[] = $columnName . ' = EXCLUDED.' . $columnName;
147
		}
148 37
		$this->on_conflict = '
149
			ON CONFLICT (' . implode(', ', $keys) . ') DO
150 25
			UPDATE SET ' . implode(',
151
				', $sets);
152
153
		$this->insert('', $table, $columns, $data, $keys, $disable_trans);
154 34
		$this->on_conflict = '';
155
156
		$this->result->updateDetails([
157 37
			'replaceResults' => $this->result->getDetail('insertedResults')
158
		]);
159
160 37
		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...
161
	}
162
163 37
	/**
164 37
	 * {@inheritDoc}
165
	 */
166 37
	public function transaction($type = 'commit')
167
	{
168
		if ($type === 'begin')
169 37
		{
170 37
			$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...
171 37
172
			return @pg_query($this->connection, 'BEGIN');
173
		}
174 37
175 37
		if ($type === 'rollback')
176
		{
177 37
			return @pg_query($this->connection, 'ROLLBACK');
178
		}
179
180 37
		if ($type === 'commit')
181
		{
182
			$this->_in_transaction = false;
183 37
184
			return @pg_query($this->connection, 'COMMIT');
185 37
		}
186
187 28
		return false;
188
	}
189
190
	/**
191 37
	 * {@inheritDoc}
192
	 */
193 33
	protected function initialChecks($db_string, $db_values, $identifier = '')
194
	{
195
		// Special queries that need processing.
196 37
		$replacements = array(
197
			'pm_conversation_list' => array(
198 37
				'~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']) : ''),
199
			),
200
		);
201 37
202 37
		if (isset($replacements[$identifier]))
203 37
		{
204 37
			$db_string = preg_replace(array_keys($replacements[$identifier]), array_values($replacements[$identifier]), $db_string);
205 37
		}
206 37
207
		// Limits need to be a little different, left in place for non conformance addons
208
		$db_string = preg_replace('~\sLIMIT\s(\d+|{int:.+}),\s*(\d+|{int:.+})(.*)~i', ' LIMIT $2 OFFSET $1 $3', $db_string);
209
210 37
		return $db_string;
211
	}
212
213
	/**
214
	 * {@inheritDoc}
215
	 */
216 33
	protected function executeQuery($db_string)
217
	{
218 33
		$this->_db_last_result = @pg_query($this->connection, $db_string);
0 ignored issues
show
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...
219
220 33
		$this->result = new Result($this->_db_last_result);
221
	}
222 33
223
	/**
224
	 * {@inheritDoc}
225 33
	 */
226
	public function error($db_string)
227
	{
228
		global $txt, $modSettings;
229
230 33
		// We'll try recovering the file and line number the original db query was called from.
231
		list ($file, $line) = $this->backtrace_message();
232 33
233
		// Just in case nothing can be found from debug_backtrace
234 33
		$file = $file ?? __FILE__;
235
		$line = $line ?? __LINE__;
236
237
		// Decide which connection to use
238
		// This is the error message...
239
		$query_error = @pg_last_error($this->connection);
240
241
		// Log the error.
242
		Errors::instance()->log_error($txt['database_error'] . ': ' . $query_error . (!empty($modSettings['enableErrorQueryLogging']) ? "\n\n" . $db_string : ''), 'database', $file, $line);
243 148
244
		$this->throwError($db_string, $query_error, $file, $line);
245
	}
246
247
	/**
248 148
	 * Last inserted id.
249
	 *
250
	 * @param string $table
251
	 *
252
	 * @return bool|int
253
	 * @throws \ElkArte\Exceptions\Exception
254
	 */
255
	public function insert_id($table)
256
	{
257
		$table = str_replace('{db_prefix}', $this->_db_prefix, $table);
258
259
		$this->skip_next_error();
260
261
		// Try get the last ID for the auto increment field.
262
		$request = $this->query('', '
263
			SELECT CURRVAL(\'' . $table . '_seq\') AS insertID',
264
			array('security_override' => true)
265
		);
266
267
		if (!$request)
0 ignored issues
show
introduced by
$request is of type ElkArte\Database\Postgresql\Result, thus it always evaluated to true.
Loading history...
268
		{
269
			return false;
270
		}
271 148
272
		list ($lastID) = $request->fetch_row();
273
		$request->free_result();
274
275
		return $lastID;
276
	}
277
278 148
	/**
279
	 * Unescape an escaped string!
280
	 *
281
	 * @param string $string
282
	 *
283
	 * @return string
284 148
	 */
285
	public function unescape_string($string)
286 148
	{
287
		return strtr($string, array('\'\'' => '\''));
288
	}
289
290
	/**
291
	 * {@inheritDoc}
292 148
	 */
293 148
	public function support_ignore()
294
	{
295 148
		return false;
296
	}
297
298 148
	/**
299
	 * {@inheritDoc}
300 148
	 */
301
	public function server_version()
302 148
	{
303
		$version = pg_version();
304 148
305
		return $version['server'];
306
	}
307
308
	/**
309
	 * {@inheritDoc}
310 148
	 */
311
	public function title()
312 37
	{
313
		return 'PostgreSQL';
314
	}
315
316 148
	/**
317
	 * {@inheritDoc}
318 148
	 */
319
	public function case_sensitive()
320
	{
321
		return true;
322 148
	}
323
324 34
	/**
325
	 * {@inheritDoc}
326
	 */
327 148
	public function escape_string($string)
328
	{
329
		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

329
		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...
330
	}
331
332
	/**
333
	 * {@inheritDoc}
334
	 */
335
	public function server_info()
336
	{
337
		// give info on client! we use it in install and upgrade and such things.
338
		$version = pg_version();
339
340
		return $version['client'];
341
	}
342
343
	/**
344
	 * {@inheritDoc}
345
	 */
346
	public function client_version()
347
	{
348
		$version = pg_version();
349
350
		return $version['client'];
351
	}
352
353
	/**
354
	 * Dummy function really. Doesn't do anything in PostgreSQL.
355
	 *
356
	 * {@inheritDoc}
357
	 */
358
	public function select_db($db_name = null)
359
	{
360
		return true;
361
	}
362
363
	/**
364
	 * Returns the result resouce of the last query executed
365
	 *
366
	 * @return resource
367
	 */
368
	public function lastResult()
369
	{
370
		return $this->_db_last_result;
371
	}
372
373
	/**
374
	 * {@inheritDoc}
375
	 */
376
	public function validConnection()
377
	{
378
		return (is_resource($this->connection) || $this->connection instanceof \PgSql\Connection);
379
	}
380
381
	/**
382
	 * {@inheritDoc}
383
	 */
384 37
	public function list_tables($db_name_str = false, $filter = false)
385
	{
386 37
		$dump = new Dump($this);
387
388 37
		return $dump->list_tables($db_name_str, $filter);
389
	}
390
391 37
	/**
392 37
	 * {@inheritDoc}
393 37
	 */
394
	protected function _replaceIdentifier($replacement)
395
	{
396 37
		if (preg_match('~[a-z_][0-9a-zA-Z$,_]{0,60}~', $replacement) !== 1)
397
		{
398 34
			$this->error_backtrace('Wrong value type sent to the database. Invalid identifier used. (' . $replacement . ')', '', E_USER_ERROR, __FILE__, __LINE__);
399
		}
400
401 19
		return '"' . $replacement . '"';
402 19
	}
403
}
404