Skip to content

Commit

Permalink
schema manager - schema browser, transaction support, conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
pounard committed Mar 12, 2024
1 parent 9f550ca commit 741b16d
Show file tree
Hide file tree
Showing 57 changed files with 1,797 additions and 694 deletions.
2 changes: 1 addition & 1 deletion bin/generate_changes.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

use MakinaCorpus\QueryBuilder\Schema\Diff\Change\Template\Generator;
use MakinaCorpus\QueryBuilder\Schema\Diff\Generator\Generator;

require_once \dirname(__DIR__) . '/vendor/autoload.php';

Expand Down
80 changes: 80 additions & 0 deletions docs/content/query/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,86 @@ transaction started. For now, only PostgreSQL will benefit from a real database
transaction.
:::

### Basic transaction

Considering the API is fluent and all methods can be chained, here is an example
of a simple table creation:

```php
$schemaManager
->modify(database: 'my_database')
->createTable(name: 'user')
->column(name: 'id', type: 'serial', nullable: false)
->primaryKey(['id'])
->column(name: 'enabled', type: 'bool', nullable: false, default: 'false')
->column('email', 'text', true)
->uniqueKey(columns: ['email'])
->index(columns: ['email'])
->endTable()
->commit()
```

:::warning
Because API may evolve and add new parameters to builder functions, it is recommended
to use argument naming when using the builder.
:::

You may also simply issue direct statements, considering the manipulated table already
exists in your schema:

```php
$schemaManager
->modify(database: 'my_database')
->column(table: 'user', name: 'id', type: 'serial', nullable: false)
->primaryKey(table: 'user', ['id'])
->column(table: 'user', name: 'enabled', type: 'bool', nullable: false, default: 'false')
->column(table: 'user', 'email', 'text', true)
->uniqueKey(table: 'user', columns: ['email'])
->index(table: 'user', columns: ['email'])
->commit()
```

:::note
Table creation requires you to use the `createTable()` method nesting.
:::

### Conditions and branches

You may write re-entrant schema alteration procedures by using conditional branches
in the code, consider the previous example:

```php
$schemaManager
->modify(database: 'my_database')
->ifTableNotExists(table: 'user')
->createTable(name: 'user')
->column(name: 'id', type: 'serial', nullable: false)
->primaryKey(['id'])
->column(name: 'enabled', type: 'bool', nullable: false, default: 'false')
->endTable()
->endIf()
->ifColumnNotExists(table: 'user', column: 'email')
->column(table: 'user', 'email', 'text', true)
->uniqueKey(table: 'user', columns: ['email'])
->index(table: 'user', columns: ['email'])
->endIf()
->commit()
```

:::note
There is no `else()` statement yet, it might be implemented in in the future.
:::

### Arbitrary SQL queries

@todo

### Using arbitrary SQL as condition

@todo

### Self-documenting example

The `$transaction` object is an instance of `MakinaCorpus\QueryBuilder\Schema\Diff\SchemaTransaction`
implementing the builder pattern, i.e. allowing method chaining.

Expand Down
27 changes: 14 additions & 13 deletions src/Platform/Schema/MySQLSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@

namespace MakinaCorpus\QueryBuilder\Platform\Schema;

use MakinaCorpus\QueryBuilder\Expression;
use MakinaCorpus\QueryBuilder\Error\QueryBuilderError;
use MakinaCorpus\QueryBuilder\Error\UnsupportedFeatureError;
use MakinaCorpus\QueryBuilder\Expression;
use MakinaCorpus\QueryBuilder\Result\ResultRow;
use MakinaCorpus\QueryBuilder\Schema\Column;
use MakinaCorpus\QueryBuilder\Schema\ForeignKey;
use MakinaCorpus\QueryBuilder\Schema\Key;
use MakinaCorpus\QueryBuilder\Schema\SchemaManager;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnModify;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ForeignKeyAdd;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\IndexCreate;
Expand All @@ -20,7 +16,10 @@
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\PrimaryKeyDrop;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\UniqueKeyAdd;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\UniqueKeyDrop;

