Passed
Push — master ( 5564a3...f5c0ea )
by Morris
10:35 queued 10s
created

Connection::insertIgnoreConflict()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Lukas Reschke <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Philipp Schaffrath <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Robin McCorkell <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OC\DB;
32
33
use Doctrine\DBAL\DBALException;
34
use Doctrine\DBAL\Driver;
35
use Doctrine\DBAL\Configuration;
36
use Doctrine\DBAL\Cache\QueryCacheProfile;
37
use Doctrine\Common\EventManager;
38
use Doctrine\DBAL\Platforms\MySqlPlatform;
39
use Doctrine\DBAL\Exception\ConstraintViolationException;
40
use Doctrine\DBAL\Schema\Schema;
41
use OC\DB\QueryBuilder\QueryBuilder;
42
use OCP\DB\QueryBuilder\IQueryBuilder;
43
use OCP\IDBConnection;
44
use OCP\PreConditionNotMetException;
45
46
class Connection extends ReconnectWrapper implements IDBConnection {
47
	/**
48
	 * @var string $tablePrefix
49
	 */
50
	protected $tablePrefix;
51
52
	/**
53
	 * @var \OC\DB\Adapter $adapter
54
	 */
55
	protected $adapter;
56
57
	protected $lockedTable = null;
58
59
	public function connect() {
60
		try {
61
			return parent::connect();
62
		} catch (DBALException $e) {
63
			// throw a new exception to prevent leaking info from the stacktrace
64
			throw new DBALException('Failed to connect to the database: ' . $e->getMessage(), $e->getCode());
65
		}
66
	}
67
68
	/**
69
	 * Returns a QueryBuilder for the connection.
70
	 *
71
	 * @return \OCP\DB\QueryBuilder\IQueryBuilder
72
	 */
73
	public function getQueryBuilder() {
74
		return new QueryBuilder(
75
			$this,
76
			\OC::$server->getSystemConfig(),
77
			\OC::$server->getLogger()
78
		);
79
	}
80
81
	/**
82
	 * Gets the QueryBuilder for the connection.
83
	 *
84
	 * @return \Doctrine\DBAL\Query\QueryBuilder
85
	 * @deprecated please use $this->getQueryBuilder() instead
86
	 */
87
	public function createQueryBuilder() {
88
		$backtrace = $this->getCallerBacktrace();
89
		\OC::$server->getLogger()->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
90
		return parent::createQueryBuilder();
91
	}
92
93
	/**
94
	 * Gets the ExpressionBuilder for the connection.
95
	 *
96
	 * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
97
	 * @deprecated please use $this->getQueryBuilder()->expr() instead
98
	 */
99
	public function getExpressionBuilder() {
100
		$backtrace = $this->getCallerBacktrace();
101
		\OC::$server->getLogger()->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
102
		return parent::getExpressionBuilder();
103
	}
104
105
	/**
106
	 * Get the file and line that called the method where `getCallerBacktrace()` was used
107
	 *
108
	 * @return string
109
	 */
110
	protected function getCallerBacktrace() {
111
		$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
112
113
		// 0 is the method where we use `getCallerBacktrace`
114
		// 1 is the target method which uses the method we want to log
115
		if (isset($traces[1])) {
116
			return $traces[1]['file'] . ':' . $traces[1]['line'];
117
		}
118
119
		return '';
120
	}
121
122
	/**
123
	 * @return string
124
	 */
125
	public function getPrefix() {
126
		return $this->tablePrefix;
127
	}
128
129
	/**
130
	 * Initializes a new instance of the Connection class.
131
	 *
132
	 * @param array $params  The connection parameters.
133
	 * @param \Doctrine\DBAL\Driver $driver
134
	 * @param \Doctrine\DBAL\Configuration $config
135
	 * @param \Doctrine\Common\EventManager $eventManager
136
	 * @throws \Exception
137
	 */
138
	public function __construct(array $params, Driver $driver, Configuration $config = null,
139
		EventManager $eventManager = null)
140
	{
141
		if (!isset($params['adapter'])) {
142
			throw new \Exception('adapter not set');
143
		}
144
		if (!isset($params['tablePrefix'])) {
145
			throw new \Exception('tablePrefix not set');
146
		}
147
		parent::__construct($params, $driver, $config, $eventManager);
148
		$this->adapter = new $params['adapter']($this);
149
		$this->tablePrefix = $params['tablePrefix'];
150
151
		parent::setTransactionIsolation(parent::TRANSACTION_READ_COMMITTED);
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Connection...NSACTION_READ_COMMITTED has been deprecated: Use TransactionIsolationLevel::READ_COMMITTED. ( Ignorable by Annotation )

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

151
		parent::setTransactionIsolation(/** @scrutinizer ignore-deprecated */ parent::TRANSACTION_READ_COMMITTED);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
152
	}
153
154
	/**
155
	 * Prepares an SQL statement.
156
	 *
157
	 * @param string $statement The SQL statement to prepare.
158
	 * @param int $limit
159
	 * @param int $offset
160
	 * @return \Doctrine\DBAL\Driver\Statement The prepared statement.
161
	 */
162
	public function prepare( $statement, $limit=null, $offset=null ) {
163
		if ($limit === -1) {
164
			$limit = null;
165
		}
166
		if (!is_null($limit)) {
167
			$platform = $this->getDatabasePlatform();
168
			$statement = $platform->modifyLimitQuery($statement, $limit, $offset);
169
		}
170
		$statement = $this->replaceTablePrefix($statement);
171
		$statement = $this->adapter->fixupStatement($statement);
172
173
		return parent::prepare($statement);
174
	}
175
176
	/**
177
	 * Executes an, optionally parametrized, SQL query.
178
	 *
179
	 * If the query is parametrized, a prepared statement is used.
180
	 * If an SQLLogger is configured, the execution is logged.
181
	 *
182
	 * @param string                                      $query  The SQL query to execute.
183
	 * @param array                                       $params The parameters to bind to the query, if any.
184
	 * @param array                                       $types  The types the previous parameters are in.
185
	 * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp    The query cache profile, optional.
186
	 *
187
	 * @return \Doctrine\DBAL\Driver\Statement The executed statement.
188
	 *
189
	 * @throws \Doctrine\DBAL\DBALException
190
	 */
191
	public function executeQuery($query, array $params = array(), $types = array(), QueryCacheProfile $qcp = null)
192
	{
193
		$query = $this->replaceTablePrefix($query);
194
		$query = $this->adapter->fixupStatement($query);
195
		return parent::executeQuery($query, $params, $types, $qcp);
196
	}
197
198
	/**
199
	 * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
200
	 * and returns the number of affected rows.
201
	 *
202
	 * This method supports PDO binding types as well as DBAL mapping types.
203
	 *
204
	 * @param string $query  The SQL query.
205
	 * @param array  $params The query parameters.
206
	 * @param array  $types  The parameter types.
207
	 *
208
	 * @return integer The number of affected rows.
209
	 *
210
	 * @throws \Doctrine\DBAL\DBALException
211
	 */
212
	public function executeUpdate($query, array $params = array(), array $types = array())
213
	{
214
		$query = $this->replaceTablePrefix($query);
215
		$query = $this->adapter->fixupStatement($query);
216
		return parent::executeUpdate($query, $params, $types);
217
	}
218
219
	/**
220
	 * Returns the ID of the last inserted row, or the last value from a sequence object,
221
	 * depending on the underlying driver.
222
	 *
223
	 * Note: This method may not return a meaningful or consistent result across different drivers,
224
	 * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
225
	 * columns or sequences.
226
	 *
227
	 * @param string $seqName Name of the sequence object from which the ID should be returned.
228
	 * @return string A string representation of the last inserted ID.
229
	 */
230
	public function lastInsertId($seqName = null) {
231
		if ($seqName) {
232
			$seqName = $this->replaceTablePrefix($seqName);
233
		}
234
		return $this->adapter->lastInsertId($seqName);
235
	}
236
237
	// internal use
238
	public function realLastInsertId($seqName = null) {
239
		return parent::lastInsertId($seqName);
240
	}
241
242
	/**
243
	 * Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
244
	 * it is needed that there is also a unique constraint on the values. Then this method will
245
	 * catch the exception and return 0.
246
	 *
247
	 * @param string $table The table name (will replace *PREFIX* with the actual prefix)
248
	 * @param array $input data that should be inserted into the table  (column name => value)
249
	 * @param array|null $compare List of values that should be checked for "if not exists"
250
	 *				If this is null or an empty array, all keys of $input will be compared
251
	 *				Please note: text fields (clob) must not be used in the compare array
252
	 * @return int number of inserted rows
253
	 * @throws \Doctrine\DBAL\DBALException
254
	 * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
255
	 */
256
	public function insertIfNotExist($table, $input, array $compare = null) {
257
		return $this->adapter->insertIfNotExist($table, $input, $compare);
0 ignored issues
show
Deprecated Code introduced by
The function OC\DB\Adapter::insertIfNotExist() has been deprecated: 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371 ( Ignorable by Annotation )

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

257
		return /** @scrutinizer ignore-deprecated */ $this->adapter->insertIfNotExist($table, $input, $compare);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
258
	}
259
260
	public function insertIgnoreConflict(string $table, array $values) : int {
261
		return $this->adapter->insertIgnoreConflict($table, $values);
262
	}
263
264
	private function getType($value) {
265
		if (is_bool($value)) {
266
			return IQueryBuilder::PARAM_BOOL;
267
		} else if (is_int($value)) {
268
			return IQueryBuilder::PARAM_INT;
269
		} else {
270
			return IQueryBuilder::PARAM_STR;
271
		}
272
	}
273
274
	/**
275
	 * Insert or update a row value
276
	 *
277
	 * @param string $table
278
	 * @param array $keys (column name => value)
279
	 * @param array $values (column name => value)
280
	 * @param array $updatePreconditionValues ensure values match preconditions (column name => value)
281
	 * @return int number of new rows
282
	 * @throws \Doctrine\DBAL\DBALException
283
	 * @throws PreConditionNotMetException
284
	 * @suppress SqlInjectionChecker
285
	 */
286
	public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []) {
287
		try {
288
			$insertQb = $this->getQueryBuilder();
289
			$insertQb->insert($table)
290
				->values(
291
					array_map(function($value) use ($insertQb) {
292
						return $insertQb->createNamedParameter($value, $this->getType($value));
293
					}, array_merge($keys, $values))
294
				);
295
			return $insertQb->execute();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $insertQb->execute() also could return the type Doctrine\DBAL\Driver\Statement which is incompatible with the documented return type integer.
Loading history...
296
		} catch (ConstraintViolationException $e) {
297
			// value already exists, try update
298
			$updateQb = $this->getQueryBuilder();
299
			$updateQb->update($table);
300
			foreach ($values as $name => $value) {
301
				$updateQb->set($name, $updateQb->createNamedParameter($value, $this->getType($value)));
302
			}
303
			$where = $updateQb->expr()->andX();
304
			$whereValues = array_merge($keys, $updatePreconditionValues);
305
			foreach ($whereValues as $name => $value) {
306
				$where->add($updateQb->expr()->eq(
307
					$name,
308
					$updateQb->createNamedParameter($value, $this->getType($value)),
309
					$this->getType($value)
310
				));
311
			}
312
			$updateQb->where($where);
313
			$affected = $updateQb->execute();
314
315
			if ($affected === 0 && !empty($updatePreconditionValues)) {
316
				throw new PreConditionNotMetException();
317
			}
318
319
			return 0;
320
		}
321
	}
