Passed
Push — master ( c023a6...88e5c0 )
by Julius
15:34 queued 12s
created
lib/private/DB/MigrationService.php 2 patches
Indentation   +626 added lines, -626 removed lines patch added patch discarded remove patch
@@ -46,630 +46,630 @@
 block discarded – undo
46 46
 
47 47
 class MigrationService {
48 48
 
49
-	/** @var boolean */
50
-	private $migrationTableCreated;
51
-	/** @var array */
52
-	private $migrations;
53
-	/** @var IOutput */
54
-	private $output;
55
-	/** @var Connection */
56
-	private $connection;
57
-	/** @var string */
58
-	private $appName;
59
-	/** @var bool */
60
-	private $checkOracle;
61
-
62
-	/**
63
-	 * MigrationService constructor.
64
-	 *
65
-	 * @param $appName
66
-	 * @param Connection $connection
67
-	 * @param AppLocator $appLocator
68
-	 * @param IOutput|null $output
69
-	 * @throws \Exception
70
-	 */
71
-	public function __construct($appName, Connection $connection, IOutput $output = null, AppLocator $appLocator = null) {
72
-		$this->appName = $appName;
73
-		$this->connection = $connection;
74
-		$this->output = $output;
75
-		if (null === $this->output) {
76
-			$this->output = new SimpleOutput(\OC::$server->get(LoggerInterface::class), $appName);
77
-		}
78
-
79
-		if ($appName === 'core') {
80
-			$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
81
-			$this->migrationsNamespace = 'OC\\Core\\Migrations';
82
-			$this->checkOracle = true;
83
-		} else {
84
-			if (null === $appLocator) {
85
-				$appLocator = new AppLocator();
86
-			}
87
-			$appPath = $appLocator->getAppPath($appName);
88
-			$namespace = App::buildAppNamespace($appName);
89
-			$this->migrationsPath = "$appPath/lib/Migration";
90
-			$this->migrationsNamespace = $namespace . '\\Migration';
91
-
92
-			$infoParser = new InfoParser();
93
-			$info = $infoParser->parse($appPath . '/appinfo/info.xml');
94
-			if (!isset($info['dependencies']['database'])) {
95
-				$this->checkOracle = true;
96
-			} else {
97
-				$this->checkOracle = false;
98
-				foreach ($info['dependencies']['database'] as $database) {
99
-					if (\is_string($database) && $database === 'oci') {
100
-						$this->checkOracle = true;
101
-					} elseif (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
102
-						$this->checkOracle = true;
103
-					}
104
-				}
105
-			}
106
-		}
107
-	}
108
-
109
-	/**
110
-	 * Returns the name of the app for which this migration is executed
111
-	 *
112
-	 * @return string
113
-	 */
114
-	public function getApp() {
115
-		return $this->appName;
116
-	}
117
-
118
-	/**
119
-	 * @return bool
120
-	 * @codeCoverageIgnore - this will implicitly tested on installation
121
-	 */
122
-	private function createMigrationTable() {
123
-		if ($this->migrationTableCreated) {
124
-			return false;
125
-		}
126
-
127
-		if ($this->connection->tableExists('migrations') && \OC::$server->getConfig()->getAppValue('core', 'vendor', '') !== 'owncloud') {
128
-			$this->migrationTableCreated = true;
129
-			return false;
130
-		}
131
-
132
-		$schema = new SchemaWrapper($this->connection);
133
-
134
-		/**
135
-		 * We drop the table when it has different columns or the definition does not
136
-		 * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
137
-		 */
138
-		try {
139
-			$table = $schema->getTable('migrations');
140
-			$columns = $table->getColumns();
141
-
142
-			if (count($columns) === 2) {
143
-				try {
144
-					$column = $table->getColumn('app');
145
-					$schemaMismatch = $column->getLength() !== 255;
146
-
147
-					if (!$schemaMismatch) {
148
-						$column = $table->getColumn('version');
149
-						$schemaMismatch = $column->getLength() !== 255;
150
-					}
151
-				} catch (SchemaException $e) {
152
-					// One of the columns is missing
153
-					$schemaMismatch = true;
154
-				}
155
-
156
-				if (!$schemaMismatch) {
157
-					// Table exists and schema matches: return back!
158
-					$this->migrationTableCreated = true;
159
-					return false;
160
-				}
161
-			}
162
-
163
-			// Drop the table, when it didn't match our expectations.
164
-			$this->connection->dropTable('migrations');
165
-
166
-			// Recreate the schema after the table was dropped.
167
-			$schema = new SchemaWrapper($this->connection);
168
-		} catch (SchemaException $e) {
169
-			// Table not found, no need to panic, we will create it.
170
-		}
171
-
172
-		$table = $schema->createTable('migrations');
173
-		$table->addColumn('app', Types::STRING, ['length' => 255]);
174
-		$table->addColumn('version', Types::STRING, ['length' => 255]);
175
-		$table->setPrimaryKey(['app', 'version']);
176
-
177
-		$this->connection->migrateToSchema($schema->getWrappedSchema());
178
-
179
-		$this->migrationTableCreated = true;
180
-
181
-		return true;
182
-	}
183
-
184
-	/**
185
-	 * Returns all versions which have already been applied
186
-	 *
187
-	 * @return string[]
188
-	 * @codeCoverageIgnore - no need to test this
189
-	 */
190
-	public function getMigratedVersions() {
191
-		$this->createMigrationTable();
192
-		$qb = $this->connection->getQueryBuilder();
193
-
194
-		$qb->select('version')
195
-			->from('migrations')
196
-			->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
197
-			->orderBy('version');
198
-
199
-		$result = $qb->execute();
200
-		$rows = $result->fetchAll(\PDO::FETCH_COLUMN);
201
-		$result->closeCursor();
202
-
203
-		return $rows;
204
-	}
205
-
206
-	/**
207
-	 * Returns all versions which are available in the migration folder
208
-	 *
209
-	 * @return array
210
-	 */
211
-	public function getAvailableVersions() {
212
-		$this->ensureMigrationsAreLoaded();
213
-		return array_map('strval', array_keys($this->migrations));
214
-	}
215
-
216
-	protected function findMigrations() {
217
-		$directory = realpath($this->migrationsPath);
218
-		if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
219
-			return [];
220
-		}
221
-
222
-		$iterator = new \RegexIterator(
223
-			new \RecursiveIteratorIterator(
224
-				new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
225
-				\RecursiveIteratorIterator::LEAVES_ONLY
226
-			),
227
-			'#^.+\\/Version[^\\/]{1,255}\\.php$#i',
228
-			\RegexIterator::GET_MATCH);
229
-
230
-		$files = array_keys(iterator_to_array($iterator));
231
-		uasort($files, function ($a, $b) {
232
-			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
233
-			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
234
-			if (!empty($matchA) && !empty($matchB)) {
235
-				if ($matchA[1] !== $matchB[1]) {
236
-					return ($matchA[1] < $matchB[1]) ? -1 : 1;
237
-				}
238
-				return ($matchA[2] < $matchB[2]) ? -1 : 1;
239
-			}
240
-			return (basename($a) < basename($b)) ? -1 : 1;
241
-		});
242
-
243
-		$migrations = [];
244
-
245
-		foreach ($files as $file) {
246
-			$className = basename($file, '.php');
247
-			$version = (string) substr($className, 7);
248
-			if ($version === '0') {
249
-				throw new \InvalidArgumentException(
250
-					"Cannot load a migrations with the name '$version' because it is a reserved number"
251
-				);
252
-			}
253
-			$migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
254
-		}
255
-
256
-		return $migrations;
257
-	}
258
-
259
-	/**
260
-	 * @param string $to
261
-	 * @return string[]
262
-	 */
263
-	private function getMigrationsToExecute($to) {
264
-		$knownMigrations = $this->getMigratedVersions();
265
-		$availableMigrations = $this->getAvailableVersions();
266
-
267
-		$toBeExecuted = [];
268
-		foreach ($availableMigrations as $v) {
269
-			if ($to !== 'latest' && $v > $to) {
270
-				continue;
271
-			}
272
-			if ($this->shallBeExecuted($v, $knownMigrations)) {
273
-				$toBeExecuted[] = $v;
274
-			}
275
-		}
276
-
277
-		return $toBeExecuted;
278
-	}
279
-
280
-	/**
281
-	 * @param string $m
282
-	 * @param string[] $knownMigrations
283
-	 * @return bool
284
-	 */
285
-	private function shallBeExecuted($m, $knownMigrations) {
286
-		if (in_array($m, $knownMigrations)) {
287
-			return false;
288
-		}
289
-
290
-		return true;
291
-	}
292
-
293
-	/**
294
-	 * @param string $version
295
-	 */
296
-	private function markAsExecuted($version) {
297
-		$this->connection->insertIfNotExist('*PREFIX*migrations', [
298
-			'app' => $this->appName,
299
-			'version' => $version
300
-		]);
301
-	}
302
-
303
-	/**
304
-	 * Returns the name of the table which holds the already applied versions
305
-	 *
306
-	 * @return string
307
-	 */
308
-	public function getMigrationsTableName() {
309
-		return $this->connection->getPrefix() . 'migrations';
310
-	}
311
-
312
-	/**
313
-	 * Returns the namespace of the version classes
314
-	 *
315
-	 * @return string
316
-	 */
317
-	public function getMigrationsNamespace() {
318
-		return $this->migrationsNamespace;
319
-	}
320
-
321
-	/**
322
-	 * Returns the directory which holds the versions
323
-	 *
324
-	 * @return string
325
-	 */
326
-	public function getMigrationsDirectory() {
327
-		return $this->migrationsPath;
328
-	}
329
-
330
-	/**
331
-	 * Return the explicit version for the aliases; current, next, prev, latest
332
-	 *
333
-	 * @param string $alias
334
-	 * @return mixed|null|string
335
-	 */
336
-	public function getMigration($alias) {
337
-		switch ($alias) {
338
-			case 'current':
339
-				return $this->getCurrentVersion();
340
-			case 'next':
341
-				return $this->getRelativeVersion($this->getCurrentVersion(), 1);
342
-			case 'prev':
343
-				return $this->getRelativeVersion($this->getCurrentVersion(), -1);
344
-			case 'latest':
345
-				$this->ensureMigrationsAreLoaded();
346
-
347
-				$migrations = $this->getAvailableVersions();
348
-				return @end($migrations);
349
-		}
350
-		return '0';
351
-	}
352
-
353
-	/**
354
-	 * @param string $version
355
-	 * @param int $delta
356
-	 * @return null|string
357
-	 */
358
-	private function getRelativeVersion($version, $delta) {
359
-		$this->ensureMigrationsAreLoaded();
360
-
361
-		$versions = $this->getAvailableVersions();
362
-		array_unshift($versions, 0);
363
-		$offset = array_search($version, $versions, true);
364
-		if ($offset === false || !isset($versions[$offset + $delta])) {
365
-			// Unknown version or delta out of bounds.
366
-			return null;
367
-		}
368
-
369
-		return (string) $versions[$offset + $delta];
370
-	}
371
-
372
-	/**
373
-	 * @return string
374
-	 */
375
-	private function getCurrentVersion() {
376
-		$m = $this->getMigratedVersions();
377
-		if (count($m) === 0) {
378
-			return '0';
379
-		}
380
-		$migrations = array_values($m);
381
-		return @end($migrations);
382
-	}
383
-
384
-	/**
385
-	 * @param string $version
386
-	 * @return string
387
-	 * @throws \InvalidArgumentException
388
-	 */
389
-	private function getClass($version) {
390
-		$this->ensureMigrationsAreLoaded();
391
-
392
-		if (isset($this->migrations[$version])) {
393
-			return $this->migrations[$version];
394
-		}
395
-
396
-		throw new \InvalidArgumentException("Version $version is unknown.");
397
-	}
398
-
399
-	/**
400
-	 * Allows to set an IOutput implementation which is used for logging progress and messages
401
-	 *
402
-	 * @param IOutput $output
403
-	 */
404
-	public function setOutput(IOutput $output) {
405
-		$this->output = $output;
406
-	}
407
-
408
-	/**
409
-	 * Applies all not yet applied versions up to $to
410
-	 *
411
-	 * @param string $to
412
-	 * @param bool $schemaOnly
413
-	 * @throws \InvalidArgumentException
414
-	 */
415
-	public function migrate($to = 'latest', $schemaOnly = false) {
416
-		if ($schemaOnly) {
417
-			$this->migrateSchemaOnly($to);
418
-			return;
419
-		}
420
-
421
-		// read known migrations
422
-		$toBeExecuted = $this->getMigrationsToExecute($to);
423
-		foreach ($toBeExecuted as $version) {
424
-			try {
425
-				$this->executeStep($version, $schemaOnly);
426
-			} catch (\Exception $e) {
427
-				// The exception itself does not contain the name of the migration,
428
-				// so we wrap it here, to make debugging easier.
429
-				throw new \Exception('Database error when running migration ' . $version . ' for app ' . $this->getApp() . PHP_EOL. $e->getMessage(), 0, $e);
430
-			}
431
-		}
432
-	}
433
-
434
-	/**
435
-	 * Applies all not yet applied versions up to $to
436
-	 *
437
-	 * @param string $to
438
-	 * @throws \InvalidArgumentException
439
-	 */
440
-	public function migrateSchemaOnly($to = 'latest') {
441
-		// read known migrations
442
-		$toBeExecuted = $this->getMigrationsToExecute($to);
443
-
444
-		if (empty($toBeExecuted)) {
445
-			return;
446
-		}
447
-
448
-		$toSchema = null;
449
-		foreach ($toBeExecuted as $version) {
450
-			$instance = $this->createInstance($version);
451
-
452
-			$toSchema = $instance->changeSchema($this->output, function () use ($toSchema) {
453
-				return $toSchema ?: new SchemaWrapper($this->connection);
454
-			}, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
455
-		}
456
-
457
-		if ($toSchema instanceof SchemaWrapper) {
458
-			$targetSchema = $toSchema->getWrappedSchema();
459
-			if ($this->checkOracle) {
460
-				$beforeSchema = $this->connection->createSchema();
461
-				$this->ensureOracleConstraints($beforeSchema, $targetSchema, strlen($this->connection->getPrefix()));
462
-			}
463
-			$this->connection->migrateToSchema($targetSchema);
464
-			$toSchema->performDropTableCalls();
465
-		}
466
-
467
-		foreach ($toBeExecuted as $version) {
468
-			$this->markAsExecuted($version);
469
-		}
470
-	}
471
-
472
-	/**
473
-	 * Get the human readable descriptions for the migration steps to run
474
-	 *
475
-	 * @param string $to
476
-	 * @return string[] [$name => $description]
477
-	 */
478
-	public function describeMigrationStep($to = 'latest') {
479
-		$toBeExecuted = $this->getMigrationsToExecute($to);
480
-		$description = [];
481
-		foreach ($toBeExecuted as $version) {
482
-			$migration = $this->createInstance($version);
483
-			if ($migration->name()) {
484
-				$description[$migration->name()] = $migration->description();
485
-			}
486
-		}
487
-		return $description;
488
-	}
489
-
490
-	/**
491
-	 * @param string $version
492
-	 * @return IMigrationStep
493
-	 * @throws \InvalidArgumentException
494
-	 */
495
-	protected function createInstance($version) {
496
-		$class = $this->getClass($version);
497
-		try {
498
-			$s = \OC::$server->query($class);
499
-
500
-			if (!$s instanceof IMigrationStep) {
501
-				throw new \InvalidArgumentException('Not a valid migration');
502
-			}
503
-		} catch (QueryException $e) {
504
-			if (class_exists($class)) {
505
-				$s = new $class();
506
-			} else {
507
-				throw new \InvalidArgumentException("Migration step '$class' is unknown");
508
-			}
509
-		}
510
-
511
-		return $s;
512
-	}
513
-
514
-	/**
515
-	 * Executes one explicit version
516
-	 *
517
-	 * @param string $version
518
-	 * @param bool $schemaOnly
519
-	 * @throws \InvalidArgumentException
520
-	 */
521
-	public function executeStep($version, $schemaOnly = false) {
522
-		$instance = $this->createInstance($version);
523
-
524
-		if (!$schemaOnly) {
525
-			$instance->preSchemaChange($this->output, function () {
526
-				return new SchemaWrapper($this->connection);
527
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
528
-		}
529
-
530
-		$toSchema = $instance->changeSchema($this->output, function () {
531
-			return new SchemaWrapper($this->connection);
532
-		}, ['tablePrefix' => $this->connection->getPrefix()]);
533
-
534
-		if ($toSchema instanceof SchemaWrapper) {
535
-			$targetSchema = $toSchema->getWrappedSchema();
536
-			if ($this->checkOracle) {
537
-				$sourceSchema = $this->connection->createSchema();
538
-				$this->ensureOracleConstraints($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
539
-			}
540
-			$this->connection->migrateToSchema($targetSchema);
541
-			$toSchema->performDropTableCalls();
542
-		}
543
-
544
-		if (!$schemaOnly) {
545
-			$instance->postSchemaChange($this->output, function () {
546
-				return new SchemaWrapper($this->connection);
547
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
548
-		}
549
-
550
-		$this->markAsExecuted($version);
551
-	}
552
-
553
-	/**
554
-	 * Naming constraints:
555
-	 * - Tables names must be 30 chars or shorter (27 + oc_ prefix)
556
-	 * - Column names must be 30 chars or shorter
557
-	 * - Index names must be 30 chars or shorter
558
-	 * - Sequence names must be 30 chars or shorter
559
-	 * - Primary key names must be set or the table name 23 chars or shorter
560
-	 *
561
-	 * Data constraints:
562
-	 * - Tables need a primary key (Not specific to Oracle, but required for performant clustering support)
563
-	 * - Columns with "NotNull" can not have empty string as default value
564
-	 * - Columns with "NotNull" can not have number 0 as default value
565
-	 * - Columns with type "bool" (which is in fact integer of length 1) can not be "NotNull" as it can not store 0/false
566
-	 * - Columns with type "string" can not be longer than 4.000 characters, use "text" instead
567
-	 *
568
-	 * @see https://github.com/nextcloud/documentation/blob/master/developer_manual/basics/storage/database.rst
569
-	 *
570
-	 * @param Schema $sourceSchema
571
-	 * @param Schema $targetSchema
572
-	 * @param int $prefixLength
573
-	 * @throws \Doctrine\DBAL\Exception
574
-	 */
575
-	public function ensureOracleConstraints(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
576
-		$sequences = $targetSchema->getSequences();
577
-
578
-		foreach ($targetSchema->getTables() as $table) {
579
-			try {
580
-				$sourceTable = $sourceSchema->getTable($table->getName());
581
-			} catch (SchemaException $e) {
582
-				if (\strlen($table->getName()) - $prefixLength > 27) {
583
-					throw new \InvalidArgumentException('Table name "' . $table->getName() . '" is too long.');
584
-				}
585
-				$sourceTable = null;
586
-			}
587
-
588
-			foreach ($table->getColumns() as $thing) {
589
-				// If the table doesn't exist OR if the column doesn't exist in the table
590
-				if (!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) {
591
-					if (\strlen($thing->getName()) > 30) {
592
-						throw new \InvalidArgumentException('Column name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
593
-					}
594
-
595
-					if ($thing->getNotnull() && $thing->getDefault() === ''
596
-						&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
597
-						throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
598
-					}
599
-
600
-					if ($thing->getNotnull() && $thing->getType()->getName() === Types::BOOLEAN) {
601
-						throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type Bool and also NotNull, so it can not store "false".');
602
-					}
603
-
604
-					$sourceColumn = null;
605
-				} else {
606
-					$sourceColumn = $sourceTable->getColumn($thing->getName());
607
-				}
608
-
609
-				// If the column was just created OR the length changed OR the type changed
610
-				// we will NOT detect invalid length if the column is not modified
611
-				if (($sourceColumn === null || $sourceColumn->getLength() !== $thing->getLength() || $sourceColumn->getType()->getName() !== Types::STRING)
612
-					&& $thing->getLength() > 4000 && $thing->getType()->getName() === Types::STRING) {
613
-					throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type String, but exceeding the 4.000 length limit.');
614
-				}
615
-			}
616
-
617
-			foreach ($table->getIndexes() as $thing) {
618
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
619
-					throw new \InvalidArgumentException('Index name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
620
-				}
621
-			}
622
-
623
-			foreach ($table->getForeignKeys() as $thing) {
624
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
625
-					throw new \InvalidArgumentException('Foreign key name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
626
-				}
627
-			}
628
-
629
-			$primaryKey = $table->getPrimaryKey();
630
-			if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
631
-				$indexName = strtolower($primaryKey->getName());
632
-				$isUsingDefaultName = $indexName === 'primary';
633
-
634
-				if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
635
-					$defaultName = $table->getName() . '_pkey';
636
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
637
-
638
-					if ($isUsingDefaultName) {
639
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
640
-						$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
641
-							return $sequence->getName() !== $sequenceName;
642
-						});
643
-					}
644
-				} elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
645
-					$defaultName = $table->getName() . '_seq';
646
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
647
-				}
648
-
649
-				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
650
-					throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
651
-				}
652
-				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
653
-					throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
654
-				}
655
-			} elseif (!$primaryKey instanceof Index && !$sourceTable instanceof Table) {
656
-				/** @var LoggerInterface $logger */
657
-				$logger = \OC::$server->get(LoggerInterface::class);
658
-				$logger->error('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
659
-				// throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
660
-			}
661
-		}
662
-
663
-		foreach ($sequences as $sequence) {
664
-			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
665
-				throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" is too long.');
666
-			}
667
-		}
668
-	}
669
-
670
-	private function ensureMigrationsAreLoaded() {
671
-		if (empty($this->migrations)) {
672
-			$this->migrations = $this->findMigrations();
673
-		}
674
-	}
49
+    /** @var boolean */
50
+    private $migrationTableCreated;
51
+    /** @var array */
52
+    private $migrations;
53
+    /** @var IOutput */
54
+    private $output;
55
+    /** @var Connection */
56
+    private $connection;
57
+    /** @var string */
58
+    private $appName;
59
+    /** @var bool */
60
+    private $checkOracle;
61
+
62
+    /**
63
+     * MigrationService constructor.
64
+     *
65
+     * @param $appName
66
+     * @param Connection $connection
67
+     * @param AppLocator $appLocator
68
+     * @param IOutput|null $output
69
+     * @throws \Exception
70
+     */
71
+    public function __construct($appName, Connection $connection, IOutput $output = null, AppLocator $appLocator = null) {
72
+        $this->appName = $appName;
73
+        $this->connection = $connection;
74
+        $this->output = $output;
75
+        if (null === $this->output) {
76
+            $this->output = new SimpleOutput(\OC::$server->get(LoggerInterface::class), $appName);
77
+        }
78
+
79
+        if ($appName === 'core') {
80
+            $this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
81
+            $this->migrationsNamespace = 'OC\\Core\\Migrations';
82
+            $this->checkOracle = true;
83
+        } else {
84
+            if (null === $appLocator) {
85
+                $appLocator = new AppLocator();
86
+            }
87
+            $appPath = $appLocator->getAppPath($appName);
88
+            $namespace = App::buildAppNamespace($appName);
89
+            $this->migrationsPath = "$appPath/lib/Migration";
90
+            $this->migrationsNamespace = $namespace . '\\Migration';
91
+
92
+            $infoParser = new InfoParser();
93
+            $info = $infoParser->parse($appPath . '/appinfo/info.xml');
94
+            if (!isset($info['dependencies']['database'])) {
95
+                $this->checkOracle = true;
96
+            } else {
97
+                $this->checkOracle = false;
98
+                foreach ($info['dependencies']['database'] as $database) {
99
+                    if (\is_string($database) && $database === 'oci') {
100
+                        $this->checkOracle = true;
101
+                    } elseif (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
102
+                        $this->checkOracle = true;
103
+                    }
104
+                }
105
+            }
106
+        }
107
+    }
108
+
109
+    /**
110
+     * Returns the name of the app for which this migration is executed
111
+     *
112
+     * @return string
113
+     */
114
+    public function getApp() {
115
+        return $this->appName;
116
+    }
117
+
118
+    /**
119
+     * @return bool
120
+     * @codeCoverageIgnore - this will implicitly tested on installation
121
+     */
122
+    private function createMigrationTable() {
123
+        if ($this->migrationTableCreated) {
124
+            return false;
125
+        }
126
+
127
+        if ($this->connection->tableExists('migrations') && \OC::$server->getConfig()->getAppValue('core', 'vendor', '') !== 'owncloud') {
128
+            $this->migrationTableCreated = true;
129
+            return false;
130
+        }
131
+
132
+        $schema = new SchemaWrapper($this->connection);
133
+
134
+        /**
135
+         * We drop the table when it has different columns or the definition does not
136
+         * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
137
+         */
138
+        try {
139
+            $table = $schema->getTable('migrations');
140
+            $columns = $table->getColumns();
141
+
142
+            if (count($columns) === 2) {
143
+                try {
144
+                    $column = $table->getColumn('app');
145
+                    $schemaMismatch = $column->getLength() !== 255;
146
+
147
+                    if (!$schemaMismatch) {
148
+                        $column = $table->getColumn('version');
149
+                        $schemaMismatch = $column->getLength() !== 255;
150
+                    }
151
+                } catch (SchemaException $e) {
152
+                    // One of the columns is missing
153
+                    $schemaMismatch = true;
154
+                }
155
+
156
+                if (!$schemaMismatch) {
157
+                    // Table exists and schema matches: return back!
158
+                    $this->migrationTableCreated = true;
159
+                    return false;
160
+                }
161
+            }
162
+
163
+            // Drop the table, when it didn't match our expectations.
164
+            $this->connection->dropTable('migrations');
165
+
166
+            // Recreate the schema after the table was dropped.
167
+            $schema = new SchemaWrapper($this->connection);
168
+        } catch (SchemaException $e) {
169
+            // Table not found, no need to panic, we will create it.
170
+        }
171
+
172
+        $table = $schema->createTable('migrations');
173
+        $table->addColumn('app', Types::STRING, ['length' => 255]);
174
+        $table->addColumn('version', Types::STRING, ['length' => 255]);
175
+        $table->setPrimaryKey(['app', 'version']);
176
+
177
+        $this->connection->migrateToSchema($schema->getWrappedSchema());
178
+
179
+        $this->migrationTableCreated = true;
180
+
181
+        return true;
182
+    }
183
+
184
+    /**
185
+     * Returns all versions which have already been applied
186
+     *
187
+     * @return string[]
188
+     * @codeCoverageIgnore - no need to test this
189
+     */
190
+    public function getMigratedVersions() {
191
+        $this->createMigrationTable();
192
+        $qb = $this->connection->getQueryBuilder();
193
+
194
+        $qb->select('version')
195
+            ->from('migrations')
196
+            ->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
197
+            ->orderBy('version');
198
+
199
+        $result = $qb->execute();
200
+        $rows = $result->fetchAll(\PDO::FETCH_COLUMN);
201
+        $result->closeCursor();
202
+
203
+        return $rows;
204
+    }
205
+
206
+    /**
207
+     * Returns all versions which are available in the migration folder
208
+     *
209
+     * @return array
210
+     */
211
+    public function getAvailableVersions() {
212
+        $this->ensureMigrationsAreLoaded();
213
+        return array_map('strval', array_keys($this->migrations));
214
+    }
215
+
216
+    protected function findMigrations() {
217
+        $directory = realpath($this->migrationsPath);
218
+        if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
219
+            return [];
220
+        }
221
+
222
+        $iterator = new \RegexIterator(
223
+            new \RecursiveIteratorIterator(
224
+                new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
225
+                \RecursiveIteratorIterator::LEAVES_ONLY
226
+            ),
227
+            '#^.+\\/Version[^\\/]{1,255}\\.php$#i',
228
+            \RegexIterator::GET_MATCH);
229
+
230
+        $files = array_keys(iterator_to_array($iterator));
231
+        uasort($files, function ($a, $b) {
232
+            preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
233
+            preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
234
+            if (!empty($matchA) && !empty($matchB)) {
235
+                if ($matchA[1] !== $matchB[1]) {
236
+                    return ($matchA[1] < $matchB[1]) ? -1 : 1;
237
+                }
238
+                return ($matchA[2] < $matchB[2]) ? -1 : 1;
239
+            }
240
+            return (basename($a) < basename($b)) ? -1 : 1;
241
+        });
242
+
243
+        $migrations = [];
244
+
245
+        foreach ($files as $file) {
246
+            $className = basename($file, '.php');
247
+            $version = (string) substr($className, 7);
248
+            if ($version === '0') {
249
+                throw new \InvalidArgumentException(
250
+                    "Cannot load a migrations with the name '$version' because it is a reserved number"
251
+                );
252
+            }
253
+            $migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
254
+        }
255
+
256
+        return $migrations;
257
+    }
258
+
259
+    /**
260
+     * @param string $to
261
+     * @return string[]
262
+     */
263
+    private function getMigrationsToExecute($to) {
264
+        $knownMigrations = $this->getMigratedVersions();
265
+        $availableMigrations = $this->getAvailableVersions();
266
+
267
+        $toBeExecuted = [];
268
+        foreach ($availableMigrations as $v) {
269
+            if ($to !== 'latest' && $v > $to) {
270
+                continue;
271
+            }
272
+            if ($this->shallBeExecuted($v, $knownMigrations)) {
273
+                $toBeExecuted[] = $v;
274
+            }
275
+        }
276
+
277
+        return $toBeExecuted;
278
+    }
279
+
280
+    /**
281
+     * @param string $m
282
+     * @param string[] $knownMigrations
283
+     * @return bool
284
+     */
285
+    private function shallBeExecuted($m, $knownMigrations) {
286
+        if (in_array($m, $knownMigrations)) {
287
+            return false;
288
+        }
289
+
290
+        return true;
291
+    }
292
+
293
+    /**
294
+     * @param string $version
295
+     */
296
+    private function markAsExecuted($version) {
297
+        $this->connection->insertIfNotExist('*PREFIX*migrations', [
298
+            'app' => $this->appName,
299
+            'version' => $version
300
+        ]);
301
+    }
302
+
303
+    /**
304
+     * Returns the name of the table which holds the already applied versions
305
+     *
306
+     * @return string
307
+     */
308
+    public function getMigrationsTableName() {
309
+        return $this->connection->getPrefix() . 'migrations';
310
+    }
311
+
312
+    /**
313
+     * Returns the namespace of the version classes
314
+     *
315
+     * @return string
316
+     */
317
+    public function getMigrationsNamespace() {
318
+        return $this->migrationsNamespace;
319
+    }
320
+
321
+    /**
322
+     * Returns the directory which holds the versions
323
+     *
324
+     * @return string
325
+     */
326
+    public function getMigrationsDirectory() {
327
+        return $this->migrationsPath;
328
+    }
329
+
330
+    /**
331
+     * Return the explicit version for the aliases; current, next, prev, latest
332
+     *
333
+     * @param string $alias
334
+     * @return mixed|null|string
335
+     */
336
+    public function getMigration($alias) {
337
+        switch ($alias) {
338
+            case 'current':
339
+                return $this->getCurrentVersion();
340
+            case 'next':
341
+                return $this->getRelativeVersion($this->getCurrentVersion(), 1);
342
+            case 'prev':
343
+                return $this->getRelativeVersion($this->getCurrentVersion(), -1);
344
+            case 'latest':
345
+                $this->ensureMigrationsAreLoaded();
346
+
347
+                $migrations = $this->getAvailableVersions();
348
+                return @end($migrations);
349
+        }
350
+        return '0';
351
+    }
352
+
353
+    /**
354
+     * @param string $version
355
+     * @param int $delta
356
+     * @return null|string
357
+     */
358
+    private function getRelativeVersion($version, $delta) {
359
+        $this->ensureMigrationsAreLoaded();
360
+
361
+        $versions = $this->getAvailableVersions();
362
+        array_unshift($versions, 0);
363
+        $offset = array_search($version, $versions, true);
364
+        if ($offset === false || !isset($versions[$offset + $delta])) {
365
+            // Unknown version or delta out of bounds.
366
+            return null;
367
+        }
368
+
369
+        return (string) $versions[$offset + $delta];
370
+    }
371
+
372
+    /**
373
+     * @return string
374
+     */
375
+    private function getCurrentVersion() {
376
+        $m = $this->getMigratedVersions();
377
+        if (count($m) === 0) {
378
+            return '0';
379
+        }
380
+        $migrations = array_values($m);
381
+        return @end($migrations);
382
+    }
383
+
384
+    /**
385
+     * @param string $version
386
+     * @return string
387
+     * @throws \InvalidArgumentException
388
+     */
389
+    private function getClass($version) {
390
+        $this->ensureMigrationsAreLoaded();
391
+
392
+        if (isset($this->migrations[$version])) {
393
+            return $this->migrations[$version];
394
+        }
395
+
396
+        throw new \InvalidArgumentException("Version $version is unknown.");
397
+    }
398
+
399
+    /**
400
+     * Allows to set an IOutput implementation which is used for logging progress and messages
401
+     *
402
+     * @param IOutput $output
403
+     */
404
+    public function setOutput(IOutput $output) {
405
+        $this->output = $output;
406
+    }
407
+
408
+    /**
409
+     * Applies all not yet applied versions up to $to
410
+     *
411
+     * @param string $to
412
+     * @param bool $schemaOnly
413
+     * @throws \InvalidArgumentException
414
+     */
415
+    public function migrate($to = 'latest', $schemaOnly = false) {
416
+        if ($schemaOnly) {
417
+            $this->migrateSchemaOnly($to);
418
+            return;
419
+        }
420
+
421
+        // read known migrations
422
+        $toBeExecuted = $this->getMigrationsToExecute($to);
423
+        foreach ($toBeExecuted as $version) {
424
+            try {
425
+                $this->executeStep($version, $schemaOnly);
426
+            } catch (\Exception $e) {
427
+                // The exception itself does not contain the name of the migration,
428
+                // so we wrap it here, to make debugging easier.
429
+                throw new \Exception('Database error when running migration ' . $version . ' for app ' . $this->getApp() . PHP_EOL. $e->getMessage(), 0, $e);
430
+            }
431
+        }
432
+    }
433
+
434
+    /**
435
+     * Applies all not yet applied versions up to $to
436
+     *
437
+     * @param string $to
438
+     * @throws \InvalidArgumentException
439
+     */
440
+    public function migrateSchemaOnly($to = 'latest') {
441
+        // read known migrations
442
+        $toBeExecuted = $this->getMigrationsToExecute($to);
443
+
444
+        if (empty($toBeExecuted)) {
445
+            return;
446
+        }
447
+
448
+        $toSchema = null;
449
+        foreach ($toBeExecuted as $version) {
450
+            $instance = $this->createInstance($version);
451
+
452
+            $toSchema = $instance->changeSchema($this->output, function () use ($toSchema) {
453
+                return $toSchema ?: new SchemaWrapper($this->connection);
454
+            }, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
455
+        }
456
+
457
+        if ($toSchema instanceof SchemaWrapper) {
458
+            $targetSchema = $toSchema->getWrappedSchema();
459
+            if ($this->checkOracle) {
460
+                $beforeSchema = $this->connection->createSchema();
461
+                $this->ensureOracleConstraints($beforeSchema, $targetSchema, strlen($this->connection->getPrefix()));
462
+            }
463
+            $this->connection->migrateToSchema($targetSchema);
464
+            $toSchema->performDropTableCalls();
465
+        }
466
+
467
+        foreach ($toBeExecuted as $version) {
468
+            $this->markAsExecuted($version);
469
+        }
470
+    }
471
+
472
+    /**
473
+     * Get the human readable descriptions for the migration steps to run
474
+     *
475
+     * @param string $to
476
+     * @return string[] [$name => $description]
477
+     */
478
+    public function describeMigrationStep($to = 'latest') {
479
+        $toBeExecuted = $this->getMigrationsToExecute($to);
480
+        $description = [];
481
+        foreach ($toBeExecuted as $version) {
482
+            $migration = $this->createInstance($version);
483
+            if ($migration->name()) {
484
+                $description[$migration->name()] = $migration->description();
485
+            }
486
+        }
487
+        return $description;
488
+    }
489
+
490
+    /**
491
+     * @param string $version
492
+     * @return IMigrationStep
493
+     * @throws \InvalidArgumentException
494
+     */
495
+    protected function createInstance($version) {
496
+        $class = $this->getClass($version);
497
+        try {
498
+            $s = \OC::$server->query($class);
499
+
500
+            if (!$s instanceof IMigrationStep) {
501
+                throw new \InvalidArgumentException('Not a valid migration');
502
+            }
503
+        } catch (QueryException $e) {
504
+            if (class_exists($class)) {
505
+                $s = new $class();
506
+            } else {
507
+                throw new \InvalidArgumentException("Migration step '$class' is unknown");
508
+            }
509
+        }
510
+
511
+        return $s;
512
+    }
513
+
514
+    /**
515
+     * Executes one explicit version
516
+     *
517
+     * @param string $version
518
+     * @param bool $schemaOnly
519
+     * @throws \InvalidArgumentException
520
+     */
521
+    public function executeStep($version, $schemaOnly = false) {
522
+        $instance = $this->createInstance($version);
523
+
524
+        if (!$schemaOnly) {
525
+            $instance->preSchemaChange($this->output, function () {
526
+                return new SchemaWrapper($this->connection);
527
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
528
+        }
529
+
530
+        $toSchema = $instance->changeSchema($this->output, function () {
531
+            return new SchemaWrapper($this->connection);
532
+        }, ['tablePrefix' => $this->connection->getPrefix()]);
533
+
534
+        if ($toSchema instanceof SchemaWrapper) {
535
+            $targetSchema = $toSchema->getWrappedSchema();
536
+            if ($this->checkOracle) {
537
+                $sourceSchema = $this->connection->createSchema();
538
+                $this->ensureOracleConstraints($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
539
+            }
540
+            $this->connection->migrateToSchema($targetSchema);
541
+            $toSchema->performDropTableCalls();
542
+        }
543
+
544
+        if (!$schemaOnly) {
545
+            $instance->postSchemaChange($this->output, function () {
546
+                return new SchemaWrapper($this->connection);
547
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
548
+        }
549
+
550
+        $this->markAsExecuted($version);
551
+    }
552
+
553
+    /**
554
+     * Naming constraints:
555
+     * - Tables names must be 30 chars or shorter (27 + oc_ prefix)
556
+     * - Column names must be 30 chars or shorter
557
+     * - Index names must be 30 chars or shorter
558
+     * - Sequence names must be 30 chars or shorter
559
+     * - Primary key names must be set or the table name 23 chars or shorter
560
+     *
561
+     * Data constraints:
562
+     * - Tables need a primary key (Not specific to Oracle, but required for performant clustering support)
563
+     * - Columns with "NotNull" can not have empty string as default value
564
+     * - Columns with "NotNull" can not have number 0 as default value
565
+     * - Columns with type "bool" (which is in fact integer of length 1) can not be "NotNull" as it can not store 0/false
566
+     * - Columns with type "string" can not be longer than 4.000 characters, use "text" instead
567
+     *
568
+     * @see https://github.com/nextcloud/documentation/blob/master/developer_manual/basics/storage/database.rst
569
+     *
570
+     * @param Schema $sourceSchema
571
+     * @param Schema $targetSchema
572
+     * @param int $prefixLength
573
+     * @throws \Doctrine\DBAL\Exception
574
+     */
575
+    public function ensureOracleConstraints(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
576
+        $sequences = $targetSchema->getSequences();
577
+
578
+        foreach ($targetSchema->getTables() as $table) {
579
+            try {
580
+                $sourceTable = $sourceSchema->getTable($table->getName());
581
+            } catch (SchemaException $e) {
582
+                if (\strlen($table->getName()) - $prefixLength > 27) {
583
+                    throw new \InvalidArgumentException('Table name "' . $table->getName() . '" is too long.');
584
+                }
585
+                $sourceTable = null;
586
+            }
587
+
588
+            foreach ($table->getColumns() as $thing) {
589
+                // If the table doesn't exist OR if the column doesn't exist in the table
590
+                if (!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) {
591
+                    if (\strlen($thing->getName()) > 30) {
592
+                        throw new \InvalidArgumentException('Column name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
593
+                    }
594
+
595
+                    if ($thing->getNotnull() && $thing->getDefault() === ''
596
+                        && $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
597
+                        throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
598
+                    }
599
+
600
+                    if ($thing->getNotnull() && $thing->getType()->getName() === Types::BOOLEAN) {
601
+                        throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type Bool and also NotNull, so it can not store "false".');
602
+                    }
603
+
604
+                    $sourceColumn = null;
605
+                } else {
606
+                    $sourceColumn = $sourceTable->getColumn($thing->getName());
607
+                }
608
+
609
+                // If the column was just created OR the length changed OR the type changed
610
+                // we will NOT detect invalid length if the column is not modified
611
+                if (($sourceColumn === null || $sourceColumn->getLength() !== $thing->getLength() || $sourceColumn->getType()->getName() !== Types::STRING)
612
+                    && $thing->getLength() > 4000 && $thing->getType()->getName() === Types::STRING) {
613
+                    throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type String, but exceeding the 4.000 length limit.');
614
+                }
615
+            }
616
+
617
+            foreach ($table->getIndexes() as $thing) {
618
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
619
+                    throw new \InvalidArgumentException('Index name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
620
+                }
621
+            }
622
+
623
+            foreach ($table->getForeignKeys() as $thing) {
624
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
625
+                    throw new \InvalidArgumentException('Foreign key name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
626
+                }
627
+            }
628
+
629
+            $primaryKey = $table->getPrimaryKey();
630
+            if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
631
+                $indexName = strtolower($primaryKey->getName());
632
+                $isUsingDefaultName = $indexName === 'primary';
633
+
634
+                if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
635
+                    $defaultName = $table->getName() . '_pkey';
636
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
637
+
638
+                    if ($isUsingDefaultName) {
639
+                        $sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
640
+                        $sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
641
+                            return $sequence->getName() !== $sequenceName;
642
+                        });
643
+                    }
644
+                } elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
645
+                    $defaultName = $table->getName() . '_seq';
646
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
647
+                }
648
+
649
+                if (!$isUsingDefaultName && \strlen($indexName) > 30) {
650
+                    throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
651
+                }
652
+                if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
653
+                    throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
654
+                }
655
+            } elseif (!$primaryKey instanceof Index && !$sourceTable instanceof Table) {
656
+                /** @var LoggerInterface $logger */
657
+                $logger = \OC::$server->get(LoggerInterface::class);
658
+                $logger->error('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
659
+                // throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
660
+            }
661
+        }
662
+
663
+        foreach ($sequences as $sequence) {
664
+            if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
665
+                throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" is too long.');
666
+            }
667
+        }
668
+    }
669
+
670
+    private function ensureMigrationsAreLoaded() {
671
+        if (empty($this->migrations)) {
672
+            $this->migrations = $this->findMigrations();
673
+        }
674
+    }
675 675
 }
