Skip to content

Commit

Permalink
Added unique constraints support (#289)
Browse files Browse the repository at this point in the history
  • Loading branch information
aksafan authored Jun 1, 2023
1 parent 74697ad commit 70359e8
Show file tree
Hide file tree
Showing 20 changed files with 433 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/composer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
run: composer update --no-progress --no-interaction

- name: Composer outdated
run: composer outdated -D --strict --ignore symfony/console --ignore symfony/finder --ignore symfony/yaml
run: composer outdated -D --strict --ignore symfony/console --ignore symfony/finder --ignore symfony/yaml --ignore phpunit/phpunit
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ Framework agnostic database migrations for PHP.
- [indexes](migrations/indexes.md)
- [namespaces](migrations/namespaces.md)
- [custom template](migrations/custom_template.md)
- [unique_constraints](migrations/unique_constraints.md)
51 changes: 51 additions & 0 deletions docs/migrations/unique_constraints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## Unique Constraints

You can create unique constraints in migration where new table is created.
```php
// create table with a unique constraint
$this->table('users')
->addColumn('username', 'string')
->addColumn('password', 'string')
->addColumn('created_at', 'datetime')
->addColumn('updated_at', 'datetime')
->addColumn('another_column', 'integer')
->addUniqueConstraint('username', 'u_username'));
->create();
```

Or add a unique constraint to an existing table same way:
```php
// create table
$this->table('users')
->addColumn('username', 'string')
->addColumn('password', 'string')
->addColumn('created_at', 'datetime')
->addColumn('updated_at', 'datetime')
->addColumn('another_column', 'integer')
->create();

// add index
$this->table('users')
->addUniqueConstraint('username', 'u_username'));
->save();
```

You can also specify a few columns to one unique constraint:
```php
// create table
$this->table('users')
->addColumn('username', 'string')
->addColumn('sku', 'string')
->addColumn('password', 'string')
->addColumn('created_at', 'datetime')
->addColumn('updated_at', 'datetime')
->addColumn('another_column', 'integer')
->addUniqueConstraint(['username', 'sku'], 'u_username_sku'));
->create();
```

Keep in mind that this is the preferred way to add a unique constraint (at least for [PostgreSQL](https://www.postgresql.org/docs/9.4/indexes-unique.html)) and NOT a unique index.
The use of indexes to enforce unique constraints could be considered an implementation detail that should not be accessed directly.


**One should, however, be aware that there's no need to manually create indexes on unique columns; doing so would just duplicate the automatically-created index.**
22 changes: 22 additions & 0 deletions src/Database/Adapter/Behavior/StructureBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ public function getStructure(): Structure
$columns = $this->loadColumns($database);
$indexes = $this->loadIndexes($database);
$foreignKeys = $this->loadForeignKeys($database);
$uniqueConstraints = $this->loadUniqueConstraints($database);
foreach ($tables as $table) {
$tableName = $table['table_name'];
$migrationTable = $this->createMigrationTable($table);
$this->addColumns($migrationTable, $columns[$tableName] ?? []);
$this->addIndexes($migrationTable, $indexes[$tableName] ?? []);
$this->addForeignKeys($migrationTable, $foreignKeys[$tableName] ?? []);
$this->addUniqueConstraints($migrationTable, $uniqueConstraints[$tableName] ?? []);
$migrationTable->create();
$structure->update($migrationTable);
}
Expand Down Expand Up @@ -71,6 +73,11 @@ abstract protected function loadIndexes(string $database): array;
*/
abstract protected function loadForeignKeys(string $database): array;

/**
* @return array<string, array<string, array<string, mixed>>>
*/
abstract protected function loadUniqueConstraints(string $database): array;

/**
* @param array<string, mixed> $column
*/
Expand Down Expand Up @@ -115,4 +122,19 @@ private function addForeignKeys(MigrationTable $migrationTable, array $foreignKe
);
}
}

/**
* @param array<string, array<string, mixed>> $uniqueConstraints
*/
private function addUniqueConstraints(MigrationTable $migrationTable, array $uniqueConstraints): void
{
foreach ($uniqueConstraints as $name => $uniqueConstraint) {
$columns = $uniqueConstraint['columns'];
ksort($columns);
$migrationTable->addUniqueConstraint(
array_values($columns),
$name
);
}
}
}
5 changes: 5 additions & 0 deletions src/Database/Adapter/MysqlAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ protected function loadForeignKeys(string $database): array
return $foreignKeys;
}