322
323
	/**
324
	 * Create an exclusive read+write lock on a table
325
	 *
326
	 * @param string $tableName
327
	 * @throws \BadMethodCallException When trying to acquire a second lock
328
	 * @since 9.1.0
329
	 */
330
	public function lockTable($tableName) {
331
		if ($this->lockedTable !== null) {
332
			throw new \BadMethodCallException('Can not lock a new table until the previous lock is released.');
333
		}
334
335
		$tableName = $this->tablePrefix . $tableName;
336
		$this->lockedTable = $tableName;
337
		$this->adapter->lockTable($tableName);
338
	}
339
340
	/**
341
	 * Release a previous acquired lock again
342
	 *
343
	 * @since 9.1.0
344
	 */
345
	public function unlockTable() {
346
		$this->adapter->unlockTable();
347
		$this->lockedTable = null;
348
	}
349
350
	/**
351
	 * returns the error code and message as a string for logging
352
	 * works with DoctrineException
353
	 * @return string
354
	 */
355
	public function getError() {
356
		$msg = $this->errorCode() . ': ';
357
		$errorInfo = $this->errorInfo();
358
		if (is_array($errorInfo)) {
0 ignored issues
show
introduced by
The condition is_array($errorInfo) is always true.
Loading history...
359
			$msg .= 'SQLSTATE = '.$errorInfo[0] . ', ';
360
			$msg .= 'Driver Code = '.$errorInfo[1] . ', ';
361
			$msg .= 'Driver Message = '.$errorInfo[2];
362
		}
363
		return $msg;
364
	}
