Passed
Push — master ( 671d27...84c76a )
by Blizzz
15:12 queued 13s
created
lib/private/DB/Connection.php 1 patch
Indentation   +551 added lines, -551 removed lines patch added patch discarded remove patch
@@ -57,555 +57,555 @@
 block discarded – undo
57 57
 use Psr\Log\LoggerInterface;
58 58
 
59 59
 class Connection extends \Doctrine\DBAL\Connection {
60
-	/** @var string */
61
-	protected $tablePrefix;
62
-
63
-	/** @var \OC\DB\Adapter $adapter */
64
-	protected $adapter;
65
-
66
-	/** @var SystemConfig */
67
-	private $systemConfig;
68
-
69
-	private LoggerInterface $logger;
70
-
71
-	protected $lockedTable = null;
72
-
73
-	/** @var int */
74
-	protected $queriesBuilt = 0;
75
-
76
-	/** @var int */
77
-	protected $queriesExecuted = 0;
78
-
79
-	/** @var DbDataCollector|null */
80
-	protected $dbDataCollector = null;
81
-
82
-	/**
83
-	 * Initializes a new instance of the Connection class.
84
-	 *
85
-	 * @throws \Exception
86
-	 */
87
-	public function __construct(
88
-		array $params,
89
-		Driver $driver,
90
-		?Configuration $config = null,
91
-		?EventManager $eventManager = null
92
-	) {
93
-		if (!isset($params['adapter'])) {
94
-			throw new \Exception('adapter not set');
95
-		}
96
-		if (!isset($params['tablePrefix'])) {
97
-			throw new \Exception('tablePrefix not set');
98
-		}
99
-		/**
100
-		 * @psalm-suppress InternalMethod
101
-		 */
102
-		parent::__construct($params, $driver, $config, $eventManager);
103
-		$this->adapter = new $params['adapter']($this);
104
-		$this->tablePrefix = $params['tablePrefix'];
105
-
106
-		$this->systemConfig = \OC::$server->getSystemConfig();
107
-		$this->logger = \OC::$server->get(LoggerInterface::class);
108
-
109
-		/** @var \OCP\Profiler\IProfiler */
110
-		$profiler = \OC::$server->get(IProfiler::class);
111
-		if ($profiler->isEnabled()) {
112
-			$this->dbDataCollector = new DbDataCollector($this);
113
-			$profiler->add($this->dbDataCollector);
114
-			$debugStack = new BacktraceDebugStack();
115
-			$this->dbDataCollector->setDebugStack($debugStack);
116
-			$this->_config->setSQLLogger($debugStack);
117
-		}
118
-	}
119
-
120
-	/**
121
-	 * @throws Exception
122
-	 */
123
-	public function connect() {
124
-		try {
125
-			if ($this->_conn) {
126
-				/** @psalm-suppress InternalMethod */
127
-				return parent::connect();
128
-			}
129
-
130
-			// Only trigger the event logger for the initial connect call
131
-			$eventLogger = \OC::$server->get(IEventLogger::class);
132
-			$eventLogger->start('connect:db', 'db connection opened');
133
-			/** @psalm-suppress InternalMethod */
134
-			$status = parent::connect();
135
-			$eventLogger->end('connect:db');
136
-
137
-			return $status;
138
-		} catch (Exception $e) {
139
-			// throw a new exception to prevent leaking info from the stacktrace
140
-			throw new Exception('Failed to connect to the database: ' . $e->getMessage(), $e->getCode());
141
-		}
142
-	}
143
-
144
-	public function getStats(): array {
145
-		return [
146
-			'built' => $this->queriesBuilt,
147
-			'executed' => $this->queriesExecuted,
148
-		];
149
-	}
150
-
151
-	/**
152
-	 * Returns a QueryBuilder for the connection.
153
-	 */
154
-	public function getQueryBuilder(): IQueryBuilder {
155
-		$this->queriesBuilt++;
156
-		return new QueryBuilder(
157
-			new ConnectionAdapter($this),
158
-			$this->systemConfig,
159
-			$this->logger
160
-		);
161
-	}
162
-
163
-	/**
164
-	 * Gets the QueryBuilder for the connection.
165
-	 *
166
-	 * @return \Doctrine\DBAL\Query\QueryBuilder
167
-	 * @deprecated please use $this->getQueryBuilder() instead
168
-	 */
169
-	public function createQueryBuilder() {
170
-		$backtrace = $this->getCallerBacktrace();
171
-		$this->logger->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
172
-		$this->queriesBuilt++;
173
-		return parent::createQueryBuilder();
174
-	}
175
-
176
-	/**
177
-	 * Gets the ExpressionBuilder for the connection.
178
-	 *
179
-	 * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
180
-	 * @deprecated please use $this->getQueryBuilder()->expr() instead
181
-	 */
182
-	public function getExpressionBuilder() {
183
-		$backtrace = $this->getCallerBacktrace();
184
-		$this->logger->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
185
-		$this->queriesBuilt++;
186
-		return parent::getExpressionBuilder();
187
-	}
188
-
189
-	/**
190
-	 * Get the file and line that called the method where `getCallerBacktrace()` was used
191
-	 *
192
-	 * @return string
193
-	 */
194
-	protected function getCallerBacktrace() {
195
-		$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
196
-
197
-		// 0 is the method where we use `getCallerBacktrace`
198
-		// 1 is the target method which uses the method we want to log
199
-		if (isset($traces[1])) {
200
-			return $traces[1]['file'] . ':' . $traces[1]['line'];
201
-		}
202
-
203
-		return '';
204
-	}
205
-
206
-	/**
207
-	 * @return string
208
-	 */
209
-	public function getPrefix() {
210
-		return $this->tablePrefix;
211
-	}
212
-
213
-	/**
214
-	 * Prepares an SQL statement.
215
-	 *
216
-	 * @param string $statement The SQL statement to prepare.
217
-	 * @param int|null $limit
218
-	 * @param int|null $offset
219
-	 *
220
-	 * @return Statement The prepared statement.
221
-	 * @throws Exception
222
-	 */
223
-	public function prepare($statement, $limit = null, $offset = null): Statement {
224
-		if ($limit === -1 || $limit === null) {
225
-			$limit = null;
226
-		} else {
227
-			$limit = (int) $limit;
228
-		}
229
-		if ($offset !== null) {
230
-			$offset = (int) $offset;
231
-		}
232
-		if (!is_null($limit)) {
233
-			$platform = $this->getDatabasePlatform();
234
-			$statement = $platform->modifyLimitQuery($statement, $limit, $offset);
235
-		}
236
-		$statement = $this->replaceTablePrefix($statement);
237
-		$statement = $this->adapter->fixupStatement($statement);
238
-
239
-		return parent::prepare($statement);
240
-	}
241
-
242
-	/**
243
-	 * Executes an, optionally parametrized, SQL query.
244
-	 *
245
-	 * If the query is parametrized, a prepared statement is used.
246
-	 * If an SQLLogger is configured, the execution is logged.
247
-	 *
248
-	 * @param string                                      $sql  The SQL query to execute.
249
-	 * @param array                                       $params The parameters to bind to the query, if any.
250
-	 * @param array                                       $types  The types the previous parameters are in.
251
-	 * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp    The query cache profile, optional.
252
-	 *
253
-	 * @return Result The executed statement.
254
-	 *
255
-	 * @throws \Doctrine\DBAL\Exception
256
-	 */
257
-	public function executeQuery(string $sql, array $params = [], $types = [], QueryCacheProfile $qcp = null): Result {
258
-		$sql = $this->replaceTablePrefix($sql);
259
-		$sql = $this->adapter->fixupStatement($sql);
260
-		$this->queriesExecuted++;
261
-		$this->logQueryToFile($sql);
262
-		return parent::executeQuery($sql, $params, $types, $qcp);
263
-	}
264
-
265
-	/**
266
-	 * @throws Exception
267
-	 */
268
-	public function executeUpdate(string $sql, array $params = [], array $types = []): int {
269
-		$sql = $this->replaceTablePrefix($sql);
270
-		$sql = $this->adapter->fixupStatement($sql);
271
-		$this->queriesExecuted++;
272
-		$this->logQueryToFile($sql);
273
-		return parent::executeUpdate($sql, $params, $types);
274
-	}
275
-
276
-	/**
277
-	 * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
278
-	 * and returns the number of affected rows.
279
-	 *
280
-	 * This method supports PDO binding types as well as DBAL mapping types.
281
-	 *
282
-	 * @param string $sql  The SQL query.
283
-	 * @param array  $params The query parameters.
284
-	 * @param array  $types  The parameter types.
285
-	 *
286
-	 * @return int The number of affected rows.
287
-	 *
288
-	 * @throws \Doctrine\DBAL\Exception
289
-	 */
290
-	public function executeStatement($sql, array $params = [], array $types = []): int {
291
-		$sql = $this->replaceTablePrefix($sql);
292
-		$sql = $this->adapter->fixupStatement($sql);
293
-		$this->queriesExecuted++;
294
-		$this->logQueryToFile($sql);
295
-		return (int)parent::executeStatement($sql, $params, $types);
296
-	}
297
-
298
-	protected function logQueryToFile(string $sql): void {
299
-		$logFile = $this->systemConfig->getValue('query_log_file');
300
-		if ($logFile !== '' && is_writable(dirname($logFile)) && (!file_exists($logFile) || is_writable($logFile))) {
301
-			$prefix = '';
302
-			if ($this->systemConfig->getValue('query_log_file_requestid') === 'yes') {
303
-				$prefix .= \OC::$server->get(IRequestId::class)->getId() . "\t";
304
-			}
305
-
306
-			file_put_contents(
307
-				$this->systemConfig->getValue('query_log_file', ''),
308
-				$prefix . $sql . "\n",
309
-				FILE_APPEND
310
-			);
311
-		}
312
-	}
313
-
314
-	/**
315
-	 * Returns the ID of the last inserted row, or the last value from a sequence object,
316
-	 * depending on the underlying driver.
317
-	 *
318
-	 * Note: This method may not return a meaningful or consistent result across different drivers,
319
-	 * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
320
-	 * columns or sequences.
321
-	 *
322
-	 * @param string $seqName Name of the sequence object from which the ID should be returned.
323
-	 *
324
-	 * @return string the last inserted ID.
325
-	 * @throws Exception
326
-	 */
327
-	public function lastInsertId($seqName = null) {
328
-		if ($seqName) {
329
-			$seqName = $this->replaceTablePrefix($seqName);
330
-		}
331
-		return $this->adapter->lastInsertId($seqName);
332
-	}
333
-
334
-	/**
335
-	 * @internal
336
-	 * @throws Exception
337
-	 */
338
-	public function realLastInsertId($seqName = null) {
339
-		return parent::lastInsertId($seqName);
340
-	}
341
-
342
-	/**
343
-	 * Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
344
-	 * it is needed that there is also a unique constraint on the values. Then this method will
345
-	 * catch the exception and return 0.
346
-	 *
347
-	 * @param string $table The table name (will replace *PREFIX* with the actual prefix)
348
-	 * @param array $input data that should be inserted into the table  (column name => value)
349
-	 * @param array|null $compare List of values that should be checked for "if not exists"
350
-	 *				If this is null or an empty array, all keys of $input will be compared
351
-	 *				Please note: text fields (clob) must not be used in the compare array
352
-	 * @return int number of inserted rows
353
-	 * @throws \Doctrine\DBAL\Exception
354
-	 * @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
355
-	 */
356
-	public function insertIfNotExist($table, $input, array $compare = null) {
357
-		return $this->adapter->insertIfNotExist($table, $input, $compare);
358
-	}
359
-
360
-	public function insertIgnoreConflict(string $table, array $values) : int {
361
-		return $this->adapter->insertIgnoreConflict($table, $values);
362
-	}
363
-
364
-	private function getType($value) {
365
-		if (is_bool($value)) {
366
-			return IQueryBuilder::PARAM_BOOL;
367
-		} elseif (is_int($value)) {
368
-			return IQueryBuilder::PARAM_INT;
369
-		} else {
370
-			return IQueryBuilder::PARAM_STR;
371
-		}
372
-	}
373
-
374
-	/**
375
-	 * Insert or update a row value
376
-	 *
377
-	 * @param string $table
378
-	 * @param array $keys (column name => value)
379
-	 * @param array $values (column name => value)
380
-	 * @param array $updatePreconditionValues ensure values match preconditions (column name => value)
381
-	 * @return int number of new rows
382
-	 * @throws \OCP\DB\Exception
383
-	 * @throws PreConditionNotMetException
384
-	 */
385
-	public function setValues(string $table, array $keys, array $values, array $updatePreconditionValues = []): int {
386
-		try {
387
-			$insertQb = $this->getQueryBuilder();
388
-			$insertQb->insert($table)
389
-				->values(
390
-					array_map(function ($value) use ($insertQb) {
391
-						return $insertQb->createNamedParameter($value, $this->getType($value));
392
-					}, array_merge($keys, $values))
393
-				);
394
-			return $insertQb->executeStatement();
395
-		} catch (\OCP\DB\Exception $e) {
396
-			if (!in_array($e->getReason(), [
397
-				\OCP\DB\Exception::REASON_CONSTRAINT_VIOLATION,
398
-				\OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION,
399
-			])
400
-			) {
401
-				throw $e;
402
-			}
403
-
404
-			// value already exists, try update
405
-			$updateQb = $this->getQueryBuilder();
406
-			$updateQb->update($table);
407
-			foreach ($values as $name => $value) {
408
-				$updateQb->set($name, $updateQb->createNamedParameter($value, $this->getType($value)));
409
-			}
410
-			$where = $updateQb->expr()->andX();
411
-			$whereValues = array_merge($keys, $updatePreconditionValues);
412
-			foreach ($whereValues as $name => $value) {
413
-				if ($value === '') {
414
-					$where->add($updateQb->expr()->emptyString(
415
-						$name
416
-					));
417
-				} else {
418
-					$where->add($updateQb->expr()->eq(
419
-						$name,
420
-						$updateQb->createNamedParameter($value, $this->getType($value)),
421
-						$this->getType($value)
422
-					));
423
-				}
424
-			}
425
-			$updateQb->where($where);
426
-			$affected = $updateQb->executeStatement();
427
-
428
-			if ($affected === 0 && !empty($updatePreconditionValues)) {
429
-				throw new PreConditionNotMetException();
430
-			}
431
-
432
-			return 0;
433
-		}
434
-	}
435
-
436
-	/**
437
-	 * Create an exclusive read+write lock on a table
438
-	 *
439
-	 * @param string $tableName
440
-	 *
441
-	 * @throws \BadMethodCallException When trying to acquire a second lock
442
-	 * @throws Exception
443
-	 * @since 9.1.0
444
-	 */
445
-	public function lockTable($tableName) {
446
-		if ($this->lockedTable !== null) {
447
-			throw new \BadMethodCallException('Can not lock a new table until the previous lock is released.');
448
-		}
449
-
450
-		$tableName = $this->tablePrefix . $tableName;
451
-		$this->lockedTable = $tableName;
452
-		$this->adapter->lockTable($tableName);
453
-	}
454
-
455
-	/**
456
-	 * Release a previous acquired lock again
457
-	 *
458
-	 * @throws Exception
459
-	 * @since 9.1.0
460
-	 */
461
-	public function unlockTable() {
462
-		$this->adapter->unlockTable();
463
-		$this->lockedTable = null;
464
-	}
465
-
466
-	/**
467
-	 * returns the error code and message as a string for logging
468
-	 * works with DoctrineException
469
-	 * @return string
470
-	 */
471
-	public function getError() {
472
-		$msg = $this->errorCode() . ': ';
473
-		$errorInfo = $this->errorInfo();
474
-		if (!empty($errorInfo)) {
475
-			$msg .= 'SQLSTATE = '.$errorInfo[0] . ', ';
476
-			$msg .= 'Driver Code = '.$errorInfo[1] . ', ';
477
-			$msg .= 'Driver Message = '.$errorInfo[2];
478
-		}
479
-		return $msg;
480
-	}
481
-
482
-	public function errorCode() {
483
-		return -1;
484
-	}
485
-
486
-	public function errorInfo() {
487
-		return [];
488
-	}
489
-
490
-	/**
491
-	 * Drop a table from the database if it exists
492
-	 *
493
-	 * @param string $table table name without the prefix
494
-	 *
495
-	 * @throws Exception
496
-	 */
497
-	public function dropTable($table) {
498
-		$table = $this->tablePrefix . trim($table);
499
-		$schema = $this->getSchemaManager();
500
-		if ($schema->tablesExist([$table])) {
501
-			$schema->dropTable($table);
502
-		}
503
-	}
504
-
505
-	/**
506
-	 * Check if a table exists
507
-	 *
508
-	 * @param string $table table name without the prefix
509
-	 *
510
-	 * @return bool
511
-	 * @throws Exception
512
-	 */
513
-	public function tableExists($table) {
514
-		$table = $this->tablePrefix . trim($table);
515
-		$schema = $this->getSchemaManager();
516
-		return $schema->tablesExist([$table]);
517
-	}
518
-
519
-	// internal use
520
-	/**
521
-	 * @param string $statement
522
-	 * @return string
523
-	 */
524
-	protected function replaceTablePrefix($statement) {
525
-		return str_replace('*PREFIX*', $this->tablePrefix, $statement);
526
-	}
527
-
528
-	/**
529
-	 * Check if a transaction is active
530
-	 *
531
-	 * @return bool
532
-	 * @since 8.2.0
533
-	 */
534
-	public function inTransaction() {
535
-		return $this->getTransactionNestingLevel() > 0;
536
-	}
537
-
538
-	/**
539
-	 * Escape a parameter to be used in a LIKE query
540
-	 *
541
-	 * @param string $param
542
-	 * @return string
543
-	 */
544
-	public function escapeLikeParameter($param) {
545
-		return addcslashes($param, '\\_%');
546
-	}
547
-
548
-	/**
549
-	 * Check whether or not the current database support 4byte wide unicode
550
-	 *
551
-	 * @return bool
552
-	 * @since 11.0.0
553
-	 */
554
-	public function supports4ByteText() {
555
-		if (!$this->getDatabasePlatform() instanceof MySQLPlatform) {
556
-			return true;
557
-		}
558
-		return $this->getParams()['charset'] === 'utf8mb4';
559
-	}
560
-
561
-
562
-	/**
563
-	 * Create the schema of the connected database
564
-	 *
565
-	 * @return Schema
566
-	 * @throws Exception
567
-	 */
568
-	public function createSchema() {
569
-		$migrator = $this->getMigrator();
570
-		return $migrator->createSchema();
571
-	}
572
-
573
-	/**
574
-	 * Migrate the database to the given schema
575
-	 *
576
-	 * @param Schema $toSchema
577
-	 * @param bool $dryRun If true, will return the sql queries instead of running them.
578
-	 *
579
-	 * @throws Exception
580
-	 *
581
-	 * @return string|null Returns a string only if $dryRun is true.
582
-	 */
583
-	public function migrateToSchema(Schema $toSchema, bool $dryRun = false) {
584
-		$migrator = $this->getMigrator();
585
-
586
-		if ($dryRun) {
587
-			return $migrator->generateChangeScript($toSchema);
588
-		} else {
589
-			$migrator->migrate($toSchema);
590
-		}
591
-	}
592
-
593
-	private function getMigrator() {
594
-		// TODO properly inject those dependencies
595
-		$random = \OC::$server->getSecureRandom();
596
-		$platform = $this->getDatabasePlatform();
597
-		$config = \OC::$server->getConfig();
598
-		$dispatcher = \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class);
599
-		if ($platform instanceof SqlitePlatform) {
600
-			return new SQLiteMigrator($this, $config, $dispatcher);
601
-		} elseif ($platform instanceof OraclePlatform) {
602
-			return new OracleMigrator($this, $config, $dispatcher);
603
-		} elseif ($platform instanceof MySQLPlatform) {
604
-			return new MySQLMigrator($this, $config, $dispatcher);
605
-		} elseif ($platform instanceof PostgreSQL94Platform) {
606
-			return new PostgreSqlMigrator($this, $config, $dispatcher);
607
-		} else {
608
-			return new Migrator($this, $config, $dispatcher);
609
-		}
610
-	}
60
+    /** @var string */
61
+    protected $tablePrefix;
62
+
63
+    /** @var \OC\DB\Adapter $adapter */
64
+    protected $adapter;
65
+
66
+    /** @var SystemConfig */
67
+    private $systemConfig;
68
+
69
+    private LoggerInterface $logger;
70
+
71
+    protected $lockedTable = null;
72
+
73
+    /** @var int */
74
+    protected $queriesBuilt = 0;
75
+
76
+    /** @var int */
77
+    protected $queriesExecuted = 0;
78
+
79
+    /** @var DbDataCollector|null */
80
+    protected $dbDataCollector = null;
81
+
82
+    /**
83
+     * Initializes a new instance of the Connection class.
84
+     *
85
+     * @throws \Exception
86
+     */
87
+    public function __construct(
88
+        array $params,
89
+        Driver $driver,
90
+        ?Configuration $config = null,
91
+        ?EventManager $eventManager = null
92
+    ) {
93
+        if (!isset($params['adapter'])) {
94
+            throw new \Exception('adapter not set');
95
+        }
96
+        if (!isset($params['tablePrefix'])) {
97
+            throw new \Exception('tablePrefix not set');
98
+        }
99
+        /**
100
+         * @psalm-suppress InternalMethod
101
+         */
102
+        parent::__construct($params, $driver, $config, $eventManager);
103
+        $this->adapter = new $params['adapter']($this);
104
+        $this->tablePrefix = $params['tablePrefix'];
105
+
106
+        $this->systemConfig = \OC::$server->getSystemConfig();
107
+        $this->logger = \OC::$server->get(LoggerInterface::class);
108
+
109
+        /** @var \OCP\Profiler\IProfiler */
110
+        $profiler = \OC::$server->get(IProfiler::class);
111
+        if ($profiler->isEnabled()) {
112
+            $this->dbDataCollector = new DbDataCollector($this);
113
+            $profiler->add($this->dbDataCollector);
114
+            $debugStack = new BacktraceDebugStack();
115
+            $this->dbDataCollector->setDebugStack($debugStack);
116
+            $this->_config->setSQLLogger($debugStack);
117
+        }
118
+    }
119
+
120
+    /**
121
+     * @throws Exception
122
+     */
123
+    public function connect() {
124
+        try {
125
+            if ($this->_conn) {
126
+                /** @psalm-suppress InternalMethod */
127
+                return parent::connect();
128
+            }
129
+
130
+            // Only trigger the event logger for the initial connect call
131
+            $eventLogger = \OC::$server->get(IEventLogger::class);
132
+            $eventLogger->start('connect:db', 'db connection opened');
133
+            /** @psalm-suppress InternalMethod */
134
+            $status = parent::connect();
135
+            $eventLogger->end('connect:db');
136
+
137
+            return $status;
138
+        } catch (Exception $e) {
139
+            // throw a new exception to prevent leaking info from the stacktrace
140
+            throw new Exception('Failed to connect to the database: ' . $e->getMessage(), $e->getCode());
141
+        }
142
+    }
143
+
144
+    public function getStats(): array {
145
+        return [
146
+            'built' => $this->queriesBuilt,
147
+            'executed' => $this->queriesExecuted,
148
+        ];
149
+    }
150
+
151
+    /**
152
+     * Returns a QueryBuilder for the connection.
153
+     */
154
+    public function getQueryBuilder(): IQueryBuilder {
155
+        $this->queriesBuilt++;
156
+        return new QueryBuilder(
157
+            new ConnectionAdapter($this),
158
+            $this->systemConfig,
159
+            $this->logger
160
+        );
161
+    }
162
+
163
+    /**
164
+     * Gets the QueryBuilder for the connection.
165
+     *
166
+     * @return \Doctrine\DBAL\Query\QueryBuilder
167
+     * @deprecated please use $this->getQueryBuilder() instead
168
+     */
169
+    public function createQueryBuilder() {
170
+        $backtrace = $this->getCallerBacktrace();
171
+        $this->logger->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
172
+        $this->queriesBuilt++;
173
+        return parent::createQueryBuilder();
174
+    }
175
+
176
+    /**
177
+     * Gets the ExpressionBuilder for the connection.
178
+     *
179
+     * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
180
+     * @deprecated please use $this->getQueryBuilder()->expr() instead
181
+     */
182
+    public function getExpressionBuilder() {
183
+        $backtrace = $this->getCallerBacktrace();
184
+        $this->logger->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
185
+        $this->queriesBuilt++;
186
+        return parent::getExpressionBuilder();
187
+    }
188
+
189
+    /**
190
+     * Get the file and line that called the method where `getCallerBacktrace()` was used
191
+     *
192
+     * @return string
193
+     */
194
+    protected function getCallerBacktrace() {
195
+        $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
196
+
197
+        // 0 is the method where we use `getCallerBacktrace`
198
+        // 1 is the target method which uses the method we want to log
199
+        if (isset($traces[1])) {
200
+            return $traces[1]['file'] . ':' . $traces[1]['line'];
201
+        }
202
+
203
+        return '';
204
+    }
205
+
206
+    /**
207
+     * @return string
208
+     */
209
+    public function getPrefix() {
210
+        return $this->tablePrefix;
211
+    }
212
+
213
+    /**
214
+     * Prepares an SQL statement.
215
+     *
216
+     * @param string $statement The SQL statement to prepare.
217
+     * @param int|null $limit
218
+     * @param int|null $offset
219
+     *
220
+     * @return Statement The prepared statement.
221
+     * @throws Exception
222
+     */
223
+    public function prepare($statement, $limit = null, $offset = null): Statement {
224
+        if ($limit === -1 || $limit === null) {
225
+            $limit = null;
226
+        } else {
227
+            $limit = (int) $limit;
228
+        }
229
+        if ($offset !== null) {
230
+            $offset = (int) $offset;
231
+        }
232
+        if (!is_null($limit)) {
233
+            $platform = $this->getDatabasePlatform();
234
+            $statement = $platform->modifyLimitQuery($statement, $limit, $offset);
235
+        }
236
+        $statement = $this->replaceTablePrefix($statement);
237
+        $statement = $this->adapter->fixupStatement($statement);
238
+
239
+        return parent::prepare($statement);
240
+    }
241
+
242
+    /**
243
+     * Executes an, optionally parametrized, SQL query.
244
+     *
245
+     * If the query is parametrized, a prepared statement is used.
246
+     * If an SQLLogger is configured, the execution is logged.
247
+     *
248
+     * @param string                                      $sql  The SQL query to execute.
249
+     * @param array                                       $params The parameters to bind to the query, if any.
250
+     * @param array                                       $types  The types the previous parameters are in.
251
+     * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp    The query cache profile, optional.
252
+     *
253
+     * @return Result The executed statement.
254
+     *
255
+     * @throws \Doctrine\DBAL\Exception
256
+     */
257
+    public function executeQuery(string $sql, array $params = [], $types = [], QueryCacheProfile $qcp = null): Result {
258
+        $sql = $this->replaceTablePrefix($sql);
259
+        $sql = $this->adapter->fixupStatement($sql);
260
+        $this->queriesExecuted++;
261
+        $this->logQueryToFile($sql);
262
+        return parent::executeQuery($sql, $params, $types, $qcp);
263
+    }
264
+
265
+    /**
266
+     * @throws Exception
267
+     */
268
+    public function executeUpdate(string $sql, array $params = [], array $types = []): int {
269
+        $sql = $this->replaceTablePrefix($sql);
270
+        $sql = $this->adapter->fixupStatement($sql);
271
+        $this->queriesExecuted++;
272
+        $this->logQueryToFile($sql);
273
+        return parent::executeUpdate($sql, $params, $types);
274
+    }
275
+
276
+    /**
277
+     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
278
+     * and returns the number of affected rows.
279
+     *
280
+     * This method supports PDO binding types as well as DBAL mapping types.
281
+     *
282
+     * @param string $sql  The SQL query.
283
+     * @param array  $params The query parameters.
284
+     * @param array  $types  The parameter types.
285
+     *
286
+     * @return int The number of affected rows.
287
+     *
288
+     * @throws \Doctrine\DBAL\Exception
289
+     */
290
+    public function executeStatement($sql, array $params = [], array $types = []): int {
291
+        $sql = $this->replaceTablePrefix($sql);
292
+        $sql = $this->adapter->fixupStatement($sql);
293
+        $this->queriesExecuted++;
294
+        $this->logQueryToFile($sql);
295
+        return (int)parent::executeStatement($sql, $params, $types);
296
+    }
297
+
298
+    protected function logQueryToFile(string $sql): void {
299
+        $logFile = $this->systemConfig->getValue('query_log_file');
300
+        if ($logFile !== '' && is_writable(dirname($logFile)) && (!file_exists($logFile) || is_writable($logFile))) {
301
+            $prefix = '';
302
+            if ($this->systemConfig->getValue('query_log_file_requestid') === 'yes') {
303
+                $prefix .= \OC::$server->get(IRequestId::class)->getId() . "\t";
304
+            }
305
+
306
+            file_put_contents(
307
+                $this->systemConfig->getValue('query_log_file', ''),
308
+                $prefix . $sql . "\n",
309
+                FILE_APPEND
310
+            );
311
+        }
312
+    }
313
+
314
+    /**
315
+     * Returns the ID of the last inserted row, or the last value from a sequence object,
316
+     * depending on the underlying driver.
317
+     *
318
+     * Note: This method may not return a meaningful or consistent result across different drivers,
319
+     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
320
+     * columns or sequences.
321
+     *
322
+     * @param string $seqName Name of the sequence object from which the ID should be returned.
323
+     *
324
+     * @return string the last inserted ID.
325
+     * @throws Exception
326
+     */
327
+    public function lastInsertId($seqName = null) {
328
+        if ($seqName) {
329
+            $seqName = $this->replaceTablePrefix($seqName);
330
+        }
331
+        return $this->adapter->lastInsertId($seqName);
332
+    }
333
+
334
+    /**
335
+     * @internal
336
+     * @throws Exception
337
+     */
338
+    public function realLastInsertId($seqName = null) {
339
+        return parent::lastInsertId($seqName);
340
+    }
341
+
342
+    /**
343
+     * Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
344
+     * it is needed that there is also a unique constraint on the values. Then this method will
345
+     * catch the exception and return 0.
346
+     *
347
+     * @param string $table The table name (will replace *PREFIX* with the actual prefix)
348
+     * @param array $input data that should be inserted into the table  (column name => value)
349
+     * @param array|null $compare List of values that should be checked for "if not exists"
350
+     *				If this is null or an empty array, all keys of $input will be compared
351
+     *				Please note: text fields (clob) must not be used in the compare array
352
+     * @return int number of inserted rows
353
+     * @throws \Doctrine\DBAL\Exception
354
+     * @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
355
+     */
356
+    public function insertIfNotExist($table, $input, array $compare = null) {
357
+        return $this->adapter->insertIfNotExist($table, $input, $compare);
358
+    }
359
+
360
+    public function insertIgnoreConflict(string $table, array $values) : int {
361
+        return $this->adapter->insertIgnoreConflict($table, $values);
362
+    }
363
+
364
+    private function getType($value) {
365
+        if (is_bool($value)) {
366
+            return IQueryBuilder::PARAM_BOOL;
367
+        } elseif (is_int($value)) {
368
+            return IQueryBuilder::PARAM_INT;
369
+        } else {
370
+            return IQueryBuilder::PARAM_STR;
371
+        }
372
+    }
373
+
374
+    /**
375
+     * Insert or update a row value
376
+     *
377
+     * @param string $table
378
+     * @param array $keys (column name => value)
379
+     * @param array $values (column name => value)
380
+     * @param array $updatePreconditionValues ensure values match preconditions (column name => value)
381
+     * @return int number of new rows
382
+     * @throws \OCP\DB\Exception
383
+     * @throws PreConditionNotMetException
384
+     */
385
+    public function setValues(string $table, array $keys, array $values, array $updatePreconditionValues = []): int {
386
+        try {
387
+            $insertQb = $this->getQueryBuilder();
388
+            $insertQb->insert($table)
389
+                ->values(
390
+                    array_map(function ($value) use ($insertQb) {
391
+                        return $insertQb->createNamedParameter($value, $this->getType($value));
392
+                    }, array_merge($keys, $values))
393
+                );
394
+            return $insertQb->executeStatement();
395
+        } catch (\OCP\DB\Exception $e) {
396
+            if (!in_array($e->getReason(), [
397
+                \OCP\DB\Exception::REASON_CONSTRAINT_VIOLATION,
398
+                \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION,
399
+            ])
400
+            ) {
401
+                throw $e;
402
+            }
403
+
404
+            // value already exists, try update
405
+            $updateQb = $this->getQueryBuilder();
406
+            $updateQb->update($table);
407
+            foreach ($values as $name => $value) {
408
+                $updateQb->set($name, $updateQb->createNamedParameter($value, $this->getType($value)));
409
+            }
410
+            $where = $updateQb->expr()->andX();
411
+            $whereValues = array_merge($keys, $updatePreconditionValues);
412
+            foreach ($whereValues as $name => $value) {
413
+                if ($value === '') {
414
+                    $where->add($updateQb->expr()->emptyString(
415
+                        $name
416
+                    ));
417
+                } else {
418
+                    $where->add($updateQb->expr()->eq(
419
+                        $name,
420
+                        $updateQb->createNamedParameter($value, $this->getType($value)),
421
+                        $this->getType($value)
422
+                    ));
423
+                }
424
+            }
425
+            $updateQb->where($where);
426
+            $affected = $updateQb->executeStatement();
427
+
428
+            if ($affected === 0 && !empty($updatePreconditionValues)) {
429
+                throw new PreConditionNotMetException();
430
+            }
431
+
432
+            return 0;
433
+        }
434
+    }
435
+
436
+    /**
437
+     * Create an exclusive read+write lock on a table
438
+     *
439
+     * @param string $tableName
440
+     *
441
+     * @throws \BadMethodCallException When trying to acquire a second lock
442
+     * @throws Exception
443
+     * @since 9.1.0
444
+     */
445
+    public function lockTable($tableName) {
446
+        if ($this->lockedTable !== null) {
447
+            throw new \BadMethodCallException('Can not lock a new table until the previous lock is released.');
448
+        }
449
+
450
+        $tableName = $this->tablePrefix . $tableName;
451
+        $this->lockedTable = $tableName;
452
+        $this->adapter->lockTable($tableName);
453
+    }
454
+
455
+    /**
456
+     * Release a previous acquired lock again
457
+     *
458
+     * @throws Exception
459
+     * @since 9.1.0
460
+     */
461
+    public function unlockTable() {
462
+        $this->adapter->unlockTable();
463
+        $this->lockedTable = null;
464
+    }
465
+
466
+    /**
467
+     * returns the error code and message as a string for logging
468
+     * works with DoctrineException
469
+     * @return string
470
+     */
471
+    public function getError() {
472
+        $msg = $this->errorCode() . ': ';
473
+        $errorInfo = $this->errorInfo();
474
+        if (!empty($errorInfo)) {
475
+            $msg .= 'SQLSTATE = '.$errorInfo[0] . ', ';
476
+            $msg .= 'Driver Code = '.$errorInfo[1] . ', ';
477
+            $msg .= 'Driver Message = '.$errorInfo[2];
478
+        }
479
+        return $msg;
480
+    }
481
+
482
+    public function errorCode() {
483
+        return -1;
484
+    }
485
+
486
+    public function errorInfo() {
487
+        return [];
488
+    }
489
+
490
+    /**
491
+     * Drop a table from the database if it exists
492
+     *
493
+     * @param string $table table name without the prefix
494
+     *
495
+     * @throws Exception
496
+     */
497
+    public function dropTable($table) {
498
+        $table = $this->tablePrefix . trim($table);
499
+        $schema = $this->getSchemaManager();
500
+        if ($schema->tablesExist([$table])) {
501
+            $schema->dropTable($table);
502
+        }
503
+    }
504
+
505
+    /**
506
+     * Check if a table exists
507
+     *
508
+     * @param string $table table name without the prefix
509
+     *
510
+     * @return bool
511
+     * @throws Exception
512
+     */
513
+    public function tableExists($table) {
514
+        $table = $this->tablePrefix . trim($table);
515
+        $schema = $this->getSchemaManager();
516
+        return $schema->tablesExist([$table]);
517
+    }
518
+
519
+    // internal use
520
+    /**
521
+     * @param string $statement
522
+     * @return string
523
+     */
524
+    protected function replaceTablePrefix($statement) {
525
+        return str_replace('*PREFIX*', $this->tablePrefix, $statement);
526
+    }
527
+
528
+    /**
529
+     * Check if a transaction is active
530
+     *
531
+     * @return bool
532
+     * @since 8.2.0
533
+     */
534
+    public function inTransaction() {
535
+        return $this->getTransactionNestingLevel() > 0;
536
+    }
537
+
538
+    /**
539
+     * Escape a parameter to be used in a LIKE query
540
+     *
541
+     * @param string $param
542
+     * @return string
543
+     */
544
+    public function escapeLikeParameter($param) {
545
+        return addcslashes($param, '\\_%');
546
+    }
547
+
548
+    /**
549
+     * Check whether or not the current database support 4byte wide unicode
550
+     *
551
+     * @return bool
552
+     * @since 11.0.0
553
+     */
554
+    public function supports4ByteText() {
555
+        if (!$this->getDatabasePlatform() instanceof MySQLPlatform) {
556
+            return true;
557
+        }
558
+        return $this->getParams()['charset'] === 'utf8mb4';
559
+    }
560
+
561
+
562
+    /**
563
+     * Create the schema of the connected database
564
+     *
565
+     * @return Schema
566
+     * @throws Exception
567
+     */
568
+    public function createSchema() {
569
+        $migrator = $this->getMigrator();
570
+        return $migrator->createSchema();
571
+    }
572
+
573
+    /**
574
+     * Migrate the database to the given schema
575
+     *
576
+     * @param Schema $toSchema
577
+     * @param bool $dryRun If true, will return the sql queries instead of running them.
578
+     *
579
+     * @throws Exception
580
+     *
581
+     * @return string|null Returns a string only if $dryRun is true.
582
+     */
583
+    public function migrateToSchema(Schema $toSchema, bool $dryRun = false) {
584
+        $migrator = $this->getMigrator();
585
+
586
+        if ($dryRun) {
587
+            return $migrator->generateChangeScript($toSchema);
588
+        } else {
589
+            $migrator->migrate($toSchema);
590
+        }
591
+    }
592
+
593
+    private function getMigrator() {
594
+        // TODO properly inject those dependencies
595
+        $random = \OC::$server->getSecureRandom();
596
+        $platform = $this->getDatabasePlatform();
597
+        $config = \OC::$server->getConfig();
598
+        $dispatcher = \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class);
599
+        if ($platform instanceof SqlitePlatform) {
600
+            return new SQLiteMigrator($this, $config, $dispatcher);
601
+        } elseif ($platform instanceof OraclePlatform) {
602
+            return new OracleMigrator($this, $config, $dispatcher);
603
+        } elseif ($platform instanceof MySQLPlatform) {
604
+            return new MySQLMigrator($this, $config, $dispatcher);
605
+        } elseif ($platform instanceof PostgreSQL94Platform) {
606
+            return new PostgreSqlMigrator($this, $config, $dispatcher);
607
+        } else {
608
+            return new Migrator($this, $config, $dispatcher);
609
+        }
610
+    }
611 611
 }
Please login to merge, or discard this patch.