use MakinaCorpus\QueryBuilder\Schema\Read\Column;
use MakinaCorpus\QueryBuilder\Schema\Read\ForeignKey;
use MakinaCorpus\QueryBuilder\Schema\Read\Key;
use MakinaCorpus\QueryBuilder\Schema\SchemaManager;

/**
* Please note that some functions here might use information_schema tables
Expand Down Expand Up @@ -328,7 +327,7 @@ private function getAllTableKeysInfo(string $database, string $name, string $sch
}

#[\Override]
protected function writeColumnSpecCollation(string $collation): Expression
protected function doWriteColumnCollation(string $collation): Expression
{
// This is very hasardous, but let's do it.
if ('binary' === $collation) {
Expand All @@ -348,16 +347,17 @@ protected function writeColumnSpecCollation(string $collation): Expression
protected function writeColumnModify(ColumnModify $change): iterable|Expression
{
$name = $change->getName();
$type = $change->getType();

// When modifying a column in MySQL, you need to use the CHANGE COLUMN
// syntax if you change anything else than the DEFAULT value.
// That's kind of very weird, but that's it. Live with it.
if ($change->getType() || null !== $change->isNullable() || $change->getCollation() || $change->getType()) {
if ($type || null !== $change->isNullable() || $change->getCollation()) {
return $this->raw(
'ALTER TABLE ? MODIFY COLUMN ?',
[
$this->table($change),
$this->writeColumnSpec($this->columnModifyToAdd($change)),
$this->doWriteColumn($this->columnModifyToAdd($change)),
]
);
}
Expand All @@ -371,20 +371,21 @@ protected function writeColumnModify(ColumnModify $change): iterable|Expression
}
$pieces[] = $this->raw('ALTER COLUMN ?::id DROP DEFAULT', [$name]);
} else if ($default = $change->getDefault()) {
$pieces[] = $this->raw('ALTER COLUMN ?::id SET DEFAULT ?', [$name, $this->raw($default)]);
// @todo
$pieces[] = $this->raw('ALTER COLUMN ?::id SET DEFAULT ?', [$name, $this->doWriteColumnDefault($type ?? 'unknown', $default)]);
}

return $this->raw('ALTER TABLE ? ' . \implode(', ', \array_fill(0, \count($pieces), '?')), [$this->table($change), ...$pieces]);
}

#[\Override]
protected function writeForeignKeySpec(ForeignKeyAdd $change): Expression
protected function doWriteForeignKey(ForeignKeyAdd $change): Expression
{
if ($change->isDeferrable()) {
// @todo log that MySQL doesn't support deferring?
}

return $this->writeConstraintSpec(
return $this->doWriteConstraint(
$change->getName(),
$this->raw(
'FOREIGN KEY (?::id[]) REFERENCES ?::table (?::id[])',
Expand Down Expand Up @@ -440,6 +441,6 @@ protected function writeUniqueKeyAdd(UniqueKeyAdd $change): Expression
#[\Override]
protected function writeUniqueKeyDrop(UniqueKeyDrop $change): Expression
{
return $this->writeConstraintDropSpec($change->getName(), $change->getTable(), $change->getSchema());
return $this->doWriteConstraintDrop($change->getName(), $change->getTable(), $change->getSchema());
}
}
8 changes: 4 additions & 4 deletions src/Platform/Schema/PostgreSQLSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
use MakinaCorpus\QueryBuilder\Expression;
use MakinaCorpus\QueryBuilder\Result\Result;
use MakinaCorpus\QueryBuilder\Result\ResultRow;
use MakinaCorpus\QueryBuilder\Schema\Column;
use MakinaCorpus\QueryBuilder\Schema\ForeignKey;
use MakinaCorpus\QueryBuilder\Schema\Key;
use MakinaCorpus\QueryBuilder\Schema\SchemaManager;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\IndexRename;
use MakinaCorpus\QueryBuilder\Schema\Read\Column;
use MakinaCorpus\QueryBuilder\Schema\Read\ForeignKey;
use MakinaCorpus\QueryBuilder\Schema\Read\Key;
use MakinaCorpus\QueryBuilder\Schema\SchemaManager;

/**
* Please note that some functions here might use information_schema tables
Expand Down
10 changes: 5 additions & 5 deletions src/Platform/Schema/SQLiteSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@

namespace MakinaCorpus\QueryBuilder\Platform\Schema;

use MakinaCorpus\QueryBuilder\Expression;
use MakinaCorpus\QueryBuilder\Error\UnsupportedFeatureError;
use MakinaCorpus\QueryBuilder\Expression;
use MakinaCorpus\QueryBuilder\Result\ResultRow;
use MakinaCorpus\QueryBuilder\Schema\Column;
use MakinaCorpus\QueryBuilder\Schema\ForeignKey;
use MakinaCorpus\QueryBuilder\Schema\Key;
use MakinaCorpus\QueryBuilder\Schema\SchemaManager;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnModify;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ConstraintDrop;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ConstraintModify;
Expand All @@ -24,6 +20,10 @@
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\PrimaryKeyAdd;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\PrimaryKeyDrop;
use MakinaCorpus\QueryBuilder\Schema\Diff\Change\UniqueKeyAdd;
use MakinaCorpus\QueryBuilder\Schema\Read\Column;
use MakinaCorpus\QueryBuilder\Schema\Read\ForeignKey;
use MakinaCorpus\QueryBuilder\Schema\Read\Key;
use MakinaCorpus\QueryBuilder\Schema\SchemaManager;

/**
* Please note that some functions here might use information_schema tables
Expand Down
110 changes: 110 additions & 0 deletions src/Schema/Diff/Browser/ChangeLogBrowser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace MakinaCorpus\QueryBuilder\Schema\Diff\Browser;

use MakinaCorpus\QueryBuilder\Schema\Diff\Condition\AbstractCondition;
use MakinaCorpus\QueryBuilder\Schema\Diff\SchemaTransaction;
use MakinaCorpus\QueryBuilder\Schema\Diff\Transaction\AbstractNestedSchemaTransaction;

class ChangeLogBrowser
{
/** @var ChangeLogVisitor[] */
private array $visitors = [];