365
366
	/**
367
	 * Drop a table from the database if it exists
368
	 *
369
	 * @param string $table table name without the prefix
370
	 */
371
	public function dropTable($table) {
372
		$table = $this->tablePrefix . trim($table);
373
		$schema = $this->getSchemaManager();
374
		if($schema->tablesExist(array($table))) {
375
			$schema->dropTable($table);
376
		}
377
	}
378
379
	/**
380
	 * Check if a table exists
381
	 *
382
	 * @param string $table table name without the prefix
383
	 * @return bool
384
	 */
385
	public function tableExists($table){
386
		$table = $this->tablePrefix . trim($table);
387
		$schema = $this->getSchemaManager();
388
		return $schema->tablesExist(array($table));
389
	}
390
391
	// internal use
392
	/**
393
	 * @param string $statement
394
	 * @return string
395
	 */
396
	protected function replaceTablePrefix($statement) {
397
		return str_replace( '*PREFIX*', $this->tablePrefix, $statement );
398
	}
399
400
	/**
401
	 * Check if a transaction is active
402
	 *
403
	 * @return bool
404
	 * @since 8.2.0
405
	 */
406
	public function inTransaction() {
407
		return $this->getTransactionNestingLevel() > 0;
408
	}
409
410
	/**
411
	 * Escape a parameter to be used in a LIKE query
412
	 *
413
	 * @param string $param
414
	 * @return string
415
	 */
416
	public function escapeLikeParameter($param) {
417
		return addcslashes($param, '\\_%');
418
	}
419
420
	/**
421
	 * Check whether or not the current database support 4byte wide unicode
422
	 *
423
	 * @return bool
424
	 * @since 11.0.0
425
	 */
426
	public function supports4ByteText() {
427
		if (!$this->getDatabasePlatform() instanceof MySqlPlatform) {
428
			return true;
429
		}
430
		return $this->getParams()['charset'] === 'utf8mb4';
431
	}
432
433
434
	/**
435
	 * Create the schema of the connected database
436
	 *
437
	 * @return Schema
438
	 */
439
	public function createSchema() {
440
		$schemaManager = new MDB2SchemaManager($this);
441
		$migrator = $schemaManager->getMigrator();
442
		return $migrator->createSchema();
443
	}
444
445
	/**
446
	 * Migrate the database to the given schema
447
	 *
448
	 * @param Schema $toSchema
449
	 */
450
	public function migrateToSchema(Schema $toSchema) {
451
		$schemaManager = new MDB2SchemaManager($this);
452
		$migrator = $schemaManager->getMigrator();
453
		$migrator->migrate($toSchema);
454
	}
455
}
456