protected function loadUniqueConstraints(string $database): array
{
return [];
}

protected function escapeString(string $string): string
{
return '`' . $string . '`';
Expand Down
22 changes: 22 additions & 0 deletions src/Database/Adapter/PgsqlAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,28 @@ protected function loadForeignKeys(string $database): array
return $foreignKeys;
}

protected function loadUniqueConstraints(string $database): array
{
$query = "SELECT tc.constraint_name, tc.table_name, kcu.column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'UNIQUE'
AND tc.constraint_schema = 'public'";

/** @var array<mixed[]> $uniqueConstraintKeys */
$uniqueConstraintKeys = $this->query($query)->fetchAll(PDO::FETCH_ASSOC);
$uniqueConstraints = [];
foreach ($uniqueConstraintKeys as $uniqueConstraintKey) {
/** @var string $tableName */
$tableName = $uniqueConstraintKey['table_name'];
/** @var string $constraintName */
$constraintName = $uniqueConstraintKey['constraint_name'];
$uniqueConstraints[$tableName][$constraintName]['columns'][] = $uniqueConstraintKey['column_name'];
}

return $uniqueConstraints;
}

private function remapForeignKeyAction(string $action): string
{
$actionMap = [
Expand Down
58 changes: 58 additions & 0 deletions src/Database/Element/Behavior/UniqueConstraintBehavior.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Phoenix\Database\Element\Behavior;

use Phoenix\Database\Element\MigrationTable;
use Phoenix\Database\Element\UniqueConstraint;

trait UniqueConstraintBehavior
{
/** @var UniqueConstraint[] */
private array $uniqueConstraints = [];

/** @var string[] */
private array $uniqueConstraintsToDrop = [];

/**
* One should be aware that for postgres there's no need to manually create indexes on unique columns.
* Doing so would just duplicate the automatically-created index.
*
* @param string|string[] $columns
* @param string $name
* @return MigrationTable
*/
public function addUniqueConstraint($columns, string $name): MigrationTable
{
if (!is_array($columns)) {
$columns = [$columns];
}
$this->uniqueConstraints[] = new UniqueConstraint($columns, $name);

return $this;
}

/**
* @return UniqueConstraint[]
*/
public function getUniqueConstraints(): array
{
return $this->uniqueConstraints;
}

public function dropUniqueConstraint(string $name): MigrationTable
{
$this->uniqueConstraintsToDrop[] = $name;

return $this;
}

/**
* @return string[]
*/
public function getUniqueConstraintsToDrop(): array
{
return $this->uniqueConstraintsToDrop;
}
}
6 changes: 6 additions & 0 deletions src/Database/Element/MigrationTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Phoenix\Database\Element\Behavior\ForeignKeyBehavior;
use Phoenix\Database\Element\Behavior\IndexBehavior;
use Phoenix\Database\Element\Behavior\PrimaryColumnsBehavior;
use Phoenix\Database\Element\Behavior\UniqueConstraintBehavior;
use Phoenix\Exception\InvalidArgumentValueException;

final class MigrationTable
Expand All @@ -30,6 +31,7 @@ final class MigrationTable
use ForeignKeyBehavior;
use IndexBehavior;
use PrimaryColumnsBehavior;
use UniqueConstraintBehavior;

public const ACTION_CREATE = 'create';

Expand Down Expand Up @@ -208,6 +210,10 @@ public function toTable(): Table
foreach ($this->getForeignKeys() as $foreignKey) {
$table->addForeignKey($foreignKey);
}
foreach ($this->getUniqueConstraints() as $uniqueConstraint) {
$table->addUniqueConstraint($uniqueConstraint);
}

return $table;
}
}
6 changes: 6 additions & 0 deletions src/Database/Element/Structure.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public function update(MigrationTable $migrationTable): Structure
foreach ($migrationTable->getForeignKeysToDrop() as $foreignKey) {
$table->removeForeignKey($foreignKey);
}
foreach ($migrationTable->getUniqueConstraintsToDrop() as $uniqueConstraint) {
$table->removeUniqueConstraint($uniqueConstraint);
}
foreach ($migrationTable->getColumnsToChange() as $oldName => $column) {
$table->changeColumn($oldName, $column);
}
Expand All @@ -52,6 +55,9 @@ public function update(MigrationTable $migrationTable): Structure
foreach ($migrationTable->getForeignKeys() as $foreignKey) {
$table->addForeignKey($foreignKey);
}
foreach ($migrationTable->getUniqueConstraints() as $uniqueConstraint) {
$table->addUniqueConstraint($uniqueConstraint);
}
}

