2017-06-01 10:56:34 -04:00
< ? php
2025-06-30 09:04:05 -04:00
2017-06-01 10:56:34 -04:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2017 ownCloud GmbH
* SPDX - License - Identifier : AGPL - 3.0 - only
2017-06-01 10:56:34 -04:00
*/
namespace OC\DB ;
2018-07-19 04:28:52 -04:00
use Doctrine\DBAL\Schema\Index ;
2018-07-12 10:52:08 -04:00
use Doctrine\DBAL\Schema\Schema ;
2018-01-04 08:58:01 -05:00
use Doctrine\DBAL\Schema\SchemaException ;
2018-07-20 06:31:52 -04:00
use Doctrine\DBAL\Schema\Sequence ;
2018-08-06 12:25:09 -04:00
use Doctrine\DBAL\Schema\Table ;
2025-10-10 09:52:33 -04:00
use Doctrine\DBAL\Types\Type ;
2018-08-06 12:36:38 -04:00
use OC\App\InfoParser ;
2017-06-01 10:56:34 -04:00
use OC\Migration\SimpleOutput ;
2025-07-31 09:14:48 -04:00
use OCP\App\IAppManager ;
2017-06-02 08:30:02 -04:00
use OCP\AppFramework\App ;
2022-10-09 15:31:27 -04:00
use OCP\DB\ISchemaWrapper ;
2024-07-01 10:59:47 -04:00
use OCP\DB\Types ;
2025-03-19 16:03:11 -04:00
use OCP\IConfig ;
2024-07-01 10:59:47 -04:00
use OCP\IDBConnection ;
2017-06-02 07:54:09 -04:00
use OCP\Migration\IMigrationStep ;
2017-06-01 10:56:34 -04:00
use OCP\Migration\IOutput ;
2024-02-08 03:24:52 -05:00
use OCP\Server ;
2025-03-19 16:03:11 -04:00
use Psr\Container\NotFoundExceptionInterface ;
2022-03-17 12:26:27 -04:00
use Psr\Log\LoggerInterface ;
2017-06-01 10:56:34 -04:00
class MigrationService {
2022-06-21 10:03:45 -04:00
private bool $migrationTableCreated ;
private array $migrations ;
private string $migrationsPath ;
private string $migrationsNamespace ;
private IOutput $output ;
2024-02-08 03:24:52 -05:00
private LoggerInterface $logger ;
2022-06-21 10:03:45 -04:00
private Connection $connection ;
private string $appName ;
private bool $checkOracle ;
2017-06-01 10:56:34 -04:00
/**
* @ throws \Exception
*/
2025-07-31 09:14:48 -04:00
public function __construct (
string $appName ,
Connection $connection ,
? IOutput $output = null ,
? LoggerInterface $logger = null ,
) {
2017-06-01 10:56:34 -04:00
$this -> appName = $appName ;
2025-03-19 16:03:11 -04:00
$this -> checkOracle = false ;
2017-06-01 10:56:34 -04:00
$this -> connection = $connection ;
2024-02-08 03:24:52 -05:00
if ( $logger === null ) {
$this -> logger = Server :: get ( LoggerInterface :: class );
} else {
$this -> logger = $logger ;
}
2022-06-21 10:03:45 -04:00
if ( $output === null ) {
2024-02-08 03:24:52 -05:00
$this -> output = new SimpleOutput ( $this -> logger , $appName );
2022-06-21 10:03:45 -04:00
} else {
$this -> output = $output ;
2017-06-01 10:56:34 -04:00
}
if ( $appName === 'core' ) {
$this -> migrationsPath = \OC :: $SERVERROOT . '/core/Migrations' ;
2017-07-05 08:44:24 -04:00
$this -> migrationsNamespace = 'OC\\Core\\Migrations' ;
2018-08-06 12:36:38 -04:00
$this -> checkOracle = true ;
2017-06-01 10:56:34 -04:00
} else {
2025-07-31 09:14:48 -04:00
$appManager = Server :: get ( IAppManager :: class );
$appPath = $appManager -> getAppPath ( $appName );
2017-06-02 08:30:02 -04:00
$namespace = App :: buildAppNamespace ( $appName );
2017-06-02 08:22:04 -04:00
$this -> migrationsPath = " $appPath /lib/Migration " ;
$this -> migrationsNamespace = $namespace . '\\Migration' ;
2018-08-06 12:36:38 -04:00
$infoParser = new InfoParser ();
$info = $infoParser -> parse ( $appPath . '/appinfo/info.xml' );
if ( ! isset ( $info [ 'dependencies' ][ 'database' ])) {
$this -> checkOracle = true ;
} else {
$this -> checkOracle = false ;
foreach ( $info [ 'dependencies' ][ 'database' ] as $database ) {
if ( \is_string ( $database ) && $database === 'oci' ) {
$this -> checkOracle = true ;
2020-04-10 04:35:09 -04:00
} elseif ( \is_array ( $database ) && isset ( $database [ '@value' ]) && $database [ '@value' ] === 'oci' ) {
2018-08-06 12:36:38 -04:00
$this -> checkOracle = true ;
}
}
}
2017-06-01 10:56:34 -04:00
}
2022-06-21 10:03:45 -04:00
$this -> migrationTableCreated = false ;
2017-06-01 10:56:34 -04:00
}
/**
* Returns the name of the app for which this migration is executed
*/
2022-12-01 07:28:11 -05:00
public function getApp () : string {
2017-06-01 10:56:34 -04:00
return $this -> appName ;
}
/**
* @ codeCoverageIgnore - this will implicitly tested on installation
*/
2022-12-01 07:28:11 -05:00
private function createMigrationTable () : bool {
2017-06-01 10:56:34 -04:00
if ( $this -> migrationTableCreated ) {
return false ;
}
2025-03-19 16:03:11 -04:00
if ( $this -> connection -> tableExists ( 'migrations' ) && \OCP\Server :: get ( IConfig :: class ) -> getAppValue ( 'core' , 'vendor' , '' ) !== 'owncloud' ) {
2020-11-11 14:12:13 -05:00
$this -> migrationTableCreated = true ;
return false ;
}
2018-01-04 08:58:01 -05:00
$schema = new SchemaWrapper ( $this -> connection );
/**
* We drop the table when it has different columns or the definition does not
* match . E . g . ownCloud uses a length of 177 for app and 14 for version .
*/
try {
$table = $schema -> getTable ( 'migrations' );
$columns = $table -> getColumns ();
if ( count ( $columns ) === 2 ) {
try {
$column = $table -> getColumn ( 'app' );
$schemaMismatch = $column -> getLength () !== 255 ;
if ( ! $schemaMismatch ) {
$column = $table -> getColumn ( 'version' );
$schemaMismatch = $column -> getLength () !== 255 ;
}
} catch ( SchemaException $e ) {
// One of the columns is missing
$schemaMismatch = true ;
}
if ( ! $schemaMismatch ) {
// Table exists and schema matches: return back!
$this -> migrationTableCreated = true ;
return false ;
}
}
// Drop the table, when it didn't match our expectations.
2018-01-17 06:17:41 -05:00
$this -> connection -> dropTable ( 'migrations' );
2018-01-31 07:13:14 -05:00
// Recreate the schema after the table was dropped.
$schema = new SchemaWrapper ( $this -> connection );
2018-01-04 08:58:01 -05:00
} catch ( SchemaException $e ) {
// Table not found, no need to panic, we will create it.
2017-06-01 10:56:34 -04:00
}
2018-01-31 07:13:14 -05:00
$table = $schema -> createTable ( 'migrations' );
2020-06-30 16:12:06 -04:00
$table -> addColumn ( 'app' , Types :: STRING , [ 'length' => 255 ]);
$table -> addColumn ( 'version' , Types :: STRING , [ 'length' => 255 ]);
2018-01-31 07:13:14 -05:00
$table -> setPrimaryKey ([ 'app' , 'version' ]);
$this -> connection -> migrateToSchema ( $schema -> getWrappedSchema ());
2017-06-01 10:56:34 -04:00
$this -> migrationTableCreated = true ;
return true ;
}
/**
* Returns all versions which have already been applied
*
2024-08-27 06:33:20 -04:00
* @ return list < string >
2017-06-01 10:56:34 -04:00
* @ codeCoverageIgnore - no need to test this
*/
public function getMigratedVersions () {
$this -> createMigrationTable ();
$qb = $this -> connection -> getQueryBuilder ();
$qb -> select ( 'version' )
-> from ( 'migrations' )
-> where ( $qb -> expr () -> eq ( 'app' , $qb -> createNamedParameter ( $this -> getApp ())))
-> orderBy ( 'version' );
2022-12-01 07:28:11 -05:00
$result = $qb -> executeQuery ();
2017-06-01 10:56:34 -04:00
$rows = $result -> fetchAll ( \PDO :: FETCH_COLUMN );
$result -> closeCursor ();
2024-08-27 06:33:20 -04:00
usort ( $rows , $this -> sortMigrations ( ... ));
2017-06-01 10:56:34 -04:00
return $rows ;
}
/**
* Returns all versions which are available in the migration folder
2022-12-01 07:28:11 -05:00
* @ return list < string >
2017-06-01 10:56:34 -04:00
*/
2022-12-01 07:28:11 -05:00
public function getAvailableVersions () : array {
2017-06-01 10:56:34 -04:00
$this -> ensureMigrationsAreLoaded ();
2024-08-27 06:33:20 -04:00
$versions = array_map ( 'strval' , array_keys ( $this -> migrations ));
usort ( $versions , $this -> sortMigrations ( ... ));
return $versions ;
}
protected function sortMigrations ( string $a , string $b ) : int {
preg_match ( '/(\d+)Date(\d+)/' , basename ( $a ), $matchA );
preg_match ( '/(\d+)Date(\d+)/' , basename ( $b ), $matchB );
if ( ! empty ( $matchA ) && ! empty ( $matchB )) {
$versionA = ( int ) $matchA [ 1 ];
$versionB = ( int ) $matchB [ 1 ];
if ( $versionA !== $versionB ) {
return ( $versionA < $versionB ) ? - 1 : 1 ;
}
2025-04-02 08:29:08 -04:00
return strnatcmp ( $matchA [ 2 ], $matchB [ 2 ]);
2024-08-27 06:33:20 -04:00
}
2025-04-02 08:29:08 -04:00
return strnatcmp ( basename ( $a ), basename ( $b ));
2017-06-01 10:56:34 -04:00
}
2022-12-01 07:28:11 -05:00
/**
* @ return array < string , string >
*/
protected function findMigrations () : array {
2017-06-01 10:56:34 -04:00
$directory = realpath ( $this -> migrationsPath );
2018-01-12 09:45:51 -05:00
if ( $directory === false || ! file_exists ( $directory ) || ! is_dir ( $directory )) {
2017-07-06 03:58:39 -04:00
return [];
}
2017-06-01 10:56:34 -04:00
$iterator = new \RegexIterator (
new \RecursiveIteratorIterator (
new \RecursiveDirectoryIterator ( $directory , \FilesystemIterator :: SKIP_DOTS ),
\RecursiveIteratorIterator :: LEAVES_ONLY
),
'#^.+\\/Version[^\\/]{1,255}\\.php$#i' ,
\RegexIterator :: GET_MATCH );
$files = array_keys ( iterator_to_array ( $iterator ));
2024-08-27 06:33:20 -04:00
usort ( $files , $this -> sortMigrations ( ... ));
2017-06-01 10:56:34 -04:00
$migrations = [];
foreach ( $files as $file ) {
$className = basename ( $file , '.php' );
$version = ( string ) substr ( $className , 7 );
if ( $version === '0' ) {
throw new \InvalidArgumentException (
" Cannot load a migrations with the name ' $version ' because it is a reserved number "
);
}
$migrations [ $version ] = sprintf ( '%s\\%s' , $this -> migrationsNamespace , $className );
}
return $migrations ;
}
/**
* @ param string $to
2017-06-02 08:30:02 -04:00
* @ return string []
2017-06-01 10:56:34 -04:00
*/
private function getMigrationsToExecute ( $to ) {
$knownMigrations = $this -> getMigratedVersions ();
$availableMigrations = $this -> getAvailableVersions ();
$toBeExecuted = [];
foreach ( $availableMigrations as $v ) {
2025-04-02 08:29:08 -04:00
if ( $to !== 'latest' && ( $this -> sortMigrations ( $v , $to ) > 0 )) {
2017-06-01 10:56:34 -04:00
continue ;
}
if ( $this -> shallBeExecuted ( $v , $knownMigrations )) {
$toBeExecuted [] = $v ;
}
}
return $toBeExecuted ;
}
/**
2017-06-02 08:30:02 -04:00
* @ param string $m
2017-06-01 10:56:34 -04:00
* @ param string [] $knownMigrations
2017-06-02 08:30:02 -04:00
* @ return bool
2017-06-01 10:56:34 -04:00
*/
private function shallBeExecuted ( $m , $knownMigrations ) {
if ( in_array ( $m , $knownMigrations )) {
return false ;
}
return true ;
}
/**
* @ param string $version
*/
2025-03-19 16:03:11 -04:00
private function markAsExecuted ( $version ) : void {
2017-06-01 10:56:34 -04:00
$this -> connection -> insertIfNotExist ( '*PREFIX*migrations' , [
'app' => $this -> appName ,
'version' => $version
]);
}
/**
* Returns the name of the table which holds the already applied versions
*
* @ return string
*/
public function getMigrationsTableName () {
return $this -> connection -> getPrefix () . 'migrations' ;
}
/**
* Returns the namespace of the version classes
*
* @ return string
*/
public function getMigrationsNamespace () {
return $this -> migrationsNamespace ;
}
/**
* Returns the directory which holds the versions
*
* @ return string
*/
public function getMigrationsDirectory () {
return $this -> migrationsPath ;
}
/**
* Return the explicit version for the aliases ; current , next , prev , latest
*
* @ return mixed | null | string
*/
2022-12-01 07:28:11 -05:00
public function getMigration ( string $alias ) {
2017-06-01 10:56:34 -04:00
switch ( $alias ) {
case 'current' :
return $this -> getCurrentVersion ();
case 'next' :
return $this -> getRelativeVersion ( $this -> getCurrentVersion (), 1 );
case 'prev' :
return $this -> getRelativeVersion ( $this -> getCurrentVersion (), - 1 );
case 'latest' :
$this -> ensureMigrationsAreLoaded ();
2017-07-19 10:18:11 -04:00
$migrations = $this -> getAvailableVersions ();
return @ end ( $migrations );
2017-06-01 10:56:34 -04:00
}
return '0' ;
}
2022-12-01 07:28:11 -05:00
private function getRelativeVersion ( string $version , int $delta ) : ? string {
2017-06-01 10:56:34 -04:00
$this -> ensureMigrationsAreLoaded ();
$versions = $this -> getAvailableVersions ();
2022-12-01 07:28:11 -05:00
array_unshift ( $versions , '0' );
2025-03-19 16:03:11 -04:00
/** @var int|false $offset */
2017-06-02 08:30:02 -04:00
$offset = array_search ( $version , $versions , true );
2017-06-01 10:56:34 -04:00
if ( $offset === false || ! isset ( $versions [ $offset + $delta ])) {
// Unknown version or delta out of bounds.
return null ;
}
2022-12-01 07:28:11 -05:00
return ( string ) $versions [ $offset + $delta ];
2017-06-01 10:56:34 -04:00
}
2022-12-01 07:28:11 -05:00
private function getCurrentVersion () : string {
2017-06-01 10:56:34 -04:00
$m = $this -> getMigratedVersions ();
if ( count ( $m ) === 0 ) {
return '0' ;
}
2025-03-19 16:03:11 -04:00
return @ end ( $m );
2017-06-01 10:56:34 -04:00
}
/**
2017-06-02 08:30:02 -04:00
* @ throws \InvalidArgumentException
2017-06-01 10:56:34 -04:00
*/
2022-12-01 07:28:11 -05:00
private function getClass ( string $version ) : string {
2017-06-01 10:56:34 -04:00
$this -> ensureMigrationsAreLoaded ();
if ( isset ( $this -> migrations [ $version ])) {
return $this -> migrations [ $version ];
}
throw new \InvalidArgumentException ( " Version $version is unknown. " );
}
/**
* Allows to set an IOutput implementation which is used for logging progress and messages
*/
2022-12-01 07:28:11 -05:00
public function setOutput ( IOutput $output ) : void {
2017-06-01 10:56:34 -04:00
$this -> output = $output ;
}
/**
* Applies all not yet applied versions up to $to
2017-06-02 08:30:02 -04:00
* @ throws \InvalidArgumentException
2017-06-01 10:56:34 -04:00
*/
2022-12-01 07:28:11 -05:00
public function migrate ( string $to = 'latest' , bool $schemaOnly = false ) : void {
2020-11-11 14:12:13 -05:00
if ( $schemaOnly ) {
2023-10-31 07:06:09 -04:00
$this -> output -> debug ( 'Migrating schema only' );
2020-11-11 14:12:13 -05:00
$this -> migrateSchemaOnly ( $to );
return ;
}
2017-06-01 10:56:34 -04:00
// read known migrations
$toBeExecuted = $this -> getMigrationsToExecute ( $to );
foreach ( $toBeExecuted as $version ) {
2020-12-07 13:35:01 -05:00
try {
$this -> executeStep ( $version , $schemaOnly );
2022-04-07 05:45:54 -04:00
} catch ( \Exception $e ) {
2020-12-07 13:35:01 -05:00
// The exception itself does not contain the name of the migration,
// so we wrap it here, to make debugging easier.
2022-04-07 05:45:54 -04:00
throw new \Exception ( 'Database error when running migration ' . $version . ' for app ' . $this -> getApp () . PHP_EOL . $e -> getMessage (), 0 , $e );
2020-12-07 13:35:01 -05:00
}
2017-06-01 10:56:34 -04:00
}
}
2020-11-11 14:12:13 -05:00
/**
* Applies all not yet applied versions up to $to
* @ throws \InvalidArgumentException
*/
2022-12-01 07:28:11 -05:00
public function migrateSchemaOnly ( string $to = 'latest' ) : void {
2020-11-11 14:12:13 -05:00
// read known migrations
$toBeExecuted = $this -> getMigrationsToExecute ( $to );
if ( empty ( $toBeExecuted )) {
return ;
}
$toSchema = null ;
foreach ( $toBeExecuted as $version ) {
2023-10-31 07:06:09 -04:00
$this -> output -> debug ( '- Reading ' . $version );
2020-11-11 14:12:13 -05:00
$instance = $this -> createInstance ( $version );
2022-10-09 15:31:27 -04:00
$toSchema = $instance -> changeSchema ( $this -> output , function () use ( $toSchema ) : ISchemaWrapper {
2020-11-11 14:12:13 -05:00
return $toSchema ? : new SchemaWrapper ( $this -> connection );
}, [ 'tablePrefix' => $this -> connection -> getPrefix ()]) ? : $toSchema ;
}
if ( $toSchema instanceof SchemaWrapper ) {
2023-10-31 07:06:09 -04:00
$this -> output -> debug ( '- Checking target database schema' );
2020-11-11 14:12:13 -05:00
$targetSchema = $toSchema -> getWrappedSchema ();
2025-03-19 16:03:11 -04:00
$beforeSchema = $this -> connection -> createSchema ();
2024-02-08 03:24:52 -05:00
$this -> ensureUniqueNamesConstraints ( $targetSchema , true );
2025-03-19 16:03:11 -04:00
$this -> ensureNamingConstraints ( $beforeSchema , $targetSchema , \strlen ( $this -> connection -> getPrefix ()));
2020-11-11 14:12:13 -05:00
if ( $this -> checkOracle ) {
2025-03-19 16:03:11 -04:00
$this -> ensureOracleConstraints ( $beforeSchema , $targetSchema );
2020-11-11 14:12:13 -05:00
}
2023-10-31 07:06:09 -04:00
$this -> output -> debug ( '- Migrate database schema' );
2020-11-11 14:12:13 -05:00
$this -> connection -> migrateToSchema ( $targetSchema );
$toSchema -> performDropTableCalls ();
}
2021-03-04 02:49:42 -05:00
2023-10-31 07:06:09 -04:00
$this -> output -> debug ( '- Mark migrations as executed' );
2021-03-04 02:49:42 -05:00
foreach ( $toBeExecuted as $version ) {
$this -> markAsExecuted ( $version );
}
2020-11-11 14:12:13 -05:00
}
2018-04-12 11:47:40 -04:00
/**
* Get the human readable descriptions for the migration steps to run
*
* @ param string $to
* @ return string [] [ $name => $description ]
*/
public function describeMigrationStep ( $to = 'latest' ) {
$toBeExecuted = $this -> getMigrationsToExecute ( $to );
$description = [];
foreach ( $toBeExecuted as $version ) {
$migration = $this -> createInstance ( $version );
if ( $migration -> name ()) {
$description [ $migration -> name ()] = $migration -> description ();
}
}
return $description ;
}
2017-06-01 10:56:34 -04:00
/**
* @ param string $version
2018-04-12 11:47:40 -04:00
* @ return IMigrationStep
2017-06-02 08:30:02 -04:00
* @ throws \InvalidArgumentException
2017-06-01 10:56:34 -04:00
*/
2024-06-28 11:08:46 -04:00
public function createInstance ( $version ) {
2025-03-19 16:03:11 -04:00
/** @psalm-var class-string<IMigrationStep> $class */
2017-06-01 10:56:34 -04:00
$class = $this -> getClass ( $version );
try {
2023-06-06 05:09:24 -04:00
$s = \OCP\Server :: get ( $class );
2025-03-19 16:03:11 -04:00
} catch ( NotFoundExceptionInterface ) {
2017-06-01 10:56:34 -04:00
if ( class_exists ( $class )) {
$s = new $class ();
} else {
2017-06-02 08:30:02 -04:00
throw new \InvalidArgumentException ( " Migration step ' $class ' is unknown " );
2017-06-01 10:56:34 -04:00
}
}
2025-03-19 16:03:11 -04:00
if ( ! $s instanceof IMigrationStep ) {
throw new \InvalidArgumentException ( 'Not a valid migration' );
}
2017-06-01 10:56:34 -04:00
return $s ;
}
/**
* Executes one explicit version
*
* @ param string $version
2018-07-19 09:32:36 -04:00
* @ param bool $schemaOnly
2017-06-02 08:30:02 -04:00
* @ throws \InvalidArgumentException
2017-06-01 10:56:34 -04:00
*/
2025-03-19 16:03:11 -04:00
public function executeStep ( $version , $schemaOnly = false ) : void {
2017-06-01 10:56:34 -04:00
$instance = $this -> createInstance ( $version );
2017-06-02 07:54:09 -04:00
2018-07-19 09:32:36 -04:00
if ( ! $schemaOnly ) {
2022-10-09 15:31:27 -04:00
$instance -> preSchemaChange ( $this -> output , function () : ISchemaWrapper {
2018-07-19 09:32:36 -04:00
return new SchemaWrapper ( $this -> connection );
}, [ 'tablePrefix' => $this -> connection -> getPrefix ()]);
}
2017-06-02 07:54:09 -04:00
2022-10-09 15:31:27 -04:00
$toSchema = $instance -> changeSchema ( $this -> output , function () : ISchemaWrapper {
2017-06-07 09:15:53 -04:00
return new SchemaWrapper ( $this -> connection );
2017-06-02 07:54:09 -04:00
}, [ 'tablePrefix' => $this -> connection -> getPrefix ()]);
2017-06-07 09:15:53 -04:00
if ( $toSchema instanceof SchemaWrapper ) {
2018-07-12 10:52:08 -04:00
$targetSchema = $toSchema -> getWrappedSchema ();
2025-03-19 16:03:11 -04:00
$sourceSchema = $this -> connection -> createSchema ();
2024-02-08 03:24:52 -05:00
$this -> ensureUniqueNamesConstraints ( $targetSchema , $schemaOnly );
2025-03-19 16:03:11 -04:00
$this -> ensureNamingConstraints ( $sourceSchema , $targetSchema , \strlen ( $this -> connection -> getPrefix ()));
2018-08-06 12:36:38 -04:00
if ( $this -> checkOracle ) {
2025-03-19 16:03:11 -04:00
$this -> ensureOracleConstraints ( $sourceSchema , $targetSchema );
2018-08-06 12:36:38 -04:00
}
2018-07-12 10:52:08 -04:00
$this -> connection -> migrateToSchema ( $targetSchema );
2017-06-07 09:15:53 -04:00
$toSchema -> performDropTableCalls ();
2017-06-01 10:56:34 -04:00
}
2017-06-02 07:54:09 -04:00
2018-07-19 09:32:36 -04:00
if ( ! $schemaOnly ) {
2022-10-09 15:31:27 -04:00
$instance -> postSchemaChange ( $this -> output , function () : ISchemaWrapper {
2018-07-19 09:32:36 -04:00
return new SchemaWrapper ( $this -> connection );
}, [ 'tablePrefix' => $this -> connection -> getPrefix ()]);
}
2017-06-02 07:54:09 -04:00
2017-06-01 10:56:34 -04:00
$this -> markAsExecuted ( $version );
}
2020-11-11 08:33:47 -05:00
/**
2025-03-19 16:03:11 -04:00
* Enforces some naming conventions to make sure tables can be used on all supported database engines .
*
2020-11-11 08:33:47 -05:00
* Naming constraints :
2025-03-19 16:03:11 -04:00
* - Tables names must be 63 chars or shorter ( including its prefix ( default 'oc_' ))
* - Column names must be 63 chars or shorter
* - Index names must be 63 chars or shorter
* - Sequence names must be 63 chars or shorter
* - Primary key names must be set to 63 chars or shorter - or the table name must be <= 58 characters ( 63 - 5 for '_pKey' suffix ) including the table name prefix
*
* This is based on the identifier limits set by our supported database engines :
* - MySQL and MariaDB support 64 characters
* - Oracle supports 128 characters ( since 12 c )
* - PostgreSQL support 63
* - SQLite does not have any limits
*
* @ see https :// github . com / nextcloud / documentation / blob / master / developer_manual / basics / storage / database . rst
*
* @ throws \Doctrine\DBAL\Exception
*/
public function ensureNamingConstraints ( Schema $sourceSchema , Schema $targetSchema , int $prefixLength ) : void {
$MAX_NAME_LENGTH = 63 ;
$sequences = $targetSchema -> getSequences ();
foreach ( $targetSchema -> getTables () as $table ) {
try {
$sourceTable = $sourceSchema -> getTable ( $table -> getName ());
} catch ( SchemaException $e ) {
// we only validate new tables
if ( \strlen ( $table -> getName ()) + $prefixLength > $MAX_NAME_LENGTH ) {
throw new \InvalidArgumentException ( 'Table name "' . $table -> getName () . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH );
}
$sourceTable = null ;
}
foreach ( $table -> getColumns () as $thing ) {
// If the table doesn't exist OR if the column doesn't exist in the table
if (( ! $sourceTable instanceof Table || ! $sourceTable -> hasColumn ( $thing -> getName ()))
&& \strlen ( $thing -> getName ()) > $MAX_NAME_LENGTH
) {
throw new \InvalidArgumentException ( 'Column name "' . $table -> getName () . '"."' . $thing -> getName () . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH );
}
}
foreach ( $table -> getIndexes () as $thing ) {
if (( ! $sourceTable instanceof Table || ! $sourceTable -> hasIndex ( $thing -> getName ()))
&& \strlen ( $thing -> getName ()) > $MAX_NAME_LENGTH
) {
throw new \InvalidArgumentException ( 'Index name "' . $table -> getName () . '"."' . $thing -> getName () . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH );
}
}
foreach ( $table -> getForeignKeys () as $thing ) {
if (( ! $sourceTable instanceof Table || ! $sourceTable -> hasForeignKey ( $thing -> getName ()))
&& \strlen ( $thing -> getName ()) > $MAX_NAME_LENGTH
) {
throw new \InvalidArgumentException ( 'Foreign key name "' . $table -> getName () . '"."' . $thing -> getName () . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH );
}
}
$primaryKey = $table -> getPrimaryKey ();
// only check if there is a primary key
// and there was non in the old table or there was no old table
if ( $primaryKey !== null && ( $sourceTable === null || $sourceTable -> getPrimaryKey () === null )) {
$indexName = strtolower ( $primaryKey -> getName ());
$isUsingDefaultName = $indexName === 'primary' ;
// This is the default name when using postgres - we use this for length comparison
// as this is the longest default names for the DB engines provided by doctrine
$defaultName = strtolower ( $table -> getName () . '_pkey' );
if ( $this -> connection -> getDatabaseProvider () === IDBConnection :: PLATFORM_POSTGRES ) {
$isUsingDefaultName = $defaultName === $indexName ;
if ( $isUsingDefaultName ) {
$sequenceName = $table -> getName () . '_' . implode ( '_' , $primaryKey -> getColumns ()) . '_seq' ;
$sequences = array_filter ( $sequences , function ( Sequence $sequence ) use ( $sequenceName ) {
return $sequence -> getName () !== $sequenceName ;
});
}
} elseif ( $this -> connection -> getDatabaseProvider () === IDBConnection :: PLATFORM_ORACLE ) {
$isUsingDefaultName = strtolower ( $table -> getName () . '_seq' ) === $indexName ;
}
if ( ! $isUsingDefaultName && \strlen ( $indexName ) > $MAX_NAME_LENGTH ) {
throw new \InvalidArgumentException ( 'Primary index name on "' . $table -> getName () . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH );
}
if ( $isUsingDefaultName && \strlen ( $defaultName ) + $prefixLength > $MAX_NAME_LENGTH ) {
throw new \InvalidArgumentException ( 'Primary index name on "' . $table -> getName () . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH );
}
}
}
foreach ( $sequences as $sequence ) {
if ( ! $sourceSchema -> hasSequence ( $sequence -> getName ())
&& \strlen ( $sequence -> getName ()) > $MAX_NAME_LENGTH
) {
throw new \InvalidArgumentException ( 'Sequence name "' . $sequence -> getName () . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH );
}
}
}
/**
* Enforces some data conventions to make sure tables can be used on Oracle SQL .
2020-11-11 08:33:47 -05:00
*
* Data constraints :
2022-04-04 09:56:54 -04:00
* - Tables need a primary key ( Not specific to Oracle , but required for performant clustering support )
2020-11-11 08:33:47 -05:00
* - Columns with " NotNull " can not have empty string as default value
* - Columns with " NotNull " can not have number 0 as default value
2020-11-11 08:34:24 -05:00
* - Columns with type " bool " ( which is in fact integer of length 1 ) can not be " NotNull " as it can not store 0 / false
2022-04-04 09:56:54 -04:00
* - Columns with type " string " can not be longer than 4.000 characters , use " text " instead
*
* @ see https :// github . com / nextcloud / documentation / blob / master / developer_manual / basics / storage / database . rst
2020-11-11 08:33:47 -05:00
* @ throws \Doctrine\DBAL\Exception
*/
2025-03-19 16:03:11 -04:00
public function ensureOracleConstraints ( Schema $sourceSchema , Schema $targetSchema ) : void {
2018-08-06 12:25:09 -04:00
$sequences = $targetSchema -> getSequences ();
2018-07-20 06:31:52 -04:00
2018-08-06 12:25:09 -04:00
foreach ( $targetSchema -> getTables () as $table ) {
try {
$sourceTable = $sourceSchema -> getTable ( $table -> getName ());
} catch ( SchemaException $e ) {
$sourceTable = null ;
2018-07-12 10:52:08 -04:00
}
2025-03-19 16:03:11 -04:00
foreach ( $table -> getColumns () as $column ) {
2022-04-07 06:42:52 -04:00
// If the table doesn't exist OR if the column doesn't exist in the table
2025-03-19 16:03:11 -04:00
if ( ! $sourceTable instanceof Table || ! $sourceTable -> hasColumn ( $column -> getName ())) {
if ( $column -> getNotnull () && $column -> getDefault () === ''
&& $sourceTable instanceof Table && ! $sourceTable -> hasColumn ( $column -> getName ())) {
// null and empty string are the same on Oracle SQL
throw new \InvalidArgumentException ( 'Column "' . $table -> getName () . '"."' . $column -> getName () . '" is NotNull, but has empty string or null as default.' );
2022-04-07 05:18:14 -04:00
}
2022-04-07 06:42:52 -04:00
2025-03-19 16:03:11 -04:00
if ( $this -> connection -> getDatabaseProvider () === IDBConnection :: PLATFORM_ORACLE
&& $column -> getNotnull ()
&& Type :: lookupName ( $column -> getType ()) === Types :: BOOLEAN
) {
2025-09-17 08:45:48 -04:00
// Oracle doesn't support boolean column with non-null value
2025-03-19 16:03:11 -04:00
// to still allow lighter DB schemas on other providers we force it to not null
// see https://github.com/nextcloud/server/pull/55156
$column -> setNotnull ( false );
2022-04-07 05:18:14 -04:00
}
2020-11-11 08:34:24 -05:00
2022-04-07 05:18:14 -04:00
$sourceColumn = null ;
} else {
2025-03-19 16:03:11 -04:00
$sourceColumn = $sourceTable -> getColumn ( $column -> getName ());
2020-11-11 08:34:24 -05:00
}
2022-04-07 06:42:52 -04:00
2022-04-07 05:18:14 -04:00
// If the column was just created OR the length changed OR the type changed
// we will NOT detect invalid length if the column is not modified
2025-03-19 16:03:11 -04:00
if (( $sourceColumn === null || $sourceColumn -> getLength () !== $column -> getLength () || Type :: lookupName ( $sourceColumn -> getType ()) !== Types :: STRING )
&& $column -> getLength () > 4000 && Type :: lookupName ( $column -> getType ()) === Types :: STRING ) {
throw new \InvalidArgumentException ( 'Column "' . $table -> getName () . '"."' . $column -> getName () . '" is type String, but exceeding the 4.000 length limit.' );
2018-07-12 10:52:08 -04:00
}
}
2018-07-19 04:28:52 -04:00
$primaryKey = $table -> getPrimaryKey ();
2025-10-10 09:44:43 -04:00
if ( $primaryKey instanceof Index && ( ! $sourceTable instanceof Table || $sourceTable -> getPrimaryKey () === null )) {
2018-07-19 04:28:52 -04:00
$indexName = strtolower ( $primaryKey -> getName ());
$isUsingDefaultName = $indexName === 'primary' ;
2024-07-01 10:59:47 -04:00
if ( $this -> connection -> getDatabaseProvider () === IDBConnection :: PLATFORM_POSTGRES ) {
2018-07-27 08:31:19 -04:00
$defaultName = $table -> getName () . '_pkey' ;
2018-07-19 04:28:52 -04:00
$isUsingDefaultName = strtolower ( $defaultName ) === $indexName ;
2018-07-20 06:31:52 -04:00
if ( $isUsingDefaultName ) {
2018-07-27 08:31:19 -04:00
$sequenceName = $table -> getName () . '_' . implode ( '_' , $primaryKey -> getColumns ()) . '_seq' ;
2020-04-09 07:53:40 -04:00
$sequences = array_filter ( $sequences , function ( Sequence $sequence ) use ( $sequenceName ) {
2018-07-27 08:31:19 -04:00
return $sequence -> getName () !== $sequenceName ;
2018-07-20 06:31:52 -04:00
});
}
2024-07-01 10:59:47 -04:00
} elseif ( $this -> connection -> getDatabaseProvider () === IDBConnection :: PLATFORM_ORACLE ) {
2018-07-19 04:28:52 -04:00
$defaultName = $table -> getName () . '_seq' ;
$isUsingDefaultName = strtolower ( $defaultName ) === $indexName ;
}
2022-04-08 04:47:24 -04:00
} elseif ( ! $primaryKey instanceof Index && ! $sourceTable instanceof Table ) {
/** @var LoggerInterface $logger */
2025-03-19 16:03:11 -04:00
$logger = \OCP\Server :: get ( LoggerInterface :: class );
2022-04-08 04:47:24 -04:00
$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.' );
2022-03-16 10:17:28 -04:00
// throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
2018-07-12 10:52:08 -04:00
}
}
}
2023-07-20 08:27:26 -04:00
/**
2024-02-08 03:24:52 -05:00
* Ensure naming constraints
*
2023-07-20 08:27:26 -04:00
* Naming constraints :
* - Index , sequence and primary key names must be unique within a Postgres Schema
*
2024-02-08 03:24:52 -05:00
* Only on installation we want to break hard , so that all developers notice
* the bugs when installing the app on any database or CI , and can work on
* fixing their migrations before releasing a version incompatible with Postgres .
*
* In case of updates we might be running on production instances and the
* administrators being faced with the error would not know how to resolve it
* anyway . This can also happen with instances , that had the issue before the
* current update , so we don ' t want to make their life more complicated
* than needed .
*
2023-07-20 08:27:26 -04:00
* @ param Schema $targetSchema
2024-02-08 03:24:52 -05:00
* @ param bool $isInstalling
2023-07-20 08:27:26 -04:00
*/
2024-02-08 03:24:52 -05:00
public function ensureUniqueNamesConstraints ( Schema $targetSchema , bool $isInstalling ) : void {
2023-07-20 08:27:26 -04:00
$constraintNames = [];
$sequences = $targetSchema -> getSequences ();
foreach ( $targetSchema -> getTables () as $table ) {
foreach ( $table -> getIndexes () as $thing ) {
$indexName = strtolower ( $thing -> getName ());
if ( $indexName === 'primary' || $thing -> isPrimary ()) {
continue ;
}
if ( isset ( $constraintNames [ $thing -> getName ()])) {
2024-02-08 03:24:52 -05:00
if ( $isInstalling ) {
throw new \InvalidArgumentException ( 'Index name "' . $thing -> getName () . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
}
$this -> logErrorOrWarning ( 'Index name "' . $thing -> getName () . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
2023-07-20 08:27:26 -04:00
}
$constraintNames [ $thing -> getName ()] = $table -> getName ();
}
foreach ( $table -> getForeignKeys () as $thing ) {
if ( isset ( $constraintNames [ $thing -> getName ()])) {
2024-02-08 03:24:52 -05:00
if ( $isInstalling ) {
throw new \InvalidArgumentException ( 'Foreign key name "' . $thing -> getName () . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
}
$this -> logErrorOrWarning ( 'Foreign key name "' . $thing -> getName () . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
2023-07-20 08:27:26 -04:00
}
$constraintNames [ $thing -> getName ()] = $table -> getName ();
}
$primaryKey = $table -> getPrimaryKey ();
if ( $primaryKey instanceof Index ) {
$indexName = strtolower ( $primaryKey -> getName ());
if ( $indexName === 'primary' ) {
continue ;
}
if ( isset ( $constraintNames [ $indexName ])) {
2024-02-08 03:24:52 -05:00
if ( $isInstalling ) {
throw new \InvalidArgumentException ( 'Primary index name "' . $indexName . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
}
$this -> logErrorOrWarning ( 'Primary index name "' . $indexName . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
2023-07-20 08:27:26 -04:00
}
$constraintNames [ $indexName ] = $table -> getName ();
}
}
foreach ( $sequences as $sequence ) {
if ( isset ( $constraintNames [ $sequence -> getName ()])) {
2024-02-08 03:24:52 -05:00
if ( $isInstalling ) {
throw new \InvalidArgumentException ( 'Sequence name "' . $sequence -> getName () . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
}
$this -> logErrorOrWarning ( 'Sequence name "' . $sequence -> getName () . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
2023-07-20 08:27:26 -04:00
}
$constraintNames [ $sequence -> getName ()] = 'sequence' ;
}
}
2024-02-08 03:24:52 -05:00
protected function logErrorOrWarning ( string $log ) : void {
if ( $this -> output instanceof SimpleOutput ) {
$this -> output -> warning ( $log );
} else {
$this -> logger -> error ( $log );
}
}
2025-07-31 09:14:48 -04:00
private function ensureMigrationsAreLoaded () : void {
2017-06-01 10:56:34 -04:00
if ( empty ( $this -> migrations )) {
$this -> migrations = $this -> findMigrations ();
}
}
}