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 ;
2025-09-17 08:45:48 -04:00
use Doctrine\DBAL\Platforms\OraclePlatform ;
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 ;
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 ;
2017-06-01 10:56:34 -04:00
use OCP\AppFramework\QueryException ;
2022-10-09 15:31:27 -04:00
use OCP\DB\ISchemaWrapper ;
2024-07-01 10:59:47 -04:00
use OCP\DB\Types ;
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 ;
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 ;
$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 ;
}
2020-12-09 04:10:51 -05:00
if ( $this -> connection -> tableExists ( 'migrations' ) && \OC :: $server -> getConfig () -> 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
*/
private function markAsExecuted ( $version ) {
$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' );
/** @var int $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' ;
}
2017-07-19 10:18:11 -04:00
$migrations = array_values ( $m );
return @ end ( $migrations );
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 ();
2024-02-08 03:24:52 -05:00
$this -> ensureUniqueNamesConstraints ( $targetSchema , true );
2020-11-11 14:12:13 -05:00
if ( $this -> checkOracle ) {
$beforeSchema = $this -> connection -> createSchema ();
2020-11-11 08:40:26 -05:00
$this -> ensureOracleConstraints ( $beforeSchema , $targetSchema , strlen ( $this -> connection -> getPrefix ()));
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 ) {
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 );
2018-04-12 11:47:40 -04:00
if ( ! $s instanceof IMigrationStep ) {
throw new \InvalidArgumentException ( 'Not a valid migration' );
}
2017-06-01 10:56:34 -04:00
} catch ( QueryException $e ) {
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
}
}
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
*/
2018-07-19 09:32:36 -04:00
public function executeStep ( $version , $schemaOnly = false ) {
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 ();
2024-02-08 03:24:52 -05:00
$this -> ensureUniqueNamesConstraints ( $targetSchema , $schemaOnly );
2018-08-06 12:36:38 -04:00
if ( $this -> checkOracle ) {
$sourceSchema = $this -> connection -> createSchema ();
2020-11-11 08:40:26 -05:00
$this -> ensureOracleConstraints ( $sourceSchema , $targetSchema , strlen ( $this -> connection -> getPrefix ()));
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
/**
* Naming constraints :
* - Tables names must be 30 chars or shorter ( 27 + oc_ prefix )
* - Column names must be 30 chars or shorter
* - Index names must be 30 chars or shorter
* - Sequence names must be 30 chars or shorter
* - Primary key names must be set or the table name 23 chars or shorter
*
* 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
*
* @ param Schema $sourceSchema
* @ param Schema $targetSchema
* @ param int $prefixLength
* @ throws \Doctrine\DBAL\Exception
*/
2020-11-11 08:40:26 -05:00
public function ensureOracleConstraints ( Schema $sourceSchema , Schema $targetSchema , int $prefixLength ) {
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 ) {
if ( \strlen ( $table -> getName ()) - $prefixLength > 27 ) {
2021-02-18 04:14:12 -05:00
throw new \InvalidArgumentException ( 'Table name "' . $table -> getName () . '" is too long.' );
2018-08-06 12:25:09 -04:00
}
$sourceTable = null ;
2018-07-12 10:52:08 -04:00
}
foreach ( $table -> getColumns () as $thing ) {
2022-04-07 06:42:52 -04:00
// If the table doesn't exist OR if the column doesn't exist in the table
2022-04-07 05:18:14 -04:00
if ( ! $sourceTable instanceof Table || ! $sourceTable -> hasColumn ( $thing -> getName ())) {
if ( \strlen ( $thing -> getName ()) > 30 ) {
throw new \InvalidArgumentException ( 'Column name "' . $table -> getName () . '"."' . $thing -> getName () . '" is too long.' );
}
2022-04-07 06:42:52 -04:00
2022-04-07 05:18:14 -04:00
if ( $thing -> getNotnull () && $thing -> getDefault () === ''
&& $sourceTable instanceof Table && ! $sourceTable -> hasColumn ( $thing -> getName ())) {
throw new \InvalidArgumentException ( 'Column "' . $table -> getName () . '"."' . $thing -> getName () . '" is NotNull, but has empty string or null as default.' );
}
2022-04-07 06:42:52 -04:00
2025-09-17 08:45:48 -04:00
if ( $this -> connection -> getDatabasePlatform () instanceof OraclePlatform ) {
// Oracle doesn't support boolean column with non-null value
if ( $thing -> getNotnull () && $thing -> getType () -> getName () === Types :: BOOLEAN ) {
$thing -> 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 {
$sourceColumn = $sourceTable -> getColumn ( $thing -> 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
if (( $sourceColumn === null || $sourceColumn -> getLength () !== $thing -> getLength () || $sourceColumn -> getType () -> getName () !== Types :: STRING )
&& $thing -> getLength () > 4000 && $thing -> getType () -> getName () === Types :: STRING ) {
2022-03-23 10:04:18 -04:00
throw new \InvalidArgumentException ( 'Column "' . $table -> getName () . '"."' . $thing -> getName () . '" is type String, but exceeding the 4.000 length limit.' );
}
2018-07-12 10:52:08 -04:00
}
foreach ( $table -> getIndexes () as $thing ) {
2019-03-28 09:51:11 -04:00
if (( ! $sourceTable instanceof Table || ! $sourceTable -> hasIndex ( $thing -> getName ())) && \strlen ( $thing -> getName ()) > 30 ) {
2021-02-18 04:14:12 -05:00
throw new \InvalidArgumentException ( 'Index name "' . $table -> getName () . '"."' . $thing -> getName () . '" is too long.' );
2018-07-12 10:52:08 -04:00
}
}
foreach ( $table -> getForeignKeys () as $thing ) {
2019-03-28 09:51:11 -04:00
if (( ! $sourceTable instanceof Table || ! $sourceTable -> hasForeignKey ( $thing -> getName ())) && \strlen ( $thing -> getName ()) > 30 ) {
2020-11-11 08:33:29 -05:00
throw new \InvalidArgumentException ( 'Foreign key name "' . $table -> getName () . '"."' . $thing -> getName () . '" is too long.' );
2018-07-12 10:52:08 -04:00
}
}
2018-07-19 04:28:52 -04:00
$primaryKey = $table -> getPrimaryKey ();
2018-08-06 12:25:09 -04:00
if ( $primaryKey instanceof Index && ( ! $sourceTable instanceof Table || ! $sourceTable -> hasPrimaryKey ())) {
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 ;
}
2019-03-28 09:51:11 -04:00
if ( ! $isUsingDefaultName && \strlen ( $indexName ) > 30 ) {
2021-02-18 04:14:12 -05:00
throw new \InvalidArgumentException ( 'Primary index name on "' . $table -> getName () . '" is too long.' );
2018-07-19 04:28:52 -04:00
}
2019-12-05 08:38:28 -05:00
if ( $isUsingDefaultName && \strlen ( $table -> getName ()) - $prefixLength >= 23 ) {
2021-02-18 04:14:12 -05:00
throw new \InvalidArgumentException ( 'Primary index name on "' . $table -> getName () . '" is too long.' );
2018-07-19 04:28:52 -04:00
}
2022-04-08 04:47:24 -04:00
} elseif ( ! $primaryKey instanceof Index && ! $sourceTable instanceof Table ) {
/** @var LoggerInterface $logger */
$logger = \OC :: $server -> get ( LoggerInterface :: class );
$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
}
}
2018-07-20 06:31:52 -04:00
foreach ( $sequences as $sequence ) {
2019-03-28 09:51:11 -04:00
if ( ! $sourceSchema -> hasSequence ( $sequence -> getName ()) && \strlen ( $sequence -> getName ()) > 30 ) {
2021-02-18 04:14:12 -05:00
throw new \InvalidArgumentException ( 'Sequence name "' . $sequence -> getName () . '" is too long.' );
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 ();
}
}
}