return $this;
Expand Down
29 changes: 29 additions & 0 deletions src/Database/Element/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ final class Table
/** @var ForeignKey[] */
private array $foreignKeys = [];

/** @var UniqueConstraint[] */
private array $uniqueConstraints = [];

/** @var array<string, Index> */
private array $indexes = [];

Expand Down Expand Up @@ -160,6 +163,21 @@ public function addForeignKey(ForeignKey $foreignKey): Table
return $this;
}

/**
* @return UniqueConstraint[]
*/
public function getUniqueConstraints(): array
{
return $this->uniqueConstraints;
}

public function addUniqueConstraint(UniqueConstraint $uniqueConstraint): Table
{
$this->uniqueConstraints[$uniqueConstraint->getName()] = $uniqueConstraint;

return $this;
}

public function getForeignKey(string $name): ?ForeignKey
{
return isset($this->foreignKeys[$name]) ? $this->foreignKeys[$name] : null;
Expand All @@ -179,6 +197,13 @@ public function removeForeignKey(string $foreignKeyName): Table
return $this;
}

public function removeUniqueConstraint(string $uniqueConstraintName): Table
{
unset($this->uniqueConstraints[$uniqueConstraintName]);

return $this;
}

public function toMigrationTable(): MigrationTable
{
$table = clone $this;
Expand All @@ -198,6 +223,10 @@ public function toMigrationTable(): MigrationTable
foreach ($table->getForeignKeys() as $foreignKey) {
$migrationTable->addForeignKey($foreignKey->getColumns(), $foreignKey->getReferencedTable(), $foreignKey->getReferencedColumns(), $foreignKey->getOnDelete(), $foreignKey->getOnUpdate());
}
foreach ($table->getUniqueConstraints() as $uniqueConstraint) {
$migrationTable->addUniqueConstraint($uniqueConstraint->getColumns(), $uniqueConstraint->getName());
}

return $migrationTable;
}
}
40 changes: 40 additions & 0 deletions src/Database/Element/UniqueConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Phoenix\Database\Element;

use Phoenix\Behavior\ParamsCheckerBehavior;

final class UniqueConstraint
{
use ParamsCheckerBehavior;

/** @var string[] */
private array $columns;

private string $name;

/**
* @param string[] $columns
* @param string $name
*/
public function __construct(array $columns, string $name)
{
$this->columns = $columns;
$this->name = $name;
}

public function getName(): string
{
return $this->name;
}

/**
* @return string[]
*/
public function getColumns(): array
{
return $this->columns;
}
}
Loading

0 comments on commit 70359e8

Please sign in to comment.