public function addVisitor(ChangeLogVisitor $visitor): void
{
$this->visitors[] = $visitor;
}

public function browse(SchemaTransaction $transaction): void
{
try {
foreach ($this->visitors as $visitor) {
$visitor->start($transaction);
}

foreach ($transaction->getChangeLog()->getAll() as $change) {
if ($change instanceof AbstractNestedSchemaTransaction) {
$this->reduceNested($change, 1, $this->visitors);
} else {
foreach ($this->visitors as $visitor) {
$visitor->apply($change);
}
}
}

foreach ($this->visitors as $visitor) {
$visitor->stop($transaction);
}
} catch (\Throwable $error) {
foreach ($this->visitors as $visitor) {
$visitor->error($transaction, $error);
}

throw $error;
}
}

/**
* @param AbstractCondition[] $conditions
*/
protected function evaluateConditions(array $conditions, ChangeLogVisitor $visitor): bool
{
foreach ($conditions as $condition) {
if (!$visitor->evaluate($condition)) {
return false;
}
}
return true;
}

/**
* @param ChangeLogVisitor[] $visitors
*/
protected function reduceNested(AbstractNestedSchemaTransaction $new, int $depth, array $visitors)
{
if ($conditions = $new->getConditions()) {
$candidates = [];

foreach ($visitors as $visitor) {
\assert($visitor instanceof ChangeLogVisitor);

if ($this->evaluateConditions($conditions, $visitor)) {
$candidates[] = $visitor;
} else {
$visitor->skip($new, $depth);
}
}

if ($candidates) {
$this->browseNested($new, $depth, $candidates);
}
}
}

/**
* @param ChangeLogVisitor[] $visitors
*/
protected function browseNested(AbstractNestedSchemaTransaction $new, int $depth, array $visitors): void
{
foreach ($visitors as $visitor) {
$visitor->enter($new, $depth);
}

foreach ($new->getChangeLog()->getAll() as $change) {
if ($change instanceof AbstractNestedSchemaTransaction) {
$this->reduceNested($change, $depth + 1, $visitors);
} else {
foreach ($visitors as $visitor) {
$visitor->apply($change);
}
}
}

foreach ($visitors as $visitor) {
$visitor->leave($new, $depth);
}
}
}
Loading

0 comments on commit 741b16d

Please sign in to comment.