Skip to content

Commit

Permalink
Create a new database abstraction layer
Browse files Browse the repository at this point in the history
  • Loading branch information
fisharebest committed Jan 6, 2025
1 parent 1c67d2d commit c975735
Show file tree
Hide file tree
Showing 26 changed files with 2,714 additions and 143 deletions.
151 changes: 151 additions & 0 deletions app/Arr.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.3)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:concat\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.3)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:filter\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.3)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:map\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), T\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.3)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:reverse\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.3)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:sortKeys\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.3)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:sort\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.3)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:unique\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.4)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:concat\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.4)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:filter\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.4)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:map\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), T\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.4)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:reverse\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.4)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:sortKeys\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.4)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:sort\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

Check failure on line 1 in app/Arr.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.4)

Ignored error pattern #^Method Fisharebest\\Webtrees\\Arr\:\:unique\(\) should return Fisharebest\\Webtrees\\Arr\<TKey of \(int\|string\), TValue\> but returns Fisharebest\\Webtrees\\Arr\<\(int\|string\), mixed\>\.$# (return.type) in path /home/runner/work/webtrees/webtrees/app/Arr.php was not matched in reported errors.

/**
* 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()));
}
}
147 changes: 147 additions & 0 deletions app/Cli/Commands/DatabaseRepair.php
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

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.3)

Strict comparison using !== between 45 and 45 will always evaluate to false.

Check failure on line 49 in app/Cli/Commands/DatabaseRepair.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.4)

Strict comparison using !== between 45 and 45 will always evaluate to false.
$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

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.3)

Cannot call method spansColumns() on Doctrine\DBAL\Schema\Index|null.

Check failure on line 72 in app/Cli/Commands/DatabaseRepair.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.4)

Cannot call method spansColumns() on Doctrine\DBAL\Schema\Index|null.
$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');
}
}
1 change: 1 addition & 0 deletions app/Cli/Console.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class Console extends Application
{
private const array COMMANDS = [
Commands\CompilePoFiles::class,
Commands\DatabaseRepair::class,
Commands\TreeCreate::class,
Commands\TreeList::class,
Commands\UserCreate::class,
Expand Down
Loading

0 comments on commit c975735

Please sign in to comment.