diff --git a/src/Driver/MySQL/Schema/MySQLColumn.php b/src/Driver/MySQL/Schema/MySQLColumn.php index f9a3b084..6e34add4 100644 --- a/src/Driver/MySQL/Schema/MySQLColumn.php +++ b/src/Driver/MySQL/Schema/MySQLColumn.php @@ -33,6 +33,7 @@ * @method $this|AbstractColumn unsigned(bool $value) * @method $this|AbstractColumn zerofill(bool $value) * @method $this|AbstractColumn comment(string $value) + * @method $this|AbstractColumn after(string $column) */ class MySQLColumn extends AbstractColumn { @@ -41,7 +42,7 @@ class MySQLColumn extends AbstractColumn */ public const DATETIME_NOW = 'CURRENT_TIMESTAMP'; - public const EXCLUDE_FROM_COMPARE = ['size', 'timezone', 'userType', 'attributes']; + public const EXCLUDE_FROM_COMPARE = ['size', 'timezone', 'userType', 'attributes', 'first', 'after']; protected const INTEGER_TYPES = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint']; protected array $mapping = [ @@ -184,6 +185,18 @@ class MySQLColumn extends AbstractColumn #[ColumnAttribute] protected string $comment = ''; + /** + * Column name to position after. + */ + #[ColumnAttribute] + protected string $after = ''; + + /** + * Whether the column should be positioned first. + */ + #[ColumnAttribute] + protected bool $first = false; + /** * @psalm-param non-empty-string $table */ @@ -283,23 +296,30 @@ public static function createInstance(string $table, array $schema, ?\DateTimeZo public function sqlStatement(DriverInterface $driver): string { if (\in_array($this->type, self::INTEGER_TYPES, true)) { - return $this->sqlStatementInteger($driver); - } + $statement = $this->sqlStatementInteger($driver); + } else { + $defaultValue = $this->defaultValue; - $defaultValue = $this->defaultValue; + if (\in_array($this->type, $this->forbiddenDefaults, true)) { + // Flushing default value for forbidden types + $this->defaultValue = null; + } + + $statement = parent::sqlStatement($driver); - if (\in_array($this->type, $this->forbiddenDefaults, true)) { - //Flushing default value for forbidden types - $this->defaultValue = null; + $this->defaultValue = $defaultValue; } - $statement = parent::sqlStatement($driver); + $this->comment === '' or $statement .= " COMMENT {$driver->quote($this->comment)}"; - $this->defaultValue = $defaultValue; + $first = $this->first; + $after = $first ? '' : $this->after; - if ($this->comment !== '') { - return "{$statement} COMMENT {$driver->quote($this->comment)}"; - } + $statement .= match (true) { + $first => ' FIRST', + $after !== '' => " AFTER {$driver->identifier($after)}", + default => '', + }; return $statement; } @@ -325,6 +345,13 @@ public function isZerofill(): bool return $this->zerofill; } + public function first(bool $value = true): self + { + $this->first = $value; + + return $this; + } + public function set(string|array $values): self { $this->type('set'); @@ -395,12 +422,11 @@ protected function formatDatetime( private function sqlStatementInteger(DriverInterface $driver): string { return \sprintf( - '%s %s(%s)%s%s%s%s%s%s', + '%s %s(%s)%s%s%s%s%s', $driver->identifier($this->name), $this->type, $this->size, $this->unsigned ? ' UNSIGNED' : '', - $this->comment !== '' ? " COMMENT {$driver->quote($this->comment)}" : '', $this->zerofill ? ' ZEROFILL' : '', $this->nullable ? ' NULL' : ' NOT NULL', $this->defaultValue !== null ? " DEFAULT {$this->quoteDefault($driver)}" : '', diff --git a/tests/Database/Functional/Driver/Common/Schema/PositionColumnTest.php b/tests/Database/Functional/Driver/Common/Schema/PositionColumnTest.php new file mode 100644 index 00000000..20a80dab --- /dev/null +++ b/tests/Database/Functional/Driver/Common/Schema/PositionColumnTest.php @@ -0,0 +1,110 @@ +sampleSchema('table'); + + $this->assertTrue($schema->exists()); + $this->assertSameAsInDB($schema); + + $schema->string('identifier')->nullable(false)->first(); + $schema->save(); + + $this->assertSameAsInDB($schema); + + $updatedSchema = $this->fetchSchema($schema); + $updatedColumnNames = \array_map(static fn(AbstractColumn $column) => $column->getName(), $updatedSchema->getColumns()); + + $expectedColumnNames = [ + 'identifier' => 'identifier', + 'id' => 'id', + 'first_name' => 'first_name', + 'last_name' => 'last_name', + 'email' => 'email', + 'status' => 'status', + 'balance' => 'balance', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at', + ]; + + $this->assertSame($expectedColumnNames, $updatedColumnNames); + } + + public function testPositionAfter(): void + { + $schema = $this->sampleSchema('table'); + + $this->assertTrue($schema->exists()); + $this->assertSameAsInDB($schema); + + $schema->string('identifier')->nullable(false)->after('email'); + $schema->save(); + + $this->assertSameAsInDB($schema); + + $updatedSchema = $this->fetchSchema($schema); + $updatedColumnNames = \array_map(static fn(AbstractColumn $column) => $column->getName(), $updatedSchema->getColumns()); + + $expectedColumnNames = [ + 'id' => 'id', + 'first_name' => 'first_name', + 'last_name' => 'last_name', + 'email' => 'email', + 'identifier' => 'identifier', + 'status' => 'status', + 'balance' => 'balance', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at', + ]; + + $this->assertSame($expectedColumnNames, $updatedColumnNames); + } + + public function testPositionAfterThrowsException(): void + { + $schema = $this->sampleSchema('table'); + + $this->assertTrue($schema->exists()); + $this->assertSameAsInDB($schema); + + $this->expectException(DBALException::class); + $this->expectExceptionMessage("Unknown column 'nonexistent'"); + + $schema->string('identifier')->nullable(false)->after('nonexistent'); + $schema->save(); + } + + private function sampleSchema(string $table): AbstractTable + { + $schema = $this->schema($table); + + if (! $schema->exists()) { + $schema->primary('id'); + $schema->string('first_name')->nullable(false); + $schema->string('last_name')->nullable(false); + $schema->string('email', 64)->nullable(false); + $schema->enum('status', ['active', 'disabled'])->defaultValue('active'); + $schema->double('balance')->defaultValue(0); + $schema->datetime('created_at')->defaultValue(AbstractColumn::DATETIME_NOW); + $schema->datetime('updated_at')->nullable(true); + + $schema->save(Handler::DO_ALL); + } + + return $schema; + } +} diff --git a/tests/Database/Functional/Driver/MySQL/Schema/PositionColumnTest.php b/tests/Database/Functional/Driver/MySQL/Schema/PositionColumnTest.php new file mode 100644 index 00000000..04d8f049 --- /dev/null +++ b/tests/Database/Functional/Driver/MySQL/Schema/PositionColumnTest.php @@ -0,0 +1,16 @@ +