diff --git a/core/Command/Db/CheckSchema.php b/core/Command/Db/CheckSchema.php
index d8f95a5d00051..204bb4f3a7c6a 100644
--- a/core/Command/Db/CheckSchema.php
+++ b/core/Command/Db/CheckSchema.php
@@ -37,18 +37,45 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$onlyTable = $input->getArgument('table');
$findings = $this->schemaChecker->getFindings($onlyTable);
+ $blocking = array_values(array_filter($findings, static fn (array $finding): bool => $finding['enabled']));
+
if ($input->getOption('output') === self::OUTPUT_FORMAT_PLAIN) {
if ($findings === []) {
$output->writeln('The live database schema matches the expected schema.');
} else {
- foreach ($findings as $finding) {
+ foreach ($blocking as $finding) {
$output->writeln('' . $this->schemaChecker->formatFinding($finding) . '');
}
+ $this->printDisabledAppFindings($findings, $output);
}
} else {
$this->writeArrayInOutputFormat($input, $output, $findings);
}
- return $findings === [] ? 0 : 1;
+ return $blocking === [] ? 0 : 1;
+ }
+
+ /**
+ * @param list, app: ?string, enabled: bool}> $findings
+ */
+ private function printDisabledAppFindings(array $findings, OutputInterface $output): void {
+ $byApp = [];
+ foreach ($findings as $finding) {
+ if (!$finding['enabled']) {
+ $byApp[$finding['app']][] = $finding;
+ }
+ }
+
+ if ($byApp === []) {
+ return;
+ }
+
+ $output->writeln('Disabled apps (not affecting exit code):');
+ foreach ($byApp as $app => $appFindings) {
+ $output->writeln(" {$app}:");
+ foreach ($appFindings as $finding) {
+ $output->writeln(' - ' . $this->schemaChecker->formatFinding($finding) . '');
+ }
+ }
}
}
diff --git a/core/Command/Upgrade.php b/core/Command/Upgrade.php
index 9fd0f2f8de48a..e706b22a36a91 100644
--- a/core/Command/Upgrade.php
+++ b/core/Command/Upgrade.php
@@ -252,10 +252,26 @@ private function checkSchema(OutputInterface $output): void {
return;
}
- $output->writeln('The database schema does not match what is expected for the installed version:');
+ $byApp = [];
foreach ($findings as $finding) {
+ $byApp[$finding['enabled'] ? '' : $finding['app']][] = $finding;
+ }
+
+ $output->writeln('The database schema does not match what is expected for the installed version:');
+ foreach ($byApp[''] ?? [] as $finding) {
$output->writeln(' - ' . $this->schemaChecker->formatFinding($finding));
}
+ unset($byApp['']);
+
+ if ($byApp !== []) {
+ $output->writeln('Disabled apps:');
+ foreach ($byApp as $app => $appFindings) {
+ $output->writeln(" {$app}:");
+ foreach ($appFindings as $finding) {
+ $output->writeln(' - ' . $this->schemaChecker->formatFinding($finding));
+ }
+ }
+ }
$output->writeln('Run "occ db:schema:check" for details.');
}
}
diff --git a/lib/private/DB/SchemaChecker.php b/lib/private/DB/SchemaChecker.php
index 0c6b9d7422f53..7aa6689601cb0 100644
--- a/lib/private/DB/SchemaChecker.php
+++ b/lib/private/DB/SchemaChecker.php
@@ -13,8 +13,11 @@
use Doctrine\DBAL\Schema\SchemaDiff;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types\Types;
+use OC\App\AppManager;
use OC\Migration\NullOutput;
+use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager;
+use OCP\IAppConfig;
/**
* Compares the live database schema against the schema expected for the
@@ -24,18 +27,21 @@
class SchemaChecker {
public function __construct(
private readonly Connection $connection,
+ private readonly IAppConfig $appConfig,
private readonly IAppManager $appManager,
) {
}
/**
- * @return list}>
+ * @return list, app: ?string, enabled: bool}>
*/
public function getFindings(?string $onlyTable = null): array {
$expectedSchema = new Schema();
- $this->applyMigrations('core', $expectedSchema);
- foreach ($this->appManager->getEnabledApps() as $app) {
- $this->applyMigrations($app, $expectedSchema);
+ $tableOwners = [];
+ $this->applyMigrations('core', $expectedSchema, $tableOwners);
+ // Disabled apps keep their tables, so replay their migrations too.
+ foreach (array_keys($this->appConfig->getAppInstalledVersions()) as $app) {
+ $this->applyMigrations($app, $expectedSchema, $tableOwners);
}
$this->addMigrationsTable($expectedSchema);
$this->materializeUniqueConstraints($expectedSchema);
@@ -50,11 +56,19 @@ public function getFindings(?string $onlyTable = null): array {
$comparator = $this->connection->createSchemaManager()->createComparator();
$diff = $comparator->compareSchemas($liveSchema, $expectedSchema);
- return $this->buildFindings($diff);
+ $enabledApps = array_flip($this->appManager->getEnabledApps());
+
+ return array_map(function (array $finding) use ($tableOwners, $enabledApps): array {
+ $app = $tableOwners[$finding['table']] ?? null;
+ $finding['app'] = $app;
+ // Only tables owned by a disabled app are non-blocking.
+ $finding['enabled'] = $app === null || $app === 'core' || isset($enabledApps[$app]);
+ return $finding;
+ }, $this->buildFindings($diff));
}
/**
- * @param array{table: string, type: string, name?: string, changes?: list} $finding
+ * @param array{table: string, type: string, name?: string, changes?: list, app?: ?string, enabled?: bool} $finding
*/
public function formatFinding(array $finding): string {
return match ($finding['type']) {
@@ -69,7 +83,28 @@ public function formatFinding(array $finding): string {
};
}
- private function applyMigrations(string $app, Schema $schema): void {
+ /**
+ * @param array $tableOwners table name => owning app id, updated in place
+ */
+ private function applyMigrations(string $app, Schema $schema, array &$tableOwners): void {
+ if ($app !== 'core') {
+ try {
+ $appPath = $this->appManager->getAppPath($app);
+ } catch (AppPathNotFoundException) {
+ // Installed, but code is gone: no migrations to replay.
+ return;
+ }
+ // Disabled apps are not autoloaded on boot.
+ /** @var AppManager $appManager */
+ $appManager = $this->appManager;
+ $appManager->registerAutoloading($app, $appPath);
+ }
+
+ $existingTables = [];
+ foreach ($schema->getTables() as $table) {
+ $existingTables[$table->getName()] = true;
+ }
+
$output = new NullOutput();
$ms = new MigrationService($app, $this->connection, $output);
foreach ($ms->getAvailableVersions() as $version) {
@@ -78,6 +113,12 @@ private function applyMigrations(string $app, Schema $schema): void {
return new SchemaWrapper($this->connection, $schema);
}, []);
}
+
+ foreach ($schema->getTables() as $table) {
+ if (!isset($existingTables[$table->getName()])) {
+ $tableOwners[$table->getName()] = $app;
+ }
+ }
}
/**