2017-06-01 16:56:34 +02:00
< ? php
/**
2017-06-02 14:45:34 +02:00
* @ copyright Copyright ( c ) 2017 Joas Schilling < coding @ schilljs . com >
2017-06-01 16:56:34 +02:00
* @ copyright Copyright ( c ) 2017 , ownCloud GmbH
2017-06-02 14:45:34 +02:00
*
2020-04-29 11:57:22 +02:00
* @ author Christoph Wurst < christoph @ winzerhof - wurst . at >
2020-08-24 14:54:25 +02:00
* @ author Daniel Kesselberg < mail @ danielkesselberg . de >
2017-11-06 15:56:42 +01:00
* @ author Joas Schilling < coding @ schilljs . com >
2020-12-16 14:54:15 +01:00
* @ author Julius Härtl < jus @ bitgrid . net >
2019-12-03 19:57:53 +01:00
* @ author Morris Jobke < hey @ morrisjobke . de >
* @ author Robin Appelman < robin @ icewind . nl >
2017-11-06 15:56:42 +01:00
*
2017-06-01 16:56:34 +02:00
* @ license AGPL - 3.0
*
* This code is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License , version 3 ,
* as published by the Free Software Foundation .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License , version 3 ,
2019-12-03 19:57:53 +01:00
* along with this program . If not , see < http :// www . gnu . org / licenses />
2017-06-01 16:56:34 +02:00
*
*/
namespace OC\DB ;
2018-07-19 10:28:52 +02:00
use Doctrine\DBAL\Platforms\OraclePlatform ;
2021-01-03 15:28:31 +01:00
use Doctrine\DBAL\Platforms\PostgreSQL94Platform ;
2018-07-19 10:28:52 +02:00
use Doctrine\DBAL\Schema\Index ;
2018-07-12 16:52:08 +02:00
use Doctrine\DBAL\Schema\Schema ;
2018-01-04 14:58:01 +01:00
use Doctrine\DBAL\Schema\SchemaException ;
2018-07-20 12:31:52 +02:00
use Doctrine\DBAL\Schema\Sequence ;
2018-08-06 18:25:09 +02:00
use Doctrine\DBAL\Schema\Table ;
2020-06-30 22:12:06 +02:00
use Doctrine\DBAL\Types\Types ;
2018-08-06 18:36:38 +02:00
use OC\App\InfoParser ;
2017-06-01 16:56:34 +02:00
use OC\IntegrityCheck\Helpers\AppLocator ;
use OC\Migration\SimpleOutput ;
2017-06-02 14:30:02 +02:00
use OCP\AppFramework\App ;
2017-06-01 16:56:34 +02:00
use OCP\AppFramework\QueryException ;
2022-10-09 21:31:27 +02:00
use OCP\DB\ISchemaWrapper ;
2017-06-02 13:54:09 +02:00
use OCP\Migration\IMigrationStep ;
2017-06-01 16:56:34 +02:00
use OCP\Migration\IOutput ;
2022-03-17 17:26:27 +01:00
use Psr\Log\LoggerInterface ;
2017-06-01 16:56:34 +02:00
class MigrationService {
2022-06-21 16:03:45 +02:00
private bool $migrationTableCreated ;
private array $migrations ;
private string $migrationsPath ;
private string $migrationsNamespace ;
private IOutput $output ;
private Connection $connection ;
private string $appName ;
private bool $checkOracle ;
2017-06-01 16:56:34 +02:00
/**
* @ throws \Exception
*/
2022-12-01 13:28:11 +01:00
public function __construct ( string $appName , Connection $connection , ? IOutput $output = null , ? AppLocator $appLocator = null ) {
2017-06-01 16:56:34 +02:00
$this -> appName = $appName ;
$this -> connection = $connection ;
2022-06-21 16:03:45 +02:00
if ( $output === null ) {
2022-03-17 17:26:27 +01:00
$this -> output = new SimpleOutput ( \OC :: $server -> get ( LoggerInterface :: class ), $appName );
2022-06-21 16:03:45 +02:00
} else {
$this -> output = $output ;
2017-06-01 16:56:34 +02:00
}
if ( $appName === 'core' ) {
$this -> migrationsPath = \OC :: $SERVERROOT . '/core/Migrations' ;
2017-07-05 14:44:24 +02:00
$this -> migrationsNamespace = 'OC\\Core\\Migrations' ;
2018-08-06 18:36:38 +02:00
$this -> checkOracle = true ;
2017-06-01 16:56:34 +02:00
} else {
2017-06-02 14:30:02 +02:00
if ( null === $appLocator ) {
2017-06-01 16:56:34 +02:00
$appLocator = new AppLocator ();
}
$appPath = $appLocator -> getAppPath ( $appName );
2017-06-02 14:30:02 +02:00
$namespace = App :: buildAppNamespace ( $appName );
2017-06-02 14:22:04 +02:00
$this -> migrationsPath = " $appPath /lib/Migration " ;
$this -> migrationsNamespace = $namespace . '\\Migration' ;
2018-08-06 18:36:38 +02: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 10:35:09 +02:00
} elseif ( \is_array ( $database ) && isset ( $database [ '@value' ]) && $database [ '@value' ] === 'oci' ) {
2018-08-06 18:36:38 +02:00
$this -> checkOracle = true ;
}
}
}
2017-06-01 16:56:34 +02:00
}
2022-06-21 16:03:45 +02:00
$this -> migrationTableCreated = false ;
2017-06-01 16:56:34 +02:00
}
/**
* Returns the name of the app for which this migration is executed
*/
2022-12-01 13:28:11 +01:00
public function getApp () : string {
2017-06-01 16:56:34 +02:00
return $this -> appName ;
}
/**
* @ codeCoverageIgnore - this will implicitly tested on installation
*/
2022-12-01 13:28:11 +01:00
private function createMigrationTable () : bool {
2017-06-01 16:56:34 +02:00
if ( $this -> migrationTableCreated ) {
return false ;
}
2020-12-09 10:10:51 +01:00
if ( $this -> connection -> tableExists ( 'migrations' ) && \OC :: $server -> getConfig () -> getAppValue ( 'core' , 'vendor' , '' ) !== 'owncloud' ) {
2020-11-11 20:12:13 +01:00
$this -> migrationTableCreated = true ;
return false ;
}
2018-01-04 14:58:01 +01: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 12:17:41 +01:00
$this -> connection -> dropTable ( 'migrations' );
2018-01-31 13:13:14 +01:00
// Recreate the schema after the table was dropped.
$schema = new SchemaWrapper ( $this -> connection );
2018-01-04 14:58:01 +01:00
} catch ( SchemaException $e ) {
// Table not found, no need to panic, we will create it.
2017-06-01 16:56:34 +02:00
}
2018-01-31 13:13:14 +01:00
$table = $schema -> createTable ( 'migrations' );
2020-06-30 22:12:06 +02:00
$table -> addColumn ( 'app' , Types :: STRING , [ 'length' => 255 ]);
$table -> addColumn ( 'version' , Types :: STRING , [ 'length' => 255 ]);
2018-01-31 13:13:14 +01:00
$table -> setPrimaryKey ([ 'app' , 'version' ]);
$this -> connection -> migrateToSchema ( $schema -> getWrappedSchema ());
2017-06-01 16:56:34 +02:00
$this -> migrationTableCreated = true ;
return true ;
}
/**
* Returns all versions which have already been applied
*
* @ return string []
* @ 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 13:28:11 +01:00
$result = $qb -> executeQuery ();
2017-06-01 16:56:34 +02:00
$rows = $result -> fetchAll ( \PDO :: FETCH_COLUMN );
$result -> closeCursor ();
return $rows ;
}
/**
* Returns all versions which are available in the migration folder
2022-12-01 13:28:11 +01:00
* @ return list < string >
2017-06-01 16:56:34 +02:00
*/
2022-12-01 13:28:11 +01:00
public function getAvailableVersions () : array {
2017-06-01 16:56:34 +02:00
$this -> ensureMigrationsAreLoaded ();
2017-06-09 16:45:12 +02:00
return array_map ( 'strval' , array_keys ( $this -> migrations ));
2017-06-01 16:56:34 +02:00
}
2022-12-01 13:28:11 +01:00
/**
* @ return array < string , string >
*/
protected function findMigrations () : array {
2017-06-01 16:56:34 +02:00
$directory = realpath ( $this -> migrationsPath );
2018-01-12 15:45:51 +01:00
if ( $directory === false || ! file_exists ( $directory ) || ! is_dir ( $directory )) {
2017-07-06 09:58:39 +02:00
return [];
}
2017-06-01 16:56:34 +02: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 ));
uasort ( $files , function ( $a , $b ) {
2017-06-07 12:27:16 +02:00
preg_match ( '/^Version(\d+)Date(\d+)\\.php$/' , basename ( $a ), $matchA );
preg_match ( '/^Version(\d+)Date(\d+)\\.php$/' , basename ( $b ), $matchB );
if ( ! empty ( $matchA ) && ! empty ( $matchB )) {
if ( $matchA [ 1 ] !== $matchB [ 1 ]) {
return ( $matchA [ 1 ] < $matchB [ 1 ]) ? - 1 : 1 ;
}
return ( $matchA [ 2 ] < $matchB [ 2 ]) ? - 1 : 1 ;
}
2017-06-01 16:56:34 +02:00
return ( basename ( $a ) < basename ( $b )) ? - 1 : 1 ;
});
$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 14:30:02 +02:00
* @ return string []
2017-06-01 16:56:34 +02:00
*/
private function getMigrationsToExecute ( $to ) {
$knownMigrations = $this -> getMigratedVersions ();
$availableMigrations = $this -> getAvailableVersions ();
$toBeExecuted = [];
foreach ( $availableMigrations as $v ) {
if ( $to !== 'latest' && $v > $to ) {
continue ;
}
if ( $this -> shallBeExecuted ( $v , $knownMigrations )) {
$toBeExecuted [] = $v ;
}
}
return $toBeExecuted ;
}
/**
2017-06-02 14:30:02 +02:00
* @ param string $m
2017-06-01 16:56:34 +02:00
* @ param string [] $knownMigrations
2017-06-02 14:30:02 +02:00
* @ return bool
2017-06-01 16:56:34 +02: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 13:28:11 +01:00
public function getMigration ( string $alias ) {
2020-04-10 14:19:56 +02:00
switch ( $alias ) {
2017-06-01 16:56:34 +02:00
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 16:18:11 +02:00
$migrations = $this -> getAvailableVersions ();
return @ end ( $migrations );
2017-06-01 16:56:34 +02:00
}
return '0' ;
}
2022-12-01 13:28:11 +01:00
private function getRelativeVersion ( string $version , int $delta ) : ? string {
2017-06-01 16:56:34 +02:00
$this -> ensureMigrationsAreLoaded ();
$versions = $this -> getAvailableVersions ();
2022-12-01 13:28:11 +01:00
array_unshift ( $versions , '0' );
/** @var int $offset */
2017-06-02 14:30:02 +02:00
$offset = array_search ( $version , $versions , true );
2017-06-01 16:56:34 +02:00
if ( $offset === false || ! isset ( $versions [ $offset + $delta ])) {
// Unknown version or delta out of bounds.
return null ;
}
2022-12-01 13:28:11 +01:00
return ( string ) $versions [ $offset + $delta ];
2017-06-01 16:56:34 +02:00
}
2022-12-01 13:28:11 +01:00
private function getCurrentVersion () : string {
2017-06-01 16:56:34 +02:00
$m = $this -> getMigratedVersions ();
if ( count ( $m ) === 0 ) {
return '0' ;
}
2017-07-19 16:18:11 +02:00
$migrations = array_values ( $m );
return @ end ( $migrations );
2017-06-01 16:56:34 +02:00
}
/**
2017-06-02 14:30:02 +02:00
* @ throws \InvalidArgumentException
2017-06-01 16:56:34 +02:00
*/
2022-12-01 13:28:11 +01:00
private function getClass ( string $version ) : string {
2017-06-01 16:56:34 +02: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 13:28:11 +01:00
public function setOutput ( IOutput $output ) : void {
2017-06-01 16:56:34 +02:00
$this -> output = $output ;
}
/**
* Applies all not yet applied versions up to $to
2017-06-02 14:30:02 +02:00
* @ throws \InvalidArgumentException
2017-06-01 16:56:34 +02:00
*/
2022-12-01 13:28:11 +01:00
public function migrate ( string $to = 'latest' , bool $schemaOnly = false ) : void {
2020-11-11 20:12:13 +01:00
if ( $schemaOnly ) {
2023-10-31 12:06:09 +01:00
$this -> output -> debug ( 'Migrating schema only' );
2020-11-11 20:12:13 +01:00
$this -> migrateSchemaOnly ( $to );
return ;
}
2017-06-01 16:56:34 +02:00
// read known migrations
$toBeExecuted = $this -> getMigrationsToExecute ( $to );
foreach ( $toBeExecuted as $version ) {
2020-12-07 19:35:01 +01:00
try {
$this -> executeStep ( $version , $schemaOnly );
2022-04-07 11:45:54 +02:00
} catch ( \Exception $e ) {
2020-12-07 19:35:01 +01:00
// The exception itself does not contain the name of the migration,
// so we wrap it here, to make debugging easier.
2022-04-07 11:45:54 +02:00
throw new \Exception ( 'Database error when running migration ' . $version . ' for app ' . $this -> getApp () . PHP_EOL . $e -> getMessage (), 0 , $e );
2020-12-07 19:35:01 +01:00
}
2017-06-01 16:56:34 +02:00
}
}
2020-11-11 20:12:13 +01:00
/**
* Applies all not yet applied versions up to $to
* @ throws \InvalidArgumentException
*/
2022-12-01 13:28:11 +01:00
public function migrateSchemaOnly ( string $to = 'latest' ) : void {
2020-11-11 20:12:13 +01:00
// read known migrations
$toBeExecuted = $this -> getMigrationsToExecute ( $to );
if ( empty ( $toBeExecuted )) {
return ;
}
$toSchema = null ;
foreach ( $toBeExecuted as $version ) {
2023-10-31 12:06:09 +01:00
$this -> output -> debug ( '- Reading ' . $version );
2020-11-11 20:12:13 +01:00
$instance = $this -> createInstance ( $version );
2022-10-09 21:31:27 +02:00
$toSchema = $instance -> changeSchema ( $this -> output , function () use ( $toSchema ) : ISchemaWrapper {
2020-11-11 20:12:13 +01:00
return $toSchema ? : new SchemaWrapper ( $this -> connection );
}, [ 'tablePrefix' => $this -> connection -> getPrefix ()]) ? : $toSchema ;
}
if ( $toSchema instanceof SchemaWrapper ) {
2023-10-31 12:06:09 +01:00
$this -> output -> debug ( '- Checking target database schema' );
2020-11-11 20:12:13 +01:00
$targetSchema = $toSchema -> getWrappedSchema ();
2023-07-20 14:27:26 +02:00
$this -> ensureUniqueNamesConstraints ( $targetSchema );
2020-11-11 20:12:13 +01:00
if ( $this -> checkOracle ) {
$beforeSchema = $this -> connection -> createSchema ();
2020-11-11 14:40:26 +01:00
$this -> ensureOracleConstraints ( $beforeSchema , $targetSchema , strlen ( $this -> connection -> getPrefix ()));
2020-11-11 20:12:13 +01:00
}
2023-10-31 12:06:09 +01:00
$this -> output -> debug ( '- Migrate database schema' );
2020-11-11 20:12:13 +01:00
$this -> connection -> migrateToSchema ( $targetSchema );
$toSchema -> performDropTableCalls ();
}
2021-03-04 08:49:42 +01:00
2023-10-31 12:06:09 +01:00
$this -> output -> debug ( '- Mark migrations as executed' );
2021-03-04 08:49:42 +01:00
foreach ( $toBeExecuted as $version ) {
$this -> markAsExecuted ( $version );
}
2020-11-11 20:12:13 +01:00
}
2018-04-12 17:47:40 +02: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 16:56:34 +02:00
/**
* @ param string $version
2018-04-12 17:47:40 +02:00
* @ return IMigrationStep
2017-06-02 14:30:02 +02:00
* @ throws \InvalidArgumentException
2017-06-01 16:56:34 +02:00
*/
protected function createInstance ( $version ) {
$class = $this -> getClass ( $version );
try {
2023-06-06 11:09:24 +02:00
$s = \OCP\Server :: get ( $class );
2018-04-12 17:47:40 +02:00
if ( ! $s instanceof IMigrationStep ) {
throw new \InvalidArgumentException ( 'Not a valid migration' );
}
2017-06-01 16:56:34 +02:00
} catch ( QueryException $e ) {
if ( class_exists ( $class )) {
$s = new $class ();
} else {
2017-06-02 14:30:02 +02:00
throw new \InvalidArgumentException ( " Migration step ' $class ' is unknown " );
2017-06-01 16:56:34 +02:00
}
}
return $s ;
}
/**
* Executes one explicit version
*
* @ param string $version
2018-07-19 15:32:36 +02:00
* @ param bool $schemaOnly
2017-06-02 14:30:02 +02:00
* @ throws \InvalidArgumentException
2017-06-01 16:56:34 +02:00
*/
2018-07-19 15:32:36 +02:00
public function executeStep ( $version , $schemaOnly = false ) {
2017-06-01 16:56:34 +02:00
$instance = $this -> createInstance ( $version );
2017-06-02 13:54:09 +02:00
2018-07-19 15:32:36 +02:00
if ( ! $schemaOnly ) {
2022-10-09 21:31:27 +02:00
$instance -> preSchemaChange ( $this -> output , function () : ISchemaWrapper {
2018-07-19 15:32:36 +02:00
return new SchemaWrapper ( $this -> connection );
}, [ 'tablePrefix' => $this -> connection -> getPrefix ()]);
}
2017-06-02 13:54:09 +02:00
2022-10-09 21:31:27 +02:00
$toSchema = $instance -> changeSchema ( $this -> output , function () : ISchemaWrapper {
2017-06-07 15:15:53 +02:00
return new SchemaWrapper ( $this -> connection );
2017-06-02 13:54:09 +02:00
}, [ 'tablePrefix' => $this -> connection -> getPrefix ()]);
2017-06-07 15:15:53 +02:00
if ( $toSchema instanceof SchemaWrapper ) {
2018-07-12 16:52:08 +02:00
$targetSchema = $toSchema -> getWrappedSchema ();
2023-07-20 14:27:26 +02:00
$this -> ensureUniqueNamesConstraints ( $targetSchema );
2018-08-06 18:36:38 +02:00
if ( $this -> checkOracle ) {
$sourceSchema = $this -> connection -> createSchema ();
2020-11-11 14:40:26 +01:00
$this -> ensureOracleConstraints ( $sourceSchema , $targetSchema , strlen ( $this -> connection -> getPrefix ()));
2018-08-06 18:36:38 +02:00
}
2018-07-12 16:52:08 +02:00
$this -> connection -> migrateToSchema ( $targetSchema );
2017-06-07 15:15:53 +02:00
$toSchema -> performDropTableCalls ();
2017-06-01 16:56:34 +02:00
}
2017-06-02 13:54:09 +02:00
2018-07-19 15:32:36 +02:00
if ( ! $schemaOnly ) {
2022-10-09 21:31:27 +02:00
$instance -> postSchemaChange ( $this -> output , function () : ISchemaWrapper {
2018-07-19 15:32:36 +02:00
return new SchemaWrapper ( $this -> connection );
}, [ 'tablePrefix' => $this -> connection -> getPrefix ()]);
}
2017-06-02 13:54:09 +02:00
2017-06-01 16:56:34 +02:00
$this -> markAsExecuted ( $version );
}
2020-11-11 14:33:47 +01: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 15:56:54 +02:00
* - Tables need a primary key ( Not specific to Oracle , but required for performant clustering support )
2020-11-11 14:33:47 +01: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 14:34:24 +01: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 15:56:54 +02: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 14:33:47 +01:00
*
* @ param Schema $sourceSchema
* @ param Schema $targetSchema
* @ param int $prefixLength
* @ throws \Doctrine\DBAL\Exception
*/
2020-11-11 14:40:26 +01:00
public function ensureOracleConstraints ( Schema $sourceSchema , Schema $targetSchema , int $prefixLength ) {
2018-08-06 18:25:09 +02:00
$sequences = $targetSchema -> getSequences ();
2018-07-20 12:31:52 +02:00
2018-08-06 18:25:09 +02:00
foreach ( $targetSchema -> getTables () as $table ) {
try {
$sourceTable = $sourceSchema -> getTable ( $table -> getName ());
} catch ( SchemaException $e ) {
if ( \strlen ( $table -> getName ()) - $prefixLength > 27 ) {
2021-02-18 10:14:12 +01:00
throw new \InvalidArgumentException ( 'Table name "' . $table -> getName () . '" is too long.' );
2018-08-06 18:25:09 +02:00
}
$sourceTable = null ;
2018-07-12 16:52:08 +02:00
}
foreach ( $table -> getColumns () as $thing ) {
2022-04-07 12:42:52 +02:00
// If the table doesn't exist OR if the column doesn't exist in the table
2022-04-07 11:18:14 +02: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 12:42:52 +02:00
2022-04-07 11:18:14 +02: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 12:42:52 +02:00
2022-04-07 11:18:14 +02:00
if ( $thing -> getNotnull () && $thing -> getType () -> getName () === Types :: BOOLEAN ) {
throw new \InvalidArgumentException ( 'Column "' . $table -> getName () . '"."' . $thing -> getName () . '" is type Bool and also NotNull, so it can not store "false".' );
}
2020-11-11 14:34:24 +01:00
2022-04-07 11:18:14 +02:00
$sourceColumn = null ;
} else {
$sourceColumn = $sourceTable -> getColumn ( $thing -> getName ());
2020-11-11 14:34:24 +01:00
}
2022-04-07 12:42:52 +02:00
2022-04-07 11:18:14 +02: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 15:04:18 +01:00
throw new \InvalidArgumentException ( 'Column "' . $table -> getName () . '"."' . $thing -> getName () . '" is type String, but exceeding the 4.000 length limit.' );
}
2018-07-12 16:52:08 +02:00
}
foreach ( $table -> getIndexes () as $thing ) {
2019-03-28 14:51:11 +01:00
if (( ! $sourceTable instanceof Table || ! $sourceTable -> hasIndex ( $thing -> getName ())) && \strlen ( $thing -> getName ()) > 30 ) {
2021-02-18 10:14:12 +01:00
throw new \InvalidArgumentException ( 'Index name "' . $table -> getName () . '"."' . $thing -> getName () . '" is too long.' );
2018-07-12 16:52:08 +02:00
}
}
foreach ( $table -> getForeignKeys () as $thing ) {
2019-03-28 14:51:11 +01:00
if (( ! $sourceTable instanceof Table || ! $sourceTable -> hasForeignKey ( $thing -> getName ())) && \strlen ( $thing -> getName ()) > 30 ) {
2020-11-11 14:33:29 +01:00
throw new \InvalidArgumentException ( 'Foreign key name "' . $table -> getName () . '"."' . $thing -> getName () . '" is too long.' );
2018-07-12 16:52:08 +02:00
}
}
2018-07-19 10:28:52 +02:00
$primaryKey = $table -> getPrimaryKey ();
2018-08-06 18:25:09 +02:00
if ( $primaryKey instanceof Index && ( ! $sourceTable instanceof Table || ! $sourceTable -> hasPrimaryKey ())) {
2018-07-19 10:28:52 +02:00
$indexName = strtolower ( $primaryKey -> getName ());
$isUsingDefaultName = $indexName === 'primary' ;
2021-01-03 15:28:31 +01:00
if ( $this -> connection -> getDatabasePlatform () instanceof PostgreSQL94Platform ) {
2018-07-27 14:31:19 +02:00
$defaultName = $table -> getName () . '_pkey' ;
2018-07-19 10:28:52 +02:00
$isUsingDefaultName = strtolower ( $defaultName ) === $indexName ;
2018-07-20 12:31:52 +02:00
if ( $isUsingDefaultName ) {
2018-07-27 14:31:19 +02:00
$sequenceName = $table -> getName () . '_' . implode ( '_' , $primaryKey -> getColumns ()) . '_seq' ;
2020-04-09 13:53:40 +02:00
$sequences = array_filter ( $sequences , function ( Sequence $sequence ) use ( $sequenceName ) {
2018-07-27 14:31:19 +02:00
return $sequence -> getName () !== $sequenceName ;
2018-07-20 12:31:52 +02:00
});
}
2020-04-10 10:35:09 +02:00
} elseif ( $this -> connection -> getDatabasePlatform () instanceof OraclePlatform ) {
2018-07-19 10:28:52 +02:00
$defaultName = $table -> getName () . '_seq' ;
$isUsingDefaultName = strtolower ( $defaultName ) === $indexName ;
}
2019-03-28 14:51:11 +01:00
if ( ! $isUsingDefaultName && \strlen ( $indexName ) > 30 ) {
2021-02-18 10:14:12 +01:00
throw new \InvalidArgumentException ( 'Primary index name on "' . $table -> getName () . '" is too long.' );
2018-07-19 10:28:52 +02:00
}
2019-12-05 14:38:28 +01:00
if ( $isUsingDefaultName && \strlen ( $table -> getName ()) - $prefixLength >= 23 ) {
2021-02-18 10:14:12 +01:00
throw new \InvalidArgumentException ( 'Primary index name on "' . $table -> getName () . '" is too long.' );
2018-07-19 10:28:52 +02:00
}
2022-04-08 10:47:24 +02: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 15:17:28 +01:00
// throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
2018-07-12 16:52:08 +02:00
}
}
2018-07-20 12:31:52 +02:00
foreach ( $sequences as $sequence ) {
2019-03-28 14:51:11 +01:00
if ( ! $sourceSchema -> hasSequence ( $sequence -> getName ()) && \strlen ( $sequence -> getName ()) > 30 ) {
2021-02-18 10:14:12 +01:00
throw new \InvalidArgumentException ( 'Sequence name "' . $sequence -> getName () . '" is too long.' );
2018-07-12 16:52:08 +02:00
}
}
}
2023-07-20 14:27:26 +02:00
/**
* Naming constraints :
* - Index , sequence and primary key names must be unique within a Postgres Schema
*
* @ param Schema $targetSchema
*/
public function ensureUniqueNamesConstraints ( Schema $targetSchema ) : void {
$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 ()])) {
throw new \InvalidArgumentException ( 'Index name "' . $thing -> getName () . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
}
$constraintNames [ $thing -> getName ()] = $table -> getName ();
}
foreach ( $table -> getForeignKeys () as $thing ) {
if ( isset ( $constraintNames [ $thing -> getName ()])) {
throw new \InvalidArgumentException ( 'Foreign key name "' . $thing -> getName () . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
}
$constraintNames [ $thing -> getName ()] = $table -> getName ();
}
$primaryKey = $table -> getPrimaryKey ();
if ( $primaryKey instanceof Index ) {
$indexName = strtolower ( $primaryKey -> getName ());
if ( $indexName === 'primary' ) {
continue ;
}
if ( isset ( $constraintNames [ $indexName ])) {
throw new \InvalidArgumentException ( 'Primary index name "' . $indexName . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
}
$constraintNames [ $indexName ] = $table -> getName ();
}
}
foreach ( $sequences as $sequence ) {
if ( isset ( $constraintNames [ $sequence -> getName ()])) {
throw new \InvalidArgumentException ( 'Sequence name "' . $sequence -> getName () . '" for table "' . $table -> getName () . '" collides with the constraint on table "' . $constraintNames [ $thing -> getName ()] . '".' );
}
$constraintNames [ $sequence -> getName ()] = 'sequence' ;
}
}
2017-06-01 16:56:34 +02:00
private function ensureMigrationsAreLoaded () {
if ( empty ( $this -> migrations )) {
$this -> migrations = $this -> findMigrations ();
}
}
}