Please login to merge, or discard this patch.
Spacing   +25 added lines, -25 removed lines patch added patch discarded remove patch
@@ -77,7 +77,7 @@  discard block
 block discarded – undo
77 77
 		}
78 78
 
79 79
 		if ($appName === 'core') {
80
-			$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
80
+			$this->migrationsPath = \OC::$SERVERROOT.'/core/Migrations';
81 81
 			$this->migrationsNamespace = 'OC\\Core\\Migrations';
82 82
 			$this->checkOracle = true;
83 83
 		} else {
@@ -87,10 +87,10 @@  discard block
 block discarded – undo
87 87
 			$appPath = $appLocator->getAppPath($appName);
88 88
 			$namespace = App::buildAppNamespace($appName);
89 89
 			$this->migrationsPath = "$appPath/lib/Migration";
90
-			$this->migrationsNamespace = $namespace . '\\Migration';
90
+			$this->migrationsNamespace = $namespace.'\\Migration';
91 91
 
92 92
 			$infoParser = new InfoParser();
93
-			$info = $infoParser->parse($appPath . '/appinfo/info.xml');
93
+			$info = $infoParser->parse($appPath.'/appinfo/info.xml');
94 94
 			if (!isset($info['dependencies']['database'])) {
95 95
 				$this->checkOracle = true;
96 96
 			} else {
@@ -228,7 +228,7 @@  discard block
 block discarded – undo
228 228
 			\RegexIterator::GET_MATCH);
229 229
 
230 230
 		$files = array_keys(iterator_to_array($iterator));
231
-		uasort($files, function ($a, $b) {
231
+		uasort($files, function($a, $b) {
232 232
 			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
233 233
 			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
234 234
 			if (!empty($matchA) && !empty($matchB)) {
@@ -306,7 +306,7 @@  discard block
 block discarded – undo
306 306
 	 * @return string
307 307
 	 */
308 308
 	public function getMigrationsTableName() {
309
-		return $this->connection->getPrefix() . 'migrations';
309
+		return $this->connection->getPrefix().'migrations';
310 310
 	}
311 311
 
312 312
 	/**
@@ -426,7 +426,7 @@  discard block
 block discarded – undo
426 426
 			} catch (\Exception $e) {
427 427
 				// The exception itself does not contain the name of the migration,
428 428
 				// so we wrap it here, to make debugging easier.
429
-				throw new \Exception('Database error when running migration ' . $version . ' for app ' . $this->getApp() . PHP_EOL. $e->getMessage(), 0, $e);
429
+				throw new \Exception('Database error when running migration '.$version.' for app '.$this->getApp().PHP_EOL.$e->getMessage(), 0, $e);
430 430
 			}
431 431
 		}
432 432
 	}
@@ -449,7 +449,7 @@  discard block
 block discarded – undo
449 449
 		foreach ($toBeExecuted as $version) {
450 450
 			$instance = $this->createInstance($version);
451 451
 
452
-			$toSchema = $instance->changeSchema($this->output, function () use ($toSchema) {
452
+			$toSchema = $instance->changeSchema($this->output, function() use ($toSchema) {
453 453
 				return $toSchema ?: new SchemaWrapper($this->connection);
454 454
 			}, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
455 455
 		}
@@ -522,12 +522,12 @@  discard block
 block discarded – undo
522 522
 		$instance = $this->createInstance($version);
523 523
 
524 524
 		if (!$schemaOnly) {
525
-			$instance->preSchemaChange($this->output, function () {
525
+			$instance->preSchemaChange($this->output, function() {
526 526
 				return new SchemaWrapper($this->connection);
527 527
 			}, ['tablePrefix' => $this->connection->getPrefix()]);
528 528
 		}
529 529
 
530
-		$toSchema = $instance->changeSchema($this->output, function () {
530
+		$toSchema = $instance->changeSchema($this->output, function() {
531 531
 			return new SchemaWrapper($this->connection);
532 532
 		}, ['tablePrefix' => $this->connection->getPrefix()]);
533 533
 
@@ -542,7 +542,7 @@  discard block
 block discarded – undo
542 542
 		}
543 543
 
544 544
 		if (!$schemaOnly) {
545
-			$instance->postSchemaChange($this->output, function () {
545
+			$instance->postSchemaChange($this->output, function() {
546 546
 				return new SchemaWrapper($this->connection);
547 547
 			}, ['tablePrefix' => $this->connection->getPrefix()]);
548 548
 		}
@@ -580,7 +580,7 @@  discard block
 block discarded – undo
580 580
 				$sourceTable = $sourceSchema->getTable($table->getName());
581 581
 			} catch (SchemaException $e) {
582 582
 				if (\strlen($table->getName()) - $prefixLength > 27) {
583
-					throw new \InvalidArgumentException('Table name "' . $table->getName() . '" is too long.');
583
+					throw new \InvalidArgumentException('Table name "'.$table->getName().'" is too long.');
584 584
 				}
585 585
 				$sourceTable = null;
586 586
 			}
@@ -589,16 +589,16 @@  discard block
 block discarded – undo
589 589
 				// If the table doesn't exist OR if the column doesn't exist in the table
590 590
 				if (!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) {
591 591
 					if (\strlen($thing->getName()) > 30) {
592
-						throw new \InvalidArgumentException('Column name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
592
+						throw new \InvalidArgumentException('Column name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
593 593
 					}
594 594
 
595 595
 					if ($thing->getNotnull() && $thing->getDefault() === ''
596 596
 						&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
597
-						throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
597
+						throw new \InvalidArgumentException('Column "'.$table->getName().'"."'.$thing->getName().'" is NotNull, but has empty string or null as default.');
598 598
 					}
599 599
 
600 600
 					if ($thing->getNotnull() && $thing->getType()->getName() === Types::BOOLEAN) {
601
-						throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type Bool and also NotNull, so it can not store "false".');
601
+						throw new \InvalidArgumentException('Column "'.$table->getName().'"."'.$thing->getName().'" is type Bool and also NotNull, so it can not store "false".');
602 602
 					}
603 603
 
604 604
 					$sourceColumn = null;
@@ -610,19 +610,19 @@  discard block
 block discarded – undo
610 610
 				// we will NOT detect invalid length if the column is not modified
611 611
 				if (($sourceColumn === null || $sourceColumn->getLength() !== $thing->getLength() || $sourceColumn->getType()->getName() !== Types::STRING)
612 612
 					&& $thing->getLength() > 4000 && $thing->getType()->getName() === Types::STRING) {
613
-					throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type String, but exceeding the 4.000 length limit.');
613
+					throw new \InvalidArgumentException('Column "'.$table->getName().'"."'.$thing->getName().'" is type String, but exceeding the 4.000 length limit.');
614 614
 				}
615 615
 			}
616 616
 
617 617
 			foreach ($table->getIndexes() as $thing) {
618 618
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
619
-					throw new \InvalidArgumentException('Index name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
619
+					throw new \InvalidArgumentException('Index name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
620 620
 				}
621 621
 			}
622 622
 
623 623
 			foreach ($table->getForeignKeys() as $thing) {
624 624
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
625
-					throw new \InvalidArgumentException('Foreign key name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
625
+					throw new \InvalidArgumentException('Foreign key name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
626 626
 				}
627 627
 			}
628 628
 
@@ -632,37 +632,37 @@  discard block
 block discarded – undo
632 632
 				$isUsingDefaultName = $indexName === 'primary';
633 633
 
634 634
 				if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
635
-					$defaultName = $table->getName() . '_pkey';
635
+					$defaultName = $table->getName().'_pkey';
636 636
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
637 637
 
638 638
 					if ($isUsingDefaultName) {
639
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
640
-						$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
639
+						$sequenceName = $table->getName().'_'.implode('_', $primaryKey->getColumns()).'_seq';
640
+						$sequences = array_filter($sequences, function(Sequence $sequence) use ($sequenceName) {
641 641
 							return $sequence->getName() !== $sequenceName;
642 642
 						});
643 643
 					}
644 644
 				} elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
645
-					$defaultName = $table->getName() . '_seq';
645
+					$defaultName = $table->getName().'_seq';
646 646
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
647 647
 				}
648 648
 
649 649
 				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
650
-					throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
650
+					throw new \InvalidArgumentException('Primary index name on "'.$table->getName().'" is too long.');
651 651
 				}
652 652
 				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
653
-					throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
653
+					throw new \InvalidArgumentException('Primary index name on "'.$table->getName().'" is too long.');
654 654
 				}
655 655
 			} elseif (!$primaryKey instanceof Index && !$sourceTable instanceof Table) {
656 656
 				/** @var LoggerInterface $logger */
657 657
 				$logger = \OC::$server->get(LoggerInterface::class);
658
-				$logger->error('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
658
+				$logger->error('Table "'.$table->getName().'" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
659 659
 				// throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
660 660
 			}
661 661
 		}
662 662
 
663 663
 		foreach ($sequences as $sequence) {
664 664
 			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
665
-				throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" is too long.');
665
+				throw new \InvalidArgumentException('Sequence name "'.$sequence->getName().'" is too long.');
666 666
 			}
667 667
 		}
668 668
 	}
Please login to merge, or discard this patch.