Passed
Push — master ( 0e6344...34d2ae )
by Morris
10:49 queued 10s
created
lib/private/DB/MigrationService.php 1 patch
Indentation   +527 added lines, -527 removed lines patch added patch discarded remove patch
@@ -46,531 +46,531 @@
 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 IDBConnection $connection
67
-	 * @param AppLocator $appLocator
68
-	 * @param IOutput|null $output
69
-	 * @throws \Exception
70
-	 */
71
-	public function __construct($appName, IDBConnection $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->getLogger(), $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
-		$schema = new SchemaWrapper($this->connection);
128
-
129
-		/**
130
-		 * We drop the table when it has different columns or the definition does not
131
-		 * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
132
-		 */
133
-		try {
134
-			$table = $schema->getTable('migrations');
135
-			$columns = $table->getColumns();
136
-
137
-			if (count($columns) === 2) {
138
-				try {
139
-					$column = $table->getColumn('app');
140
-					$schemaMismatch = $column->getLength() !== 255;
141
-
142
-					if (!$schemaMismatch) {
143
-						$column = $table->getColumn('version');
144
-						$schemaMismatch = $column->getLength() !== 255;
145
-					}
146
-				} catch (SchemaException $e) {
147
-					// One of the columns is missing
148
-					$schemaMismatch = true;
149
-				}
150
-
151
-				if (!$schemaMismatch) {
152
-					// Table exists and schema matches: return back!
153
-					$this->migrationTableCreated = true;
154
-					return false;
155
-				}
156
-			}
157
-
158
-			// Drop the table, when it didn't match our expectations.
159
-			$this->connection->dropTable('migrations');
160
-
161
-			// Recreate the schema after the table was dropped.
162
-			$schema = new SchemaWrapper($this->connection);
163
-		} catch (SchemaException $e) {
164
-			// Table not found, no need to panic, we will create it.
165
-		}
166
-
167
-		$table = $schema->createTable('migrations');
168
-		$table->addColumn('app', Types::STRING, ['length' => 255]);
169
-		$table->addColumn('version', Types::STRING, ['length' => 255]);
170
-		$table->setPrimaryKey(['app', 'version']);
171
-
172
-		$this->connection->migrateToSchema($schema->getWrappedSchema());
173
-
174
-		$this->migrationTableCreated = true;
175
-
176
-		return true;
177
-	}
178
-
179
-	/**
180
-	 * Returns all versions which have already been applied
181
-	 *
182
-	 * @return string[]
183
-	 * @codeCoverageIgnore - no need to test this
184
-	 */
185
-	public function getMigratedVersions() {
186
-		$this->createMigrationTable();
187
-		$qb = $this->connection->getQueryBuilder();
188
-
189
-		$qb->select('version')
190
-			->from('migrations')
191
-			->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
192
-			->orderBy('version');
193
-
194
-		$result = $qb->execute();
195
-		$rows = $result->fetchAll(\PDO::FETCH_COLUMN);
196
-		$result->closeCursor();
197
-
198
-		return $rows;
199
-	}
200
-
201
-	/**
202
-	 * Returns all versions which are available in the migration folder
203
-	 *
204
-	 * @return array
205
-	 */
206
-	public function getAvailableVersions() {
207
-		$this->ensureMigrationsAreLoaded();
208
-		return array_map('strval', array_keys($this->migrations));
209
-	}
210
-
211
-	protected function findMigrations() {
212
-		$directory = realpath($this->migrationsPath);
213
-		if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
214
-			return [];
215
-		}
216
-
217
-		$iterator = new \RegexIterator(
218
-			new \RecursiveIteratorIterator(
219
-				new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
220
-				\RecursiveIteratorIterator::LEAVES_ONLY
221
-			),
222
-			'#^.+\\/Version[^\\/]{1,255}\\.php$#i',
223
-			\RegexIterator::GET_MATCH);
224
-
225
-		$files = array_keys(iterator_to_array($iterator));
226
-		uasort($files, function ($a, $b) {
227
-			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
228
-			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
229
-			if (!empty($matchA) && !empty($matchB)) {
230
-				if ($matchA[1] !== $matchB[1]) {
231
-					return ($matchA[1] < $matchB[1]) ? -1 : 1;
232
-				}
233
-				return ($matchA[2] < $matchB[2]) ? -1 : 1;
234
-			}
235
-			return (basename($a) < basename($b)) ? -1 : 1;
236
-		});
237
-
238
-		$migrations = [];
239
-
240
-		foreach ($files as $file) {
241
-			$className = basename($file, '.php');
242
-			$version = (string) substr($className, 7);
243
-			if ($version === '0') {
244
-				throw new \InvalidArgumentException(
245
-					"Cannot load a migrations with the name '$version' because it is a reserved number"
246
-				);
247
-			}
248
-			$migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
249
-		}
250
-
251
-		return $migrations;
252
-	}
253
-
254
-	/**
255
-	 * @param string $to
256
-	 * @return string[]
257
-	 */
258
-	private function getMigrationsToExecute($to) {
259
-		$knownMigrations = $this->getMigratedVersions();
260
-		$availableMigrations = $this->getAvailableVersions();
261
-
262
-		$toBeExecuted = [];
263
-		foreach ($availableMigrations as $v) {
264
-			if ($to !== 'latest' && $v > $to) {
265
-				continue;
266
-			}
267
-			if ($this->shallBeExecuted($v, $knownMigrations)) {
268
-				$toBeExecuted[] = $v;
269
-			}
270
-		}
271
-
272
-		return $toBeExecuted;
273
-	}
274
-
275
-	/**
276
-	 * @param string $m
277
-	 * @param string[] $knownMigrations
278
-	 * @return bool
279
-	 */
280
-	private function shallBeExecuted($m, $knownMigrations) {
281
-		if (in_array($m, $knownMigrations)) {
282
-			return false;
283
-		}
284
-
285
-		return true;
286
-	}
287
-
288
-	/**
289
-	 * @param string $version
290
-	 */
291
-	private function markAsExecuted($version) {
292
-		$this->connection->insertIfNotExist('*PREFIX*migrations', [
293
-			'app' => $this->appName,
294
-			'version' => $version
295
-		]);
296
-	}
297
-
298
-	/**
299
-	 * Returns the name of the table which holds the already applied versions
300
-	 *
301
-	 * @return string
302
-	 */
303
-	public function getMigrationsTableName() {
304
-		return $this->connection->getPrefix() . 'migrations';
305
-	}
306
-
307
-	/**
308
-	 * Returns the namespace of the version classes
309
-	 *
310
-	 * @return string
311
-	 */
312
-	public function getMigrationsNamespace() {
313
-		return $this->migrationsNamespace;
314
-	}
315
-
316
-	/**
317
-	 * Returns the directory which holds the versions
318
-	 *
319
-	 * @return string
320
-	 */
321
-	public function getMigrationsDirectory() {
322
-		return $this->migrationsPath;
323
-	}
324
-
325
-	/**
326
-	 * Return the explicit version for the aliases; current, next, prev, latest
327
-	 *
328
-	 * @param string $alias
329
-	 * @return mixed|null|string
330
-	 */
331
-	public function getMigration($alias) {
332
-		switch ($alias) {
333
-			case 'current':
334
-				return $this->getCurrentVersion();
335
-			case 'next':
336
-				return $this->getRelativeVersion($this->getCurrentVersion(), 1);
337
-			case 'prev':
338
-				return $this->getRelativeVersion($this->getCurrentVersion(), -1);
339
-			case 'latest':
340
-				$this->ensureMigrationsAreLoaded();
341
-
342
-				$migrations = $this->getAvailableVersions();
343
-				return @end($migrations);
344
-		}
345
-		return '0';
346
-	}
347
-
348
-	/**
349
-	 * @param string $version
350
-	 * @param int $delta
351
-	 * @return null|string
352
-	 */
353
-	private function getRelativeVersion($version, $delta) {
354
-		$this->ensureMigrationsAreLoaded();
355
-
356
-		$versions = $this->getAvailableVersions();
357
-		array_unshift($versions, 0);
358
-		$offset = array_search($version, $versions, true);
359
-		if ($offset === false || !isset($versions[$offset + $delta])) {
360
-			// Unknown version or delta out of bounds.
361
-			return null;
362
-		}
363
-
364
-		return (string) $versions[$offset + $delta];
365
-	}
366
-
367
-	/**
368
-	 * @return string
369
-	 */
370
-	private function getCurrentVersion() {
371
-		$m = $this->getMigratedVersions();
372
-		if (count($m) === 0) {
373
-			return '0';
374
-		}
375
-		$migrations = array_values($m);
376
-		return @end($migrations);
377
-	}
378
-
379
-	/**
380
-	 * @param string $version
381
-	 * @return string
382
-	 * @throws \InvalidArgumentException
383
-	 */
384
-	private function getClass($version) {
385
-		$this->ensureMigrationsAreLoaded();
386
-
387
-		if (isset($this->migrations[$version])) {
388
-			return $this->migrations[$version];
389
-		}
390
-
391
-		throw new \InvalidArgumentException("Version $version is unknown.");
392
-	}
393
-
394
-	/**
395
-	 * Allows to set an IOutput implementation which is used for logging progress and messages
396
-	 *
397
-	 * @param IOutput $output
398
-	 */
399
-	public function setOutput(IOutput $output) {
400
-		$this->output = $output;
401
-	}
402
-
403
-	/**
404
-	 * Applies all not yet applied versions up to $to
405
-	 *
406
-	 * @param string $to
407
-	 * @param bool $schemaOnly
408
-	 * @throws \InvalidArgumentException
409
-	 */
410
-	public function migrate($to = 'latest', $schemaOnly = false) {
411
-		// read known migrations
412
-		$toBeExecuted = $this->getMigrationsToExecute($to);
413
-		foreach ($toBeExecuted as $version) {
414
-			$this->executeStep($version, $schemaOnly);
415
-		}
416
-	}
417
-
418
-	/**
419
-	 * Get the human readable descriptions for the migration steps to run
420
-	 *
421
-	 * @param string $to
422
-	 * @return string[] [$name => $description]
423
-	 */
424
-	public function describeMigrationStep($to = 'latest') {
425
-		$toBeExecuted = $this->getMigrationsToExecute($to);
426
-		$description = [];
427
-		foreach ($toBeExecuted as $version) {
428
-			$migration = $this->createInstance($version);
429
-			if ($migration->name()) {
430
-				$description[$migration->name()] = $migration->description();
431
-			}
432
-		}
433
-		return $description;
434
-	}
435
-
436
-	/**
437
-	 * @param string $version
438
-	 * @return IMigrationStep
439
-	 * @throws \InvalidArgumentException
440
-	 */
441
-	protected function createInstance($version) {
442
-		$class = $this->getClass($version);
443
-		try {
444
-			$s = \OC::$server->query($class);
445
-
446
-			if (!$s instanceof IMigrationStep) {
447
-				throw new \InvalidArgumentException('Not a valid migration');
448
-			}
449
-		} catch (QueryException $e) {
450
-			if (class_exists($class)) {
451
-				$s = new $class();
452
-			} else {
453
-				throw new \InvalidArgumentException("Migration step '$class' is unknown");
454
-			}
455
-		}
456
-
457
-		return $s;
458
-	}
459
-
460
-	/**
461
-	 * Executes one explicit version
462
-	 *
463
-	 * @param string $version
464
-	 * @param bool $schemaOnly
465
-	 * @throws \InvalidArgumentException
466
-	 */
467
-	public function executeStep($version, $schemaOnly = false) {
468
-		$instance = $this->createInstance($version);
469
-
470
-		if (!$schemaOnly) {
471
-			$instance->preSchemaChange($this->output, function () {
472
-				return new SchemaWrapper($this->connection);
473
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
474
-		}
475
-
476
-		$toSchema = $instance->changeSchema($this->output, function () {
477
-			return new SchemaWrapper($this->connection);
478
-		}, ['tablePrefix' => $this->connection->getPrefix()]);
479
-
480
-		if ($toSchema instanceof SchemaWrapper) {
481
-			$targetSchema = $toSchema->getWrappedSchema();
482
-			if ($this->checkOracle) {
483
-				$sourceSchema = $this->connection->createSchema();
484
-				$this->ensureOracleIdentifierLengthLimit($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
485
-			}
486
-			$this->connection->migrateToSchema($targetSchema);
487
-			$toSchema->performDropTableCalls();
488
-		}
489
-
490
-		if (!$schemaOnly) {
491
-			$instance->postSchemaChange($this->output, function () {
492
-				return new SchemaWrapper($this->connection);
493
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
494
-		}
495
-
496
-		$this->markAsExecuted($version);
497
-	}
498
-
499
-	public function ensureOracleIdentifierLengthLimit(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
500
-		$sequences = $targetSchema->getSequences();
501
-
502
-		foreach ($targetSchema->getTables() as $table) {
503
-			try {
504
-				$sourceTable = $sourceSchema->getTable($table->getName());
505
-			} catch (SchemaException $e) {
506
-				if (\strlen($table->getName()) - $prefixLength > 27) {
507
-					throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
508
-				}
509
-				$sourceTable = null;
510
-			}
511
-
512
-			foreach ($table->getColumns() as $thing) {
513
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
514
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
515
-				}
516
-
517
-				if ($thing->getNotnull() && $thing->getDefault() === ''
518
-					&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
519
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
520
-				}
521
-			}
522
-
523
-			foreach ($table->getIndexes() as $thing) {
524
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
525
-					throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
526
-				}
527
-			}
528
-
529
-			foreach ($table->getForeignKeys() as $thing) {
530
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
531
-					throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
532
-				}
533
-			}
534
-
535
-			$primaryKey = $table->getPrimaryKey();
536
-			if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
537
-				$indexName = strtolower($primaryKey->getName());
538
-				$isUsingDefaultName = $indexName === 'primary';
539
-
540
-				if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
541
-					$defaultName = $table->getName() . '_pkey';
542
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
543
-
544
-					if ($isUsingDefaultName) {
545
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
546
-						$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
547
-							return $sequence->getName() !== $sequenceName;
548
-						});
549
-					}
550
-				} elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
551
-					$defaultName = $table->getName() . '_seq';
552
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
553
-				}
554
-
555
-				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
556
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
557
-				}
558
-				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
559
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
560
-				}
561
-			}
562
-		}
563
-
564
-		foreach ($sequences as $sequence) {
565
-			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
566
-				throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
567
-			}
568
-		}
569
-	}
570
-
571
-	private function ensureMigrationsAreLoaded() {
572
-		if (empty($this->migrations)) {
573
-			$this->migrations = $this->findMigrations();
574
-		}
575
-	}
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 IDBConnection $connection
67
+     * @param AppLocator $appLocator
68
+     * @param IOutput|null $output
69
+     * @throws \Exception
70
+     */
71
+    public function __construct($appName, IDBConnection $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->getLogger(), $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
+        $schema = new SchemaWrapper($this->connection);
128
+
129
+        /**
130
+         * We drop the table when it has different columns or the definition does not
131
+         * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
132
+         */
133
+        try {
134
+            $table = $schema->getTable('migrations');
135
+            $columns = $table->getColumns();
136
+
137
+            if (count($columns) === 2) {
138
+                try {
139
+                    $column = $table->getColumn('app');
140
+                    $schemaMismatch = $column->getLength() !== 255;
141
+
142
+                    if (!$schemaMismatch) {
143
+                        $column = $table->getColumn('version');
144
+                        $schemaMismatch = $column->getLength() !== 255;
145
+                    }
146
+                } catch (SchemaException $e) {
147
+                    // One of the columns is missing
148
+                    $schemaMismatch = true;
149
+                }
150
+
151
+                if (!$schemaMismatch) {
152
+                    // Table exists and schema matches: return back!
153
+                    $this->migrationTableCreated = true;
154
+                    return false;
155
+                }
156
+            }
157
+
158
+            // Drop the table, when it didn't match our expectations.
159
+            $this->connection->dropTable('migrations');
160
+
161
+            // Recreate the schema after the table was dropped.
162
+            $schema = new SchemaWrapper($this->connection);
163
+        } catch (SchemaException $e) {
164
+            // Table not found, no need to panic, we will create it.
165
+        }
166
+
167
+        $table = $schema->createTable('migrations');
168
+        $table->addColumn('app', Types::STRING, ['length' => 255]);
169
+        $table->addColumn('version', Types::STRING, ['length' => 255]);
170
+        $table->setPrimaryKey(['app', 'version']);
171
+
172
+        $this->connection->migrateToSchema($schema->getWrappedSchema());
173
+
174
+        $this->migrationTableCreated = true;
175
+
176
+        return true;
177
+    }
178
+
179
+    /**
180
+     * Returns all versions which have already been applied
181
+     *
182
+     * @return string[]
183
+     * @codeCoverageIgnore - no need to test this
184
+     */
185
+    public function getMigratedVersions() {
186
+        $this->createMigrationTable();
187
+        $qb = $this->connection->getQueryBuilder();
188
+
189
+        $qb->select('version')
190
+            ->from('migrations')
191
+            ->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
192
+            ->orderBy('version');
193
+
194
+        $result = $qb->execute();
195
+        $rows = $result->fetchAll(\PDO::FETCH_COLUMN);
196
+        $result->closeCursor();
197
+
198
+        return $rows;
199
+    }
200
+
201
+    /**
202
+     * Returns all versions which are available in the migration folder
203
+     *
204
+     * @return array
205
+     */
206
+    public function getAvailableVersions() {
207
+        $this->ensureMigrationsAreLoaded();
208
+        return array_map('strval', array_keys($this->migrations));
209
+    }
210
+
211
+    protected function findMigrations() {
212
+        $directory = realpath($this->migrationsPath);
213
+        if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
214
+            return [];
215
+        }
216
+
217
+        $iterator = new \RegexIterator(
218
+            new \RecursiveIteratorIterator(
219
+                new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
220
+                \RecursiveIteratorIterator::LEAVES_ONLY
221
+            ),
222
+            '#^.+\\/Version[^\\/]{1,255}\\.php$#i',
223
+            \RegexIterator::GET_MATCH);
224
+
225
+        $files = array_keys(iterator_to_array($iterator));
226
+        uasort($files, function ($a, $b) {
227
+            preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
228
+            preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
229
+            if (!empty($matchA) && !empty($matchB)) {
230
+                if ($matchA[1] !== $matchB[1]) {
231
+                    return ($matchA[1] < $matchB[1]) ? -1 : 1;
232
+                }
233
+                return ($matchA[2] < $matchB[2]) ? -1 : 1;
234
+            }
235
+            return (basename($a) < basename($b)) ? -1 : 1;
236
+        });
237
+
238
+        $migrations = [];
239
+
240
+        foreach ($files as $file) {
241
+            $className = basename($file, '.php');
242
+            $version = (string) substr($className, 7);
243
+            if ($version === '0') {
244
+                throw new \InvalidArgumentException(
245
+                    "Cannot load a migrations with the name '$version' because it is a reserved number"
246
+                );
247
+            }
248
+            $migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
249
+        }
250
+
251
+        return $migrations;
252
+    }
253
+
254
+    /**
255
+     * @param string $to
256
+     * @return string[]
257
+     */
258
+    private function getMigrationsToExecute($to) {
259
+        $knownMigrations = $this->getMigratedVersions();
260
+        $availableMigrations = $this->getAvailableVersions();
261
+
262
+        $toBeExecuted = [];
263
+        foreach ($availableMigrations as $v) {
264
+            if ($to !== 'latest' && $v > $to) {
265
+                continue;
266
+            }
267
+            if ($this->shallBeExecuted($v, $knownMigrations)) {
268
+                $toBeExecuted[] = $v;
269
+            }
270
+        }
271
+
272
+        return $toBeExecuted;
273
+    }
274
+
275
+    /**
276
+     * @param string $m
277
+     * @param string[] $knownMigrations
278
+     * @return bool
279
+     */
280
+    private function shallBeExecuted($m, $knownMigrations) {
281
+        if (in_array($m, $knownMigrations)) {
282
+            return false;
283
+        }
284
+
285
+        return true;
286
+    }
287
+
288
+    /**
289
+     * @param string $version
290
+     */
291
+    private function markAsExecuted($version) {
292
+        $this->connection->insertIfNotExist('*PREFIX*migrations', [
293
+            'app' => $this->appName,
294
+            'version' => $version
295
+        ]);
296
+    }
297
+
298
+    /**
299
+     * Returns the name of the table which holds the already applied versions
300
+     *
301
+     * @return string
302
+     */
303
+    public function getMigrationsTableName() {
304
+        return $this->connection->getPrefix() . 'migrations';
305
+    }
306
+
307
+    /**
308
+     * Returns the namespace of the version classes
309
+     *
310
+     * @return string
311
+     */
312
+    public function getMigrationsNamespace() {
313
+        return $this->migrationsNamespace;
314
+    }
315
+
316
+    /**
317
+     * Returns the directory which holds the versions
318
+     *
319
+     * @return string
320
+     */
321
+    public function getMigrationsDirectory() {
322
+        return $this->migrationsPath;
323
+    }
324
+
325
+    /**
326
+     * Return the explicit version for the aliases; current, next, prev, latest
327
+     *
328
+     * @param string $alias
329
+     * @return mixed|null|string
330
+     */
331
+    public function getMigration($alias) {
332
+        switch ($alias) {
333
+            case 'current':
334
+                return $this->getCurrentVersion();
335
+            case 'next':
336
+                return $this->getRelativeVersion($this->getCurrentVersion(), 1);
337
+            case 'prev':
338
+                return $this->getRelativeVersion($this->getCurrentVersion(), -1);
339
+            case 'latest':
340
+                $this->ensureMigrationsAreLoaded();
341
+
342
+                $migrations = $this->getAvailableVersions();
343
+                return @end($migrations);
344
+        }
345
+        return '0';
346
+    }
347
+
348
+    /**
349
+     * @param string $version
350
+     * @param int $delta
351
+     * @return null|string
352
+     */
353
+    private function getRelativeVersion($version, $delta) {
354
+        $this->ensureMigrationsAreLoaded();
355
+
356
+        $versions = $this->getAvailableVersions();
357
+        array_unshift($versions, 0);
358
+        $offset = array_search($version, $versions, true);
359
+        if ($offset === false || !isset($versions[$offset + $delta])) {
360
+            // Unknown version or delta out of bounds.
361
+            return null;
362
+        }
363
+
364
+        return (string) $versions[$offset + $delta];
365
+    }
366
+
367
+    /**
368
+     * @return string
369
+     */
370
+    private function getCurrentVersion() {
371
+        $m = $this->getMigratedVersions();
372
+        if (count($m) === 0) {
373
+            return '0';
374
+        }
375
+        $migrations = array_values($m);
376
+        return @end($migrations);
377
+    }
378
+
379
+    /**
380
+     * @param string $version
381
+     * @return string
382
+     * @throws \InvalidArgumentException
383
+     */
384
+    private function getClass($version) {
385
+        $this->ensureMigrationsAreLoaded();
386
+
387
+        if (isset($this->migrations[$version])) {
388
+            return $this->migrations[$version];
389
+        }
390
+
391
+        throw new \InvalidArgumentException("Version $version is unknown.");
392
+    }
393
+
394
+    /**
395
+     * Allows to set an IOutput implementation which is used for logging progress and messages
396
+     *
397
+     * @param IOutput $output
398
+     */
399
+    public function setOutput(IOutput $output) {
400
+        $this->output = $output;
401
+    }
402
+
403
+    /**
404
+     * Applies all not yet applied versions up to $to
405
+     *
406
+     * @param string $to
407
+     * @param bool $schemaOnly
408
+     * @throws \InvalidArgumentException
409
+     */
410
+    public function migrate($to = 'latest', $schemaOnly = false) {
411
+        // read known migrations
412
+        $toBeExecuted = $this->getMigrationsToExecute($to);
413
+        foreach ($toBeExecuted as $version) {
414
+            $this->executeStep($version, $schemaOnly);
415
+        }
416
+    }
417
+
418
+    /**
419
+     * Get the human readable descriptions for the migration steps to run
420
+     *
421
+     * @param string $to
422
+     * @return string[] [$name => $description]
423
+     */
424
+    public function describeMigrationStep($to = 'latest') {
425
+        $toBeExecuted = $this->getMigrationsToExecute($to);
426
+        $description = [];
427
+        foreach ($toBeExecuted as $version) {
428
+            $migration = $this->createInstance($version);
429
+            if ($migration->name()) {
430
+                $description[$migration->name()] = $migration->description();
431
+            }
432
+        }
433
+        return $description;
434
+    }
435
+
436
+    /**
437
+     * @param string $version
438
+     * @return IMigrationStep
439
+     * @throws \InvalidArgumentException
440
+     */
441
+    protected function createInstance($version) {
442
+        $class = $this->getClass($version);
443
+        try {
444
+            $s = \OC::$server->query($class);
445
+
446
+            if (!$s instanceof IMigrationStep) {
447
+                throw new \InvalidArgumentException('Not a valid migration');
448
+            }
449
+        } catch (QueryException $e) {
450
+            if (class_exists($class)) {
451
+                $s = new $class();
452
+            } else {
453
+                throw new \InvalidArgumentException("Migration step '$class' is unknown");
454
+            }
455
+        }
456
+
457
+        return $s;
458
+    }
459
+
460
+    /**
461
+     * Executes one explicit version
462
+     *
463
+     * @param string $version
464
+     * @param bool $schemaOnly
465
+     * @throws \InvalidArgumentException
466
+     */
467
+    public function executeStep($version, $schemaOnly = false) {
468
+        $instance = $this->createInstance($version);
469
+
470
+        if (!$schemaOnly) {
471
+            $instance->preSchemaChange($this->output, function () {
472
+                return new SchemaWrapper($this->connection);
473
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
474
+        }
475
+
476
+        $toSchema = $instance->changeSchema($this->output, function () {
477
+            return new SchemaWrapper($this->connection);
478
+        }, ['tablePrefix' => $this->connection->getPrefix()]);
479
+
480
+        if ($toSchema instanceof SchemaWrapper) {
481
+            $targetSchema = $toSchema->getWrappedSchema();
482
+            if ($this->checkOracle) {
483
+                $sourceSchema = $this->connection->createSchema();
484
+                $this->ensureOracleIdentifierLengthLimit($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
485
+            }
486
+            $this->connection->migrateToSchema($targetSchema);
487
+            $toSchema->performDropTableCalls();
488
+        }
489
+
490
+        if (!$schemaOnly) {
491
+            $instance->postSchemaChange($this->output, function () {
492
+                return new SchemaWrapper($this->connection);
493
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
494
+        }
495
+
496
+        $this->markAsExecuted($version);
497
+    }
498
+
499
+    public function ensureOracleIdentifierLengthLimit(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
500
+        $sequences = $targetSchema->getSequences();
501
+
502
+        foreach ($targetSchema->getTables() as $table) {
503
+            try {
504
+                $sourceTable = $sourceSchema->getTable($table->getName());
505
+            } catch (SchemaException $e) {
506
+                if (\strlen($table->getName()) - $prefixLength > 27) {
507
+                    throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
508
+                }
509
+                $sourceTable = null;
510
+            }
511
+
512
+            foreach ($table->getColumns() as $thing) {
513
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
514
+                    throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
515
+                }
516
+
517
+                if ($thing->getNotnull() && $thing->getDefault() === ''
518
+                    && $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
519
+                    throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
520
+                }
521
+            }
522
+
523
+            foreach ($table->getIndexes() as $thing) {
524
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
525
+                    throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
526
+                }
527
+            }
528
+
529
+            foreach ($table->getForeignKeys() as $thing) {
530
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
531
+                    throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
532
+                }
533
+            }
534
+
535
+            $primaryKey = $table->getPrimaryKey();
536
+            if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
537
+                $indexName = strtolower($primaryKey->getName());
538
+                $isUsingDefaultName = $indexName === 'primary';
539
+
540
+                if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
541
+                    $defaultName = $table->getName() . '_pkey';
542
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
543
+
544
+                    if ($isUsingDefaultName) {
545
+                        $sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
546
+                        $sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
547
+                            return $sequence->getName() !== $sequenceName;
548
+                        });
549
+                    }
550
+                } elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
551
+                    $defaultName = $table->getName() . '_seq';
552
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
553
+                }
554
+
555
+                if (!$isUsingDefaultName && \strlen($indexName) > 30) {
556
+                    throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
557
+                }
558
+                if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
559
+                    throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
560
+                }
561
+            }
562
+        }
563
+
564
+        foreach ($sequences as $sequence) {
565
+            if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
566
+                throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
567
+            }
568
+        }
569
+    }
570
+
571
+    private function ensureMigrationsAreLoaded() {
572
+        if (empty($this->migrations)) {
573
+            $this->migrations = $this->findMigrations();
574
+        }
575
+    }
576 576
 }
