Passed
Push — dbal ( 337422...2a3d8b )
by Greg
13:40
created

DatabaseRepair::execute()   B

Complexity

Conditions 7
Paths 19

Size

Total Lines 50
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 29
nc 19
nop 2
dl 0
loc 50
rs 8.5226
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Cli\Commands;
21
22
use Doctrine\DBAL\Platforms\AbstractPlatform;
23
use Doctrine\DBAL\Schema\Name;
24
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
0 ignored issues
show
Bug introduced by
The type Doctrine\DBAL\Schema\Name\OptionallyQualifiedName was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use Doctrine\DBAL\Schema\Schema;
26
use Fisharebest\Webtrees\DB;
27
use Fisharebest\Webtrees\DB\WebtreesSchema;
28
use Fisharebest\Webtrees\Webtrees;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Webtrees was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
29
use Symfony\Component\Console\Command\Command;
30
use Symfony\Component\Console\Input\InputInterface;
31
use Symfony\Component\Console\Output\OutputInterface;
32
use Symfony\Component\Console\Style\SymfonyStyle;
33
34
use function array_filter;
35
use function array_map;
36
use function implode;
37
use function str_contains;
38
use function var_dump;
39
40
class DatabaseRepair extends Command
41
{
42
    protected function configure(): void
43
    {
44
        $this
45
            ->setName(name: 'database-repair')
46
            ->setDescription(description: 'Repair the database schema');
47
    }
48
49
    protected function execute(InputInterface $input, OutputInterface $output): int
50
    {
51
        $io = new SymfonyStyle(input: $input, output: $output);
52
53
        if (Webtrees::SCHEMA_VERSION !== 45) {
54
            $io->error(message: 'This script only works with schema version 45');
55
56
            return Command::FAILURE;
57
        }
58
59
        $platform       = DB::getDBALConnection()->getDatabasePlatform();
60
        $schema_manager = DB::getDBALConnection()->createSchemaManager();
61
        $comparator     = $schema_manager->createComparator();
62
        $source         = $schema_manager->introspectSchema();
63
        $target         = WebtreesSchema::schema();
64
65
        // Do not delete any other tables.  They may have been created by modules.
66
        foreach ($source->getTables() as $table) {
67
            if (!$target->hasTable(name: $table->getObjectName()->toString())) {
68
                $source->dropTable(name: $table->getObjectName()->toString());
69
            }
70
        }
71
72
        $schema_diff = $comparator->compareSchemas(oldSchema: $source, newSchema: $target);
73
        $queries     = $platform->getAlterSchemaSQL(diff: $schema_diff);
74
75
        // Workaround for https://github.com/doctrine/dbal/issues/6092
76
        $phase1 = array_filter(array: $queries, callback: $this->phase1(...));
77
        $phase2 = array_filter(array: $queries, callback: $this->phase2(...));
78
        $phase3 = array_filter(array: $queries, callback: $this->phase3(...));
79
80
        if ($phase3 === []) {
81
            $phase3a = [];
82
        } else {
83
            // If we are creating foreign keys, delete any invalid references first.
84
            $phase3a = $this->deleteOrphans(target: $target, io: $io);
85
        }
86
87
        $queries = [...$phase1, ...$phase2, ...$phase3a, ...$phase3];
88
89
        foreach ($queries as $query) {
90
            $io->info(message: $query);
91
            if (DB::exec(sql: $query) === false) {
92
                $io->error(message: 'FAILED');
93
            } else {
94
                $io->success(message: 'OK');
95
            }
96
        }
97
98
        return Command::SUCCESS;
99
    }
100
101
    /** @return list<string> */
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Cli\Commands\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
102
    private function deleteOrphans(Schema $target, $io): array
103
    {
104
        $queries = [];
105
106
        foreach ($target->getTables() as $table) {
107
            foreach ($table->getForeignKeys() as $foreign_key) {
108
                $local_table_name   = $table->getObjectName()->toString();
109
                $foreign_table_name = $foreign_key->getReferencedTableName()->toString();
110
111
                if ($local_table_name !== $foreign_table_name) {
112
                    $io->info($table->getObjectName()->toString() . '!==' . $foreign_key->getReferencedTableName()->toString());
113
                    $referencing_column_names = array_map(
114
                        static fn (Name $name): string => $name->toString(),
115
                        $foreign_key->getReferencingColumnNames(),
116
                    );
117
118
                    $referenced_column_names = array_map(
119
                        static fn (Name $name): string => $name->toString(),
120
                        $foreign_key->getReferencedColumnNames(),
121
                    );
122
123
                    $local_columns = implode(separator: ',', array: $referencing_column_names);
124
                    $foreign_columns = implode(separator: ',', array: $referenced_column_names);
125
126
                    $query = DB::delete(table: $local_table_name)
127
                        ->where(
128
                            '(' . $local_columns . ') NOT IN (SELECT ' . $foreign_columns . ' FROM ' . $foreign_table_name . ')'
129
                        );
130
131
                    foreach ($foreign_key->getReferencingColumnNames() as $column) {
132
                        $query = $query->andWhere(DB::expression()->isNotNull(x: $column->toString()));
133
                    }
134
135
                    $queries[] = $query->getSQL();
136
                }
137
            }
138
        }
139
140
        return $queries;
141
    }
142
143
    private function phase1(string $query): bool
0 ignored issues
show
Unused Code introduced by
The method phase1() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
144
    {
145
        return str_contains($query, 'DROP FOREIGN KEY');
146
    }
147
148
    private function phase2(string $query): bool
0 ignored issues
show
Unused Code introduced by
The method phase2() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
149
    {
150
        return !str_contains($query, 'FOREIGN KEY');
151
    }
152
153
    private function phase3(string $query): bool
0 ignored issues
show
Unused Code introduced by
The method phase3() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
154
    {
155
        return str_contains($query, 'FOREIGN KEY') && !str_contains($query, 'DROP FOREIGN KEY');
156
    }
157
}
158