Passed
Push — master ( 265a28...4907b4 )
by Morris
15:01 queued 11s
created
lib/private/DB/MigrationService.php 2 patches
Indentation   +523 added lines, -523 removed lines patch added patch discarded remove patch
@@ -42,527 +42,527 @@
 block discarded – undo
42 42
 
43 43
 class MigrationService {
44 44
 
45
-	/** @var boolean */
46
-	private $migrationTableCreated;
47
-	/** @var array */
48
-	private $migrations;
49
-	/** @var IOutput */
50
-	private $output;
51
-	/** @var Connection */
52
-	private $connection;
53
-	/** @var string */
54
-	private $appName;
55
-	/** @var bool */
56
-	private $checkOracle;
57
-
58
-	/**
59
-	 * MigrationService constructor.
60
-	 *
61
-	 * @param $appName
62
-	 * @param IDBConnection $connection
63
-	 * @param AppLocator $appLocator
64
-	 * @param IOutput|null $output
65
-	 * @throws \Exception
66
-	 */
67
-	public function __construct($appName, IDBConnection $connection, IOutput $output = null, AppLocator $appLocator = null) {
68
-		$this->appName = $appName;
69
-		$this->connection = $connection;
70
-		$this->output = $output;
71
-		if (null === $this->output) {
72
-			$this->output = new SimpleOutput(\OC::$server->getLogger(), $appName);
73
-		}
74
-
75
-		if ($appName === 'core') {
76
-			$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
77
-			$this->migrationsNamespace = 'OC\\Core\\Migrations';
78
-			$this->checkOracle = true;
79
-		} else {
80
-			if (null === $appLocator) {
81
-				$appLocator = new AppLocator();
82
-			}
83
-			$appPath = $appLocator->getAppPath($appName);
84
-			$namespace = App::buildAppNamespace($appName);
85
-			$this->migrationsPath = "$appPath/lib/Migration";
86
-			$this->migrationsNamespace = $namespace . '\\Migration';
87
-
88
-			$infoParser = new InfoParser();
89
-			$info = $infoParser->parse($appPath . '/appinfo/info.xml');
90
-			if (!isset($info['dependencies']['database'])) {
91
-				$this->checkOracle = true;
92
-			} else {
93
-				$this->checkOracle = false;
94
-				foreach ($info['dependencies']['database'] as $database) {
95
-					if (\is_string($database) && $database === 'oci') {
96
-						$this->checkOracle = true;
97
-					} else if (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
98
-						$this->checkOracle = true;
99
-					}
100
-				}
101
-			}
102
-		}
103
-	}
104
-
105
-	/**
106
-	 * Returns the name of the app for which this migration is executed
107
-	 *
108
-	 * @return string
109
-	 */
110
-	public function getApp() {
111
-		return $this->appName;
112
-	}
113
-
114
-	/**
115
-	 * @return bool
116
-	 * @codeCoverageIgnore - this will implicitly tested on installation
117
-	 */
118
-	private function createMigrationTable() {
119
-		if ($this->migrationTableCreated) {
120
-			return false;
121
-		}
122
-
123
-		$schema = new SchemaWrapper($this->connection);
124
-
125
-		/**
126
-		 * We drop the table when it has different columns or the definition does not
127
-		 * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
128
-		 */
129
-		try {
130
-			$table = $schema->getTable('migrations');
131
-			$columns = $table->getColumns();
132
-
133
-			if (count($columns) === 2) {
134
-				try {
135
-					$column = $table->getColumn('app');
136
-					$schemaMismatch = $column->getLength() !== 255;
137
-
138
-					if (!$schemaMismatch) {
139
-						$column = $table->getColumn('version');
140
-						$schemaMismatch = $column->getLength() !== 255;
141
-					}
142
-				} catch (SchemaException $e) {
143
-					// One of the columns is missing
144
-					$schemaMismatch = true;
145
-				}
146
-
147
-				if (!$schemaMismatch) {
148
-					// Table exists and schema matches: return back!
149
-					$this->migrationTableCreated = true;
150
-					return false;
151
-				}
152
-			}
153
-
154
-			// Drop the table, when it didn't match our expectations.
155
-			$this->connection->dropTable('migrations');
156
-
157
-			// Recreate the schema after the table was dropped.
158
-			$schema = new SchemaWrapper($this->connection);
159
-
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', Type::STRING, ['length' => 255]);
166
-		$table->addColumn('version', Type::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
-		// read known migrations
409
-		$toBeExecuted = $this->getMigrationsToExecute($to);
410
-		foreach ($toBeExecuted as $version) {
411
-			$this->executeStep($version, $schemaOnly);
412
-		}
413
-	}
414
-
415
-	/**
416
-	 * Get the human readable descriptions for the migration steps to run
417
-	 *
418
-	 * @param string $to
419
-	 * @return string[] [$name => $description]
420
-	 */
421
-	public function describeMigrationStep($to = 'latest') {
422
-		$toBeExecuted = $this->getMigrationsToExecute($to);
423
-		$description = [];
424
-		foreach ($toBeExecuted as $version) {
425
-			$migration = $this->createInstance($version);
426
-			if ($migration->name()) {
427
-				$description[$migration->name()] = $migration->description();
428
-			}
429
-		}
430
-		return $description;
431
-	}
432
-
433
-	/**
434
-	 * @param string $version
435
-	 * @return IMigrationStep
436
-	 * @throws \InvalidArgumentException
437
-	 */
438
-	protected function createInstance($version) {
439
-		$class = $this->getClass($version);
440
-		try {
441
-			$s = \OC::$server->query($class);
442
-
443
-			if (!$s instanceof IMigrationStep) {
444
-				throw new \InvalidArgumentException('Not a valid migration');
445
-			}
446
-		} catch (QueryException $e) {
447
-			if (class_exists($class)) {
448
-				$s = new $class();
449
-			} else {
450
-				throw new \InvalidArgumentException("Migration step '$class' is unknown");
451
-			}
452
-		}
453
-
454
-		return $s;
455
-	}
456
-
457
-	/**
458
-	 * Executes one explicit version
459
-	 *
460
-	 * @param string $version
461
-	 * @param bool $schemaOnly
462
-	 * @throws \InvalidArgumentException
463
-	 */
464
-	public function executeStep($version, $schemaOnly = false) {
465
-		$instance = $this->createInstance($version);
466
-
467
-		if (!$schemaOnly) {
468
-			$instance->preSchemaChange($this->output, function() {
469
-				return new SchemaWrapper($this->connection);
470
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
471
-		}
472
-
473
-		$toSchema = $instance->changeSchema($this->output, function() {
474
-			return new SchemaWrapper($this->connection);
475
-		}, ['tablePrefix' => $this->connection->getPrefix()]);
476
-
477
-		if ($toSchema instanceof SchemaWrapper) {
478
-			$targetSchema = $toSchema->getWrappedSchema();
479
-			if ($this->checkOracle) {
480
-				$sourceSchema = $this->connection->createSchema();
481
-				$this->ensureOracleIdentifierLengthLimit($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
482
-			}
483
-			$this->connection->migrateToSchema($targetSchema);
484
-			$toSchema->performDropTableCalls();
485
-		}
486
-
487
-		if (!$schemaOnly) {
488
-			$instance->postSchemaChange($this->output, function() {
489
-				return new SchemaWrapper($this->connection);
490
-			}, ['tablePrefix' => $this->connection->getPrefix()]);
491
-		}
492
-
493
-		$this->markAsExecuted($version);
494
-	}
495
-
496
-	public function ensureOracleIdentifierLengthLimit(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
497
-		$sequences = $targetSchema->getSequences();
498
-
499
-		foreach ($targetSchema->getTables() as $table) {
500
-			try {
501
-				$sourceTable = $sourceSchema->getTable($table->getName());
502
-			} catch (SchemaException $e) {
503
-				if (\strlen($table->getName()) - $prefixLength > 27) {
504
-					throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
505
-				}
506
-				$sourceTable = null;
507
-			}
508
-
509
-			foreach ($table->getColumns() as $thing) {
510
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
511
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
512
-				}
513
-			}
514
-
515
-			foreach ($table->getIndexes() as $thing) {
516
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
517
-					throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
518
-				}
519
-			}
520
-
521
-			foreach ($table->getForeignKeys() as $thing) {
522
-				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
523
-					throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
524
-				}
525
-			}
526
-
527
-			$primaryKey = $table->getPrimaryKey();
528
-			if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
529
-				$indexName = strtolower($primaryKey->getName());
530
-				$isUsingDefaultName = $indexName === 'primary';
531
-
532
-				if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
533
-					$defaultName = $table->getName() . '_pkey';
534
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
535
-
536
-					if ($isUsingDefaultName) {
537
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
538
-						$sequences = array_filter($sequences, function(Sequence $sequence) use ($sequenceName) {
539
-							return $sequence->getName() !== $sequenceName;
540
-						});
541
-					}
542
-				} else if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
543
-					$defaultName = $table->getName() . '_seq';
544
-					$isUsingDefaultName = strtolower($defaultName) === $indexName;
545
-				}
546
-
547
-				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
548
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
549
-				}
550
-				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength > 23) {
551
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
552
-				}
553
-			}
554
-		}
555
-
556
-		foreach ($sequences as $sequence) {
557
-			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
558
-				throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
559
-			}
560
-		}
561
-	}
562
-
563
-	private function ensureMigrationsAreLoaded() {
564
-		if (empty($this->migrations)) {
565
-			$this->migrations = $this->findMigrations();
566
-		}
567
-	}
45
+    /** @var boolean */
46
+    private $migrationTableCreated;
47
+    /** @var array */
48
+    private $migrations;
49
+    /** @var IOutput */
50
+    private $output;
51
+    /** @var Connection */
52
+    private $connection;
53
+    /** @var string */
54
+    private $appName;
55
+    /** @var bool */
56
+    private $checkOracle;
57
+
58
+    /**
59
+     * MigrationService constructor.
60
+     *
61
+     * @param $appName
62
+     * @param IDBConnection $connection
63
+     * @param AppLocator $appLocator
64
+     * @param IOutput|null $output
65
+     * @throws \Exception
66
+     */
67
+    public function __construct($appName, IDBConnection $connection, IOutput $output = null, AppLocator $appLocator = null) {
68
+        $this->appName = $appName;
69
+        $this->connection = $connection;
70
+        $this->output = $output;
71
+        if (null === $this->output) {
72
+            $this->output = new SimpleOutput(\OC::$server->getLogger(), $appName);
73
+        }
74
+
75
+        if ($appName === 'core') {
76
+            $this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
77
+            $this->migrationsNamespace = 'OC\\Core\\Migrations';
78
+            $this->checkOracle = true;
79
+        } else {
80
+            if (null === $appLocator) {
81
+                $appLocator = new AppLocator();
82
+            }
83
+            $appPath = $appLocator->getAppPath($appName);
84
+            $namespace = App::buildAppNamespace($appName);
85
+            $this->migrationsPath = "$appPath/lib/Migration";
86
+            $this->migrationsNamespace = $namespace . '\\Migration';
87
+
88
+            $infoParser = new InfoParser();
89
+            $info = $infoParser->parse($appPath . '/appinfo/info.xml');
90
+            if (!isset($info['dependencies']['database'])) {
91
+                $this->checkOracle = true;
92
+            } else {
93
+                $this->checkOracle = false;
94
+                foreach ($info['dependencies']['database'] as $database) {
95
+                    if (\is_string($database) && $database === 'oci') {
96
+                        $this->checkOracle = true;
97
+                    } else if (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
98
+                        $this->checkOracle = true;
99
+                    }
100
+                }
101
+            }
102
+        }
103
+    }
104
+
105
+    /**
106
+     * Returns the name of the app for which this migration is executed
107
+     *
108
+     * @return string
109
+     */
110
+    public function getApp() {
111
+        return $this->appName;
112
+    }
113
+
114
+    /**
115
+     * @return bool
116
+     * @codeCoverageIgnore - this will implicitly tested on installation
117
+     */
118
+    private function createMigrationTable() {
119
+        if ($this->migrationTableCreated) {
120
+            return false;
121
+        }
122
+
123
+        $schema = new SchemaWrapper($this->connection);
124
+
125
+        /**
126
+         * We drop the table when it has different columns or the definition does not
127
+         * match. E.g. ownCloud uses a length of 177 for app and 14 for version.
128
+         */
129
+        try {
130
+            $table = $schema->getTable('migrations');
131
+            $columns = $table->getColumns();
132
+
133
+            if (count($columns) === 2) {
134
+                try {
135
+                    $column = $table->getColumn('app');
136
+                    $schemaMismatch = $column->getLength() !== 255;
137
+
138
+                    if (!$schemaMismatch) {
139
+                        $column = $table->getColumn('version');
140
+                        $schemaMismatch = $column->getLength() !== 255;
141
+                    }
142
+                } catch (SchemaException $e) {
143
+                    // One of the columns is missing
144
+                    $schemaMismatch = true;
145
+                }
146
+
147
+                if (!$schemaMismatch) {
148
+                    // Table exists and schema matches: return back!
149
+                    $this->migrationTableCreated = true;
150
+                    return false;
151
+                }
152
+            }
153
+
154
+            // Drop the table, when it didn't match our expectations.
155
+            $this->connection->dropTable('migrations');
156
+
157
+            // Recreate the schema after the table was dropped.
158
+            $schema = new SchemaWrapper($this->connection);
159
+
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', Type::STRING, ['length' => 255]);
166
+        $table->addColumn('version', Type::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
+        // read known migrations
409
+        $toBeExecuted = $this->getMigrationsToExecute($to);
410
+        foreach ($toBeExecuted as $version) {
411
+            $this->executeStep($version, $schemaOnly);
412
+        }
413
+    }
414
+
415
+    /**
416
+     * Get the human readable descriptions for the migration steps to run
417
+     *
418
+     * @param string $to
419
+     * @return string[] [$name => $description]
420
+     */
421
+    public function describeMigrationStep($to = 'latest') {
422
+        $toBeExecuted = $this->getMigrationsToExecute($to);
423
+        $description = [];
424
+        foreach ($toBeExecuted as $version) {
425
+            $migration = $this->createInstance($version);
426
+            if ($migration->name()) {
427
+                $description[$migration->name()] = $migration->description();
428
+            }
429
+        }
430
+        return $description;
431
+    }
432
+
433
+    /**
434
+     * @param string $version
435
+     * @return IMigrationStep
436
+     * @throws \InvalidArgumentException
437
+     */
438
+    protected function createInstance($version) {
439
+        $class = $this->getClass($version);
440
+        try {
441
+            $s = \OC::$server->query($class);
442
+
443
+            if (!$s instanceof IMigrationStep) {
444
+                throw new \InvalidArgumentException('Not a valid migration');
445
+            }
446
+        } catch (QueryException $e) {
447
+            if (class_exists($class)) {
448
+                $s = new $class();
449
+            } else {
450
+                throw new \InvalidArgumentException("Migration step '$class' is unknown");
451
+            }
452
+        }
453
+
454
+        return $s;
455
+    }
456
+
457
+    /**
458
+     * Executes one explicit version
459
+     *
460
+     * @param string $version
461
+     * @param bool $schemaOnly
462
+     * @throws \InvalidArgumentException
463
+     */
464
+    public function executeStep($version, $schemaOnly = false) {
465
+        $instance = $this->createInstance($version);
466
+
467
+        if (!$schemaOnly) {
468
+            $instance->preSchemaChange($this->output, function() {
469
+                return new SchemaWrapper($this->connection);
470
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
471
+        }
472
+
473
+        $toSchema = $instance->changeSchema($this->output, function() {
474
+            return new SchemaWrapper($this->connection);
475
+        }, ['tablePrefix' => $this->connection->getPrefix()]);
476
+
477
+        if ($toSchema instanceof SchemaWrapper) {
478
+            $targetSchema = $toSchema->getWrappedSchema();
479
+            if ($this->checkOracle) {
480
+                $sourceSchema = $this->connection->createSchema();
481
+                $this->ensureOracleIdentifierLengthLimit($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
482
+            }
483
+            $this->connection->migrateToSchema($targetSchema);
484
+            $toSchema->performDropTableCalls();
485
+        }
486
+
487
+        if (!$schemaOnly) {
488
+            $instance->postSchemaChange($this->output, function() {
489
+                return new SchemaWrapper($this->connection);
490
+            }, ['tablePrefix' => $this->connection->getPrefix()]);
491
+        }
492
+
493
+        $this->markAsExecuted($version);
494
+    }
495
+
496
+    public function ensureOracleIdentifierLengthLimit(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
497
+        $sequences = $targetSchema->getSequences();
498
+
499
+        foreach ($targetSchema->getTables() as $table) {
500
+            try {
501
+                $sourceTable = $sourceSchema->getTable($table->getName());
502
+            } catch (SchemaException $e) {
503
+                if (\strlen($table->getName()) - $prefixLength > 27) {
504
+                    throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
505
+                }
506
+                $sourceTable = null;
507
+            }
508
+
509
+            foreach ($table->getColumns() as $thing) {
510
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
511
+                    throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
512
+                }
513
+            }
514
+
515
+            foreach ($table->getIndexes() as $thing) {
516
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
517
+                    throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
518
+                }
519
+            }
520
+
521
+            foreach ($table->getForeignKeys() as $thing) {
522
+                if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
523
+                    throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
524
+                }
525
+            }
526
+
527
+            $primaryKey = $table->getPrimaryKey();
528
+            if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
529
+                $indexName = strtolower($primaryKey->getName());
530
+                $isUsingDefaultName = $indexName === 'primary';
531
+
532
+                if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
533
+                    $defaultName = $table->getName() . '_pkey';
534
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
535
+
536
+                    if ($isUsingDefaultName) {
537
+                        $sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
538
+                        $sequences = array_filter($sequences, function(Sequence $sequence) use ($sequenceName) {
539
+                            return $sequence->getName() !== $sequenceName;
540
+                        });
541
+                    }
542
+                } else if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
543
+                    $defaultName = $table->getName() . '_seq';
544
+                    $isUsingDefaultName = strtolower($defaultName) === $indexName;
545
+                }
546
+
547
+                if (!$isUsingDefaultName && \strlen($indexName) > 30) {
548
+                    throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
549
+                }
550
+                if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength > 23) {
551
+                    throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
552
+                }
553
+            }
554
+        }
555
+
556
+        foreach ($sequences as $sequence) {
557
+            if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
558
+                throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
559
+            }
560
+        }
561
+    }
562
+
563
+    private function ensureMigrationsAreLoaded() {
564
+        if (empty($this->migrations)) {
565
+            $this->migrations = $this->findMigrations();
566
+        }
567
+    }
568 568
 }
