]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat: add commands for exporting current and expected database schema 46194/head
authorRobin Appelman <robin@icewind.nl>
Fri, 28 Jun 2024 15:08:46 +0000 (17:08 +0200)
committerRobin Appelman <robin@icewind.nl>
Tue, 2 Jul 2024 11:45:12 +0000 (13:45 +0200)
Signed-off-by: Robin Appelman <robin@icewind.nl>
core/Command/Db/ExpectedSchema.php [new file with mode: 0644]
core/Command/Db/ExportSchema.php [new file with mode: 0644]
core/Command/Db/SchemaEncoder.php [new file with mode: 0644]
core/register_command.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/DB/MigrationService.php
lib/private/DB/SchemaWrapper.php

diff --git a/core/Command/Db/ExpectedSchema.php b/core/Command/Db/ExpectedSchema.php
new file mode 100644 (file)
index 0000000..1f35dab
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Core\Command\Db;
+
+use Doctrine\DBAL\Schema\Schema;
+use OC\Core\Command\Base;
+use OC\DB\Connection;
+use OC\DB\MigrationService;
+use OC\DB\SchemaWrapper;
+use OC\Migration\NullOutput;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ExpectedSchema extends Base {
+       public function __construct(
+               protected Connection $connection,
+       ) {
+               parent::__construct();
+       }
+
+       protected function configure(): void {
+               $this
+                       ->setName('db:schema:expected')
+                       ->setDescription('Export the expected database schema for a fresh installation')
+                       ->setHelp("Note that the expected schema might not exactly match the exported live schema as the expected schema doesn't take into account any database wide settings or defaults.")
+                       ->addOption('sql', null, InputOption::VALUE_NONE, 'Dump the SQL statements for creating the expected schema');
+               parent::configure();
+       }
+
+       protected function execute(InputInterface $input, OutputInterface $output): int {
+               $schema = new Schema();
+
+               $this->applyMigrations('core', $schema);
+
+               $apps = \OC_App::getEnabledApps();
+               foreach ($apps as $app) {
+                       $this->applyMigrations($app, $schema);
+               }
+
+               $sql = $input->getOption('sql');
+               if ($sql) {
+                       $output->writeln($schema->toSql($this->connection->getDatabasePlatform()));
+               } else {
+                       $encoder = new SchemaEncoder();
+                       $this->writeArrayInOutputFormat($input, $output, $encoder->encodeSchema($schema, $this->connection->getDatabasePlatform()));
+               }
+
+               return 0;
+       }
+
+       private function applyMigrations(string $app, Schema $schema): void {
+               $output = new NullOutput();
+               $ms = new MigrationService($app, $this->connection, $output);
+               foreach ($ms->getAvailableVersions() as $version) {
+                       $migration = $ms->createInstance($version);
+                       $migration->changeSchema($output, function () use (&$schema) {
+                               return new SchemaWrapper($this->connection, $schema);
+                       }, []);
+               }
+       }
+}
diff --git a/core/Command/Db/ExportSchema.php b/core/Command/Db/ExportSchema.php
new file mode 100644 (file)
index 0000000..581824e
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Core\Command\Db;
+
+use OC\Core\Command\Base;
+use OCP\IDBConnection;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ExportSchema extends Base {
+       public function __construct(
+               protected IDBConnection $connection,
+       ) {
+               parent::__construct();
+       }
+
+       protected function configure(): void {
+               $this
+                       ->setName('db:schema:export')
+                       ->setDescription('Export the current database schema')
+                       ->addOption('sql', null, InputOption::VALUE_NONE, 'Dump the SQL statements for creating a copy of the schema');
+               parent::configure();
+       }
+
+       protected function execute(InputInterface $input, OutputInterface $output): int {
+               $schema = $this->connection->createSchema();
+               $sql = $input->getOption('sql');
+               if ($sql) {
+                       $output->writeln($schema->toSql($this->connection->getDatabasePlatform()));
+               } else {
+                       $encoder = new SchemaEncoder();
+                       $this->writeArrayInOutputFormat($input, $output, $encoder->encodeSchema($schema, $this->connection->getDatabasePlatform()));
+               }
+
+               return 0;
+       }
+}
diff --git a/core/Command/Db/SchemaEncoder.php b/core/Command/Db/SchemaEncoder.php
new file mode 100644 (file)
index 0000000..d36a34e
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Core\Command\Db;
+
+use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Schema\Table;
+use Doctrine\DBAL\Types\PhpIntegerMappingType;
+use Doctrine\DBAL\Types\Type;
+
+class SchemaEncoder {
+       /**
+        * Encode a DBAL schema to json, performing some normalization based on the database platform
+        *
+        * @param Schema $schema
+        * @param AbstractPlatform $platform
+        * @return array
+        */
+       public function encodeSchema(Schema $schema, AbstractPlatform $platform): array {
+               $encoded = ['tables' => [], 'sequences' => []];
+               foreach ($schema->getTables() as $table) {
+                       $encoded[$table->getName()] = $this->encodeTable($table, $platform);
+               }
+               ksort($encoded);
+               return $encoded;
+       }
+
+       /**
+        * @psalm-type ColumnArrayType =
+        */
+       private function encodeTable(Table $table, AbstractPlatform $platform): array {
+               $encoded = ['columns' => [], 'indexes' => []];
+               foreach ($table->getColumns() as $column) {
+                       /**
+                        * @var array{
+                        *     name: string,
+                        *     default: mixed,
+                        *     notnull: bool,
+                        *     length: ?int,
+                        *     precision: int,
+                        *     scale: int,
+                        *     unsigned: bool,
+                        *     fixed: bool,
+                        *     autoincrement: bool,
+                        *     comment: string,
+                        *     columnDefinition: ?string,
+                        *     collation?: string,
+                        *     charset?: string,
+                        *     jsonb?: bool,
+                        * } $data
+                        **/
+                       $data = $column->toArray();
+                       $data['type'] = Type::getTypeRegistry()->lookupName($column->getType());
+                       $data['default'] = $column->getType()->convertToPHPValue($column->getDefault(), $platform);
+                       if ($platform instanceof PostgreSQLPlatform) {
+                               $data['unsigned'] = false;
+                               if ($column->getType() instanceof PhpIntegerMappingType) {
+                                       $data['length'] = null;
+                               }
+                               unset($data['jsonb']);
+                       } elseif ($platform instanceof AbstractMySqlPlatform) {
+                               if ($column->getType() instanceof PhpIntegerMappingType) {
+                                       $data['length'] = null;
+                               } elseif (in_array($data['type'], ['text', 'blob', 'datetime', 'float', 'json'])) {
+                                       $data['length'] = 0;
+                               }
+                               unset($data['collation']);
+                               unset($data['charset']);
+                       }
+                       if ($data['type'] === 'string' && $data['length'] === null) {
+                               $data['length'] = 255;
+                       }
+                       $encoded['columns'][$column->getName()] = $data;
+               }
+               ksort($encoded['columns']);
+               foreach ($table->getIndexes() as $index) {
+                       $options = $index->getOptions();
+                       if (isset($options['lengths']) && count(array_filter($options['lengths'])) === 0) {
+                               unset($options['lengths']);
+                       }
+                       if ($index->isPrimary()) {
+                               if ($platform instanceof PostgreSqlPlatform) {
+                                       $name = $table->getName() . '_pkey';
+                               } elseif ($platform instanceof AbstractMySQLPlatform) {
+                                       $name = "PRIMARY";
+                               } else {
+                                       $name = $index->getName();
+                               }
+                       } else {
+                               $name = $index->getName();
+                       }
+                       if ($platform instanceof PostgreSqlPlatform) {
+                               $name = strtolower($name);
+                       }
+                       $encoded['indexes'][$name] = [
+                               'name' => $name,
+                               'columns' => $index->getColumns(),
+                               'unique' => $index->isUnique(),
+                               'primary' => $index->isPrimary(),
+                               'flags' => $index->getFlags(),
+                               'options' => $options,
+                       ];
+               }
+               ksort($encoded['indexes']);
+               return $encoded;
+       }
+}
index 5185da496b6e4b0191dfc2457fe3b578697ace31..fbf59ebfc065f1ee796385f81324ac7a179844ef 100644 (file)
@@ -66,6 +66,8 @@ if ($config->getSystemValueBool('installed', false)) {
        $application->add(Server::get(Command\Db\AddMissingColumns::class));
        $application->add(Server::get(Command\Db\AddMissingIndices::class));
        $application->add(Server::get(Command\Db\AddMissingPrimaryKeys::class));
+       $application->add(Server::get(Command\Db\ExpectedSchema::class));
+       $application->add(Server::get(Command\Db\ExportSchema::class));
 
        if ($config->getSystemValueBool('debug', false)) {
                $application->add(Server::get(Command\Db\Migrations\StatusCommand::class));
index ae747f8d24836f731b84afffca7e44babe8f1932..0f3ef42952ef915263ed5b22910324dfdc977c6e 100644 (file)
@@ -1107,10 +1107,13 @@ return array(
     'OC\\Core\\Command\\Db\\ConvertFilecacheBigInt' => $baseDir . '/core/Command/Db/ConvertFilecacheBigInt.php',
     'OC\\Core\\Command\\Db\\ConvertMysqlToMB4' => $baseDir . '/core/Command/Db/ConvertMysqlToMB4.php',
     'OC\\Core\\Command\\Db\\ConvertType' => $baseDir . '/core/Command/Db/ConvertType.php',
+    'OC\\Core\\Command\\Db\\ExpectedSchema' => $baseDir . '/core/Command/Db/ExpectedSchema.php',
+    'OC\\Core\\Command\\Db\\ExportSchema' => $baseDir . '/core/Command/Db/ExportSchema.php',
     'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => $baseDir . '/core/Command/Db/Migrations/ExecuteCommand.php',
     'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => $baseDir . '/core/Command/Db/Migrations/GenerateCommand.php',
     'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => $baseDir . '/core/Command/Db/Migrations/MigrateCommand.php',
     'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => $baseDir . '/core/Command/Db/Migrations/StatusCommand.php',
+    'OC\\Core\\Command\\Db\\SchemaEncoder' => $baseDir . '/core/Command/Db/SchemaEncoder.php',
     'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => $baseDir . '/core/Command/Encryption/ChangeKeyStorageRoot.php',
     'OC\\Core\\Command\\Encryption\\DecryptAll' => $baseDir . '/core/Command/Encryption/DecryptAll.php',
     'OC\\Core\\Command\\Encryption\\Disable' => $baseDir . '/core/Command/Encryption/Disable.php',
index 45393c5481eb461a1befcfc42cf75f92342c5bcc..4ba270d8644035ae63a9785728c1b51ccf65e967 100644 (file)
@@ -1140,10 +1140,13 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\Core\\Command\\Db\\ConvertFilecacheBigInt' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertFilecacheBigInt.php',
         'OC\\Core\\Command\\Db\\ConvertMysqlToMB4' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertMysqlToMB4.php',
         'OC\\Core\\Command\\Db\\ConvertType' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertType.php',
+        'OC\\Core\\Command\\Db\\ExpectedSchema' => __DIR__ . '/../../..' . '/core/Command/Db/ExpectedSchema.php',
+        'OC\\Core\\Command\\Db\\ExportSchema' => __DIR__ . '/../../..' . '/core/Command/Db/ExportSchema.php',
         'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/ExecuteCommand.php',
         'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/GenerateCommand.php',
         'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/MigrateCommand.php',
         'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/StatusCommand.php',
+        'OC\\Core\\Command\\Db\\SchemaEncoder' => __DIR__ . '/../../..' . '/core/Command/Db/SchemaEncoder.php',
         'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => __DIR__ . '/../../..' . '/core/Command/Encryption/ChangeKeyStorageRoot.php',
         'OC\\Core\\Command\\Encryption\\DecryptAll' => __DIR__ . '/../../..' . '/core/Command/Encryption/DecryptAll.php',
         'OC\\Core\\Command\\Encryption\\Disable' => __DIR__ . '/../../..' . '/core/Command/Encryption/Disable.php',
index 4b91b6f0996124edbadff3125376f33858fdd1f0..19d1b2407369b4b3b24626515129633824f4541b 100644 (file)
@@ -459,7 +459,7 @@ class MigrationService {
         * @return IMigrationStep
         * @throws \InvalidArgumentException
         */
-       protected function createInstance($version) {
+       public function createInstance($version) {
                $class = $this->getClass($version);
                try {
                        $s = \OCP\Server::get($class);
index 8ff952b8710e8d5175a606c74143482e060d3ccb..5720e10fbdbeb662630300c36d2c8a151dac63f0 100644 (file)
@@ -20,9 +20,13 @@ class SchemaWrapper implements ISchemaWrapper {
        /** @var array */
        protected $tablesToDelete = [];
 
-       public function __construct(Connection $connection) {
+       public function __construct(Connection $connection, ?Schema $schema = null) {
                $this->connection = $connection;
-               $this->schema = $this->connection->createSchema();
+               if ($schema) {
+                       $this->schema = $schema;
+               } else {
+                       $this->schema = $this->connection->createSchema();
+               }
        }
 
        public function getWrappedSchema() {