diff --git a/bin/generate_changes.php b/bin/generate_changes.php index afea77d..f0e4517 100644 --- a/bin/generate_changes.php +++ b/bin/generate_changes.php @@ -1,6 +1,6 @@ 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. diff --git a/src/Platform/Schema/MySQLSchemaManager.php b/src/Platform/Schema/MySQLSchemaManager.php index 4e61a74..4444bdc 100644 --- a/src/Platform/Schema/MySQLSchemaManager.php +++ b/src/Platform/Schema/MySQLSchemaManager.php @@ -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; @@ -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 @@ -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) { @@ -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)), ] ); } @@ -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[])', @@ -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()); } } diff --git a/src/Platform/Schema/PostgreSQLSchemaManager.php b/src/Platform/Schema/PostgreSQLSchemaManager.php index b59f9e7..d5282d2 100644 --- a/src/Platform/Schema/PostgreSQLSchemaManager.php +++ b/src/Platform/Schema/PostgreSQLSchemaManager.php @@ -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 diff --git a/src/Platform/Schema/SQLiteSchemaManager.php b/src/Platform/Schema/SQLiteSchemaManager.php index ca7bb42..eb6553e 100644 --- a/src/Platform/Schema/SQLiteSchemaManager.php +++ b/src/Platform/Schema/SQLiteSchemaManager.php @@ -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; @@ -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 diff --git a/src/Schema/Diff/Browser/ChangeLogBrowser.php b/src/Schema/Diff/Browser/ChangeLogBrowser.php new file mode 100644 index 0000000..c1962bc --- /dev/null +++ b/src/Schema/Diff/Browser/ChangeLogBrowser.php @@ -0,0 +1,110 @@ +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); + } + } +} diff --git a/src/Schema/Diff/Browser/ChangeLogVisitor.php b/src/Schema/Diff/Browser/ChangeLogVisitor.php new file mode 100644 index 0000000..ba47379 --- /dev/null +++ b/src/Schema/Diff/Browser/ChangeLogVisitor.php @@ -0,0 +1,70 @@ +schema->supportsTransaction() && (!$this->transaction || !$this->transaction->isStarted())) { + throw new QueryBuilderError("Transaction should have been started here."); + } + } + + private function dieIfTransaction(): void + { + if ($this->schema->supportsTransaction() && $this->transaction && !$this->transaction->isStarted()) { + throw new QueryBuilderError("Transaction should have not been started here."); + } + } + + #[\Override] + public function start(SchemaTransaction $transaction): void + { + $this->dieIfTransaction(); + + if ($this->schema->supportsTransaction()) { + $this->transaction = $this->schema->getQueryExecutor()->beginTransaction(Transaction::SERIALIZABLE); + } + } + + #[\Override] + public function stop(SchemaTransaction $transaction): void + { + $this->dieIfNoTransaction(); + + if ($this->schema->supportsTransaction()) { + try { + $this->transaction->commit(); + } finally { + $this->transaction = null; + } + } + } + + #[\Override] + public function error(SchemaTransaction $transaction, \Throwable $error): void + { + $this->dieIfNoTransaction(); + + if ($this->schema->supportsTransaction()) { + try { + $this->transaction->rollback(); + } finally { + $this->transaction = null; + } + } + } + + #[\Override] + public function evaluate(AbstractCondition $condition): bool + { + $this->dieIfNoTransaction(); + + return $this->schema->evaluateCondition($condition); + } + + #[\Override] + public function apply(AbstractChange $change): void + { + $this->dieIfNoTransaction(); + + $this->schema->applyChange($change); + } +} diff --git a/src/Schema/Diff/AbstractChange.php b/src/Schema/Diff/Change/AbstractChange.php similarity index 64% rename from src/Schema/Diff/AbstractChange.php rename to src/Schema/Diff/Change/AbstractChange.php index 7d7335c..8e8ae6a 100644 --- a/src/Schema/Diff/AbstractChange.php +++ b/src/Schema/Diff/Change/AbstractChange.php @@ -2,23 +2,27 @@ declare(strict_types=1); -namespace MakinaCorpus\QueryBuilder\Schema\Diff; +namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; + +use MakinaCorpus\QueryBuilder\Schema\Diff\ChangeLogItem; /** * Changes on the schema. */ -abstract class AbstractChange +abstract class AbstractChange implements ChangeLogItem { public function __construct( private readonly string $database, private readonly string $schema, ) {} + #[\Override] public function getDatabase(): string { return $this->database; } + #[\Override] public function getSchema(): string { return $this->schema; diff --git a/src/Schema/Diff/Change/ColumnAdd.php b/src/Schema/Diff/Change/ColumnAdd.php index e2ac852..43dd793 100644 --- a/src/Schema/Diff/Change/ColumnAdd.php +++ b/src/Schema/Diff/Change/ColumnAdd.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Add a COLUMN. * diff --git a/src/Schema/Diff/Change/ColumnDrop.php b/src/Schema/Diff/Change/ColumnDrop.php index 6a016fd..d61bea3 100644 --- a/src/Schema/Diff/Change/ColumnDrop.php +++ b/src/Schema/Diff/Change/ColumnDrop.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Drop a COLUMN. * diff --git a/src/Schema/Diff/Change/ColumnModify.php b/src/Schema/Diff/Change/ColumnModify.php index 3436f3b..6437795 100644 --- a/src/Schema/Diff/Change/ColumnModify.php +++ b/src/Schema/Diff/Change/ColumnModify.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Add a COLUMN. * diff --git a/src/Schema/Diff/Change/ColumnRename.php b/src/Schema/Diff/Change/ColumnRename.php index 06767c9..08e0635 100644 --- a/src/Schema/Diff/Change/ColumnRename.php +++ b/src/Schema/Diff/Change/ColumnRename.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Renames a COLUMN. * diff --git a/src/Schema/Diff/Change/ConstraintDrop.php b/src/Schema/Diff/Change/ConstraintDrop.php index dd8346f..a668b0a 100644 --- a/src/Schema/Diff/Change/ConstraintDrop.php +++ b/src/Schema/Diff/Change/ConstraintDrop.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Drop an arbitrary constraint from a table. * diff --git a/src/Schema/Diff/Change/ConstraintModify.php b/src/Schema/Diff/Change/ConstraintModify.php index 69b8705..24e69d8 100644 --- a/src/Schema/Diff/Change/ConstraintModify.php +++ b/src/Schema/Diff/Change/ConstraintModify.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Modify an arbitrary constraint on a table. * diff --git a/src/Schema/Diff/Change/ConstraintRename.php b/src/Schema/Diff/Change/ConstraintRename.php index 24e511d..bc828f8 100644 --- a/src/Schema/Diff/Change/ConstraintRename.php +++ b/src/Schema/Diff/Change/ConstraintRename.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Rename an arbitrary constraint. * diff --git a/src/Schema/Diff/Change/ForeignKeyAdd.php b/src/Schema/Diff/Change/ForeignKeyAdd.php index 94841cc..a940f4b 100644 --- a/src/Schema/Diff/Change/ForeignKeyAdd.php +++ b/src/Schema/Diff/Change/ForeignKeyAdd.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Add a FOREIGN KEY constraint on a table. * diff --git a/src/Schema/Diff/Change/ForeignKeyDrop.php b/src/Schema/Diff/Change/ForeignKeyDrop.php index 1dba90e..3c75460 100644 --- a/src/Schema/Diff/Change/ForeignKeyDrop.php +++ b/src/Schema/Diff/Change/ForeignKeyDrop.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Drop a FOREIGN KEY constraint from a table. * diff --git a/src/Schema/Diff/Change/ForeignKeyModify.php b/src/Schema/Diff/Change/ForeignKeyModify.php index 8e68a9b..88959d7 100644 --- a/src/Schema/Diff/Change/ForeignKeyModify.php +++ b/src/Schema/Diff/Change/ForeignKeyModify.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Modify a FOREIGN KEY constraint on a table. * diff --git a/src/Schema/Diff/Change/ForeignKeyRename.php b/src/Schema/Diff/Change/ForeignKeyRename.php index b7b170a..80138cf 100644 --- a/src/Schema/Diff/Change/ForeignKeyRename.php +++ b/src/Schema/Diff/Change/ForeignKeyRename.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Rename an arbitrary constraint. * diff --git a/src/Schema/Diff/Change/IndexCreate.php b/src/Schema/Diff/Change/IndexCreate.php index 105b7b8..7cc2946 100644 --- a/src/Schema/Diff/Change/IndexCreate.php +++ b/src/Schema/Diff/Change/IndexCreate.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Create an INDEX on a table. * diff --git a/src/Schema/Diff/Change/IndexDrop.php b/src/Schema/Diff/Change/IndexDrop.php index a58905e..6021fa1 100644 --- a/src/Schema/Diff/Change/IndexDrop.php +++ b/src/Schema/Diff/Change/IndexDrop.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Drop an INDEX from a table. * diff --git a/src/Schema/Diff/Change/IndexRename.php b/src/Schema/Diff/Change/IndexRename.php index 97182b5..83fe4a1 100644 --- a/src/Schema/Diff/Change/IndexRename.php +++ b/src/Schema/Diff/Change/IndexRename.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Rename an arbitrary constraint. * diff --git a/src/Schema/Diff/Change/PrimaryKeyAdd.php b/src/Schema/Diff/Change/PrimaryKeyAdd.php index 79e5cec..af74484 100644 --- a/src/Schema/Diff/Change/PrimaryKeyAdd.php +++ b/src/Schema/Diff/Change/PrimaryKeyAdd.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Add the PRIMARY KEY constraint on a table. * diff --git a/src/Schema/Diff/Change/PrimaryKeyDrop.php b/src/Schema/Diff/Change/PrimaryKeyDrop.php index 759b0e0..97b98ba 100644 --- a/src/Schema/Diff/Change/PrimaryKeyDrop.php +++ b/src/Schema/Diff/Change/PrimaryKeyDrop.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Drop the PRIMARY KEY constraint from a table. * diff --git a/src/Schema/Diff/Change/TableCreate.php b/src/Schema/Diff/Change/TableCreate.php index aa40bda..9ee36e3 100644 --- a/src/Schema/Diff/Change/TableCreate.php +++ b/src/Schema/Diff/Change/TableCreate.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Create a table. * diff --git a/src/Schema/Diff/Change/TableDrop.php b/src/Schema/Diff/Change/TableDrop.php index ca4ebf6..726a99f 100644 --- a/src/Schema/Diff/Change/TableDrop.php +++ b/src/Schema/Diff/Change/TableDrop.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Drop a table. * diff --git a/src/Schema/Diff/Change/TableRename.php b/src/Schema/Diff/Change/TableRename.php index 7486a2c..b931249 100644 --- a/src/Schema/Diff/Change/TableRename.php +++ b/src/Schema/Diff/Change/TableRename.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Renames a table. * diff --git a/src/Schema/Diff/Change/UniqueKeyAdd.php b/src/Schema/Diff/Change/UniqueKeyAdd.php index 2451e80..3aa7a5e 100644 --- a/src/Schema/Diff/Change/UniqueKeyAdd.php +++ b/src/Schema/Diff/Change/UniqueKeyAdd.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Create a UNIQUE constraint on a table. * diff --git a/src/Schema/Diff/Change/UniqueKeyDrop.php b/src/Schema/Diff/Change/UniqueKeyDrop.php index baee084..07a68ad 100644 --- a/src/Schema/Diff/Change/UniqueKeyDrop.php +++ b/src/Schema/Diff/Change/UniqueKeyDrop.php @@ -4,8 +4,6 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; - /** * Drop a UNIQUE constraint from a table. * diff --git a/src/Schema/Diff/ChangeLog.php b/src/Schema/Diff/ChangeLog.php index 92082e8..7ed1e5d 100644 --- a/src/Schema/Diff/ChangeLog.php +++ b/src/Schema/Diff/ChangeLog.php @@ -4,34 +4,23 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff; -use MakinaCorpus\QueryBuilder\Schema\SchemaManager; - class ChangeLog { public function __construct( - private readonly SchemaManager $schemaManager, - /** @var AbstractChange[] */ + /** @var ChangeLogItem[] */ private array $changes = [], ) {} - public function add(AbstractChange $change): void + public function add(ChangeLogItem $change): void { $this->changes[] = $change; } + /** + * @return ChangeLogItem[] + */ public function getAll(): iterable { return $this->changes; } - - public function diff(): ChangeLog - { - foreach ($this->changes as $change) { - // load target object - // run has modified - } - - // @todo fix this - return $this; - } } diff --git a/src/Schema/Diff/ChangeLogItem.php b/src/Schema/Diff/ChangeLogItem.php new file mode 100644 index 0000000..35e80e1 --- /dev/null +++ b/src/Schema/Diff/ChangeLogItem.php @@ -0,0 +1,18 @@ +database; + } + + #[\Override] + public function getSchema(): string + { + return $this->schema; + } + + public function isNegation(): bool + { + return $this->negation; + } +} diff --git a/src/Schema/Diff/Condition/ColumnExists.php b/src/Schema/Diff/Condition/ColumnExists.php new file mode 100644 index 0000000..1078882 --- /dev/null +++ b/src/Schema/Diff/Condition/ColumnExists.php @@ -0,0 +1,28 @@ +table; + } + + public function getColumn(): string + { + return $this->column; + } +} diff --git a/src/Schema/Diff/Condition/IndexExists.php b/src/Schema/Diff/Condition/IndexExists.php new file mode 100644 index 0000000..3e4e685 --- /dev/null +++ b/src/Schema/Diff/Condition/IndexExists.php @@ -0,0 +1,30 @@ +table; + } + + /** @return string[] */ + public function getColumns(): array + { + return $this->columns; + } +} diff --git a/src/Schema/Diff/Condition/TableExists.php b/src/Schema/Diff/Condition/TableExists.php new file mode 100644 index 0000000..e576b82 --- /dev/null +++ b/src/Schema/Diff/Condition/TableExists.php @@ -0,0 +1,22 @@ +table; + } +} diff --git a/src/Schema/Diff/Change/Template/Generator.php b/src/Schema/Diff/Generator/Generator.php similarity index 94% rename from src/Schema/Diff/Change/Template/Generator.php rename to src/Schema/Diff/Generator/Generator.php index 58119d9..395faf7 100644 --- a/src/Schema/Diff/Change/Template/Generator.php +++ b/src/Schema/Diff/Generator/Generator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MakinaCorpus\QueryBuilder\Schema\Diff\Change\Template; +namespace MakinaCorpus\QueryBuilder\Schema\Diff\Generator; use MakinaCorpus\QueryBuilder\Error\QueryBuilderError; @@ -260,16 +260,15 @@ public function generateFromFile(): void $transactionMethodsString = \implode("\n\n", $additional['transaction']['methods']); $transactionUse = \implode("\n", $additional['transaction']['use']); - $schemaTransaction = <<changeLog = new ChangeLog(\$schemaManager); - } - - public function commit(): void - { - (\$this->onCommit)(\$this->changeLog->diff()); - } - - {$transactionMethodsString} - - /** - * Create a table builder. - */ - public function createTable(string \$name): TableBuilder - { - return new TableBuilder(parent: \$this, database: \$this->database, name: \$name, schema: \$this->schema); - } + protected readonly string \$database, + protected readonly string \$schema, + ) {} /** * Add new arbitrary change. @@ -313,14 +291,13 @@ public function createTable(string \$name): TableBuilder * @internal * For builders use only. */ - public function logChange(AbstractChange \$change): void - { - \$this->changeLog->add(\$change); - } + public abstract function logChange(ChangeLogItem \$change): void; + + {$transactionMethodsString} } EOT; - \file_put_contents(\dirname(__DIR__, 2) . '/SchemaTransaction.php', $schemaTransaction . "\n"); + \file_put_contents(\dirname(__DIR__) . '/Transaction/GeneratedAbstractTransaction.php', $changeLogBuilder . "\n"); } private function camelize(string $input, bool $first = true): string @@ -591,8 +568,6 @@ public function generateName(): string namespace MakinaCorpus\\QueryBuilder\\Schema\\Diff\\Change; - use MakinaCorpus\\QueryBuilder\\Schema\\Diff\\AbstractChange; - {$description} class {$className} extends AbstractChange {{$classConstantsString} @@ -610,7 +585,7 @@ public function __construct( } EOT; - \file_put_contents(\dirname(__DIR__) . '/' . $className . '.php', $file . "\n"); + \file_put_contents(\dirname(__DIR__) . '/Change/' . $className . '.php', $file . "\n"); /* * Transaction methods and code. @@ -640,7 +615,7 @@ public function {$transactionMethodName}( {$transactionPropertiesString} ?string \$schema = null, ): static { - \$this->changeLog->add( + \$this->logChange( new {$className}( {$transactionParametersString} schema: \$schema ?? \$this->schema, diff --git a/src/Schema/Diff/SchemaTransaction.php b/src/Schema/Diff/SchemaTransaction.php index ef317cb..0143602 100644 --- a/src/Schema/Diff/SchemaTransaction.php +++ b/src/Schema/Diff/SchemaTransaction.php @@ -4,537 +4,83 @@ namespace MakinaCorpus\QueryBuilder\Schema\Diff; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnAdd; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnModify; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnDrop; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnRename; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ConstraintDrop; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ConstraintModify; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ConstraintRename; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ForeignKeyAdd; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ForeignKeyModify; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ForeignKeyDrop; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ForeignKeyRename; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\IndexCreate; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\IndexDrop; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\IndexRename; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\PrimaryKeyAdd; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\PrimaryKeyDrop; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\TableDrop; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\TableRename; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\UniqueKeyAdd; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\UniqueKeyDrop; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; -use MakinaCorpus\QueryBuilder\Schema\SchemaManager; - -/** - * This code is generated using bin/generate_changes.php. - * - * Please do not modify it manually. - * - * @see \MakinaCorpus\QueryBuilder\Schema\Diff\Change\Template\Generator - * @see bin/generate_changes.php - */ -class SchemaTransaction +use MakinaCorpus\QueryBuilder\Schema\Diff\Condition\ColumnExists; +use MakinaCorpus\QueryBuilder\Schema\Diff\Condition\IndexExists; +use MakinaCorpus\QueryBuilder\Schema\Diff\Condition\TableExists; +use MakinaCorpus\QueryBuilder\Schema\Diff\Transaction\AbstractSchemaTransaction; +use MakinaCorpus\QueryBuilder\Schema\Diff\Transaction\NestedSchemaTransaction; +use MakinaCorpus\QueryBuilder\Schema\Diff\Transaction\TableBuilder; + +class SchemaTransaction extends AbstractSchemaTransaction { private ChangeLog $changeLog; public function __construct( - private readonly SchemaManager $schemaManager, - private readonly string $database, - private readonly string $schema, + string $database, + string $schema, private readonly \Closure $onCommit, ) { - $this->changeLog = new ChangeLog($schemaManager); + parent::__construct($database, $schema); } - + public function commit(): void { - ($this->onCommit)($this->changeLog->diff()); - } - - /** - * Add a COLUMN. - */ - public function addColumn( - string $table, - string $name, - string $type, - bool $nullable, - null|string $default = null, - null|string $collation = null, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ColumnAdd( - table: $table, - name: $name, - type: $type, - nullable: $nullable, - default: $default, - collation: $collation, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Add a COLUMN. - */ - public function modifyColumn( - string $table, - string $name, - null|string $type = null, - null|bool $nullable = null, - null|string $default = null, - bool $dropDefault = false, - null|string $collation = null, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ColumnModify( - table: $table, - name: $name, - type: $type, - nullable: $nullable, - default: $default, - dropDefault: $dropDefault, - collation: $collation, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Drop a COLUMN. - */ - public function dropColumn( - string $table, - string $name, - bool $cascade = false, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ColumnDrop( - table: $table, - name: $name, - cascade: $cascade, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Renames a COLUMN. - */ - public function renameColumn( - string $table, - string $name, - string $newName, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ColumnRename( - table: $table, - name: $name, - newName: $newName, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Drop an arbitrary constraint from a table. - */ - public function dropConstraint( - string $table, - string $name, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ConstraintDrop( - table: $table, - name: $name, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Modify an arbitrary constraint on a table. - */ - public function modifyConstraint( - string $table, - string $name, - bool $deferrable = true, - string $initially = ConstraintModify::INITIALLY_DEFERRED, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ConstraintModify( - table: $table, - name: $name, - deferrable: $deferrable, - initially: $initially, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Rename an arbitrary constraint. - */ - public function renameConstraint( - string $table, - string $name, - string $newName, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ConstraintRename( - table: $table, - name: $name, - newName: $newName, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Add a FOREIGN KEY constraint on a table. - */ - public function addForeignKey( - string $table, - array $columns, - string $foreignTable, - array $foreignColumns, - null|string $name = null, - null|string $foreignSchema = null, - string $onDelete = ForeignKeyAdd::ON_DELETE_NO_ACTION, - string $onUpdate = ForeignKeyAdd::ON_UPDATE_NO_ACTION, - bool $deferrable = true, - string $initially = ForeignKeyAdd::INITIALLY_DEFERRED, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ForeignKeyAdd( - table: $table, - name: $name, - columns: $columns, - foreignTable: $foreignTable, - foreignColumns: $foreignColumns, - foreignSchema: $foreignSchema, - onDelete: $onDelete, - onUpdate: $onUpdate, - deferrable: $deferrable, - initially: $initially, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Modify a FOREIGN KEY constraint on a table. - */ - public function modifyForeignKey( - string $table, - string $name, - string $onDelete = ForeignKeyModify::ON_DELETE_NO_ACTION, - string $onUpdate = ForeignKeyModify::ON_UPDATE_NO_ACTION, - bool $deferrable = true, - string $initially = ForeignKeyModify::INITIALLY_DEFERRED, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ForeignKeyModify( - table: $table, - name: $name, - onDelete: $onDelete, - onUpdate: $onUpdate, - deferrable: $deferrable, - initially: $initially, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; + ($this->onCommit)($this); } /** - * Drop a FOREIGN KEY constraint from a table. - */ - public function dropForeignKey( - string $table, - string $name, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ForeignKeyDrop( - table: $table, - name: $name, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Rename an arbitrary constraint. - */ - public function renameForeignKey( - string $table, - string $name, - string $newName, - ?string $schema = null, - ): static { - $this->changeLog->add( - new ForeignKeyRename( - table: $table, - name: $name, - newName: $newName, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Create an INDEX on a table. - */ - public function createIndex( - string $table, - array $columns, - null|string $name = null, - null|string $type = null, - ?string $schema = null, - ): static { - $this->changeLog->add( - new IndexCreate( - table: $table, - name: $name, - columns: $columns, - type: $type, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Drop an INDEX from a table. - */ - public function dropIndex( - string $table, - string $name, - ?string $schema = null, - ): static { - $this->changeLog->add( - new IndexDrop( - table: $table, - name: $name, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Rename an arbitrary constraint. - */ - public function renameIndex( - string $table, - string $name, - string $newName, - ?string $schema = null, - ): static { - $this->changeLog->add( - new IndexRename( - table: $table, - name: $name, - newName: $newName, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Add the PRIMARY KEY constraint on a table. - */ - public function addPrimaryKey( - string $table, - array $columns, - null|string $name = null, - ?string $schema = null, - ): static { - $this->changeLog->add( - new PrimaryKeyAdd( - table: $table, - name: $name, - columns: $columns, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; - } - - /** - * Drop the PRIMARY KEY constraint from a table. + * Create a table builder. */ - public function dropPrimaryKey( - string $table, - string $name, - ?string $schema = null, - ): static { - $this->changeLog->add( - new PrimaryKeyDrop( - table: $table, - name: $name, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; + public function createTable(string $name): TableBuilder + { + return new TableBuilder(parent: $this, database: $this->database, name: $name, schema: $this->schema); } /** - * Drop a table. + * If table exists then. */ - public function dropTable( - string $name, - bool $cascade = false, - ?string $schema = null, - ): static { - $this->changeLog->add( - new TableDrop( - name: $name, - cascade: $cascade, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; + public function ifTableExists(string $table): NestedSchemaTransaction + { + return $this->nestWithCondition(new TableExists($this->database, $this->schema, $table)); } /** - * Renames a table. + * If table does not exist then. */ - public function renameTable( - string $name, - string $newName, - ?string $schema = null, - ): static { - $this->changeLog->add( - new TableRename( - name: $name, - newName: $newName, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; + public function ifTableNotExists(string $table): NestedSchemaTransaction + { + return $this->nestWithCondition(new TableExists($this->database, $this->schema, $table, true)); } /** - * Create a UNIQUE constraint on a table. + * If column exists then. */ - public function addUniqueKey( - string $table, - array $columns, - null|string $name = null, - bool $nullsDistinct = true, - ?string $schema = null, - ): static { - $this->changeLog->add( - new UniqueKeyAdd( - table: $table, - name: $name, - columns: $columns, - nullsDistinct: $nullsDistinct, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; + public function ifColumnExists(string $table, string $column): NestedSchemaTransaction + { + return $this->nestWithCondition(new ColumnExists($this->database, $this->schema, $table, $column)); } /** - * Drop a UNIQUE constraint from a table. + * If column does not exist then. */ - public function dropUniqueKey( - string $table, - string $name, - ?string $schema = null, - ): static { - $this->changeLog->add( - new UniqueKeyDrop( - table: $table, - name: $name, - schema: $schema ?? $this->schema, - database: $this->database, - ) - ); - - return $this; + public function ifColumnNotExists(string $table, string $column): NestedSchemaTransaction + { + return $this->nestWithCondition(new ColumnExists($this->database, $this->schema, $table, $column, true)); } /** - * Create a table builder. + * If index exists then. */ - public function createTable(string $name): TableBuilder + public function ifIndexExists(string $table, array $columns): NestedSchemaTransaction { - return new TableBuilder(parent: $this, database: $this->database, name: $name, schema: $this->schema); + return $this->nestWithCondition(new IndexExists($this->database, $this->schema, $table, $columns)); } /** - * Add new arbitrary change. - * - * @internal - * For builders use only. + * If index does not exist then. */ - public function logChange(AbstractChange $change): void + public function ifIndexNotExists(string $table, array $columns): NestedSchemaTransaction { - $this->changeLog->add($change); + return $this->nestWithCondition(new IndexExists($this->database, $this->schema, $table, $columns, true)); } } diff --git a/src/Schema/Diff/Transaction/AbstractNestedSchemaTransaction.php b/src/Schema/Diff/Transaction/AbstractNestedSchemaTransaction.php new file mode 100644 index 0000000..601c1e5 --- /dev/null +++ b/src/Schema/Diff/Transaction/AbstractNestedSchemaTransaction.php @@ -0,0 +1,93 @@ +database; + } + + #[\Override] + public function getSchema(): string + { + return $this->schema; + } + + /** + * If table exists then. + */ + public function ifTableExists(string $table): DeepNestedSchemaTransaction + { + return $this->nestWithCondition(new TableExists($this->database, $this->schema, $table)); + } + + /** + * If table does not exist then. + */ + public function ifTableNotExists(string $table): DeepNestedSchemaTransaction + { + return $this->nestWithCondition(new TableExists($this->database, $this->schema, $table, true)); + } + + /** + * If column exists then. + */ + public function ifColumnExists(string $table, string $column): DeepNestedSchemaTransaction + { + return $this->nestWithCondition(new ColumnExists($this->database, $this->schema, $table, $column)); + } + + /** + * If column does not exist then. + */ + public function ifColumnNotExists(string $table, string $column): DeepNestedSchemaTransaction + { + return $this->nestWithCondition(new ColumnExists($this->database, $this->schema, $table, $column, true)); + } + + /** + * If index exists then. + */ + public function ifIndexExists(string $table, array $columns): DeepNestedSchemaTransaction + { + return $this->nestWithCondition(new IndexExists($this->database, $this->schema, $table, $columns)); + } + + /** + * If index does not exist then. + */ + public function ifIndexNotExists(string $table, array $columns): DeepNestedSchemaTransaction + { + return $this->nestWithCondition(new IndexExists($this->database, $this->schema, $table, $columns, true)); + } + + /** + * Get all conditions. No conditions means always execute. + */ + public function getConditions(): array + { + return $this->conditions; + } +} diff --git a/src/Schema/Diff/Transaction/AbstractSchemaTransaction.php b/src/Schema/Diff/Transaction/AbstractSchemaTransaction.php new file mode 100644 index 0000000..5ba8316 --- /dev/null +++ b/src/Schema/Diff/Transaction/AbstractSchemaTransaction.php @@ -0,0 +1,57 @@ +changeLog = new ChangeLog(); + } + + /** + * Get current change log. + */ + public function getChangeLog(): ChangeLog + { + return $this->changeLog; + } + + /** + * Create nested instance with given conditions. + */ + protected function nestWithCondition(AbstractCondition ...$conditions): NestedSchemaTransaction|DeepNestedSchemaTransaction + { + if ($this instanceof NestedSchemaTransaction || $this instanceof DeepNestedSchemaTransaction) { + $ret = new DeepNestedSchemaTransaction($this, $this->database, $this->schema, $conditions); + } else { + $ret = new NestedSchemaTransaction($this, $this->database, $this->schema, $conditions); + } + + $this->logChange($ret); + + return $ret; + } + + #[\Override] + public function logChange(ChangeLogItem $change): void + { + $this->changeLog->add($change); + } +} diff --git a/src/Schema/Diff/TableBuilder.php b/src/Schema/Diff/Transaction/AbstractTableBuilder.php similarity index 94% rename from src/Schema/Diff/TableBuilder.php rename to src/Schema/Diff/Transaction/AbstractTableBuilder.php index 5e0198b..3ece8ee 100644 --- a/src/Schema/Diff/TableBuilder.php +++ b/src/Schema/Diff/Transaction/AbstractTableBuilder.php @@ -2,19 +2,20 @@ declare(strict_types=1); -namespace MakinaCorpus\QueryBuilder\Schema\Diff; +namespace MakinaCorpus\QueryBuilder\Schema\Diff\Transaction; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnAdd; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ForeignKeyAdd; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\IndexCreate; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\PrimaryKeyAdd; -use MakinaCorpus\QueryBuilder\Schema\Diff\Change\UniqueKeyAdd; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\TableCreate; +use MakinaCorpus\QueryBuilder\Schema\Diff\Change\UniqueKeyAdd; /** - * Table builder, for chaining calls with schema transaction. + * @internal + * Exists because PHP has no genericity. */ -class TableBuilder +abstract class AbstractTableBuilder { /** @var ColumnAdd[] */ private array $columns = []; @@ -28,7 +29,7 @@ class TableBuilder private bool $temporary = false; public function __construct( - private readonly SchemaTransaction $parent, + private readonly GeneratedAbstractTransaction $parent, private readonly string $database, private readonly string $name, private string $schema, @@ -162,9 +163,9 @@ public function index(array $columns, ?string $name = null, ?string $type = null } /** - * Table is done. + * Create and log table. */ - public function endTable(): SchemaTransaction + protected function createAndLogTable(): void { $this->parent->logChange( new TableCreate( @@ -179,7 +180,5 @@ public function endTable(): SchemaTransaction uniqueKeys: $this->uniqueKeys, ), ); - - return $this->parent; } } diff --git a/src/Schema/Diff/Transaction/DeepNestedSchemaTransaction.php b/src/Schema/Diff/Transaction/DeepNestedSchemaTransaction.php new file mode 100644 index 0000000..a2a2bec --- /dev/null +++ b/src/Schema/Diff/Transaction/DeepNestedSchemaTransaction.php @@ -0,0 +1,37 @@ +database, name: $name, schema: $this->schema); + } + + /** + * End nested branch and go back to parent. + */ + public function endIf(): NestedSchemaTransaction + { + return $this->parent; + } +} diff --git a/src/Schema/Diff/Transaction/DeepNestedTableBuilder.php b/src/Schema/Diff/Transaction/DeepNestedTableBuilder.php new file mode 100644 index 0000000..5ab61e9 --- /dev/null +++ b/src/Schema/Diff/Transaction/DeepNestedTableBuilder.php @@ -0,0 +1,31 @@ +createAndLogTable(); + + return $this->parent; + } +} diff --git a/src/Schema/Diff/Transaction/GeneratedAbstractTransaction.php b/src/Schema/Diff/Transaction/GeneratedAbstractTransaction.php new file mode 100644 index 0000000..53ca612 --- /dev/null +++ b/src/Schema/Diff/Transaction/GeneratedAbstractTransaction.php @@ -0,0 +1,517 @@ +logChange( + new ColumnAdd( + table: $table, + name: $name, + type: $type, + nullable: $nullable, + default: $default, + collation: $collation, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Add a COLUMN. + */ + public function modifyColumn( + string $table, + string $name, + null|string $type = null, + null|bool $nullable = null, + null|string $default = null, + bool $dropDefault = false, + null|string $collation = null, + ?string $schema = null, + ): static { + $this->logChange( + new ColumnModify( + table: $table, + name: $name, + type: $type, + nullable: $nullable, + default: $default, + dropDefault: $dropDefault, + collation: $collation, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Drop a COLUMN. + */ + public function dropColumn( + string $table, + string $name, + bool $cascade = false, + ?string $schema = null, + ): static { + $this->logChange( + new ColumnDrop( + table: $table, + name: $name, + cascade: $cascade, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Renames a COLUMN. + */ + public function renameColumn( + string $table, + string $name, + string $newName, + ?string $schema = null, + ): static { + $this->logChange( + new ColumnRename( + table: $table, + name: $name, + newName: $newName, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Drop an arbitrary constraint from a table. + */ + public function dropConstraint( + string $table, + string $name, + ?string $schema = null, + ): static { + $this->logChange( + new ConstraintDrop( + table: $table, + name: $name, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Modify an arbitrary constraint on a table. + */ + public function modifyConstraint( + string $table, + string $name, + bool $deferrable = true, + string $initially = ConstraintModify::INITIALLY_DEFERRED, + ?string $schema = null, + ): static { + $this->logChange( + new ConstraintModify( + table: $table, + name: $name, + deferrable: $deferrable, + initially: $initially, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Rename an arbitrary constraint. + */ + public function renameConstraint( + string $table, + string $name, + string $newName, + ?string $schema = null, + ): static { + $this->logChange( + new ConstraintRename( + table: $table, + name: $name, + newName: $newName, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Add a FOREIGN KEY constraint on a table. + */ + public function addForeignKey( + string $table, + array $columns, + string $foreignTable, + array $foreignColumns, + null|string $name = null, + null|string $foreignSchema = null, + string $onDelete = ForeignKeyAdd::ON_DELETE_NO_ACTION, + string $onUpdate = ForeignKeyAdd::ON_UPDATE_NO_ACTION, + bool $deferrable = true, + string $initially = ForeignKeyAdd::INITIALLY_DEFERRED, + ?string $schema = null, + ): static { + $this->logChange( + new ForeignKeyAdd( + table: $table, + name: $name, + columns: $columns, + foreignTable: $foreignTable, + foreignColumns: $foreignColumns, + foreignSchema: $foreignSchema, + onDelete: $onDelete, + onUpdate: $onUpdate, + deferrable: $deferrable, + initially: $initially, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Modify a FOREIGN KEY constraint on a table. + */ + public function modifyForeignKey( + string $table, + string $name, + string $onDelete = ForeignKeyModify::ON_DELETE_NO_ACTION, + string $onUpdate = ForeignKeyModify::ON_UPDATE_NO_ACTION, + bool $deferrable = true, + string $initially = ForeignKeyModify::INITIALLY_DEFERRED, + ?string $schema = null, + ): static { + $this->logChange( + new ForeignKeyModify( + table: $table, + name: $name, + onDelete: $onDelete, + onUpdate: $onUpdate, + deferrable: $deferrable, + initially: $initially, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Drop a FOREIGN KEY constraint from a table. + */ + public function dropForeignKey( + string $table, + string $name, + ?string $schema = null, + ): static { + $this->logChange( + new ForeignKeyDrop( + table: $table, + name: $name, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Rename an arbitrary constraint. + */ + public function renameForeignKey( + string $table, + string $name, + string $newName, + ?string $schema = null, + ): static { + $this->logChange( + new ForeignKeyRename( + table: $table, + name: $name, + newName: $newName, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Create an INDEX on a table. + */ + public function createIndex( + string $table, + array $columns, + null|string $name = null, + null|string $type = null, + ?string $schema = null, + ): static { + $this->logChange( + new IndexCreate( + table: $table, + name: $name, + columns: $columns, + type: $type, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Drop an INDEX from a table. + */ + public function dropIndex( + string $table, + string $name, + ?string $schema = null, + ): static { + $this->logChange( + new IndexDrop( + table: $table, + name: $name, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Rename an arbitrary constraint. + */ + public function renameIndex( + string $table, + string $name, + string $newName, + ?string $schema = null, + ): static { + $this->logChange( + new IndexRename( + table: $table, + name: $name, + newName: $newName, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Add the PRIMARY KEY constraint on a table. + */ + public function addPrimaryKey( + string $table, + array $columns, + null|string $name = null, + ?string $schema = null, + ): static { + $this->logChange( + new PrimaryKeyAdd( + table: $table, + name: $name, + columns: $columns, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Drop the PRIMARY KEY constraint from a table. + */ + public function dropPrimaryKey( + string $table, + string $name, + ?string $schema = null, + ): static { + $this->logChange( + new PrimaryKeyDrop( + table: $table, + name: $name, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Drop a table. + */ + public function dropTable( + string $name, + bool $cascade = false, + ?string $schema = null, + ): static { + $this->logChange( + new TableDrop( + name: $name, + cascade: $cascade, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Renames a table. + */ + public function renameTable( + string $name, + string $newName, + ?string $schema = null, + ): static { + $this->logChange( + new TableRename( + name: $name, + newName: $newName, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Create a UNIQUE constraint on a table. + */ + public function addUniqueKey( + string $table, + array $columns, + null|string $name = null, + bool $nullsDistinct = true, + ?string $schema = null, + ): static { + $this->logChange( + new UniqueKeyAdd( + table: $table, + name: $name, + columns: $columns, + nullsDistinct: $nullsDistinct, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } + + /** + * Drop a UNIQUE constraint from a table. + */ + public function dropUniqueKey( + string $table, + string $name, + ?string $schema = null, + ): static { + $this->logChange( + new UniqueKeyDrop( + table: $table, + name: $name, + schema: $schema ?? $this->schema, + database: $this->database, + ) + ); + + return $this; + } +} diff --git a/src/Schema/Diff/Transaction/NestedSchemaTransaction.php b/src/Schema/Diff/Transaction/NestedSchemaTransaction.php new file mode 100644 index 0000000..13d3152 --- /dev/null +++ b/src/Schema/Diff/Transaction/NestedSchemaTransaction.php @@ -0,0 +1,35 @@ +database, name: $name, schema: $this->schema); + } + + /** + * End nested branch and go back to parent. + */ + public function endIf(): SchemaTransaction + { + return $this->parent; + } +} diff --git a/src/Schema/Diff/Transaction/NestedTableBuilder.php b/src/Schema/Diff/Transaction/NestedTableBuilder.php new file mode 100644 index 0000000..0b793e3 --- /dev/null +++ b/src/Schema/Diff/Transaction/NestedTableBuilder.php @@ -0,0 +1,31 @@ +createAndLogTable(); + + return $this->parent; + } +} diff --git a/src/Schema/Diff/Transaction/TableBuilder.php b/src/Schema/Diff/Transaction/TableBuilder.php new file mode 100644 index 0000000..4830af3 --- /dev/null +++ b/src/Schema/Diff/Transaction/TableBuilder.php @@ -0,0 +1,29 @@ +createAndLogTable(); + + return $this->parent; + } +} diff --git a/src/Schema/AbstractObject.php b/src/Schema/Read/AbstractObject.php similarity index 94% rename from src/Schema/AbstractObject.php rename to src/Schema/Read/AbstractObject.php index 95ae0c9..3f355be 100644 --- a/src/Schema/AbstractObject.php +++ b/src/Schema/Read/AbstractObject.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MakinaCorpus\QueryBuilder\Schema; +namespace MakinaCorpus\QueryBuilder\Schema\Read; abstract class AbstractObject extends ObjectId { diff --git a/src/Schema/Column.php b/src/Schema/Read/Column.php similarity index 98% rename from src/Schema/Column.php rename to src/Schema/Read/Column.php index 471757d..4e34613 100644 --- a/src/Schema/Column.php +++ b/src/Schema/Read/Column.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MakinaCorpus\QueryBuilder\Schema; +namespace MakinaCorpus\QueryBuilder\Schema\Read; class Column extends AbstractObject { diff --git a/src/Schema/ForeignKey.php b/src/Schema/Read/ForeignKey.php similarity index 97% rename from src/Schema/ForeignKey.php rename to src/Schema/Read/ForeignKey.php index a3edc56..c78b378 100644 --- a/src/Schema/ForeignKey.php +++ b/src/Schema/Read/ForeignKey.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MakinaCorpus\QueryBuilder\Schema; +namespace MakinaCorpus\QueryBuilder\Schema\Read; final class ForeignKey extends AbstractObject { diff --git a/src/Schema/Key.php b/src/Schema/Read/Key.php similarity index 95% rename from src/Schema/Key.php rename to src/Schema/Read/Key.php index 70b3b41..1cec736 100644 --- a/src/Schema/Key.php +++ b/src/Schema/Read/Key.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MakinaCorpus\QueryBuilder\Schema; +namespace MakinaCorpus\QueryBuilder\Schema\Read; class Key extends AbstractObject { diff --git a/src/Schema/ObjectId.php b/src/Schema/Read/ObjectId.php similarity index 97% rename from src/Schema/ObjectId.php rename to src/Schema/Read/ObjectId.php index aa20157..9c161b0 100644 --- a/src/Schema/ObjectId.php +++ b/src/Schema/Read/ObjectId.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MakinaCorpus\QueryBuilder\Schema; +namespace MakinaCorpus\QueryBuilder\Schema\Read; class ObjectId { diff --git a/src/Schema/Table.php b/src/Schema/Read/Table.php similarity index 98% rename from src/Schema/Table.php rename to src/Schema/Read/Table.php index 631c340..e82901d 100644 --- a/src/Schema/Table.php +++ b/src/Schema/Read/Table.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MakinaCorpus\QueryBuilder\Schema; +namespace MakinaCorpus\QueryBuilder\Schema\Read; use MakinaCorpus\QueryBuilder\Error\QueryBuilderError; diff --git a/src/Schema/SchemaManager.php b/src/Schema/SchemaManager.php index 57c045a..a73cd7a 100644 --- a/src/Schema/SchemaManager.php +++ b/src/Schema/SchemaManager.php @@ -4,14 +4,15 @@ namespace MakinaCorpus\QueryBuilder\Schema; +use MakinaCorpus\QueryBuilder\Error\Bridge\TableDoesNotExistError; +use MakinaCorpus\QueryBuilder\Error\QueryBuilderError; +use MakinaCorpus\QueryBuilder\Error\UnsupportedFeatureError; use MakinaCorpus\QueryBuilder\Expression; use MakinaCorpus\QueryBuilder\ExpressionFactory; use MakinaCorpus\QueryBuilder\QueryExecutor; -use MakinaCorpus\QueryBuilder\Error\QueryBuilderError; -use MakinaCorpus\QueryBuilder\Error\UnsupportedFeatureError; -use MakinaCorpus\QueryBuilder\Schema\Diff\AbstractChange; -use MakinaCorpus\QueryBuilder\Schema\Diff\ChangeLog; -use MakinaCorpus\QueryBuilder\Schema\Diff\SchemaTransaction; +use MakinaCorpus\QueryBuilder\Schema\Diff\Browser\ChangeLogBrowser; +use MakinaCorpus\QueryBuilder\Schema\Diff\Browser\SchemaWriterLogVisitor; +use MakinaCorpus\QueryBuilder\Schema\Diff\Change\AbstractChange; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnAdd; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnDrop; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\ColumnModify; @@ -33,18 +34,58 @@ use MakinaCorpus\QueryBuilder\Schema\Diff\Change\TableRename; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\UniqueKeyAdd; use MakinaCorpus\QueryBuilder\Schema\Diff\Change\UniqueKeyDrop; +use MakinaCorpus\QueryBuilder\Schema\Diff\Condition\AbstractCondition; +use MakinaCorpus\QueryBuilder\Schema\Diff\Condition\ColumnExists; +use MakinaCorpus\QueryBuilder\Schema\Diff\Condition\IndexExists; +use MakinaCorpus\QueryBuilder\Schema\Diff\Condition\TableExists; +use MakinaCorpus\QueryBuilder\Schema\Diff\SchemaTransaction; +use MakinaCorpus\QueryBuilder\Schema\Read\Column; +use MakinaCorpus\QueryBuilder\Schema\Read\ForeignKey; +use MakinaCorpus\QueryBuilder\Schema\Read\Key; +use MakinaCorpus\QueryBuilder\Schema\Read\Table; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; + +// @todo Because IDE bug, sorry. +\class_exists(Column::class); +\class_exists(ForeignKey::class); /** - * Schema alteration SQL statements in this class are mostly standard, but not - * quite. They basically are the lowest common denominator between PostgreSQL, - * SQLite and SQLServer. All three are very conservative with SQL standard so - * basically, the intersection of three is what we have closest to it. + * Most SQL here tries to be the closest possible to SQL standard. + * + * Sadly when it comes to schema introspection, SCHEMATA standard deviates + * for every vendor, so there is no real standard here, implementations will + * use it for reading because it will at least be resilient against different + * versions of the same vendor. + * + * For writing, most implementations come closer to standard than for schema + * introspection, so we can write almost standard SQL in this implementation. + * There are a few exceptions, documented along the way, sometime all vendors + * have their own dialect, in this case, we almost always prefere PostgreSQL + * in this class. */ -abstract class SchemaManager +abstract class SchemaManager implements LoggerAwareInterface { + use LoggerAwareTrait; + public function __construct( protected readonly QueryExecutor $queryExecutor, - ) {} + ) { + $this->setLogger(new NullLogger()); + } + + /** + * Get instance query executor. + * + * @internal + * In most cases, you should not use this, it will servce for some edge + * cases, such as transaction handling in the schema writer visitor. + */ + public function getQueryExecutor(): QueryExecutor + { + return $this->queryExecutor; + } /** * Does this platform supports DDL transactions. @@ -159,16 +200,13 @@ protected abstract function getTableReverseForeignKeys(string $database, string public function modify(string $database, string $schema = 'public'): SchemaTransaction { return new SchemaTransaction( - $this, $database, $schema, - function (ChangeLog $changeLog) { - // @todo start transaction - foreach ($changeLog->getAll() as $change) { - $this->apply($change); - } - // @todo end transaction - } + function (SchemaTransaction $transaction) { + $browser = new ChangeLogBrowser(); + $browser->addVisitor(new SchemaWriterLogVisitor($this)); + $browser->browse($transaction); + }, ); } @@ -249,7 +287,7 @@ protected function columnModifyToAdd(ColumnModify $change): ColumnAdd * @todo * Maybe later, some normalization here with characters sets. */ - protected function writeColumnSpecCollation(string $collation): Expression + protected function doWriteColumnCollation(string $collation): Expression { return $this->raw('COLLATE ?::id', $collation); } @@ -264,7 +302,7 @@ protected function writeColumnSpecCollation(string $collation): Expression * - but there might be constants (such as CURRENT_TIME for example), * - or function calls! */ - protected function writeColumnSpecDefault(string $type, string $default): Expression + protected function doWriteColumnDefault(string $type, string $default): Expression { return match ($type) { default => $this->raw($default), @@ -278,7 +316,7 @@ protected function writeColumnSpecDefault(string $type, string $default): Expres * Here, we might want to plug over the converter in order to normalize * type depending upon the RDBMS. */ - protected function writeColumnSpecType(ColumnAdd|ColumnModify $change): Expression + protected function doWriteColumnType(ColumnAdd|ColumnModify $change): Expression { if (!$type = $change->getType()) { throw new QueryBuilderError("You cannot change collation without specifying a new type."); @@ -288,7 +326,7 @@ protected function writeColumnSpecType(ColumnAdd|ColumnModify $change): Expressi $pieces = [$this->raw($type)]; if ($collation = $change->getCollation()) { - $pieces[] = $this->writeColumnSpecCollation($collation); + $pieces[] = $this->doWriteColumnCollation($collation); } return $this->raw(\implode(' ', \array_fill(0, \count($pieces), '?')), [...$pieces]); @@ -299,7 +337,7 @@ protected function writeColumnSpecType(ColumnAdd|ColumnModify $change): Expressi * * Column specification. */ - protected function writeColumnSpec(ColumnAdd $change): Expression + protected function doWriteColumn(ColumnAdd $change): Expression { $pieces = []; @@ -308,14 +346,14 @@ protected function writeColumnSpec(ColumnAdd $change): Expression } if ($default = $change->getDefault()) { - $this->raw('DEFAULT ?', [$default]); + $this->raw('DEFAULT ?', [$this->doWriteColumnDefault($change->getType(), $default)]); } return $this->raw( '?::id ? ' . \implode(' ', \array_fill(0, \count($pieces), '?')), [ $change->getName(), - $this->writeColumnSpecType($change), + $this->doWriteColumnType($change), ...$pieces, ], ); @@ -328,7 +366,7 @@ protected function writeColumnSpec(ColumnAdd $change): Expression */ protected function writeColumnAdd(ColumnAdd $change): iterable|Expression { - return $this->raw('ALTER TABLE ? ADD COLUMN ?', [$this->table($change), $this->writeColumnSpec($change)]); + return $this->raw('ALTER TABLE ? ADD COLUMN ?', [$this->table($change), $this->doWriteColumn($change)]); } /** @@ -365,7 +403,7 @@ protected function writeColumnModify(ColumnModify $change): iterable|Expression } if ($change->getType() || $change->getCollation()) { - $pieces[] = $this->raw('ALTER COLUMN ?::id TYPE ?', [$name, $this->writeColumnSpecType($change)]); + $pieces[] = $this->raw('ALTER COLUMN ?::id TYPE ?', [$name, $this->doWriteColumnType($change)]); } if (null !== ($nullable = $change->isNullable())) { @@ -418,7 +456,7 @@ protected function writeColumnRename(ColumnRename $change): iterable|Expression * * Constraint specification. */ - protected function writeConstraintSpec(?string $name, Expression $spec): Expression + protected function doWriteConstraint(?string $name, Expression $spec): Expression { if ($name) { return $this->raw('CONSTRAINT ?::id ?', [$name, $spec]); @@ -433,7 +471,7 @@ protected function writeConstraintSpec(?string $name, Expression $spec): Express * * @todo SQLite does not support this and requires a table copy. */ - protected function writeConstraintDropSpec(string $name, string $table, string $schema): Expression + protected function doWriteConstraintDrop(string $name, string $table, string $schema): Expression { return $this->raw('ALTER TABLE ? DROP CONSTRAINT ?::id', [$this->table($table, $schema), $name]); } @@ -445,7 +483,7 @@ protected function writeConstraintDropSpec(string $name, string $table, string $ */ protected function writeConstraintDrop(ConstraintDrop $change): iterable|Expression { - return $this->writeConstraintDropSpec($change->getName(), $change->getTable(), $change->getSchema()); + return $this->doWriteConstraintDrop($change->getName(), $change->getTable(), $change->getSchema()); } /** @@ -471,7 +509,7 @@ protected function writeConstraintRename(ConstraintRename $change): iterable|Exp /** * Write the "CASCADE|NO ACTION|RESTRICT|SET DEFAULT|SET NULL" FOREIGN KEY clause. */ - protected function writeForeignKeySpecBehavior(string $behavior): Expression + protected function doWriteForeignKeyBehavior(string $behavior): Expression { return match ($behavior) { ForeignKeyAdd::ON_DELETE_CASCADE, ForeignKeyAdd::ON_UPDATE_CASCADE => $this->raw('CASCADE'), @@ -485,7 +523,7 @@ protected function writeForeignKeySpecBehavior(string $behavior): Expression /** * Write the "INITIALLY DEFERRED|IMMEDIATE" FOREIGN KEY clause. */ - protected function writeForeignKeySpecInitially(string $initially): Expression + protected function doWriteForeignKeyInitially(string $initially): Expression { return match ($initially) { ForeignKeyAdd::INITIALLY_DEFERRED, ForeignKeyAdd::INITIALLY_DEFERRED => $this->raw('INITIALLY DEFERRED'), @@ -498,14 +536,14 @@ protected function writeForeignKeySpecInitially(string $initially): Expression * * Foreign key constraint specification. */ - protected function writeForeignKeySpec(ForeignKeyAdd $change): Expression + protected function doWriteForeignKey(ForeignKeyAdd $change): Expression { $suffix = []; if ($deleteBehaviour = $change->getOnDelete()) { - $suffix[] = $this->raw('ON DELETE ?', $this->writeForeignKeySpecBehavior($deleteBehaviour)); + $suffix[] = $this->raw('ON DELETE ?', $this->doWriteForeignKeyBehavior($deleteBehaviour)); } if ($updateBehaviour = $change->getOnUpdate()) { - $suffix[] = $this->raw('ON UPDATE ?', $this->writeForeignKeySpecBehavior($updateBehaviour)); + $suffix[] = $this->raw('ON UPDATE ?', $this->doWriteForeignKeyBehavior($updateBehaviour)); } if ($change->isDeferrable()) { $suffix[] = $this->raw('DEFERRABLE'); @@ -513,10 +551,10 @@ protected function writeForeignKeySpec(ForeignKeyAdd $change): Expression $suffix[] = $this->raw('NOT DEFERRABLE'); } if ($initially = $change->getInitially()) { - $suffix[] = $this->writeForeignKeySpecInitially($initially); + $suffix[] = $this->doWriteForeignKeyInitially($initially); } - return $this->writeConstraintSpec( + return $this->doWriteConstraint( $change->getName(), $this->raw( 'FOREIGN KEY (?::id[]) REFERENCES ? (?::id[]) ' . \implode('', \array_fill(0, \count($suffix), ' ?')), @@ -537,7 +575,7 @@ protected function writeForeignKeySpec(ForeignKeyAdd $change): Expression */ protected function writeForeignKeyAdd(ForeignKeyAdd $change): iterable|Expression { - return $this->raw('ALTER TABLE ? ADD ?', [$this->table($change), $this->writeForeignKeySpec($change)]); + return $this->raw('ALTER TABLE ? ADD ?', [$this->table($change), $this->doWriteForeignKey($change)]); } /** @@ -557,7 +595,7 @@ protected function writeForeignKeyModify(ForeignKeyModify $change): iterable|Exp */ protected function writeForeignKeyDrop(ForeignKeyDrop $change): iterable|Expression { - return $this->writeConstraintDropSpec($change->getName(), $change->getTable(), $change->getSchema()); + return $this->doWriteConstraintDrop($change->getName(), $change->getTable(), $change->getSchema()); } /** @@ -619,7 +657,7 @@ protected function writeIndexRename(IndexRename $change): iterable|Expression */ protected function writePrimaryKeyAdd(PrimaryKeyAdd $change): iterable|Expression { - return $this->raw('ALTER TABLE ? ADD ?', [$this->table($change), $this->writePrimaryKeySpec($change)]); + return $this->raw('ALTER TABLE ? ADD ?', [$this->table($change), $this->doWritePrimaryKey($change)]); } /** @@ -631,15 +669,15 @@ protected function writePrimaryKeyAdd(PrimaryKeyAdd $change): iterable|Expressio */ protected function writePrimaryKeyDrop(PrimaryKeyDrop $change): iterable|Expression { - return $this->writeConstraintDropSpec($change->getName(), $change->getTable(), $change->getSchema()); + return $this->doWriteConstraintDrop($change->getName(), $change->getTable(), $change->getSchema()); } /** * Override if standard SQL is not enough. */ - protected function writePrimaryKeySpec(PrimaryKeyAdd $change): Expression + protected function doWritePrimaryKey(PrimaryKeyAdd $change): Expression { - return $this->writeConstraintSpec( + return $this->doWriteConstraint( $change->getName(), $this->raw('PRIMARY KEY (?::id[])', [$change->getColumns()]), ); @@ -648,7 +686,7 @@ protected function writePrimaryKeySpec(PrimaryKeyAdd $change): Expression /** * Override if standard SQL is not enough. */ - protected function writeTableCreateSpecUniqueKey(UniqueKeyAdd $change): Expression + protected function doWriteTableCreateUniqueKey(UniqueKeyAdd $change): Expression { if (!$change->isNullsDistinct()) { // @todo Implement this with PostgreSQL. @@ -657,7 +695,7 @@ protected function writeTableCreateSpecUniqueKey(UniqueKeyAdd $change): Expressi $spec = $this->raw('UNIQUE (?::id[])', [$change->getColumns()]); - return $this->writeConstraintSpec($change->getName(), $spec); + return $this->doWriteConstraint($change->getName(), $spec); } /** @@ -667,15 +705,15 @@ protected function writeTableCreateSpecUniqueKey(UniqueKeyAdd $change): Expressi */ protected function writeTableCreate(TableCreate $change): iterable|Expression { - $pieces = \array_map($this->writeColumnSpec(...), $change->getColumns()); + $pieces = \array_map($this->doWriteColumn(...), $change->getColumns()); if ($primaryKey = $change->getPrimaryKey()) { - $pieces[] = $this->writePrimaryKeySpec($primaryKey); + $pieces[] = $this->doWritePrimaryKey($primaryKey); } foreach ($change->getUniqueKeys() as $uniqueKey) { - $pieces[] = $this->writeTableCreateSpecUniqueKey($uniqueKey); + $pieces[] = $this->doWriteTableCreateUniqueKey($uniqueKey); } foreach ($change->getForeignKeys() as $foreignKey) { - $pieces[] = $this->writeForeignKeySpec($foreignKey); + $pieces[] = $this->doWriteForeignKey($foreignKey); } $placeholder = \implode(', ', \array_fill(0, \count($pieces), '?')); @@ -747,10 +785,68 @@ protected function writeUniqueKeyDrop(UniqueKeyDrop $change): iterable|Expressio return $this->raw('DROP INDEX ?::id', [$change->getName()]); } + /** + * Override if standard SQL is not enough. + */ + protected function evaluateConditionColumnExists(ColumnExists $condition): bool + { + try { + return \in_array( + $condition->getColumn(), + $this + ->getTable( + $condition->getDatabase(), + $condition->getTable(), + $condition->getSchema() + ) + ->getColumnNames() + ); + } catch (TableDoesNotExistError) { + return false; + } + } + + /** + * Override if standard SQL is not enough. + */ + protected function evaluateConditionIndexExists(IndexExists $condition): bool + { + throw new UnsupportedFeatureError("Not implemented yet."); + } + + /** + * Override if standard SQL is not enough. + */ + protected function evaluateConditionTableExists(TableExists $condition): bool + { + return $this->tableExists($condition->getDatabase(), $condition->getTable(), $condition->getSchema()); + } + + /** + * Condition evualation. + * + * @internal + * For SchemaWriterLogVisitor usage only. + */ + public function evaluateCondition(AbstractCondition $condition): bool + { + $matches = (bool) match (\get_class($condition)) { + ColumnExists::class => $this->evaluateConditionColumnExists($condition), + IndexExists::class => $this->evaluateConditionIndexExists($condition), + TableExists::class => $this->evaluateConditionTableExists($condition), + default => throw new QueryBuilderError(\sprintf("Unsupported condition: %s", \get_class($condition))), + }; + + return $condition->isNegation() ? !$matches : $matches; + } + /** * Apply a given change in the current schema. + * + * @internal + * For SchemaWriterLogVisitor usage only. */ - protected function apply(AbstractChange $change): void + public function applyChange(AbstractChange $change): void { $expressions = match (\get_class($change)) { ColumnAdd::class => $this->writeColumnAdd($change), @@ -777,16 +873,6 @@ protected function apply(AbstractChange $change): void default => throw new QueryBuilderError(\sprintf("Unsupported alteration operation: %s", \get_class($change))), }; - $this->executeChange($change, $expressions); - } - - /** - * Really execute the change. - * - * @param Expression|iterable $expressions - */ - protected function executeChange(AbstractChange $change, iterable|Expression $expressions): void - { foreach (\is_iterable($expressions) ? $expressions : [$expressions] as $expression) { $this->queryExecutor->executeStatement($expression); } diff --git a/tests/Platform/Schema/AbstractSchemaTestCase.php b/tests/Platform/Schema/AbstractSchemaTestCase.php index 6d41787..826f4aa 100644 --- a/tests/Platform/Schema/AbstractSchemaTestCase.php +++ b/tests/Platform/Schema/AbstractSchemaTestCase.php @@ -4,11 +4,11 @@ namespace MakinaCorpus\QueryBuilder\Tests\Platform\Schema; -use MakinaCorpus\QueryBuilder\Platform; +use MakinaCorpus\QueryBuilder\Error\Bridge\TableDoesNotExistError; use MakinaCorpus\QueryBuilder\Error\QueryBuilderError; use MakinaCorpus\QueryBuilder\Error\UnsupportedFeatureError; -use MakinaCorpus\QueryBuilder\Error\Bridge\TableDoesNotExistError; -use MakinaCorpus\QueryBuilder\Schema\ForeignKey; +use MakinaCorpus\QueryBuilder\Platform; +use MakinaCorpus\QueryBuilder\Schema\Read\ForeignKey; use MakinaCorpus\QueryBuilder\Schema\SchemaManager; use MakinaCorpus\QueryBuilder\Tests\FunctionalTestCase; @@ -935,6 +935,62 @@ public function testUniqueKeyDrop(): void self::expectNotToPerformAssertions(); } + public function testIfTableExists(): void + { + $this + ->getSchemaManager() + ->modify('test_db') + ->ifTableExists('org') + ->addColumn('org', 'if_added_col', 'text', true) + ->endIf() + ->ifTableNotExists('org') + ->addColumn('org', 'if_not_added_col', 'text', true) + ->endIf() + ->commit() + ; + + $columnNames = $this + ->getSchemaManager() + ->getTable('test_db', 'org') + ->getColumnNames() + ; + + self::assertContains('if_added_col', $columnNames); + self::assertNotContains('if_not_added_col', $columnNames); + } + + public function testIfColumnExists(): void + { + $this + ->getSchemaManager() + ->modify('test_db') + ->ifColumnExists('org', 'role') + ->addColumn('org', 'if_added_col_2', 'text', true) + ->endIf() + ->ifColumnNotExists('org', 'role') + ->addColumn('org', 'if_not_added_col_2', 'text', true) + ->endIf() + ->ifColumnExists('org', 'role_nope') + ->addColumn('org', 'if_added_col_3', 'text', true) + ->endIf() + ->ifColumnNotExists('org', 'role_nope') + ->addColumn('org', 'if_not_added_col_3', 'text', true) + ->endIf() + ->commit() + ; + + $columnNames = $this + ->getSchemaManager() + ->getTable('test_db', 'org') + ->getColumnNames() + ; + + self::assertContains('if_added_col_2', $columnNames); + self::assertNotContains('if_not_added_col_2', $columnNames); + self::assertNotContains('if_added_col_3', $columnNames); + self::assertContains('if_not_added_col_3', $columnNames); + } + public function testListTables(): void { $tables = $this->getSchemaManager()->listTables('test_db'); diff --git a/tests/Schema/SchemaTransactionTest.php b/tests/Schema/SchemaTransactionTest.php new file mode 100644 index 0000000..27c8687 --- /dev/null +++ b/tests/Schema/SchemaTransactionTest.php @@ -0,0 +1,106 @@ + null); + + $transaction + ->ifTableNotExists('users') + ->createTable('users') + ->column('id', 'serial', false) + ->primaryKey(['id']) + ->endTable() + ->endIf() + ->ifColumnNotExists('users', 'email') + ->ifColumnNotExists('users', 'email') + ->addColumn('users', 'email', 'text', true) + ->endIf() + ->ifColumnExists('users', 'skip') + ->addColumn('users', 'email', 'text', true) + ->endIf() + ->addUniqueKey('users', ['email']) + ->endIf() + ->addColumn('users', 'name', 'text', false) + ->commit() + ; + + $visitor = new class () extends ChangeLogVisitor + { + private array $lines = []; + + #[\Override] + public function enter(AbstractNestedSchemaTransaction $nested, int $depth): void + { + $this->lines[] = "Entering level " . $depth; + } + + #[\Override] + public function leave(AbstractNestedSchemaTransaction $nested, int $depth): void + { + $this->lines[] = "Exiting level " . $depth; + } + + #[\Override] + public function skip(AbstractNestedSchemaTransaction $nested, int $depth): void + { + $this->lines[] = "Skipping level " . $depth; + } + + #[\Override] + public function evaluate(AbstractCondition $condition): bool + { + if ($condition instanceof ColumnExists && 'skip' === $condition->getColumn()) { + return false; + } + return true; + } + + #[\Override] + public function apply(AbstractChange $change): void + { + $this->lines[] = "Applying " . \get_class($change); + } + + public function getOutput(): string + { + return \implode("\n", $this->lines); + } + }; + + $browser = new ChangeLogBrowser(); + $browser->addVisitor($visitor); + $browser->browse($transaction); + + self::assertSame( + $visitor->getOutput(), + <<