Please login to merge, or discard this patch.
Spacing   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -73,7 +73,7 @@  discard block
 block discarded – undo
73 73
 		}
74 74
 
75 75
 		if ($appName === 'core') {
76
-			$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
76
+			$this->migrationsPath = \OC::$SERVERROOT.'/core/Migrations';
77 77
 			$this->migrationsNamespace = 'OC\\Core\\Migrations';
78 78
 			$this->checkOracle = true;
79 79
 		} else {
@@ -83,10 +83,10 @@  discard block
 block discarded – undo
83 83
 			$appPath = $appLocator->getAppPath($appName);
84 84
 			$namespace = App::buildAppNamespace($appName);
85 85
 			$this->migrationsPath = "$appPath/lib/Migration";
86
-			$this->migrationsNamespace = $namespace . '\\Migration';
86
+			$this->migrationsNamespace = $namespace.'\\Migration';
87 87
 
88 88
 			$infoParser = new InfoParser();
89
-			$info = $infoParser->parse($appPath . '/appinfo/info.xml');
89
+			$info = $infoParser->parse($appPath.'/appinfo/info.xml');
90 90
 			if (!isset($info['dependencies']['database'])) {
91 91
 				$this->checkOracle = true;
92 92
 			} else {
@@ -220,7 +220,7 @@  discard block
 block discarded – undo
220 220
 			\RegexIterator::GET_MATCH);
221 221
 
222 222
 		$files = array_keys(iterator_to_array($iterator));
223
-		uasort($files, function ($a, $b) {
223
+		uasort($files, function($a, $b) {
224 224
 			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
225 225
 			preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
226 226
 			if (!empty($matchA) && !empty($matchB)) {
@@ -298,7 +298,7 @@  discard block
 block discarded – undo
298 298
 	 * @return string
299 299
 	 */
300 300
 	public function getMigrationsTableName() {
301
-		return $this->connection->getPrefix() . 'migrations';
301
+		return $this->connection->getPrefix().'migrations';
302 302
 	}
303 303
 
304 304
 	/**
@@ -326,7 +326,7 @@  discard block
 block discarded – undo
326 326
 	 * @return mixed|null|string
327 327
 	 */
328 328
 	public function getMigration($alias) {
329
-		switch($alias) {
329
+		switch ($alias) {
330 330
 			case 'current':
331 331
 				return $this->getCurrentVersion();
332 332
 			case 'next':
@@ -501,26 +501,26 @@  discard block
 block discarded – undo
501 501
 				$sourceTable = $sourceSchema->getTable($table->getName());
502 502
 			} catch (SchemaException $e) {
503 503
 				if (\strlen($table->getName()) - $prefixLength > 27) {
504
-					throw new \InvalidArgumentException('Table name "'  . $table->getName() . '" is too long.');
504
+					throw new \InvalidArgumentException('Table name "'.$table->getName().'" is too long.');
505 505
 				}
506 506
 				$sourceTable = null;
507 507
 			}
508 508
 
509 509
 			foreach ($table->getColumns() as $thing) {
510 510
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) && \strlen($thing->getName()) > 30) {
511
-					throw new \InvalidArgumentException('Column name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
511
+					throw new \InvalidArgumentException('Column name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
512 512
 				}
513 513
 			}
514 514
 
515 515
 			foreach ($table->getIndexes() as $thing) {
516 516
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
517
-					throw new \InvalidArgumentException('Index name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
517
+					throw new \InvalidArgumentException('Index name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
518 518
 				}
519 519
 			}
520 520
 
521 521
 			foreach ($table->getForeignKeys() as $thing) {
522 522
 				if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
523
-					throw new \InvalidArgumentException('Foreign key name "'  . $table->getName() . '"."' . $thing->getName() . '" is too long.');
523
+					throw new \InvalidArgumentException('Foreign key name "'.$table->getName().'"."'.$thing->getName().'" is too long.');
524 524
 				}
525 525
 			}
526 526
 
@@ -530,32 +530,32 @@  discard block
 block discarded – undo
530 530
 				$isUsingDefaultName = $indexName === 'primary';
531 531
 
532 532
 				if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
533
-					$defaultName = $table->getName() . '_pkey';
533
+					$defaultName = $table->getName().'_pkey';
534 534
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
535 535
 
536 536
 					if ($isUsingDefaultName) {
537
-						$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
537
+						$sequenceName = $table->getName().'_'.implode('_', $primaryKey->getColumns()).'_seq';
538 538
 						$sequences = array_filter($sequences, function(Sequence $sequence) use ($sequenceName) {
539 539
 							return $sequence->getName() !== $sequenceName;
540 540
 						});
541 541
 					}
542 542
 				} else if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
543
-					$defaultName = $table->getName() . '_seq';
543
+					$defaultName = $table->getName().'_seq';
544 544
 					$isUsingDefaultName = strtolower($defaultName) === $indexName;
545 545
 				}
546 546
 
547 547
 				if (!$isUsingDefaultName && \strlen($indexName) > 30) {
548
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
548
+					throw new \InvalidArgumentException('Primary index name  on "'.$table->getName().'" is too long.');
549 549
 				}
550 550
 				if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength > 23) {
551
-					throw new \InvalidArgumentException('Primary index name  on "'  . $table->getName() . '" is too long.');
551
+					throw new \InvalidArgumentException('Primary index name  on "'.$table->getName().'" is too long.');
552 552
 				}
553 553
 			}
554 554
 		}
555 555
 
556 556
 		foreach ($sequences as $sequence) {
557 557
 			if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
558
-				throw new \InvalidArgumentException('Sequence name "'  . $sequence->getName() . '" is too long.');
558
+				throw new \InvalidArgumentException('Sequence name "'.$sequence->getName().'" is too long.');
559 559
 			}
560 560
 		}
561 561
 	}
Please login to merge, or discard this patch.