-
Notifications
You must be signed in to change notification settings - Fork 305
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
<?php | ||
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.3)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.3)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.3)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.3)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.3)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.3)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.3)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.4)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.4)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.4)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.4)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.4)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.4)
Check failure on line 1 in app/Arr.php GitHub Actions / phpstan (ubuntu-latest, 8.4)
|
||
|
||
/** | ||
* webtrees: online genealogy | ||
* Copyright (C) 2023 webtrees development team | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* 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 General Public License for more details. | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Fisharebest\Webtrees; | ||
|
||
use ArrayObject; | ||
use Closure; | ||
|
||
use function array_filter; | ||
use function array_map; | ||
use function array_merge; | ||
use function array_unique; | ||
use function array_values; | ||
use function uasort; | ||
|
||
/** | ||
* Arrays | ||
* | ||
* @template TKey of array-key | ||
* @template TValue | ||
* @extends ArrayObject<TKey,TValue> | ||
*/ | ||
class Arr extends ArrayObject | ||
{ | ||
/** | ||
* @param Arr<TKey,TValue> $arr2 | ||
* | ||
* @return self<TKey,TValue> | ||
*/ | ||
public function concat(Arr $arr2): self | ||
{ | ||
$arr1 = array_values(array: $this->getArrayCopy()); | ||
$arr2 = array_values(array: $arr2->getArrayCopy()); | ||
|
||
return new self(array: $arr1 + $arr2); | ||
} | ||
|
||
/** | ||
* @param Closure(TValue):bool $closure | ||
* | ||
* @return self<TKey,TValue> | ||
*/ | ||
public function filter(Closure $closure): self | ||
{ | ||
return new self(array: array_filter(array: $this->getArrayCopy(), callback: $closure)); | ||
} | ||
|
||
/** | ||
* @return self<int,TValue> | ||
*/ | ||
public function flatten(): self | ||
{ | ||
return new self(array: array_merge(...$this->getArrayCopy())); | ||
} | ||
|
||
/** | ||
* @param null|Closure(TValue):bool $closure | ||
* | ||
* @return TValue|null | ||
*/ | ||
public function first(Closure|null $closure = null): mixed | ||
{ | ||
foreach ($this->getArrayCopy() as $value) { | ||
if ($closure === null || $closure($value)) { | ||
return $value; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* @param null|Closure(TValue):bool $closure | ||
* | ||
* @return TValue|null | ||
*/ | ||
public function last(Closure|null $closure = null): mixed | ||
{ | ||
return $this->reverse()->first(closure: $closure); | ||
} | ||
|
||
/** | ||
* @template T | ||
* | ||
* @param Closure(TValue):T $closure | ||
* | ||
* @return self<TKey,T> | ||
*/ | ||
public function map(Closure $closure): self | ||
{ | ||
return new self(array: array_map(callback: $closure, array: $this->getArrayCopy())); | ||
} | ||
|
||
/** | ||
* @return self<TKey,TValue> | ||
*/ | ||
public function reverse(): self | ||
{ | ||
return new self(array: array_reverse(array: $this->getArrayCopy())); | ||
} | ||
|
||
/** | ||
* @param Closure(TValue,TValue):int $closure | ||
* | ||
* @return self<TKey,TValue> | ||
*/ | ||
public function sort(Closure $closure): self | ||
{ | ||
$arr = $this->getArrayCopy(); | ||
uasort(array: $arr, callback: $closure); | ||
|
||
return new self(array: $arr); | ||
} | ||
|
||
/** | ||
* @param Closure(TKey,TKey):int $closure | ||
* | ||
* @return self<TKey,TValue> | ||
*/ | ||
public function sortKeys(Closure $closure): self | ||
{ | ||
$arr = $this->getArrayCopy(); | ||
uksort(array: $arr, callback: $closure); | ||
|
||
return new self(array: $arr); | ||
} | ||
|
||
/** | ||
* @return self<TKey,TValue> | ||
*/ | ||
public function unique(): self | ||
{ | ||
return new self(array: array_unique(array: $this->getArrayCopy())); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
<?php | ||
|
||
/** | ||
* webtrees: online genealogy | ||
* Copyright (C) 2023 webtrees development team | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* 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 General Public License for more details. | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Fisharebest\Webtrees\Cli\Commands; | ||
|
||
use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
use Fisharebest\Webtrees\DB; | ||
use Fisharebest\Webtrees\DB\Schema; | ||
use Fisharebest\Webtrees\DB\WebtreesSchema; | ||
use Fisharebest\Webtrees\Webtrees; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
use Symfony\Component\Console\Style\SymfonyStyle; | ||
|
||
use function array_filter; | ||
use function implode; | ||
use function str_contains; | ||
|
||
class DatabaseRepair extends Command | ||
{ | ||
protected function configure(): void | ||
{ | ||
$this | ||
->setName(name: 'database-repair') | ||
->setDescription(description: 'Repair the database schema'); | ||
} | ||
|
||
protected function execute(InputInterface $input, OutputInterface $output): int | ||
{ | ||
$io = new SymfonyStyle(input: $input, output: $output); | ||
|
||
if (Webtrees::SCHEMA_VERSION !== 45) { | ||
Check failure on line 49 in app/Cli/Commands/DatabaseRepair.php GitHub Actions / phpstan (ubuntu-latest, 8.3)
|
||
$io->error(message: 'This script only works with schema version 45'); | ||
|
||
return Command::FAILURE; | ||
} | ||
|
||
$platform = DB::getDBALConnection()->getDatabasePlatform(); | ||
$schema_manager = DB::getDBALConnection()->createSchemaManager(); | ||
$comparator = $schema_manager->createComparator(); | ||
$source = $schema_manager->introspectSchema(); | ||
$target = WebtreesSchema::schema(); | ||
|
||
// doctrine/dbal 4.x does not have the concept of "saveSQL" | ||
foreach ($source->getTables() as $table) { | ||
if (!$target->hasTable(name: $table->getName())) { | ||
$source->dropTable(name: $table->getName()); | ||
} | ||
} | ||
|
||
// Workaround for https://github.com/doctrine/dbal/issues/4541 | ||
foreach ($target->getTables() as $table) { | ||
foreach ($table->getIndexes() as $index) { | ||
if (preg_match('/^IDX_[0-9A-F]+$/', $index->getName())) { | ||
if ($table->getPrimaryKey()->spansColumns($index->getColumns())) { | ||
Check failure on line 72 in app/Cli/Commands/DatabaseRepair.php GitHub Actions / phpstan (ubuntu-latest, 8.3)
|
||
$io->info('Dropping unnecessary index created by DBAL: ' . $table->getName() . '.' . $index->getName()); | ||
$table->dropIndex(name: $index->getName()); | ||
} | ||
} | ||
} | ||
} | ||
|
||
$schema_diff = $comparator->compareSchemas(oldSchema: $source, newSchema: $target); | ||
$queries = $platform->getAlterSchemaSQL(diff: $schema_diff); | ||
|
||
// Workaround for https://github.com/doctrine/dbal/issues/6092 | ||
$phase1 = array_filter(array: $queries, callback: $this->phase1(...)); | ||
$phase2 = array_filter(array: $queries, callback: $this->phase2(...)); | ||
$phase3 = array_filter(array: $queries, callback: $this->phase3(...)); | ||
|
||
if ($phase3 === []) { | ||
$phase3a = []; | ||
} else { | ||
// If we are creating foreign keys, delete any invalid references first. | ||
$phase3a = $this->deleteOrphans(target: $target, platform: $platform); | ||
} | ||
|
||
foreach ([...$phase1, ...$phase2, ...$phase3a, ...$phase3] as $query) { | ||
$io->info(message: $query); | ||
DB::exec(sql: $query); | ||
} | ||
|
||
return Command::SUCCESS; | ||
} | ||
|
||
private function phase1(string $query): bool | ||
{ | ||
return str_contains($query, 'DROP FOREIGN KEY'); | ||
} | ||
|
||
private function phase2(string $query): bool | ||
{ | ||
return !str_contains($query, 'FOREIGN KEY'); | ||
} | ||
|
||
/** @return list<string> */ | ||
private function deleteOrphans(Schema $target, AbstractPlatform $platform): array | ||
{ | ||
$queries = []; | ||
|
||
foreach ($target->getTables() as $table) { | ||
foreach ($table->getForeignKeys() as $foreign_key) { | ||
$foreign_table = $foreign_key->getQuotedForeignTableName(platform: $platform); | ||
|
||
if ($table->getName() !== $foreign_key->getForeignTableName()) { | ||
$local_columns = implode(separator: ',', array: $foreign_key->getQuotedLocalColumns(platform: $platform)); | ||
$foreign_columns = implode(separator: ',', array: $foreign_key->getQuotedForeignColumns(platform: $platform)); | ||
|
||
$query = DB::delete(table: $table->getName()) | ||
->where( | ||
'(' . $local_columns . ') NOT IN (SELECT ' . $foreign_columns . ' FROM ' . $foreign_table . ')' | ||
); | ||
|
||
foreach ($foreign_key->getLocalColumns() as $column) { | ||
$query = $query->andWhere(DB::expression()->isNotNull(x: $column)); | ||
} | ||
|
||
$queries[] = $query->getSQL(); | ||
} | ||
} | ||
} | ||
|
||
return $queries; | ||
} | ||
|
||
private function phase3(string $query): bool | ||
{ | ||
return str_contains($query, 'FOREIGN KEY') && !str_contains($query, 'DROP FOREIGN KEY'); | ||
} | ||
} |