diff --git a/composer.json b/composer.json index 1477dcd..9586f75 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": ">=8.0", - "cycle/database": "^2.0", + "cycle/database": "^2.3", "spiral/core": "^2.7", "spiral/files": "^2.7", "spiral/tokenizer": "^2.7", diff --git a/src/FileRepository.php b/src/FileRepository.php index 7a47fff..b64175d 100644 --- a/src/FileRepository.php +++ b/src/FileRepository.php @@ -136,7 +136,7 @@ private function getFiles(string $directory): \Generator { foreach ($this->files->getFiles($directory, '*.php') as $filename) { $reflection = new ReflectionFile($filename); - $definition = \explode('_', \basename($filename)); + $definition = \explode('_', \basename($filename, '.php'), 3); if (\count($definition) < 3) { throw new RepositoryException("Invalid migration filename '{$filename}'"); @@ -152,11 +152,7 @@ private function getFiles(string $directory): \Generator 'class' => $reflection->getClasses()[0], 'created' => $created, 'chunk' => $definition[1], - 'name' => \str_replace( - '.php', - '', - \implode('_', \array_slice($definition, 2)) - ), + 'name' => $definition[2], ]; } } diff --git a/src/Operation/Column/Column.php b/src/Operation/Column/Column.php index c5045da..dd9dd6f 100644 --- a/src/Operation/Column/Column.php +++ b/src/Operation/Column/Column.php @@ -43,21 +43,27 @@ protected function declareColumn(AbstractTable $schema): AbstractColumn //Type configuring if (method_exists($column, $this->type)) { $arguments = []; + $variadic = false; $method = new \ReflectionMethod($column, $this->type); foreach ($method->getParameters() as $parameter) { if ($this->hasOption($parameter->getName())) { - $arguments[] = $this->getOption($parameter->getName()); + $arguments[$parameter->getName()] = $this->getOption($parameter->getName()); } elseif (!$parameter->isOptional()) { throw new ColumnException( "Option '{$parameter->getName()}' are required to define column with type '{$this->type}'" ); - } else { - $arguments[] = $parameter->getDefaultValue(); + } elseif ($parameter->isDefaultValueAvailable()) { + $arguments[$parameter->getName()] = $parameter->getDefaultValue(); + } elseif ($parameter->isVariadic()) { + $variadic = true; } } - call_user_func_array([$column, $this->type], $arguments); + \call_user_func_array( + [$column, $this->type], + $variadic ? $arguments + $this->options + $column->getAttributes() : $arguments, + ); } else { $column->type($this->type); } diff --git a/src/Operation/Table/Truncate.php b/src/Operation/Table/Truncate.php new file mode 100644 index 0000000..f3758cb --- /dev/null +++ b/src/Operation/Table/Truncate.php @@ -0,0 +1,32 @@ +getSchema($this->getTable()); + $database = $this->database ?? '[default]'; + + if (!$schema->exists()) { + throw new TableException( + "Unable to truncate table '{$database}'.'{$this->getTable()}', table does not exists" + ); + } + + $capsule->getDatabase()->execute(sprintf('TRUNCATE "%s" %s', $this->getTable(), $this->strategy)); + } +} diff --git a/src/V2/Column.php b/src/V2/Column.php new file mode 100644 index 0000000..36ef085 --- /dev/null +++ b/src/V2/Column.php @@ -0,0 +1,62 @@ +type = $type; + $this->length = $length; + } + + public function notNull(): self + { + $this->isNotNull = true; + return $this; + } + + public function null(): self + { + $this->isNotNull = false; + return $this; + } + + public function unique(): self + { + $this->isUnique = true; + return $this; + } + + public function check($check): self + { + $this->check = $check; + return $this; + } + + public function defaultValue(?string $default): self + { + if ($default === null) { + $this->null(); + } + + $this->default = $default; + return $this; + } + + public function comment(?string $comment): self + { + $this->comment = $comment; + return $this; + } +} diff --git a/src/V2/ColumnParser.php b/src/V2/ColumnParser.php new file mode 100644 index 0000000..fdaaf89 --- /dev/null +++ b/src/V2/ColumnParser.php @@ -0,0 +1,63 @@ +column = $column; + } + + public function getType(): string + { + return $this->column->type; + } + + public function getLength(): ?int + { + return $this->column->length; + } + + public function isUnique(): bool + { + return $this->column->isUnique; + } + + public function getDefault(): ?string + { + return $this->column->default; + } + + public function isNotNull(): bool + { + return $this->column->isNotNull; + } + + public function getCheck(): ?string + { + return $this->column->check; + } + + public function getComment(): ?string + { + return $this->column->comment; + } + + public function getOptions(): array + { + $options = []; + $options['unique'] = $this->isUnique(); + $options['nullable'] = !$this->isNotNull(); + + if ($this->getDefault() !== null) { + $options['default'] = $this->getDefault(); + } + + return $options; + } +} diff --git a/src/V2/ColumnTrait.php b/src/V2/ColumnTrait.php new file mode 100644 index 0000000..dd9423f --- /dev/null +++ b/src/V2/ColumnTrait.php @@ -0,0 +1,79 @@ +notNull(); + + return $column; + } + + protected function bigPrimaryKey($length = null): Column + { + $column = new Column(ColumnType::TYPE_BIGPK, $length); + $column->notNull(); + + return $column; + } + + protected function string($length = null): Column + { + return new Column(ColumnType::TYPE_STRING, $length); + } + + protected function text(): Column + { + return new Column(ColumnType::TYPE_TEXT); + } + + protected function integer($length = null): Column + { + return new Column(ColumnType::TYPE_INTEGER, $length); + } + + protected function bigInteger($length = null): Column + { + return new Column(ColumnType::TYPE_BIGINT, $length); + } + + protected function numeric($precision = null): Column + { + return new Column(ColumnType::TYPE_NUMERIC, $precision); + } + + protected function dateTime(): Column + { + return new Column(ColumnType::TYPE_DATETIME); + } + + protected function boolean(): Column + { + return new Column(ColumnType::TYPE_BOOLEAN); + } + + protected function money(): Column + { + return new Column(ColumnType::TYPE_MONEY); + } + + protected function json(): Column + { + return new Column(ColumnType::TYPE_JSON); + } + + protected function point(): Column + { + return new Column(ColumnType::TYPE_POINT); + } + + protected function customType(string $type): Column + { + return new Column($type); + } +} diff --git a/src/V2/ColumnType.php b/src/V2/ColumnType.php new file mode 100644 index 0000000..fc70e9f --- /dev/null +++ b/src/V2/ColumnType.php @@ -0,0 +1,31 @@ +value = $value; + } + + public static function cascade(): self + { + return new self(self::CASCADE); + } + + public static function setNull(): self + { + return new self(self::SET_NULL); + } + + public static function setDefault(): self + { + return new self(self::SET_DEFAULT); + } + + public static function restrict(): self + { + return new self(self::RESTRICT); + } + + public static function noAction(): self + { + return new self(self::NO_ACTION); + } + + public function value(): string + { + return $this->value; + } +} diff --git a/src/V2/ForeignKey.php b/src/V2/ForeignKey.php new file mode 100644 index 0000000..a12791f --- /dev/null +++ b/src/V2/ForeignKey.php @@ -0,0 +1,46 @@ +innerKeys = $innerKey; + $this->outerTable = $table; + $this->outerKeys = $outerKey; + + $this->onDelete = FKAction::cascade(); + $this->onUpdate = FKAction::cascade(); + } + + public function name(?string $name): self + { + $this->name = $name; + + return $this; + } + + public function onDelete(FKAction $action): self + { + $this->onDelete = $action; + + return $this; + } + + public function onUpdate(FKAction $action): self + { + $this->onUpdate = $action; + + return $this; + } +} diff --git a/src/V2/ForeignKeyParser.php b/src/V2/ForeignKeyParser.php new file mode 100644 index 0000000..7ce3452 --- /dev/null +++ b/src/V2/ForeignKeyParser.php @@ -0,0 +1,68 @@ +foreignKey = $foreignKey; + $this->tableName = $tableName; + } + + public function getName(): string + { + return $this->index->name ?? sprintf( + '%s_%s_%s_%s_fk', + $this->getInnerTable(), + $this->getInnerKeys()[0], + $this->getOuterTable(), + $this->getOuterKeys()[0] + ); + } + + public function getInnerTable(): string + { + return $this->tableName; + } + + public function getInnerKeys(): array + { + return $this->foreignKey->innerKeys; + } + + public function getOuterTable(): string + { + return $this->foreignKey->outerTable; + } + + public function getOuterKeys(): array + { + return $this->foreignKey->outerKeys; + } + + public function getOnDelete(): string + { + return $this->foreignKey->onDelete->value(); + } + + public function getOnUpdate(): string + { + return $this->foreignKey->onUpdate->value(); + } + + public function getOptions(): array + { + return [ + 'name' => $this->getName(), + 'delete' => $this->getOnDelete(), + 'update' => $this->getOnUpdate(), + ]; + } +} diff --git a/src/V2/Index.php b/src/V2/Index.php new file mode 100644 index 0000000..43e54fe --- /dev/null +++ b/src/V2/Index.php @@ -0,0 +1,33 @@ +fields = $fields; + $this->name = $name; + } + + public function unique(): self + { + $this->unique = true; + + return $this; + } + + public function notUnique(): self + { + $this->unique = false; + + return $this; + } +} diff --git a/src/V2/IndexParser.php b/src/V2/IndexParser.php new file mode 100644 index 0000000..8c1e025 --- /dev/null +++ b/src/V2/IndexParser.php @@ -0,0 +1,42 @@ +index = $index; + $this->tableName = $tableName; + } + + public function getField(): array + { + return $this->index->fields; + } + + public function getName(): string + { + return $this->index->name + ?? sprintf('%s_%s_index', $this->tableName, implode('_', $this->getField())); + } + + public function isUnique(): bool + { + return $this->index->unique; + } + + public function getOptions(): array + { + return [ + 'name' => $this->getName(), + 'unique' => $this->isUnique(), + ]; + } +} diff --git a/src/V2/Migration.php b/src/V2/Migration.php new file mode 100644 index 0000000..fb63249 --- /dev/null +++ b/src/V2/Migration.php @@ -0,0 +1,85 @@ +capsule = $capsule; + + return $migration; + } + + public function withState(State $state): MigrationInterface + { + $migration = clone $this; + $migration->state = $state; + + return $migration; + } + + public function getState(): State + { + if (empty($this->state)) { + throw new MigrationException('Unable to get migration state, no state are set'); + } + + return $this->state; + } + + protected function database(): DatabaseInterface + { + if (empty($this->capsule)) { + throw new MigrationException('Unable to get database, no capsule are set'); + } + + return $this->capsule->getDatabase(); + } + + protected function table(string $tableName): TableBlueprint + { + return new TableBlueprint($tableName, $this->capsule); + } + + protected function index(array $fields, ?string $name = null): Index + { + return new Index($fields, $name); + } + + protected function foreignKey(array $fields, string $table, array $outerKeys): ForeignKey + { + return new ForeignKey($fields, $table, $outerKeys); + } + + protected function enum(string $name): TypeBuilder + { + return new TypeBuilder( + $name, + TypeBuilder::ENUM, + $this->database() + ); + } +} diff --git a/src/V2/TableBlueprint.php b/src/V2/TableBlueprint.php new file mode 100644 index 0000000..7f419a1 --- /dev/null +++ b/src/V2/TableBlueprint.php @@ -0,0 +1,236 @@ +tableName = $tableName; + $this->capsule = $capsule; + } + + public function getSchema(): AbstractTable + { + return $this->capsule->getSchema($this->tableName); + } + + public function addColumns(array $columns): self + { + foreach ($columns as $name => $column) { + $columnParser = new ColumnParser($column); + $this->addOperation( + new Operation\Column\Add( + $this->tableName, + $name, + $columnParser->getType(), + $columnParser->getOptions() + ) + ); + } + + return $this; + } + + public function alterColumns(array $columns): self + { + foreach ($columns as $name => $column) { + $columnParser = new ColumnParser($column); + $this->addOperation( + new Operation\Column\Alter( + $this->tableName, + $name, + $columnParser->getType(), + $columnParser->getOptions() + ) + ); + } + + return $this; + } + + public function renameColumn(string $name, string $newName): self + { + return $this->addOperation( + new Operation\Column\Rename($this->tableName, $name, $newName) + ); + } + + public function dropColumns(array $columnsName = []): self + { + foreach ($columnsName as $columnName) { + $this->addOperation( + new Operation\Column\Drop($this->tableName, $columnName) + ); + } + + return $this; + } + + public function addIndexes(array $indexes): self + { + foreach ($indexes as $index) { + $indexParser = new IndexParser($index, $this->tableName); + + $this->addOperation( + new Operation\Index\Add( + $this->tableName, + $indexParser->getField(), + $indexParser->getOptions() + ) + ); + } + + return $this; + } + + public function alterIndexes(array $indexes): self + { + foreach ($indexes as $index) { + $indexParser = new IndexParser($index, $this->tableName); + + $this->addOperation( + new Operation\Index\Alter( + $this->tableName, + $indexParser->getField(), + $indexParser->getOptions() + ) + ); + } + + return $this; + } + + /** + * @example [['email'], ['phone', 'created_at']] - drop two indexes + * @param array $indexes + * @return $this + */ + public function dropIndexesByColumns(array $indexes): self + { + foreach ($indexes as $columns) { + $this->addOperation( + new Operation\Index\Drop($this->tableName, $columns) + ); + } + + return $this; + } + + public function addForeignKeys(array $foreignKeys): self + { + foreach ($foreignKeys as $foreignKey) { + $fkParser = new ForeignKeyParser($foreignKey, $this->tableName); + + $this->addOperation( + new Operation\ForeignKey\Add( + $this->tableName, + $fkParser->getInnerKeys(), + $fkParser->getOuterTable(), + $fkParser->getOuterKeys(), + $fkParser->getOptions() + ) + ); + } + + return $this; + } + + /** + * @example [['email'], ['phone', 'created_at']] - drop two foreignKeys + * @param array $foreignKeys + * @return $this + */ + public function dropForeignKeysByColumns(array $foreignKeys): self + { + foreach ($foreignKeys as $columns) { + $this->addOperation( + new Operation\ForeignKey\Drop($this->tableName, $columns) + ); + } + + return $this; + } + + public function setPrimaryKeys(array $keys): self + { + return $this->addOperation( + new Operation\Table\PrimaryKeys($this->tableName, $keys) + ); + } + + public function create(): void + { + $this->addOperation( + new Operation\Table\Create($this->tableName) + ); + + $this->execute(); + } + + public function update(): void + { + $this->addOperation( + new Operation\Table\Update($this->tableName) + ); + + $this->execute(); + } + + public function rename(string $newName): void + { + $this->addOperation( + new Operation\Table\Rename($this->tableName, $newName) + ); + + $this->execute(); + } + + public function drop(): void + { + $this->addOperation( + new Operation\Table\Drop($this->tableName) + ); + + $this->execute(); + } + + public function truncate(string $strategy = ''): void + { + $this->addOperation( + new Operation\Table\Truncate($this->tableName, $strategy) + ); + + $this->execute(); + } + + public function addOperation(OperationInterface $operation): self + { + $this->operations[] = $operation; + + return $this; + } + + private function execute(): void + { + if ($this->executed) { + throw new BlueprintException('Only one create/update/rename/drop is allowed per blueprint.'); + } + + $this->capsule->execute($this->operations); + $this->executed = true; + } +} diff --git a/src/V2/TypeBuilder.php b/src/V2/TypeBuilder.php new file mode 100644 index 0000000..1b215cc --- /dev/null +++ b/src/V2/TypeBuilder.php @@ -0,0 +1,63 @@ +db = $db; + $this->name = $name; + $this->type = $type; + } + + public function create(): void + { + if (empty($this->values)) { + throw new OperationException('Values can\'t be empty'); + } + + $values = implode(',', array_map(static fn($v) => "'{$v}'", $this->values)); + + $query = sprintf( + 'CREATE TYPE %s AS %s (%s);', + $this->name, + $this->type, + $values + ); + + $this->db->execute($query); + $this->db->commit(); + } + + public function drop(): void + { + $query = sprintf('DROP TYPE %s;', $this->name); + $this->db->execute($query); + $this->db->commit(); + + } + + public function addValues(array $values): self + { + $this->values = $values; + + return $this; + } +} diff --git a/tests/Migrations/AtomizerTest.php b/tests/Migrations/AtomizerTest.php index f6d41e4..128c3a7 100644 --- a/tests/Migrations/AtomizerTest.php +++ b/tests/Migrations/AtomizerTest.php @@ -310,7 +310,7 @@ public function testCreateDatetimeNowColumn(): void $this->migrator->configure(); $schema = $this->schema('sample'); - $column = $schema->datetime('value'); + $column = $schema->datetime('value', size: 2, foo: 'bar'); $column->defaultValue(new Fragment($column::DATETIME_NOW)); $this->atomize('migration1', [$schema]); @@ -318,7 +318,10 @@ public function testCreateDatetimeNowColumn(): void $this->migrator->run(); $this->assertTrue($this->db->hasTable('sample')); - $this->assertSame((string)$column->getDefaultValue(), (string)$this->schema('sample')->column('value')->getDefaultValue()); + $this->assertSame( + (string)$column->getDefaultValue(), + (string)$this->schema('sample')->column('value')->getDefaultValue() + ); $this->migrator->rollback(); $this->assertFalse($this->db->hasTable('sample'));