MigrationService   F
last analyzed

Complexity

Total Complexity 127

Size/Duplication

Total Lines 617
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 276
dl 0
loc 617
rs 2
c 0
b 0
f 0
wmc 127

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getMigrationsDirectory() 0 2 1
A getAvailableVersions() 0 3 1
A getRelativeVersion() 0 12 3
A shallBeExecuted() 0 6 2
A getClass() 0 8 2
A setOutput() 0 2 1
A getMigrationsToExecute() 0 15 5
A getMigratedVersions() 0 14 1
A getCurrentVersion() 0 7 2
A getMigrationsNamespace() 0 2 1
A getMigration() 0 15 5
A markAsExecuted() 0 4 1
C findMigrations() 0 41 12
A getMigrationsTableName() 0 2 1
A executeStep() 0 30 5
A createInstance() 0 17 4
A describeMigrationStep() 0 10 3
A ensureMigrationsAreLoaded() 0 3 2
A getApp() 0 2 1
B createMigrationTable() 0 60 9
F ensureOracleConstraints() 0 91 42
B __construct() 0 38 11
B migrateSchemaOnly() 0 29 8
A migrate() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like MigrationService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MigrationService, and based on these observations, apply Extract Interface, too.

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