Please login to merge, or discard this patch.
core/Migrations/Version18000Date20190920085628.php 1 patch
Indentation   +37 added lines, -37 removed lines patch added patch discarded remove patch
@@ -37,47 +37,47 @@
 block discarded – undo
37 37
 
38 38
 class Version18000Date20190920085628 extends SimpleMigrationStep {
39 39
 
40
-	/** @var IDBConnection */
41
-	protected $connection;
40
+    /** @var IDBConnection */
41
+    protected $connection;
42 42
 
43
-	public function __construct(IDBConnection $connection) {
44
-		$this->connection = $connection;
45
-	}
43
+    public function __construct(IDBConnection $connection) {
44
+        $this->connection = $connection;
45
+    }
46 46
 
47
-	/**
48
-	 * @param IOutput $output
49
-	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
50
-	 * @param array $options
51
-	 * @return null|ISchemaWrapper
52
-	 */
53
-	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
54
-		/** @var ISchemaWrapper $schema */
55
-		$schema = $schemaClosure();
47
+    /**
48
+     * @param IOutput $output
49
+     * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
50
+     * @param array $options
51
+     * @return null|ISchemaWrapper
52
+     */
53
+    public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
54
+        /** @var ISchemaWrapper $schema */
55
+        $schema = $schemaClosure();
56 56
 
57
-		if ($schema->hasTable('groups')) {
58
-			$table = $schema->getTable('groups');
57
+        if ($schema->hasTable('groups')) {
58
+            $table = $schema->getTable('groups');
59 59
 
60
-			$table->addColumn('displayname', Types::STRING, [
61
-				'notnull' => true,
62
-				'length' => 255,
63
-				// Will be overwritten in postSchemaChange, but Oracle can not save
64
-				// empty strings in notnull columns
65
-				'default' => 'name',
66
-			]);
67
-		}
60
+            $table->addColumn('displayname', Types::STRING, [
61
+                'notnull' => true,
62
+                'length' => 255,
63
+                // Will be overwritten in postSchemaChange, but Oracle can not save
64
+                // empty strings in notnull columns
65
+                'default' => 'name',
66
+            ]);
67
+        }
68 68
 
69
-		return $schema;
70
-	}
69
+        return $schema;
70
+    }
71 71
 
72
-	/**
73
-	 * @param IOutput $output
74
-	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
75
-	 * @param array $options
76
-	 */
77
-	public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
78
-		$query = $this->connection->getQueryBuilder();
79
-		$query->update('groups')
80
-			->set('displayname', 'gid');
81
-		$query->execute();
82
-	}
72
+    /**
73
+     * @param IOutput $output
74
+     * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
75
+     * @param array $options
76
+     */
77
+    public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
78
+        $query = $this->connection->getQueryBuilder();
79
+        $query->update('groups')
80
+            ->set('displayname', 'gid');
81
+        $query->execute();
82
+    }
83 83
 }
Please login to merge, or discard this patch.