Passed
Push — dbal ( 1d4796...1c3c77 )
by Greg
07:35
created

DatabaseRepair::phase2()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
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\Schema\Index\IndexType;
23
use Doctrine\DBAL\Schema\Name;
24
use Doctrine\DBAL\Schema\Schema;
25
use Doctrine\DBAL\Schema\SchemaDiff;
26
use Exception;
27
use Fisharebest\Webtrees\DB;
28
use Fisharebest\Webtrees\DB\WebtreesSchema;
29
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...
30
use Symfony\Component\Console\Command\Command;
31
use Symfony\Component\Console\Input\InputInterface;
32
use Symfony\Component\Console\Output\OutputInterface;
33
use Symfony\Component\Console\Style\SymfonyStyle;
34
35
use function array_map;
36
use function implode;
37
use function str_contains;
38
39
class DatabaseRepair extends Command
40
{
41
    protected function configure(): void
42
    {
43
        $this
44
            ->setName(name: 'database-repair')
45
            ->setDescription(description: 'Repair the database schema');
46
    }
47
48
    protected function execute(InputInterface $input, OutputInterface $output): int
49
    {
50
        $io = new SymfonyStyle(input: $input, output: $output);
51
52
        if (Webtrees::SCHEMA_VERSION !== 45) {
53
            $io->error(message: 'This script only works with schema version 45');
54
55
            return Command::FAILURE;
56
        }
57
58
        $platform       = DB::getDBALConnection()->getDatabasePlatform();
59
        $schema_manager = DB::getDBALConnection()->createSchemaManager();
60
        $comparator     = $schema_manager->createComparator();
61
        $source         = $schema_manager->introspectSchema();
62
        $target         = WebtreesSchema::schema();
63
64
        // Do not automatically delete other tables. They may not belong to us.
65
        foreach ($source->getTables() as $table) {
66
            if (!$target->hasTable(name: $table->getObjectName()->toString())) {
67
                $source->dropTable(name: $table->getObjectName()->toString());
68
            }
69
        }
70
71
        // Remove foreign key constraints before, then add them afterwards after.
72
        // This prevents problems when the column types or collations change.
73
        $queries1 = $platform->getAlterSchemaSQL(diff: $comparator->compareSchemas(
74
            oldSchema: $source,
75
            newSchema: $this->schemaWithoutConstraints($source),
76
        ));
77
78
        $queries2 = $platform->getAlterSchemaSQL(diff: $comparator->compareSchemas(
79
            oldSchema: $this->schemaWithoutConstraints($source),
80
            newSchema: $this->schemaWithoutConstraints($target),
81
        ));
82
83
        // Delete any rows that would violate the new foreign keys.
84
        $queries3 = $this->deleteOrphans(schema: $target);
85
86
        $queries4 = $platform->getAlterSchemaSQL(diff: $comparator->compareSchemas(
87
            oldSchema: $this->schemaWithoutConstraints($target),
88
            newSchema: $target,
89
        ));
90
91
        $queries = [
92
            ...$queries1,
93
            ...$queries2,
94
            ...$queries3,
95
            ...$queries4,
96
        ];
97
98
        foreach ($queries as $query) {
99
            try {
100
                if (DB::exec(sql: $query) === false) {
101
                    $io->error(message: $query);
102
                } else {
103
                    $io->success(message: $query);
104
                }
105
            } catch (Exception) {
106
                $io->error(message: $query);
107
            }
108
        }
109
110
        return Command::SUCCESS;
111
    }
112
113
    /** @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...
114
    private function deleteOrphans(Schema $schema): array
115
    {
116
        $queries = [];
117
118
        foreach ($schema->getTables() as $table) {
119
            foreach ($table->getForeignKeys() as $foreign_key) {
120
                $local_table_name   = $table->getObjectName()->toString();
121
                $foreign_table_name = $foreign_key->getReferencedTableName()->toString();
122
123
                if ($local_table_name !== $foreign_table_name) {
124
                    $referencing_column_names = array_map(
125
                        static fn (Name $name): string => $name->toString(),
126
                        $foreign_key->getReferencingColumnNames(),
127
                    );
128
129
                    $referenced_column_names = array_map(
130
                        static fn (Name $name): string => $name->toString(),
131
                        $foreign_key->getReferencedColumnNames(),
132
                    );
133
134
                    $local_columns   = implode(separator: ',', array: $referencing_column_names);
135
                    $foreign_columns = implode(separator: ',', array: $referenced_column_names);
136
137
                    $query = DB::delete(table: $local_table_name)
138
                        ->where(
139
                            '(' . $local_columns . ') NOT IN (SELECT ' . $foreign_columns . ' FROM ' . $foreign_table_name . ')'
140
                        );
141
142
                    foreach ($foreign_key->getReferencingColumnNames() as $column) {
143
                        $query = $query->andWhere(DB::expression()->isNotNull(x: $column->toString()));
144
                    }
145
146
                    $queries[] = $query->getSQL();
147
                }
148
            }
149
        }
150
151
        return $queries;
152
    }
153
154
    private function schemaWithoutConstraints(Schema $schema): Schema {
155
        $schema_without_constraints = clone $schema;
156
157
        foreach ($schema_without_constraints->getTables() as $table) {
158
            foreach ($table->getForeignKeys() as $foreign_key) {
159
                $table->dropForeignKey($foreign_key->getObjectName()->toString());
160
            }
161
        }
162
163
        return $schema_without_constraints;
164
    }
165
}
166