Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/Exceptions/UnsupportedColumnRetirementException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace PHPNomad\Database\Exceptions;

use PHPNomad\Datastore\Exceptions\DatastoreErrorException;

/** The active database adapter cannot safely retire named columns. */
class UnsupportedColumnRetirementException extends DatastoreErrorException
{
}
46 changes: 46 additions & 0 deletions lib/Interfaces/TableColumnRetirementStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace PHPNomad\Database\Interfaces;

use PHPNomad\Database\Exceptions\TableUpdateFailedException;

/**
* Optional, explicitly named retirement of legacy table columns.
*
* Implementations use their backend's identifier and case semantics. They
* accept identifiers the backend can safely quote, reject empty or NUL-bearing
* names, and treat metadata/query failures as failures rather than absence.
*/
interface TableColumnRetirementStrategy extends TableUpdateStrategy
{
/**
* Determine whether a named column exists in the table's active schema.
*
* Metadata lookup is scoped to the active schema. Identifier comparison
* follows backend semantics (for example, MySQL column names compare
* case-insensitively). Metadata failures must propagate; they are not
* absence.
*
* @throws TableUpdateFailedException
*/
public function columnExists(Table $table, string $columnName): bool;

/**
* Retire only the explicitly named columns.
*
* The request must contain at least one name, and the whole request must be
* validated before any mutation. A requested column that is absent is an
* idempotent no-op. Multiple requested spellings that identify the same
* persisted column under backend case semantics are deduplicated to one
* retirement. A requested column that remains declared by the supplied
* table, using those same semantics, is invalid. Implementations must not
* implicitly remove indexes or foreign keys: a requested column with either
* dependency is refused before DDL. Only requested, currently present, and
* fully preflighted columns may be retired; unrelated columns are preserved.
*
* @param non-empty-string ...$columnNames
* @throws \InvalidArgumentException
* @throws TableUpdateFailedException
*/
public function retireColumns(Table $table, string ...$columnNames): void;
}
87 changes: 87 additions & 0 deletions tests/Unit/Contracts/TableColumnRetirementSurfaceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace PHPNomad\Database\Tests\Unit\Contracts;

use PHPNomad\Database\Exceptions\UnsupportedColumnRetirementException;
use PHPNomad\Database\Interfaces\Table;
use PHPNomad\Database\Interfaces\TableColumnRetirementStrategy;
use PHPNomad\Database\Interfaces\TableUpdateStrategy;
use PHPNomad\Database\Tests\TestCase;
use PHPNomad\Datastore\Exceptions\DatastoreErrorException;
use ReflectionClass;

/** @coversNothing Public compatibility contract for optional retirement. */
final class TableColumnRetirementSurfaceTest extends TestCase
{
public function testExistingTableUpdateImplementationNeedsNoNewCapability(): void
{
$legacy = new LegacyTableUpdateStrategy();

self::assertInstanceOf(TableUpdateStrategy::class, $legacy);
self::assertNotInstanceOf(TableColumnRetirementStrategy::class, $legacy);
}

public function testRetirementIsAnOptionalTableUpdateCapability(): void
{
self::assertTrue(is_a(TableColumnRetirementStrategy::class, TableUpdateStrategy::class, true));
self::assertSame(
['columnExists', 'retireColumns', 'syncColumns'],
$this->methodNames(TableColumnRetirementStrategy::class)
);
}

public function testColumnExistsSignatureCarriesTableAndName(): void
{
$method = (new ReflectionClass(TableColumnRetirementStrategy::class))->getMethod('columnExists');

self::assertSame('bool', (string) $method->getReturnType());
self::assertSame(
['table', 'columnName'],
array_map(static fn ($parameter): string => $parameter->getName(), $method->getParameters())
);
}

public function testRetireColumnsSignatureIsVariadicAndReturnsVoid(): void
{
$method = (new ReflectionClass(TableColumnRetirementStrategy::class))->getMethod('retireColumns');
$parameters = $method->getParameters();

self::assertSame('void', (string) $method->getReturnType());
self::assertSame(['table', 'columnNames'], array_map(
static fn ($parameter): string => $parameter->getName(),
$parameters
));
self::assertTrue($parameters[1]->isVariadic());
}

public function testUnsupportedFailureIsARecoverableDatastoreFailure(): void
{
self::assertInstanceOf(
DatastoreErrorException::class,
new UnsupportedColumnRetirementException('No named-column retirement adapter is active.')
);
}

/**
* @param class-string $type
* @return list<string>
*/
private function methodNames(string $type): array
{
$names = array_map(
static fn ($method): string => $method->getName(),
(new ReflectionClass($type))->getMethods()
);
sort($names);

return $names;
}
}

/** A pre-capability adapter remains a valid implementation. */
final class LegacyTableUpdateStrategy implements TableUpdateStrategy
{
public function syncColumns(Table $table): void
{
}
}
Loading