Skip to content

Commit

Permalink
Validate that columns referenced by converters are defined
Browse files Browse the repository at this point in the history
  • Loading branch information
guvra committed Mar 26, 2024
1 parent 8a82ae5 commit 16abed2
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 1 deletion.
8 changes: 8 additions & 0 deletions src/Database/Metadata/MetadataInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Smile\GdprDump\Database\Metadata;

use RuntimeException;
use Smile\GdprDump\Database\Metadata\Definition\Constraint\ForeignKey;

interface MetadataInterface
Expand All @@ -15,6 +16,13 @@ interface MetadataInterface
*/
public function getTableNames(): array;

/**
* Get the columns of the specified table.
*
* @throws RuntimeException
*/
public function getColumnNames(string $tableName): array;

/**
* Get all foreign keys.
* Each array element is an array that contains the foreign keys of a table.
Expand Down
27 changes: 27 additions & 0 deletions src/Database/Metadata/MysqlMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
namespace Smile\GdprDump\Database\Metadata;

use Doctrine\DBAL\Connection;
use RuntimeException;
use Smile\GdprDump\Database\Metadata\Definition\Constraint\ForeignKey;

class MysqlMetadata implements MetadataInterface
{
private string $schema;
private ?array $tableNames = null;
private ?array $columnNames = null;
private ?array $foreignKeys = null;

public function __construct(private Connection $connection)
Expand Down Expand Up @@ -38,6 +40,31 @@ public function getTableNames(): array
return $this->tableNames;
}

/**
* @inheritdoc
*/
public function getColumnNames(string $tableName): array
{
if ($this->columnNames === null) {
$query = 'SELECT TABLE_NAME, COLUMN_NAME '
. 'FROM INFORMATION_SCHEMA.COLUMNS '
. 'WHERE TABLE_SCHEMA=?'
. 'ORDER BY COLUMN_NAME ASC';

$statement = $this->connection->prepare($query);
$result = $statement->executeQuery([$this->schema]);

$this->columnNames = [];

while ($row = $result->fetchAssociative()) {
$this->columnNames[$row['TABLE_NAME']][] = $row['COLUMN_NAME'];
}
}

return $this->columnNames[$tableName]
?? throw new RuntimeException(sprintf('The table "%s" is not defined.', $tableName));
}

/**
* @inheritdoc
*/
Expand Down
29 changes: 28 additions & 1 deletion src/Dumper/Config/ConfigProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Smile\GdprDump\Config\ConfigInterface;
use Smile\GdprDump\Database\Metadata\MetadataInterface;
use Smile\GdprDump\Dumper\Config\Validation\ValidationException;

class ConfigProcessor
{
Expand Down Expand Up @@ -47,7 +48,8 @@ private function processTableLists(ConfigInterface $config): void
}

/**
* Remove tables that don't exist from the "tables" parameter.
* Remove tables that don't exist from the "tables" parameter,
* and raise an exception if the remaining tables have converters that reference invalid columns.
*/
private function processTablesData(ConfigInterface $config): void
{
Expand Down Expand Up @@ -86,6 +88,10 @@ private function resolveTablesData(array $tablesData): array
$matches = $this->findTablesByName((string) $tableName);

foreach ($matches as $match) {
// Throw an exception if a converter refers to a column that does not exist
$this->validateTableColumns($tableName, $tableData);

// Merge table configuration
if (!array_key_exists($match, $resolved)) {
$resolved[$match] = [];
}
Expand Down Expand Up @@ -118,4 +124,25 @@ private function findTablesByName(string $pattern): array

return $matches;
}

/**
* Raise an exception if the table data contains a converter that references an undefined column.
*/
private function validateTableColumns(string $tableName, array $tableData): void
{
if (!array_key_exists('converters', $tableData) || empty($tableData['converters'])) {
return;
}

$columns = $this->metadata->getColumnNames($tableName);

foreach ($tableData['converters'] as $columnName => $converterData) {
$disabled = $converterData['disabled'] ?? false;

if (!$disabled && !in_array($columnName, $columns)) {
$message = 'The table "%s" uses a converter on an undefined column "%s".';
throw new ValidationException(sprintf($message, $tableName, $columnName));
}
}
}
}
19 changes: 19 additions & 0 deletions tests/functional/Database/Metadata/MysqlMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Smile\GdprDump\Tests\Functional\Database\Metadata;

use RuntimeException;
use Smile\GdprDump\Database\Metadata\MetadataInterface;
use Smile\GdprDump\Database\Metadata\MysqlMetadata;
use Smile\GdprDump\Tests\Functional\TestCase;
Expand All @@ -19,6 +20,24 @@ public function testTableNames(): void
$this->assertEqualsCanonicalizing(['stores', 'customers', 'addresses', 'config'], $metadata->getTableNames());
}

/**
* Test the "getColumnNames" method.
*/
public function testColumnNames(): void
{
$columns = $this->getMetadata()->getColumnNames('stores');
$this->assertEqualsCanonicalizing(['code', 'store_id', 'parent_store_id'], $columns);
}

/**
* Assert that an exception is thrown when fetching the columns of an undefined table.
*/
public function testErrorOnInvalidTableName(): void
{
$this->expectException(RuntimeException::class);
$this->getMetadata()->getColumnNames('undefined');
}

/**
* Test the "getForeignKeys" method.
*/
Expand Down

0 comments on commit 16abed2

Please sign in to comment.