From fad7c5cd5271c915dd039ab20a64890e2f3ad0e2 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 10:39:59 -0500 Subject: [PATCH 01/23] feat(database): implement migration column types and table structure --- src/Database/Migration.php | 9 +- .../Migrations/Columns/BigInteger.php | 85 +++++ src/Database/Migrations/Columns/Binary.php | 57 ++++ src/Database/Migrations/Columns/Boolean.php | 71 ++++ src/Database/Migrations/Columns/Column.php | 67 ++++ src/Database/Migrations/Columns/Date.php | 53 +++ src/Database/Migrations/Columns/DateTime.php | 53 +++ src/Database/Migrations/Columns/Decimal.php | 90 +++++ src/Database/Migrations/Columns/Enum.php | 62 ++++ .../Migrations/Columns/FloatColumn.php | 53 +++ src/Database/Migrations/Columns/Integer.php | 90 +++++ src/Database/Migrations/Columns/Json.php | 53 +++ .../Migrations/Columns/SmallInteger.php | 85 +++++ src/Database/Migrations/Columns/Str.php | 41 +++ src/Database/Migrations/Columns/Text.php | 71 ++++ src/Database/Migrations/Columns/Timestamp.php | 85 +++++ src/Database/Migrations/Columns/Uuid.php | 53 +++ src/Database/Migrations/Table.php | 202 +++++++++++ tests/Unit/Database/Migrations/TableTest.php | 315 ++++++++++++++++++ 19 files changed, 1594 insertions(+), 1 deletion(-) create mode 100644 src/Database/Migrations/Columns/BigInteger.php create mode 100644 src/Database/Migrations/Columns/Binary.php create mode 100644 src/Database/Migrations/Columns/Boolean.php create mode 100644 src/Database/Migrations/Columns/Column.php create mode 100644 src/Database/Migrations/Columns/Date.php create mode 100644 src/Database/Migrations/Columns/DateTime.php create mode 100644 src/Database/Migrations/Columns/Decimal.php create mode 100644 src/Database/Migrations/Columns/Enum.php create mode 100644 src/Database/Migrations/Columns/FloatColumn.php create mode 100644 src/Database/Migrations/Columns/Integer.php create mode 100644 src/Database/Migrations/Columns/Json.php create mode 100644 src/Database/Migrations/Columns/SmallInteger.php create mode 100644 src/Database/Migrations/Columns/Str.php create mode 100644 src/Database/Migrations/Columns/Text.php create mode 100644 src/Database/Migrations/Columns/Timestamp.php create mode 100644 src/Database/Migrations/Columns/Uuid.php create mode 100644 src/Database/Migrations/Table.php create mode 100644 tests/Unit/Database/Migrations/TableTest.php diff --git a/src/Database/Migration.php b/src/Database/Migration.php index d6283623..967f8c0e 100644 --- a/src/Database/Migration.php +++ b/src/Database/Migration.php @@ -4,9 +4,16 @@ namespace Phenix\Database; +use Phenix\Database\Migrations\Table; use Phinx\Migration\AbstractMigration; abstract class Migration extends AbstractMigration { - // .. + public function table(string $tableName, array $options = []): Table + { + $table = new Table($tableName, $options, $this->getAdapter()); + $this->tables[] = $table; + + return $table; + } } diff --git a/src/Database/Migrations/Columns/BigInteger.php b/src/Database/Migrations/Columns/BigInteger.php new file mode 100644 index 00000000..20d7a731 --- /dev/null +++ b/src/Database/Migrations/Columns/BigInteger.php @@ -0,0 +1,85 @@ +options['identity'] = true; + $this->options['null'] = false; + } + + if (! $signed) { + $this->options['signed'] = false; + } + } + + public function getType(): string + { + return 'biginteger'; + } + + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(int $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } + + public function identity(): static + { + $this->options['identity'] = true; + $this->options['null'] = false; + + return $this; + } + + public function unsigned(): static + { + $this->options['signed'] = false; + + return $this; + } + + public function signed(): static + { + $this->options['signed'] = true; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Binary.php b/src/Database/Migrations/Columns/Binary.php new file mode 100644 index 00000000..9ac8104d --- /dev/null +++ b/src/Database/Migrations/Columns/Binary.php @@ -0,0 +1,57 @@ +options['limit'] = $limit; + } + } + + public function getType(): string + { + return 'binary'; + } + + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Boolean.php b/src/Database/Migrations/Columns/Boolean.php new file mode 100644 index 00000000..856cce07 --- /dev/null +++ b/src/Database/Migrations/Columns/Boolean.php @@ -0,0 +1,71 @@ +options['signed'] = false; + } + } + + public function getType(): string + { + return 'boolean'; + } + + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(bool $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } + + public function unsigned(): static + { + $this->options['signed'] = false; + + return $this; + } + + public function signed(): static + { + $this->options['signed'] = true; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Column.php b/src/Database/Migrations/Columns/Column.php new file mode 100644 index 00000000..5809d18c --- /dev/null +++ b/src/Database/Migrations/Columns/Column.php @@ -0,0 +1,67 @@ +name; + } + + public function getOptions(): array + { + return $this->options; + } + + abstract public function getType(): string; + + /** + * Common methods available to all column types + */ + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } + + public function first(): static + { + $this->options['after'] = MysqlAdapter::FIRST; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Date.php b/src/Database/Migrations/Columns/Date.php new file mode 100644 index 00000000..1e9eef67 --- /dev/null +++ b/src/Database/Migrations/Columns/Date.php @@ -0,0 +1,53 @@ +options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/DateTime.php b/src/Database/Migrations/Columns/DateTime.php new file mode 100644 index 00000000..b23af2d3 --- /dev/null +++ b/src/Database/Migrations/Columns/DateTime.php @@ -0,0 +1,53 @@ +options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Decimal.php b/src/Database/Migrations/Columns/Decimal.php new file mode 100644 index 00000000..43be873a --- /dev/null +++ b/src/Database/Migrations/Columns/Decimal.php @@ -0,0 +1,90 @@ +options['precision'] = $precision; + $this->options['scale'] = $scale; + + if (! $signed) { + $this->options['signed'] = false; + } + } + + public function getType(): string + { + return 'decimal'; + } + + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(float $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } + + public function unsigned(): static + { + $this->options['signed'] = false; + + return $this; + } + + public function signed(): static + { + $this->options['signed'] = true; + + return $this; + } + + public function precision(int $precision): static + { + $this->options['precision'] = $precision; + + return $this; + } + + public function scale(int $scale): static + { + $this->options['scale'] = $scale; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Enum.php b/src/Database/Migrations/Columns/Enum.php new file mode 100644 index 00000000..590e5200 --- /dev/null +++ b/src/Database/Migrations/Columns/Enum.php @@ -0,0 +1,62 @@ +options['values'] = $values; + } + + public function getType(): string + { + return 'enum'; + } + + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } + + public function values(array $values): static + { + $this->options['values'] = $values; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/FloatColumn.php b/src/Database/Migrations/Columns/FloatColumn.php new file mode 100644 index 00000000..304277d3 --- /dev/null +++ b/src/Database/Migrations/Columns/FloatColumn.php @@ -0,0 +1,53 @@ +options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(float $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Integer.php b/src/Database/Migrations/Columns/Integer.php new file mode 100644 index 00000000..eeeb74f7 --- /dev/null +++ b/src/Database/Migrations/Columns/Integer.php @@ -0,0 +1,90 @@ +options['limit'] = $limit; + } + + if ($identity) { + $this->options['identity'] = true; + $this->options['null'] = false; + } + + if (! $signed) { + $this->options['signed'] = false; + } + } + + public function getType(): string + { + return 'integer'; + } + + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(int $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } + + public function identity(): static + { + $this->options['identity'] = true; + $this->options['null'] = false; + + return $this; + } + + public function unsigned(): static + { + $this->options['signed'] = false; + + return $this; + } + + public function signed(): static + { + $this->options['signed'] = true; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Json.php b/src/Database/Migrations/Columns/Json.php new file mode 100644 index 00000000..7746107a --- /dev/null +++ b/src/Database/Migrations/Columns/Json.php @@ -0,0 +1,53 @@ +options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/SmallInteger.php b/src/Database/Migrations/Columns/SmallInteger.php new file mode 100644 index 00000000..4e9f01e1 --- /dev/null +++ b/src/Database/Migrations/Columns/SmallInteger.php @@ -0,0 +1,85 @@ +options['identity'] = true; + $this->options['null'] = false; + } + + if (! $signed) { + $this->options['signed'] = false; + } + } + + public function getType(): string + { + return 'smallinteger'; + } + + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(int $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } + + public function identity(): static + { + $this->options['identity'] = true; + $this->options['null'] = false; + + return $this; + } + + public function unsigned(): static + { + $this->options['signed'] = false; + + return $this; + } + + public function signed(): static + { + $this->options['signed'] = true; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Str.php b/src/Database/Migrations/Columns/Str.php new file mode 100644 index 00000000..cd48744a --- /dev/null +++ b/src/Database/Migrations/Columns/Str.php @@ -0,0 +1,41 @@ +options['limit'] = $limit; + } + + public function getType(): string + { + return 'string'; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function collation(string $collation): static + { + $this->options['collation'] = $collation; + + return $this; + } + + public function encoding(string $encoding): static + { + $this->options['encoding'] = $encoding; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Text.php b/src/Database/Migrations/Columns/Text.php new file mode 100644 index 00000000..fb02fb56 --- /dev/null +++ b/src/Database/Migrations/Columns/Text.php @@ -0,0 +1,71 @@ +options['limit'] = $limit; + } + } + + public function getType(): string + { + return 'text'; + } + + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } + + public function collation(string $collation): static + { + $this->options['collation'] = $collation; + + return $this; + } + + public function encoding(string $encoding): static + { + $this->options['encoding'] = $encoding; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Timestamp.php b/src/Database/Migrations/Columns/Timestamp.php new file mode 100644 index 00000000..a3ce8ba0 --- /dev/null +++ b/src/Database/Migrations/Columns/Timestamp.php @@ -0,0 +1,85 @@ +options['timezone'] = true; + } + } + + public function getType(): string + { + return 'timestamp'; + } + + public function nullable(): static + { + $this->options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } + + public function timezone(): static + { + $this->options['timezone'] = true; + + return $this; + } + + public function update(string $action): static + { + $this->options['update'] = $action; + + return $this; + } + + public function currentTimestamp(): static + { + $this->options['default'] = 'CURRENT_TIMESTAMP'; + + return $this; + } + + public function onUpdateCurrentTimestamp(): static + { + $this->options['update'] = 'CURRENT_TIMESTAMP'; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Uuid.php b/src/Database/Migrations/Columns/Uuid.php new file mode 100644 index 00000000..e2121c3c --- /dev/null +++ b/src/Database/Migrations/Columns/Uuid.php @@ -0,0 +1,53 @@ +options['null'] = true; + + return $this; + } + + public function notNull(): static + { + $this->options['null'] = false; + + return $this; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function comment(string $comment): static + { + $this->options['comment'] = $comment; + + return $this; + } + + public function after(string $column): static + { + $this->options['after'] = $column; + + return $this; + } +} diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php new file mode 100644 index 00000000..b12314fb --- /dev/null +++ b/src/Database/Migrations/Table.php @@ -0,0 +1,202 @@ + + */ + protected array $columns = []; + + public function getColumnBuilders(): array + { + return $this->columns; + } + + public function string(string $name, int $limit = 255): Str + { + $column = new Str($name, $limit); + + $this->columns[] = $column; + + return $column; + } + + public function integer(string $name, ?int $limit = null, bool $identity = false, bool $signed = true): Integer + { + $column = new Integer($name, $limit, $identity, $signed); + + $this->columns[] = $column; + + return $column; + } + + public function bigInteger(string $name, bool $identity = false, bool $signed = true): BigInteger + { + $column = new BigInteger($name, $identity, $signed); + + $this->columns[] = $column; + + return $column; + } + + public function smallInteger(string $name, bool $identity = false, bool $signed = true): SmallInteger + { + $column = new SmallInteger($name, $identity, $signed); + + $this->columns[] = $column; + + return $column; + } + + public function text(string $name, ?int $limit = null): Text + { + $column = new Text($name, $limit); + + $this->columns[] = $column; + + return $column; + } + + public function boolean(string $name, bool $signed = true): Boolean + { + $column = new Boolean($name, $signed); + + $this->columns[] = $column; + + return $column; + } + + public function decimal(string $name, int $precision = 10, int $scale = 2, bool $signed = true): Decimal + { + $column = new Decimal($name, $precision, $scale, $signed); + + $this->columns[] = $column; + + return $column; + } + + public function dateTime(string $name): DateTime + { + $column = new DateTime($name); + + $this->columns[] = $column; + + return $column; + } + + public function timestamp(string $name, bool $timezone = false): Timestamp + { + $column = new Timestamp($name, $timezone); + + $this->columns[] = $column; + + return $column; + } + + public function json(string $name): Json + { + $column = new Json($name); + + $this->columns[] = $column; + + return $column; + } + + public function uuid(string $name): Uuid + { + $column = new Uuid($name); + + $this->columns[] = $column; + + return $column; + } + + public function enum(string $name, array $values): Enum + { + $column = new Enum($name, $values); + + $this->columns[] = $column; + + return $column; + } + + public function float(string $name): FloatColumn + { + $column = new FloatColumn($name); + + $this->columns[] = $column; + + return $column; + } + + public function date(string $name): Date + { + $column = new Date($name); + + $this->columns[] = $column; + + return $column; + } + + public function binary(string $name, ?int $limit = null): Binary + { + $column = new Binary($name, $limit); + + $this->columns[] = $column; + + return $column; + } + + public function id(string $name = 'id'): Integer + { + $column = new Integer($name, null, true, false); + + $this->columns[] = $column; + + return $column; + } + + public function timestamps(bool $timezone = false): self + { + $createdAt = new Timestamp('created_at', $timezone); + $createdAt->notNull()->currentTimestamp(); + $this->columns[] = $createdAt; + + $updatedAt = new Timestamp('updated_at', $timezone); + $updatedAt->nullable()->onUpdateCurrentTimestamp(); + $this->columns[] = $updatedAt; + + return $this; + } + + public function __destruct() + { + foreach ($this->columns as $column) { + $this->addColumn($column->getName(), $column->getType(), $column->getOptions()); + } + + $this->save(); + } +} diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php new file mode 100644 index 00000000..c0060adf --- /dev/null +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -0,0 +1,315 @@ +mockAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); + + $this->mockAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $this->mockAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $this->mockAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); +}); + +it('can add string column with options', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $table->string('username', 50)->notNull()->comment('User name'); + + $columns = $table->getColumnBuilders(); + + expect(count($columns))->toBe(1); + + $column = $columns[0]; + + expect($column)->toBeInstanceOf(Str::class); + expect($column->getName())->toBe('username'); + expect($column->getType())->toBe('string'); + expect($column->getOptions())->toBe([ + 'limit' => 50, + 'null' => false, + 'comment' => 'User name', + ]); +}); + +it('can add integer column with options', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->integer('age', 10, false, true)->default(0)->comment('User age'); + + expect($column)->toBeInstanceOf(Integer::class); + expect($column->getName())->toBe('age'); + expect($column->getType())->toBe('integer'); + expect($column->getOptions())->toBe([ + 'limit' => 10, + 'default' => 0, + 'comment' => 'User age', + ]); +}); + +it('can add big integer column with identity', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->bigInteger('id', true, false)->comment('Primary key'); + + expect($column)->toBeInstanceOf(BigInteger::class); + expect($column->getName())->toBe('id'); + expect($column->getType())->toBe('biginteger'); + expect($column->getOptions())->toBe([ + 'identity' => true, + 'null' => false, + 'signed' => false, + 'comment' => 'Primary key', + ]); +}); + +it('can add small integer column', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->smallInteger('status', false, true)->default(1); + + expect($column)->toBeInstanceOf(SmallInteger::class); + expect($column->getName())->toBe('status'); + expect($column->getType())->toBe('smallinteger'); + expect($column->getOptions())->toBe([ + 'default' => 1, + ]); +}); + +it('can add text column with limit', function (): void { + $table = new Table('posts', adapter: $this->mockAdapter); + + $column = $table->text('content', 1000)->nullable()->comment('Post content'); + + expect($column)->toBeInstanceOf(Text::class); + expect($column->getName())->toBe('content'); + expect($column->getType())->toBe('text'); + expect($column->getOptions())->toBe([ + 'limit' => 1000, + 'null' => true, + 'comment' => 'Post content', + ]); +}); + +it('can add boolean column', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->boolean('is_active', true)->default(true)->comment('User status'); + + expect($column)->toBeInstanceOf(Boolean::class); + expect($column->getName())->toBe('is_active'); + expect($column->getType())->toBe('boolean'); + expect($column->getOptions())->toBe([ + 'default' => true, + 'comment' => 'User status', + ]); +}); + +it('can add decimal column with precision and scale', function (): void { + $table = new Table('products', adapter: $this->mockAdapter); + + $column = $table->decimal('price', 8, 2, true)->default(0.00)->comment('Product price'); + + expect($column)->toBeInstanceOf(Decimal::class); + expect($column->getName())->toBe('price'); + expect($column->getType())->toBe('decimal'); + expect($column->getOptions())->toBe([ + 'precision' => 8, + 'scale' => 2, + 'default' => 0.00, + 'comment' => 'Product price', + ]); +}); + +it('can add datetime column', function (): void { + $table = new Table('posts', adapter: $this->mockAdapter); + + $column = $table->dateTime('published_at')->nullable()->comment('Publication date'); + + expect($column)->toBeInstanceOf(DateTime::class); + expect($column->getName())->toBe('published_at'); + expect($column->getType())->toBe('datetime'); + expect($column->getOptions())->toBe([ + 'null' => true, + 'comment' => 'Publication date', + ]); +}); + +it('can add timestamp column with timezone', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->timestamp('created_at', true)->notNull()->currentTimestamp(); + + expect($column)->toBeInstanceOf(Timestamp::class); + expect($column->getName())->toBe('created_at'); + expect($column->getType())->toBe('timestamp'); + expect($column->getOptions())->toBe([ + 'timezone' => true, + 'null' => false, + 'default' => 'CURRENT_TIMESTAMP', + ]); +}); + +it('can add json column', function (): void { + $table = new Table('settings', adapter: $this->mockAdapter); + + $column = $table->json('data')->nullable()->comment('JSON data'); + + expect($column)->toBeInstanceOf(Json::class); + expect($column->getName())->toBe('data'); + expect($column->getType())->toBe('json'); + expect($column->getOptions())->toBe([ + 'null' => true, + 'comment' => 'JSON data', + ]); +}); + +it('can add uuid column', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->uuid('uuid')->notNull()->comment('Unique identifier'); + + expect($column)->toBeInstanceOf(Uuid::class); + expect($column->getName())->toBe('uuid'); + expect($column->getType())->toBe('uuid'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'comment' => 'Unique identifier', + ]); +}); + +it('can add enum column with values', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->enum('role', ['admin', 'user', 'guest'])->default('user')->comment('User role'); + + expect($column)->toBeInstanceOf(Enum::class); + expect($column->getName())->toBe('role'); + expect($column->getType())->toBe('enum'); + expect($column->getOptions())->toBe([ + 'values' => ['admin', 'user', 'guest'], + 'default' => 'user', + 'comment' => 'User role', + ]); +}); + +it('can add float column', function (): void { + $table = new Table('measurements', adapter: $this->mockAdapter); + + $column = $table->float('temperature')->default(0.0)->comment('Temperature value'); + + expect($column)->toBeInstanceOf(FloatColumn::class); + expect($column->getName())->toBe('temperature'); + expect($column->getType())->toBe('float'); + expect($column->getOptions())->toBe([ + 'default' => 0.0, + 'comment' => 'Temperature value', + ]); +}); + +it('can add date column', function (): void { + $table = new Table('events', adapter: $this->mockAdapter); + + $column = $table->date('event_date')->notNull()->comment('Event date'); + + expect($column)->toBeInstanceOf(Date::class); + expect($column->getName())->toBe('event_date'); + expect($column->getType())->toBe('date'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'comment' => 'Event date', + ]); +}); + +it('can add binary column with limit', function (): void { + $table = new Table('files', adapter: $this->mockAdapter); + + $column = $table->binary('file_data', 1024)->nullable()->comment('Binary file data'); + + expect($column)->toBeInstanceOf(Binary::class); + expect($column->getName())->toBe('file_data'); + expect($column->getType())->toBe('binary'); + expect($column->getOptions())->toBe([ + 'limit' => 1024, + 'null' => true, + 'comment' => 'Binary file data', + ]); +}); + +it('can add id column with auto increment', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->id('user_id'); + + expect($column)->toBeInstanceOf(Integer::class); + expect($column->getName())->toBe('user_id'); + expect($column->getType())->toBe('integer'); + expect($column->getOptions())->toBe([ + 'identity' => true, + 'null' => false, + 'signed' => false, + ]); +}); + +it('can add timestamps columns', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $table->timestamps(true); + + $columns = $table->getColumnBuilders(); + + expect(count($columns))->toBe(2); + + $createdAt = $columns[0]; + expect($createdAt)->toBeInstanceOf(Timestamp::class); + expect($createdAt->getName())->toBe('created_at'); + expect($createdAt->getType())->toBe('timestamp'); + expect($createdAt->getOptions())->toBe([ + 'timezone' => true, + 'null' => false, + 'default' => 'CURRENT_TIMESTAMP', + ]); + + $updatedAt = $columns[1]; + expect($updatedAt)->toBeInstanceOf(Timestamp::class); + expect($updatedAt->getName())->toBe('updated_at'); + expect($updatedAt->getType())->toBe('timestamp'); + expect($updatedAt->getOptions())->toBe([ + 'timezone' => true, + 'null' => true, + 'update' => 'CURRENT_TIMESTAMP', + ]); +}); From 8c9f49752cce2b248d427a830b9f379b01ca2e90 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 10:41:47 -0500 Subject: [PATCH 02/23] refactor: rename FloatColumn to Floating --- .../Migrations/Columns/{FloatColumn.php => Floating.php} | 2 +- src/Database/Migrations/Table.php | 6 +++--- tests/Unit/Database/Migrations/TableTest.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/Database/Migrations/Columns/{FloatColumn.php => Floating.php} (96%) diff --git a/src/Database/Migrations/Columns/FloatColumn.php b/src/Database/Migrations/Columns/Floating.php similarity index 96% rename from src/Database/Migrations/Columns/FloatColumn.php rename to src/Database/Migrations/Columns/Floating.php index 304277d3..4dedfcd7 100644 --- a/src/Database/Migrations/Columns/FloatColumn.php +++ b/src/Database/Migrations/Columns/Floating.php @@ -4,7 +4,7 @@ namespace Phenix\Database\Migrations\Columns; -class FloatColumn extends Column +class Floating extends Column { public function __construct( protected string $name diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index b12314fb..a058b79b 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -12,7 +12,7 @@ use Phenix\Database\Migrations\Columns\DateTime; use Phenix\Database\Migrations\Columns\Decimal; use Phenix\Database\Migrations\Columns\Enum; -use Phenix\Database\Migrations\Columns\FloatColumn; +use Phenix\Database\Migrations\Columns\Floating; use Phenix\Database\Migrations\Columns\Integer; use Phenix\Database\Migrations\Columns\Json; use Phenix\Database\Migrations\Columns\SmallInteger; @@ -142,9 +142,9 @@ public function enum(string $name, array $values): Enum return $column; } - public function float(string $name): FloatColumn + public function float(string $name): Floating { - $column = new FloatColumn($name); + $column = new Floating($name); $this->columns[] = $column; diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index c0060adf..40ecefbc 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -9,7 +9,7 @@ use Phenix\Database\Migrations\Columns\DateTime; use Phenix\Database\Migrations\Columns\Decimal; use Phenix\Database\Migrations\Columns\Enum; -use Phenix\Database\Migrations\Columns\FloatColumn; +use Phenix\Database\Migrations\Columns\Floating; use Phenix\Database\Migrations\Columns\Integer; use Phenix\Database\Migrations\Columns\Json; use Phenix\Database\Migrations\Columns\SmallInteger; @@ -231,7 +231,7 @@ $column = $table->float('temperature')->default(0.0)->comment('Temperature value'); - expect($column)->toBeInstanceOf(FloatColumn::class); + expect($column)->toBeInstanceOf(Floating::class); expect($column->getName())->toBe('temperature'); expect($column->getType())->toBe('float'); expect($column->getOptions())->toBe([ From dcdf195f40c3cb71d94418735e51ce5280d702fc Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 10:53:01 -0500 Subject: [PATCH 03/23] refactor: update parameter types to use nullable syntax in Table class methods --- src/Database/Migrations/Table.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index a058b79b..39a54dea 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -43,7 +43,7 @@ public function string(string $name, int $limit = 255): Str return $column; } - public function integer(string $name, ?int $limit = null, bool $identity = false, bool $signed = true): Integer + public function integer(string $name, int|null $limit = null, bool $identity = false, bool $signed = true): Integer { $column = new Integer($name, $limit, $identity, $signed); @@ -70,7 +70,7 @@ public function smallInteger(string $name, bool $identity = false, bool $signed return $column; } - public function text(string $name, ?int $limit = null): Text + public function text(string $name, int|null $limit = null): Text { $column = new Text($name, $limit); @@ -160,7 +160,7 @@ public function date(string $name): Date return $column; } - public function binary(string $name, ?int $limit = null): Binary + public function binary(string $name, int|null $limit = null): Binary { $column = new Binary($name, $limit); From aa32f5b6e71369696a3979ddc60e819a213a2663 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 16:06:35 -0500 Subject: [PATCH 04/23] feat(database): introduce BigInteger and UnsignedInteger column types with HasSign trait --- .../Migrations/Columns/BigInteger.php | 73 ++----------------- .../Migrations/Columns/Concerns/HasSign.php | 22 ++++++ src/Database/Migrations/Columns/Integer.php | 66 +---------------- src/Database/Migrations/Columns/Number.php | 23 ++++++ .../Migrations/Columns/UnsignedBigInteger.php | 29 ++++++++ .../Migrations/Columns/UnsignedInteger.php | 30 ++++++++ 6 files changed, 115 insertions(+), 128 deletions(-) create mode 100644 src/Database/Migrations/Columns/Concerns/HasSign.php create mode 100644 src/Database/Migrations/Columns/Number.php create mode 100644 src/Database/Migrations/Columns/UnsignedBigInteger.php create mode 100644 src/Database/Migrations/Columns/UnsignedInteger.php diff --git a/src/Database/Migrations/Columns/BigInteger.php b/src/Database/Migrations/Columns/BigInteger.php index 20d7a731..78cd4078 100644 --- a/src/Database/Migrations/Columns/BigInteger.php +++ b/src/Database/Migrations/Columns/BigInteger.php @@ -4,82 +4,23 @@ namespace Phenix\Database\Migrations\Columns; -class BigInteger extends Column +use Phenix\Database\Migrations\Columns\Concerns\HasSign; + +class BigInteger extends UnsignedBigInteger { + use HasSign; + public function __construct( protected string $name, bool $identity = false, - bool $signed = true ) { - if ($identity) { - $this->options['identity'] = true; - $this->options['null'] = false; - } + parent::__construct($name, $identity); - if (! $signed) { - $this->options['signed'] = false; - } + $this->options['signed'] = true; } public function getType(): string { return 'biginteger'; } - - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - - public function default(int $value): static - { - $this->options['default'] = $value; - - return $this; - } - - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } - - public function identity(): static - { - $this->options['identity'] = true; - $this->options['null'] = false; - - return $this; - } - - public function unsigned(): static - { - $this->options['signed'] = false; - - return $this; - } - - public function signed(): static - { - $this->options['signed'] = true; - - return $this; - } } diff --git a/src/Database/Migrations/Columns/Concerns/HasSign.php b/src/Database/Migrations/Columns/Concerns/HasSign.php new file mode 100644 index 00000000..188f5488 --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/HasSign.php @@ -0,0 +1,22 @@ +options['signed'] = false; + + return $this; + } + + public function signed(): static + { + $this->options['signed'] = true; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Integer.php b/src/Database/Migrations/Columns/Integer.php index eeeb74f7..bfc5d24f 100644 --- a/src/Database/Migrations/Columns/Integer.php +++ b/src/Database/Migrations/Columns/Integer.php @@ -4,74 +4,16 @@ namespace Phenix\Database\Migrations\Columns; -class Integer extends Column +class Integer extends UnsignedInteger { public function __construct( protected string $name, - ?int $limit = null, + int|null $limit = null, bool $identity = false, - bool $signed = true ) { - if ($limit !== null) { - $this->options['limit'] = $limit; - } + parent::__construct($name, $limit, $identity); - if ($identity) { - $this->options['identity'] = true; - $this->options['null'] = false; - } - - if (! $signed) { - $this->options['signed'] = false; - } - } - - public function getType(): string - { - return 'integer'; - } - - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - - public function default(int $value): static - { - $this->options['default'] = $value; - - return $this; - } - - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } - - public function identity(): static - { - $this->options['identity'] = true; - $this->options['null'] = false; - - return $this; + $this->options['signed'] = true; } public function unsigned(): static diff --git a/src/Database/Migrations/Columns/Number.php b/src/Database/Migrations/Columns/Number.php new file mode 100644 index 00000000..0a590ca4 --- /dev/null +++ b/src/Database/Migrations/Columns/Number.php @@ -0,0 +1,23 @@ +options['default'] = $value; + + return $this; + } + + public function identity(): static + { + $this->options['identity'] = true; + $this->options['null'] = false; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/UnsignedBigInteger.php b/src/Database/Migrations/Columns/UnsignedBigInteger.php new file mode 100644 index 00000000..a6dcdc57 --- /dev/null +++ b/src/Database/Migrations/Columns/UnsignedBigInteger.php @@ -0,0 +1,29 @@ +options['null'] = false; + $this->options['signed'] = false; + + if ($identity) { + $this->options['identity'] = true; + } + } + + public function getType(): string + { + return 'biginteger'; + } +} diff --git a/src/Database/Migrations/Columns/UnsignedInteger.php b/src/Database/Migrations/Columns/UnsignedInteger.php new file mode 100644 index 00000000..df34ed60 --- /dev/null +++ b/src/Database/Migrations/Columns/UnsignedInteger.php @@ -0,0 +1,30 @@ +options['null'] = false; + $this->options['signed'] = false; + + if ($limit) { + $this->options['limit'] = $limit; + } + + if ($identity) { + $this->options['identity'] = true; + } + } + + public function getType(): string + { + return 'integer'; + } +} From c99f969a27032469753fc2540b667d672bf65891 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 16:21:08 -0500 Subject: [PATCH 05/23] refactor(database): simplify column constructors by removing redundant null options and adding parent constructor calls --- src/Database/Migrations/Columns/Binary.php | 34 ++----------- src/Database/Migrations/Columns/Boolean.php | 49 +------------------ src/Database/Migrations/Columns/Column.php | 11 +---- src/Database/Migrations/Columns/Date.php | 29 +---------- src/Database/Migrations/Columns/DateTime.php | 29 +---------- src/Database/Migrations/Columns/Decimal.php | 29 +---------- src/Database/Migrations/Columns/Enum.php | 29 +---------- src/Database/Migrations/Columns/Floating.php | 29 +---------- src/Database/Migrations/Columns/Json.php | 29 +---------- src/Database/Migrations/Columns/Number.php | 1 - .../Migrations/Columns/SmallInteger.php | 31 +----------- src/Database/Migrations/Columns/Str.php | 1 + src/Database/Migrations/Columns/Text.php | 29 +---------- src/Database/Migrations/Columns/Timestamp.php | 29 +---------- .../Migrations/Columns/UnsignedBigInteger.php | 3 +- .../Migrations/Columns/UnsignedInteger.php | 3 +- src/Database/Migrations/Columns/Uuid.php | 29 +---------- 17 files changed, 23 insertions(+), 371 deletions(-) diff --git a/src/Database/Migrations/Columns/Binary.php b/src/Database/Migrations/Columns/Binary.php index 9ac8104d..931ea861 100644 --- a/src/Database/Migrations/Columns/Binary.php +++ b/src/Database/Migrations/Columns/Binary.php @@ -8,9 +8,11 @@ class Binary extends Column { public function __construct( protected string $name, - ?int $limit = null + int|null $limit = null ) { - if ($limit !== null) { + parent::__construct($name); + + if ($limit) { $this->options['limit'] = $limit; } } @@ -20,38 +22,10 @@ public function getType(): string return 'binary'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(string $value): static { $this->options['default'] = $value; return $this; } - - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } } diff --git a/src/Database/Migrations/Columns/Boolean.php b/src/Database/Migrations/Columns/Boolean.php index 856cce07..b92ad4f1 100644 --- a/src/Database/Migrations/Columns/Boolean.php +++ b/src/Database/Migrations/Columns/Boolean.php @@ -7,12 +7,9 @@ class Boolean extends Column { public function __construct( - protected string $name, - bool $signed = true + protected string $name ) { - if (! $signed) { - $this->options['signed'] = false; - } + parent::__construct($name); } public function getType(): string @@ -20,52 +17,10 @@ public function getType(): string return 'boolean'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(bool $value): static { $this->options['default'] = $value; return $this; } - - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } - - public function unsigned(): static - { - $this->options['signed'] = false; - - return $this; - } - - public function signed(): static - { - $this->options['signed'] = true; - - return $this; - } } diff --git a/src/Database/Migrations/Columns/Column.php b/src/Database/Migrations/Columns/Column.php index 5809d18c..c2f78222 100644 --- a/src/Database/Migrations/Columns/Column.php +++ b/src/Database/Migrations/Columns/Column.php @@ -13,6 +13,7 @@ abstract class Column public function __construct( protected string $name ) { + $this->options['null'] = false; } public function getName(): string @@ -27,9 +28,6 @@ public function getOptions(): array abstract public function getType(): string; - /** - * Common methods available to all column types - */ public function nullable(): static { $this->options['null'] = true; @@ -37,13 +35,6 @@ public function nullable(): static return $this; } - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function comment(string $comment): static { $this->options['comment'] = $comment; diff --git a/src/Database/Migrations/Columns/Date.php b/src/Database/Migrations/Columns/Date.php index 1e9eef67..19f28fcd 100644 --- a/src/Database/Migrations/Columns/Date.php +++ b/src/Database/Migrations/Columns/Date.php @@ -9,6 +9,7 @@ class Date extends Column public function __construct( protected string $name ) { + parent::__construct($name); } public function getType(): string @@ -16,38 +17,10 @@ public function getType(): string return 'date'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(string $value): static { $this->options['default'] = $value; return $this; } - - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } } diff --git a/src/Database/Migrations/Columns/DateTime.php b/src/Database/Migrations/Columns/DateTime.php index b23af2d3..fb3a2e3f 100644 --- a/src/Database/Migrations/Columns/DateTime.php +++ b/src/Database/Migrations/Columns/DateTime.php @@ -9,6 +9,7 @@ class DateTime extends Column public function __construct( protected string $name ) { + parent::__construct($name); } public function getType(): string @@ -16,38 +17,10 @@ public function getType(): string return 'datetime'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(string $value): static { $this->options['default'] = $value; return $this; } - - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } } diff --git a/src/Database/Migrations/Columns/Decimal.php b/src/Database/Migrations/Columns/Decimal.php index 43be873a..bbadd173 100644 --- a/src/Database/Migrations/Columns/Decimal.php +++ b/src/Database/Migrations/Columns/Decimal.php @@ -12,6 +12,7 @@ public function __construct( int $scale = 2, bool $signed = true ) { + parent::__construct($name); $this->options['precision'] = $precision; $this->options['scale'] = $scale; @@ -25,20 +26,6 @@ public function getType(): string return 'decimal'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(float $value): static { $this->options['default'] = $value; @@ -46,20 +33,6 @@ public function default(float $value): static return $this; } - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } - public function unsigned(): static { $this->options['signed'] = false; diff --git a/src/Database/Migrations/Columns/Enum.php b/src/Database/Migrations/Columns/Enum.php index 590e5200..bb3283dd 100644 --- a/src/Database/Migrations/Columns/Enum.php +++ b/src/Database/Migrations/Columns/Enum.php @@ -10,6 +10,7 @@ public function __construct( protected string $name, array $values ) { + parent::__construct($name); $this->options['values'] = $values; } @@ -18,20 +19,6 @@ public function getType(): string return 'enum'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(string $value): static { $this->options['default'] = $value; @@ -39,20 +26,6 @@ public function default(string $value): static return $this; } - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } - public function values(array $values): static { $this->options['values'] = $values; diff --git a/src/Database/Migrations/Columns/Floating.php b/src/Database/Migrations/Columns/Floating.php index 4dedfcd7..7e2bc73f 100644 --- a/src/Database/Migrations/Columns/Floating.php +++ b/src/Database/Migrations/Columns/Floating.php @@ -9,6 +9,7 @@ class Floating extends Column public function __construct( protected string $name ) { + parent::__construct($name); } public function getType(): string @@ -16,38 +17,10 @@ public function getType(): string return 'float'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(float $value): static { $this->options['default'] = $value; return $this; } - - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } } diff --git a/src/Database/Migrations/Columns/Json.php b/src/Database/Migrations/Columns/Json.php index 7746107a..dc2202ab 100644 --- a/src/Database/Migrations/Columns/Json.php +++ b/src/Database/Migrations/Columns/Json.php @@ -9,6 +9,7 @@ class Json extends Column public function __construct( protected string $name ) { + parent::__construct($name); } public function getType(): string @@ -16,38 +17,10 @@ public function getType(): string return 'json'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(string $value): static { $this->options['default'] = $value; return $this; } - - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } } diff --git a/src/Database/Migrations/Columns/Number.php b/src/Database/Migrations/Columns/Number.php index 0a590ca4..019926d2 100644 --- a/src/Database/Migrations/Columns/Number.php +++ b/src/Database/Migrations/Columns/Number.php @@ -16,7 +16,6 @@ public function default(int $value): static public function identity(): static { $this->options['identity'] = true; - $this->options['null'] = false; return $this; } diff --git a/src/Database/Migrations/Columns/SmallInteger.php b/src/Database/Migrations/Columns/SmallInteger.php index 4e9f01e1..888c1d2f 100644 --- a/src/Database/Migrations/Columns/SmallInteger.php +++ b/src/Database/Migrations/Columns/SmallInteger.php @@ -11,9 +11,10 @@ public function __construct( bool $identity = false, bool $signed = true ) { + parent::__construct($name); + if ($identity) { $this->options['identity'] = true; - $this->options['null'] = false; } if (! $signed) { @@ -26,20 +27,6 @@ public function getType(): string return 'smallinteger'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(int $value): static { $this->options['default'] = $value; @@ -47,20 +34,6 @@ public function default(int $value): static return $this; } - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } - public function identity(): static { $this->options['identity'] = true; diff --git a/src/Database/Migrations/Columns/Str.php b/src/Database/Migrations/Columns/Str.php index cd48744a..89f6ae52 100644 --- a/src/Database/Migrations/Columns/Str.php +++ b/src/Database/Migrations/Columns/Str.php @@ -10,6 +10,7 @@ public function __construct( protected string $name, int $limit = 255 ) { + parent::__construct($name); $this->options['limit'] = $limit; } diff --git a/src/Database/Migrations/Columns/Text.php b/src/Database/Migrations/Columns/Text.php index fb02fb56..37304a5f 100644 --- a/src/Database/Migrations/Columns/Text.php +++ b/src/Database/Migrations/Columns/Text.php @@ -10,6 +10,7 @@ public function __construct( protected string $name, ?int $limit = null ) { + parent::__construct($name); if ($limit !== null) { $this->options['limit'] = $limit; } @@ -20,20 +21,6 @@ public function getType(): string return 'text'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(string $value): static { $this->options['default'] = $value; @@ -41,20 +28,6 @@ public function default(string $value): static return $this; } - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } - public function collation(string $collation): static { $this->options['collation'] = $collation; diff --git a/src/Database/Migrations/Columns/Timestamp.php b/src/Database/Migrations/Columns/Timestamp.php index a3ce8ba0..8b33bd35 100644 --- a/src/Database/Migrations/Columns/Timestamp.php +++ b/src/Database/Migrations/Columns/Timestamp.php @@ -10,6 +10,7 @@ public function __construct( protected string $name, bool $timezone = false ) { + parent::__construct($name); if ($timezone) { $this->options['timezone'] = true; } @@ -20,20 +21,6 @@ public function getType(): string return 'timestamp'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(string $value): static { $this->options['default'] = $value; @@ -41,20 +28,6 @@ public function default(string $value): static return $this; } - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } - public function timezone(): static { $this->options['timezone'] = true; diff --git a/src/Database/Migrations/Columns/UnsignedBigInteger.php b/src/Database/Migrations/Columns/UnsignedBigInteger.php index a6dcdc57..0ac04ebe 100644 --- a/src/Database/Migrations/Columns/UnsignedBigInteger.php +++ b/src/Database/Migrations/Columns/UnsignedBigInteger.php @@ -14,7 +14,8 @@ public function __construct( protected string $name, bool $identity = false, ) { - $this->options['null'] = false; + parent::__construct($name); + $this->options['signed'] = false; if ($identity) { diff --git a/src/Database/Migrations/Columns/UnsignedInteger.php b/src/Database/Migrations/Columns/UnsignedInteger.php index df34ed60..f414f6e4 100644 --- a/src/Database/Migrations/Columns/UnsignedInteger.php +++ b/src/Database/Migrations/Columns/UnsignedInteger.php @@ -11,7 +11,8 @@ public function __construct( int|null $limit = null, bool $identity = false, ) { - $this->options['null'] = false; + parent::__construct($name); + $this->options['signed'] = false; if ($limit) { diff --git a/src/Database/Migrations/Columns/Uuid.php b/src/Database/Migrations/Columns/Uuid.php index e2121c3c..807b8f53 100644 --- a/src/Database/Migrations/Columns/Uuid.php +++ b/src/Database/Migrations/Columns/Uuid.php @@ -9,6 +9,7 @@ class Uuid extends Column public function __construct( protected string $name ) { + parent::__construct($name); } public function getType(): string @@ -16,38 +17,10 @@ public function getType(): string return 'uuid'; } - public function nullable(): static - { - $this->options['null'] = true; - - return $this; - } - - public function notNull(): static - { - $this->options['null'] = false; - - return $this; - } - public function default(string $value): static { $this->options['default'] = $value; return $this; } - - public function comment(string $comment): static - { - $this->options['comment'] = $comment; - - return $this; - } - - public function after(string $column): static - { - $this->options['after'] = $column; - - return $this; - } } From c194deaede3de0d6fdcb6aa7d033bc7356a4c492 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 16:25:42 -0500 Subject: [PATCH 06/23] refactor(database): integrate HasSign trait into Decimal, Integer, and SmallInteger columns, removing redundant signed methods --- src/Database/Migrations/Columns/Decimal.php | 18 ++++------------- src/Database/Migrations/Columns/Integer.php | 18 ++++------------- .../Migrations/Columns/SmallInteger.php | 20 +++++-------------- 3 files changed, 13 insertions(+), 43 deletions(-) diff --git a/src/Database/Migrations/Columns/Decimal.php b/src/Database/Migrations/Columns/Decimal.php index bbadd173..59f80e41 100644 --- a/src/Database/Migrations/Columns/Decimal.php +++ b/src/Database/Migrations/Columns/Decimal.php @@ -4,8 +4,12 @@ namespace Phenix\Database\Migrations\Columns; +use Phenix\Database\Migrations\Columns\Concerns\HasSign; + class Decimal extends Column { + use HasSign; + public function __construct( protected string $name, int $precision = 10, @@ -33,20 +37,6 @@ public function default(float $value): static return $this; } - public function unsigned(): static - { - $this->options['signed'] = false; - - return $this; - } - - public function signed(): static - { - $this->options['signed'] = true; - - return $this; - } - public function precision(int $precision): static { $this->options['precision'] = $precision; diff --git a/src/Database/Migrations/Columns/Integer.php b/src/Database/Migrations/Columns/Integer.php index bfc5d24f..d654a230 100644 --- a/src/Database/Migrations/Columns/Integer.php +++ b/src/Database/Migrations/Columns/Integer.php @@ -4,8 +4,12 @@ namespace Phenix\Database\Migrations\Columns; +use Phenix\Database\Migrations\Columns\Concerns\HasSign; + class Integer extends UnsignedInteger { + use HasSign; + public function __construct( protected string $name, int|null $limit = null, @@ -15,18 +19,4 @@ public function __construct( $this->options['signed'] = true; } - - public function unsigned(): static - { - $this->options['signed'] = false; - - return $this; - } - - public function signed(): static - { - $this->options['signed'] = true; - - return $this; - } } diff --git a/src/Database/Migrations/Columns/SmallInteger.php b/src/Database/Migrations/Columns/SmallInteger.php index 888c1d2f..4c4d6db6 100644 --- a/src/Database/Migrations/Columns/SmallInteger.php +++ b/src/Database/Migrations/Columns/SmallInteger.php @@ -4,15 +4,19 @@ namespace Phenix\Database\Migrations\Columns; +use Phenix\Database\Migrations\Columns\Concerns\HasSign; + class SmallInteger extends Column { + use HasSign; + public function __construct( protected string $name, bool $identity = false, bool $signed = true ) { parent::__construct($name); - + if ($identity) { $this->options['identity'] = true; } @@ -41,18 +45,4 @@ public function identity(): static return $this; } - - public function unsigned(): static - { - $this->options['signed'] = false; - - return $this; - } - - public function signed(): static - { - $this->options['signed'] = true; - - return $this; - } } From fd3cd3d17572854e7ed11a7a20b454c132dd2304 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 16:57:32 -0500 Subject: [PATCH 07/23] refactor(database): remove signed parameters from integer, bigInteger, smallInteger, boolean, and decimal column methods --- src/Database/Migrations/Table.php | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index 39a54dea..296d8279 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -19,6 +19,7 @@ use Phenix\Database\Migrations\Columns\Str; use Phenix\Database\Migrations\Columns\Text; use Phenix\Database\Migrations\Columns\Timestamp; +use Phenix\Database\Migrations\Columns\UnsignedInteger; use Phenix\Database\Migrations\Columns\Uuid; use Phinx\Db\Table as PhinxTable; @@ -43,27 +44,27 @@ public function string(string $name, int $limit = 255): Str return $column; } - public function integer(string $name, int|null $limit = null, bool $identity = false, bool $signed = true): Integer + public function integer(string $name, int|null $limit = null, bool $identity = false): Integer { - $column = new Integer($name, $limit, $identity, $signed); + $column = new Integer($name, $limit, $identity); $this->columns[] = $column; return $column; } - public function bigInteger(string $name, bool $identity = false, bool $signed = true): BigInteger + public function bigInteger(string $name, bool $identity = false): BigInteger { - $column = new BigInteger($name, $identity, $signed); + $column = new BigInteger($name, $identity); $this->columns[] = $column; return $column; } - public function smallInteger(string $name, bool $identity = false, bool $signed = true): SmallInteger + public function smallInteger(string $name, bool $identity = false): SmallInteger { - $column = new SmallInteger($name, $identity, $signed); + $column = new SmallInteger($name, $identity); $this->columns[] = $column; @@ -79,18 +80,18 @@ public function text(string $name, int|null $limit = null): Text return $column; } - public function boolean(string $name, bool $signed = true): Boolean + public function boolean(string $name): Boolean { - $column = new Boolean($name, $signed); + $column = new Boolean($name); $this->columns[] = $column; return $column; } - public function decimal(string $name, int $precision = 10, int $scale = 2, bool $signed = true): Decimal + public function decimal(string $name, int $precision = 10, int $scale = 2): Decimal { - $column = new Decimal($name, $precision, $scale, $signed); + $column = new Decimal($name, $precision, $scale); $this->columns[] = $column; @@ -169,9 +170,9 @@ public function binary(string $name, int|null $limit = null): Binary return $column; } - public function id(string $name = 'id'): Integer + public function id(string $name = 'id'): UnsignedInteger { - $column = new Integer($name, null, true, false); + $column = new UnsignedInteger($name, null, true); $this->columns[] = $column; @@ -181,7 +182,7 @@ public function id(string $name = 'id'): Integer public function timestamps(bool $timezone = false): self { $createdAt = new Timestamp('created_at', $timezone); - $createdAt->notNull()->currentTimestamp(); + $createdAt->nullable()->currentTimestamp(); $this->columns[] = $createdAt; $updatedAt = new Timestamp('updated_at', $timezone); From 704355a9180d481b1a33adedd366bf8aecf6a09e Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 16:57:43 -0500 Subject: [PATCH 08/23] refactor(database): remove signed parameter from Decimal constructor, defaulting to true --- src/Database/Migrations/Columns/Decimal.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Database/Migrations/Columns/Decimal.php b/src/Database/Migrations/Columns/Decimal.php index 59f80e41..67e8257c 100644 --- a/src/Database/Migrations/Columns/Decimal.php +++ b/src/Database/Migrations/Columns/Decimal.php @@ -14,15 +14,11 @@ public function __construct( protected string $name, int $precision = 10, int $scale = 2, - bool $signed = true ) { parent::__construct($name); $this->options['precision'] = $precision; $this->options['scale'] = $scale; - - if (! $signed) { - $this->options['signed'] = false; - } + $this->options['signed'] = true; } public function getType(): string From 7cabf092a1080828b61ccf2b4a6bfb10c71cbc54 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 16:57:52 -0500 Subject: [PATCH 09/23] refactor(tests): update column options in TableTest to align with new column type implementations --- tests/Unit/Database/Migrations/TableTest.php | 45 ++++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 40ecefbc..23d3f48c 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -16,6 +16,7 @@ use Phenix\Database\Migrations\Columns\Str; use Phenix\Database\Migrations\Columns\Text; use Phenix\Database\Migrations\Columns\Timestamp; +use Phenix\Database\Migrations\Columns\UnsignedInteger; use Phenix\Database\Migrations\Columns\Uuid; use Phenix\Database\Migrations\Table; use Phinx\Db\Adapter\AdapterInterface; @@ -47,7 +48,7 @@ it('can add string column with options', function (): void { $table = new Table('users', adapter: $this->mockAdapter); - $table->string('username', 50)->notNull()->comment('User name'); + $table->string('username', 50)->comment('User name'); $columns = $table->getColumnBuilders(); @@ -59,8 +60,8 @@ expect($column->getName())->toBe('username'); expect($column->getType())->toBe('string'); expect($column->getOptions())->toBe([ - 'limit' => 50, 'null' => false, + 'limit' => 50, 'comment' => 'User name', ]); }); @@ -68,12 +69,14 @@ it('can add integer column with options', function (): void { $table = new Table('users', adapter: $this->mockAdapter); - $column = $table->integer('age', 10, false, true)->default(0)->comment('User age'); + $column = $table->integer('age', 10, false)->default(0)->comment('User age'); expect($column)->toBeInstanceOf(Integer::class); expect($column->getName())->toBe('age'); expect($column->getType())->toBe('integer'); expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => true, 'limit' => 10, 'default' => 0, 'comment' => 'User age', @@ -83,15 +86,15 @@ it('can add big integer column with identity', function (): void { $table = new Table('users', adapter: $this->mockAdapter); - $column = $table->bigInteger('id', true, false)->comment('Primary key'); + $column = $table->bigInteger('id', true)->comment('Primary key'); expect($column)->toBeInstanceOf(BigInteger::class); expect($column->getName())->toBe('id'); expect($column->getType())->toBe('biginteger'); expect($column->getOptions())->toBe([ - 'identity' => true, 'null' => false, - 'signed' => false, + 'signed' => true, + 'identity' => true, 'comment' => 'Primary key', ]); }); @@ -99,12 +102,13 @@ it('can add small integer column', function (): void { $table = new Table('users', adapter: $this->mockAdapter); - $column = $table->smallInteger('status', false, true)->default(1); + $column = $table->smallInteger('status', false)->default(1); expect($column)->toBeInstanceOf(SmallInteger::class); expect($column->getName())->toBe('status'); expect($column->getType())->toBe('smallinteger'); expect($column->getOptions())->toBe([ + 'null' => false, 'default' => 1, ]); }); @@ -118,8 +122,8 @@ expect($column->getName())->toBe('content'); expect($column->getType())->toBe('text'); expect($column->getOptions())->toBe([ - 'limit' => 1000, 'null' => true, + 'limit' => 1000, 'comment' => 'Post content', ]); }); @@ -127,12 +131,13 @@ it('can add boolean column', function (): void { $table = new Table('users', adapter: $this->mockAdapter); - $column = $table->boolean('is_active', true)->default(true)->comment('User status'); + $column = $table->boolean('is_active')->default(true)->comment('User status'); expect($column)->toBeInstanceOf(Boolean::class); expect($column->getName())->toBe('is_active'); expect($column->getType())->toBe('boolean'); expect($column->getOptions())->toBe([ + 'null' => false, 'default' => true, 'comment' => 'User status', ]); @@ -147,8 +152,10 @@ expect($column->getName())->toBe('price'); expect($column->getType())->toBe('decimal'); expect($column->getOptions())->toBe([ + 'null' => false, 'precision' => 8, 'scale' => 2, + 'signed' => true, 'default' => 0.00, 'comment' => 'Product price', ]); @@ -171,14 +178,14 @@ it('can add timestamp column with timezone', function (): void { $table = new Table('users', adapter: $this->mockAdapter); - $column = $table->timestamp('created_at', true)->notNull()->currentTimestamp(); + $column = $table->timestamp('created_at', true)->currentTimestamp(); expect($column)->toBeInstanceOf(Timestamp::class); expect($column->getName())->toBe('created_at'); expect($column->getType())->toBe('timestamp'); expect($column->getOptions())->toBe([ - 'timezone' => true, 'null' => false, + 'timezone' => true, 'default' => 'CURRENT_TIMESTAMP', ]); }); @@ -200,7 +207,7 @@ it('can add uuid column', function (): void { $table = new Table('users', adapter: $this->mockAdapter); - $column = $table->uuid('uuid')->notNull()->comment('Unique identifier'); + $column = $table->uuid('uuid')->comment('Unique identifier'); expect($column)->toBeInstanceOf(Uuid::class); expect($column->getName())->toBe('uuid'); @@ -220,6 +227,7 @@ expect($column->getName())->toBe('role'); expect($column->getType())->toBe('enum'); expect($column->getOptions())->toBe([ + 'null' => false, 'values' => ['admin', 'user', 'guest'], 'default' => 'user', 'comment' => 'User role', @@ -235,6 +243,7 @@ expect($column->getName())->toBe('temperature'); expect($column->getType())->toBe('float'); expect($column->getOptions())->toBe([ + 'null' => false, 'default' => 0.0, 'comment' => 'Temperature value', ]); @@ -243,7 +252,7 @@ it('can add date column', function (): void { $table = new Table('events', adapter: $this->mockAdapter); - $column = $table->date('event_date')->notNull()->comment('Event date'); + $column = $table->date('event_date')->comment('Event date'); expect($column)->toBeInstanceOf(Date::class); expect($column->getName())->toBe('event_date'); @@ -263,8 +272,8 @@ expect($column->getName())->toBe('file_data'); expect($column->getType())->toBe('binary'); expect($column->getOptions())->toBe([ - 'limit' => 1024, 'null' => true, + 'limit' => 1024, 'comment' => 'Binary file data', ]); }); @@ -274,13 +283,13 @@ $column = $table->id('user_id'); - expect($column)->toBeInstanceOf(Integer::class); + expect($column)->toBeInstanceOf(UnsignedInteger::class); expect($column->getName())->toBe('user_id'); expect($column->getType())->toBe('integer'); expect($column->getOptions())->toBe([ - 'identity' => true, 'null' => false, 'signed' => false, + 'identity' => true, ]); }); @@ -298,8 +307,8 @@ expect($createdAt->getName())->toBe('created_at'); expect($createdAt->getType())->toBe('timestamp'); expect($createdAt->getOptions())->toBe([ + 'null' => true, 'timezone' => true, - 'null' => false, 'default' => 'CURRENT_TIMESTAMP', ]); @@ -308,8 +317,8 @@ expect($updatedAt->getName())->toBe('updated_at'); expect($updatedAt->getType())->toBe('timestamp'); expect($updatedAt->getOptions())->toBe([ - 'timezone' => true, 'null' => true, + 'timezone' => true, 'update' => 'CURRENT_TIMESTAMP', ]); }); From 3bad8c656a2a86b5a757efdd794f852cfd43840e Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 17:02:02 -0500 Subject: [PATCH 10/23] refactor(database): add unsignedInteger and unsignedBigInteger methods to Table class --- src/Database/Migrations/Table.php | 19 +++++++++++ tests/Unit/Database/Migrations/TableTest.php | 34 ++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index 296d8279..8e24d553 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -19,6 +19,7 @@ use Phenix\Database\Migrations\Columns\Str; use Phenix\Database\Migrations\Columns\Text; use Phenix\Database\Migrations\Columns\Timestamp; +use Phenix\Database\Migrations\Columns\UnsignedBigInteger; use Phenix\Database\Migrations\Columns\UnsignedInteger; use Phenix\Database\Migrations\Columns\Uuid; use Phinx\Db\Table as PhinxTable; @@ -62,6 +63,24 @@ public function bigInteger(string $name, bool $identity = false): BigInteger return $column; } + public function unsignedInteger(string $name, int|null $limit = null, bool $identity = false): UnsignedInteger + { + $column = new UnsignedInteger($name, $limit, $identity); + + $this->columns[] = $column; + + return $column; + } + + public function unsignedBigInteger(string $name, bool $identity = false): UnsignedBigInteger + { + $column = new UnsignedBigInteger($name, $identity); + + $this->columns[] = $column; + + return $column; + } + public function smallInteger(string $name, bool $identity = false): SmallInteger { $column = new SmallInteger($name, $identity); diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 23d3f48c..4dcdac2b 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -16,6 +16,7 @@ use Phenix\Database\Migrations\Columns\Str; use Phenix\Database\Migrations\Columns\Text; use Phenix\Database\Migrations\Columns\Timestamp; +use Phenix\Database\Migrations\Columns\UnsignedBigInteger; use Phenix\Database\Migrations\Columns\UnsignedInteger; use Phenix\Database\Migrations\Columns\Uuid; use Phenix\Database\Migrations\Table; @@ -99,6 +100,39 @@ ]); }); +it('can add unsigned integer column with options', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->unsignedInteger('count', 10, false)->default(0)->comment('Item count'); + + expect($column)->toBeInstanceOf(UnsignedInteger::class); + expect($column->getName())->toBe('count'); + expect($column->getType())->toBe('integer'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => false, + 'limit' => 10, + 'default' => 0, + 'comment' => 'Item count', + ]); +}); + +it('can add unsigned big integer column with identity', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->unsignedBigInteger('id', true)->comment('Primary key'); + + expect($column)->toBeInstanceOf(UnsignedBigInteger::class); + expect($column->getName())->toBe('id'); + expect($column->getType())->toBe('biginteger'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => false, + 'identity' => true, + 'comment' => 'Primary key', + ]); +}); + it('can add small integer column', function (): void { $table = new Table('users', adapter: $this->mockAdapter); From dcaf2c3e67b2bd83e062ce549d5f8f5c9e27e9da Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 17:09:39 -0500 Subject: [PATCH 11/23] feat(database): add UnsignedDecimal, UnsignedFloat, and UnsignedSmallInteger column types to Table class --- .../Migrations/Columns/UnsignedDecimal.php | 45 ++++++++++++ .../Migrations/Columns/UnsignedFloat.php | 27 +++++++ .../Columns/UnsignedSmallInteger.php | 33 +++++++++ src/Database/Migrations/Table.php | 30 ++++++++ tests/Unit/Database/Migrations/TableTest.php | 71 ++++++++++++++++++- 5 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 src/Database/Migrations/Columns/UnsignedDecimal.php create mode 100644 src/Database/Migrations/Columns/UnsignedFloat.php create mode 100644 src/Database/Migrations/Columns/UnsignedSmallInteger.php diff --git a/src/Database/Migrations/Columns/UnsignedDecimal.php b/src/Database/Migrations/Columns/UnsignedDecimal.php new file mode 100644 index 00000000..780e01ef --- /dev/null +++ b/src/Database/Migrations/Columns/UnsignedDecimal.php @@ -0,0 +1,45 @@ +options['precision'] = $precision; + $this->options['scale'] = $scale; + $this->options['signed'] = false; + } + + public function getType(): string + { + return 'decimal'; + } + + public function default(float $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function precision(int $precision): static + { + $this->options['precision'] = $precision; + + return $this; + } + + public function scale(int $scale): static + { + $this->options['scale'] = $scale; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/UnsignedFloat.php b/src/Database/Migrations/Columns/UnsignedFloat.php new file mode 100644 index 00000000..36831afe --- /dev/null +++ b/src/Database/Migrations/Columns/UnsignedFloat.php @@ -0,0 +1,27 @@ +options['signed'] = false; + } + + public function getType(): string + { + return 'float'; + } + + public function default(float $value): static + { + $this->options['default'] = $value; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/UnsignedSmallInteger.php b/src/Database/Migrations/Columns/UnsignedSmallInteger.php new file mode 100644 index 00000000..1a844102 --- /dev/null +++ b/src/Database/Migrations/Columns/UnsignedSmallInteger.php @@ -0,0 +1,33 @@ +options['signed'] = false; + + if ($identity) { + $this->options['identity'] = true; + } + } + + public function getType(): string + { + return 'smallinteger'; + } + + public function identity(): static + { + $this->options['identity'] = true; + $this->options['null'] = false; + + return $this; + } +} diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index 8e24d553..59e9216b 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -20,7 +20,10 @@ use Phenix\Database\Migrations\Columns\Text; use Phenix\Database\Migrations\Columns\Timestamp; use Phenix\Database\Migrations\Columns\UnsignedBigInteger; +use Phenix\Database\Migrations\Columns\UnsignedDecimal; +use Phenix\Database\Migrations\Columns\UnsignedFloat; use Phenix\Database\Migrations\Columns\UnsignedInteger; +use Phenix\Database\Migrations\Columns\UnsignedSmallInteger; use Phenix\Database\Migrations\Columns\Uuid; use Phinx\Db\Table as PhinxTable; @@ -117,6 +120,33 @@ public function decimal(string $name, int $precision = 10, int $scale = 2): Deci return $column; } + public function unsignedDecimal(string $name, int $precision = 10, int $scale = 2): UnsignedDecimal + { + $column = new UnsignedDecimal($name, $precision, $scale); + + $this->columns[] = $column; + + return $column; + } + + public function unsignedSmallInteger(string $name, bool $identity = false): UnsignedSmallInteger + { + $column = new UnsignedSmallInteger($name, $identity); + + $this->columns[] = $column; + + return $column; + } + + public function unsignedFloat(string $name): UnsignedFloat + { + $column = new UnsignedFloat($name); + + $this->columns[] = $column; + + return $column; + } + public function dateTime(string $name): DateTime { $column = new DateTime($name); diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 4dcdac2b..09fd17a0 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -17,7 +17,10 @@ use Phenix\Database\Migrations\Columns\Text; use Phenix\Database\Migrations\Columns\Timestamp; use Phenix\Database\Migrations\Columns\UnsignedBigInteger; +use Phenix\Database\Migrations\Columns\UnsignedDecimal; +use Phenix\Database\Migrations\Columns\UnsignedFloat; use Phenix\Database\Migrations\Columns\UnsignedInteger; +use Phenix\Database\Migrations\Columns\UnsignedSmallInteger; use Phenix\Database\Migrations\Columns\Uuid; use Phenix\Database\Migrations\Table; use Phinx\Db\Adapter\AdapterInterface; @@ -180,7 +183,7 @@ it('can add decimal column with precision and scale', function (): void { $table = new Table('products', adapter: $this->mockAdapter); - $column = $table->decimal('price', 8, 2, true)->default(0.00)->comment('Product price'); + $column = $table->decimal('price', 8, 2)->default(0.00)->comment('Product price'); expect($column)->toBeInstanceOf(Decimal::class); expect($column->getName())->toBe('price'); @@ -356,3 +359,69 @@ 'update' => 'CURRENT_TIMESTAMP', ]); }); + +it('can add unsigned decimal column with precision and scale', function (): void { + $table = new Table('products', adapter: $this->mockAdapter); + + $column = $table->unsignedDecimal('price', 8, 2)->default(0.00)->comment('Product price'); + + expect($column)->toBeInstanceOf(UnsignedDecimal::class); + expect($column->getName())->toBe('price'); + expect($column->getType())->toBe('decimal'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'precision' => 8, + 'scale' => 2, + 'signed' => false, + 'default' => 0.00, + 'comment' => 'Product price', + ]); +}); + +it('can add unsigned small integer column', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->unsignedSmallInteger('status', false)->default(1)->comment('User status'); + + expect($column)->toBeInstanceOf(UnsignedSmallInteger::class); + expect($column->getName())->toBe('status'); + expect($column->getType())->toBe('smallinteger'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => false, + 'default' => 1, + 'comment' => 'User status', + ]); +}); + +it('can add unsigned small integer column with identity', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->unsignedSmallInteger('id', true)->comment('Primary key'); + + expect($column)->toBeInstanceOf(UnsignedSmallInteger::class); + expect($column->getName())->toBe('id'); + expect($column->getType())->toBe('smallinteger'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => false, + 'identity' => true, + 'comment' => 'Primary key', + ]); +}); + +it('can add unsigned float column', function (): void { + $table = new Table('measurements', adapter: $this->mockAdapter); + + $column = $table->unsignedFloat('temperature')->default(0.0)->comment('Temperature value'); + + expect($column)->toBeInstanceOf(UnsignedFloat::class); + expect($column->getName())->toBe('temperature'); + expect($column->getType())->toBe('float'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => false, + 'default' => 0.0, + 'comment' => 'Temperature value', + ]); +}); From cd4b55ebb61562e3f312f227d57b30f0bb892a76 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Oct 2025 17:40:20 -0500 Subject: [PATCH 12/23] feat(database): add adapter handling methods to Column class and update Table methods to use them --- src/Database/Migrations/Columns/Column.php | 38 +++++ src/Database/Migrations/Table.php | 146 +++++-------------- tests/Unit/Database/Migrations/TableTest.php | 56 +++++-- 3 files changed, 116 insertions(+), 124 deletions(-) diff --git a/src/Database/Migrations/Columns/Column.php b/src/Database/Migrations/Columns/Column.php index c2f78222..74c41a7d 100644 --- a/src/Database/Migrations/Columns/Column.php +++ b/src/Database/Migrations/Columns/Column.php @@ -5,11 +5,17 @@ namespace Phenix\Database\Migrations\Columns; use Phinx\Db\Adapter\MysqlAdapter; +use Phinx\Db\Adapter\SQLiteAdapter; +use Phinx\Db\Adapter\PostgresAdapter; +use Phinx\Db\Adapter\AdapterInterface; +use Phinx\Db\Adapter\SqlServerAdapter; abstract class Column { protected array $options = []; + protected AdapterInterface|null $adapter = null; + public function __construct( protected string $name ) { @@ -55,4 +61,36 @@ public function first(): static return $this; } + + public function setAdapter(AdapterInterface $adapter): static + { + $this->adapter = $adapter; + + return $this; + } + + public function getAdapter(): ?AdapterInterface + { + return $this->adapter; + } + + public function isMysql(): bool + { + return $this->adapter instanceof MysqlAdapter; + } + + public function isPostgres(): bool + { + return $this->adapter instanceof PostgresAdapter; + } + + public function isSQLite(): bool + { + return $this->adapter instanceof SQLiteAdapter; + } + + public function isSqlServer(): bool + { + return $this->adapter instanceof SqlServerAdapter; + } } diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index 59e9216b..70b37c84 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -41,202 +41,116 @@ public function getColumnBuilders(): array public function string(string $name, int $limit = 255): Str { - $column = new Str($name, $limit); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Str($name, $limit)); } public function integer(string $name, int|null $limit = null, bool $identity = false): Integer { - $column = new Integer($name, $limit, $identity); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Integer($name, $limit, $identity)); } public function bigInteger(string $name, bool $identity = false): BigInteger { - $column = new BigInteger($name, $identity); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new BigInteger($name, $identity)); } public function unsignedInteger(string $name, int|null $limit = null, bool $identity = false): UnsignedInteger { - $column = new UnsignedInteger($name, $limit, $identity); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new UnsignedInteger($name, $limit, $identity)); } public function unsignedBigInteger(string $name, bool $identity = false): UnsignedBigInteger { - $column = new UnsignedBigInteger($name, $identity); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new UnsignedBigInteger($name, $identity)); } public function smallInteger(string $name, bool $identity = false): SmallInteger { - $column = new SmallInteger($name, $identity); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new SmallInteger($name, $identity)); } public function text(string $name, int|null $limit = null): Text { - $column = new Text($name, $limit); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Text($name, $limit)); } public function boolean(string $name): Boolean { - $column = new Boolean($name); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Boolean($name)); } public function decimal(string $name, int $precision = 10, int $scale = 2): Decimal { - $column = new Decimal($name, $precision, $scale); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Decimal($name, $precision, $scale)); } public function unsignedDecimal(string $name, int $precision = 10, int $scale = 2): UnsignedDecimal { - $column = new UnsignedDecimal($name, $precision, $scale); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new UnsignedDecimal($name, $precision, $scale)); } public function unsignedSmallInteger(string $name, bool $identity = false): UnsignedSmallInteger { - $column = new UnsignedSmallInteger($name, $identity); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new UnsignedSmallInteger($name, $identity)); } public function unsignedFloat(string $name): UnsignedFloat { - $column = new UnsignedFloat($name); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new UnsignedFloat($name)); } public function dateTime(string $name): DateTime { - $column = new DateTime($name); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new DateTime($name)); } public function timestamp(string $name, bool $timezone = false): Timestamp { - $column = new Timestamp($name, $timezone); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Timestamp($name, $timezone)); } public function json(string $name): Json { - $column = new Json($name); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Json($name)); } public function uuid(string $name): Uuid { - $column = new Uuid($name); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Uuid($name)); } public function enum(string $name, array $values): Enum { - $column = new Enum($name, $values); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Enum($name, $values)); } public function float(string $name): Floating { - $column = new Floating($name); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Floating($name)); } public function date(string $name): Date { - $column = new Date($name); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Date($name)); } public function binary(string $name, int|null $limit = null): Binary { - $column = new Binary($name, $limit); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new Binary($name, $limit)); } public function id(string $name = 'id'): UnsignedInteger { - $column = new UnsignedInteger($name, null, true); - - $this->columns[] = $column; - - return $column; + return $this->addColumnWithAdapter(new UnsignedInteger($name, null, true)); } public function timestamps(bool $timezone = false): self { - $createdAt = new Timestamp('created_at', $timezone); + $createdAt = $this->addColumnWithAdapter(new Timestamp('created_at', $timezone)); $createdAt->nullable()->currentTimestamp(); - $this->columns[] = $createdAt; - $updatedAt = new Timestamp('updated_at', $timezone); + $updatedAt = $this->addColumnWithAdapter(new Timestamp('updated_at', $timezone)); $updatedAt->nullable()->onUpdateCurrentTimestamp(); - $this->columns[] = $updatedAt; return $this; } @@ -249,4 +163,18 @@ public function __destruct() $this->save(); } + + /** + * @template T of Column + * @param T $column + * @return T + */ + private function addColumnWithAdapter(Column $column): Column + { + $column->setAdapter($this->getAdapter()); + + $this->columns[] = $column; + + return $column; + } } diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 09fd17a0..8328a252 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -2,29 +2,31 @@ declare(strict_types=1); -use Phenix\Database\Migrations\Columns\BigInteger; -use Phenix\Database\Migrations\Columns\Binary; -use Phenix\Database\Migrations\Columns\Boolean; +use Phinx\Db\Table\Column; +use Phinx\Db\Adapter\MysqlAdapter; +use Phenix\Database\Migrations\Table; +use Phinx\Db\Adapter\PostgresAdapter; +use Phinx\Db\Adapter\AdapterInterface; +use Phenix\Database\Migrations\Columns\Str; use Phenix\Database\Migrations\Columns\Date; -use Phenix\Database\Migrations\Columns\DateTime; -use Phenix\Database\Migrations\Columns\Decimal; use Phenix\Database\Migrations\Columns\Enum; -use Phenix\Database\Migrations\Columns\Floating; -use Phenix\Database\Migrations\Columns\Integer; use Phenix\Database\Migrations\Columns\Json; -use Phenix\Database\Migrations\Columns\SmallInteger; -use Phenix\Database\Migrations\Columns\Str; use Phenix\Database\Migrations\Columns\Text; +use Phenix\Database\Migrations\Columns\Uuid; +use Phenix\Database\Migrations\Columns\Binary; +use Phenix\Database\Migrations\Columns\Boolean; +use Phenix\Database\Migrations\Columns\Decimal; +use Phenix\Database\Migrations\Columns\Integer; +use Phenix\Database\Migrations\Columns\DateTime; +use Phenix\Database\Migrations\Columns\Floating; use Phenix\Database\Migrations\Columns\Timestamp; -use Phenix\Database\Migrations\Columns\UnsignedBigInteger; -use Phenix\Database\Migrations\Columns\UnsignedDecimal; +use Phenix\Database\Migrations\Columns\BigInteger; +use Phenix\Database\Migrations\Columns\SmallInteger; use Phenix\Database\Migrations\Columns\UnsignedFloat; +use Phenix\Database\Migrations\Columns\UnsignedDecimal; use Phenix\Database\Migrations\Columns\UnsignedInteger; +use Phenix\Database\Migrations\Columns\UnsignedBigInteger; use Phenix\Database\Migrations\Columns\UnsignedSmallInteger; -use Phenix\Database\Migrations\Columns\Uuid; -use Phenix\Database\Migrations\Table; -use Phinx\Db\Adapter\AdapterInterface; -use Phinx\Db\Table\Column; beforeEach(function (): void { $this->mockAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); @@ -425,3 +427,27 @@ 'comment' => 'Temperature value', ]); }); + +it('can change adapter for columns', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->string('name', 100); + + expect($column->getAdapter())->toBe($this->mockAdapter); + + $mysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $column->setAdapter($mysqlAdapter); + expect($column->isMysql())->toBeTrue(); + + $postgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $column->setAdapter($postgresAdapter); + + expect($column->isPostgres())->toBeTrue(); + expect($column->isMysql())->toBeFalse(); +}); From 34dab2a99e9c6445698f6f53d2f10a21f08f9f49 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 31 Oct 2025 12:16:13 -0500 Subject: [PATCH 13/23] feat(database): add new column types (Bit, Blob, Char, Cidr, Double, Inet, Interval, JsonB, MacAddr, Set, Time) and update Table methods to support them --- src/Database/Migrations/Columns/Bit.php | 41 ++++ src/Database/Migrations/Columns/Blob.php | 69 ++++++ src/Database/Migrations/Columns/Char.php | 53 +++++ src/Database/Migrations/Columns/Cidr.php | 26 +++ src/Database/Migrations/Columns/Column.php | 52 ++++- src/Database/Migrations/Columns/Double.php | 46 ++++ src/Database/Migrations/Columns/Inet.php | 26 +++ src/Database/Migrations/Columns/Interval.php | 26 +++ src/Database/Migrations/Columns/JsonB.php | 26 +++ src/Database/Migrations/Columns/MacAddr.php | 26 +++ src/Database/Migrations/Columns/Set.php | 53 +++++ src/Database/Migrations/Columns/Time.php | 40 ++++ src/Database/Migrations/Columns/Timestamp.php | 4 +- src/Database/Migrations/Table.php | 66 ++++++ tests/Unit/Database/Migrations/TableTest.php | 204 ++++++++++++++++-- 15 files changed, 737 insertions(+), 21 deletions(-) create mode 100644 src/Database/Migrations/Columns/Bit.php create mode 100644 src/Database/Migrations/Columns/Blob.php create mode 100644 src/Database/Migrations/Columns/Char.php create mode 100644 src/Database/Migrations/Columns/Cidr.php create mode 100644 src/Database/Migrations/Columns/Double.php create mode 100644 src/Database/Migrations/Columns/Inet.php create mode 100644 src/Database/Migrations/Columns/Interval.php create mode 100644 src/Database/Migrations/Columns/JsonB.php create mode 100644 src/Database/Migrations/Columns/MacAddr.php create mode 100644 src/Database/Migrations/Columns/Set.php create mode 100644 src/Database/Migrations/Columns/Time.php diff --git a/src/Database/Migrations/Columns/Bit.php b/src/Database/Migrations/Columns/Bit.php new file mode 100644 index 00000000..59349c11 --- /dev/null +++ b/src/Database/Migrations/Columns/Bit.php @@ -0,0 +1,41 @@ +options['limit'] = $limit; + } + + public function getType(): string + { + return 'bit'; + } + + public function limit(int $limit): static + { + if ($limit < 1 || $limit > 64) { + throw new InvalidArgumentException('Bit limit must be between 1 and 64'); + } + + $this->options['limit'] = $limit; + + return $this; + } + + public function default(int $default): static + { + $this->options['default'] = $default; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Blob.php b/src/Database/Migrations/Columns/Blob.php new file mode 100644 index 00000000..48c8b073 --- /dev/null +++ b/src/Database/Migrations/Columns/Blob.php @@ -0,0 +1,69 @@ +options['limit'] = $limit; + } + } + + public function getType(): string + { + return 'blob'; + } + + public function limit(int $limit): static + { + $this->options['limit'] = $limit; + + return $this; + } + + public function tiny(): static + { + if ($this->isMysql()) { + $this->options['limit'] = MysqlAdapter::BLOB_TINY; + } + + return $this; + } + + public function regular(): static + { + if ($this->isMysql()) { + $this->options['limit'] = MysqlAdapter::BLOB_REGULAR; + } + + return $this; + } + + public function medium(): static + { + if ($this->isMysql()) { + $this->options['limit'] = MysqlAdapter::BLOB_MEDIUM; + } + + return $this; + } + + public function long(): static + { + if ($this->isMysql()) { + $this->options['limit'] = MysqlAdapter::BLOB_LONG; + } + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Char.php b/src/Database/Migrations/Columns/Char.php new file mode 100644 index 00000000..332bd5be --- /dev/null +++ b/src/Database/Migrations/Columns/Char.php @@ -0,0 +1,53 @@ +options['limit'] = $limit; + } + + public function getType(): string + { + return 'char'; + } + + public function limit(int $limit): static + { + $this->options['limit'] = $limit; + + return $this; + } + + public function collation(string $collation): static + { + if ($this->isMysql()) { + $this->options['collation'] = $collation; + } + + return $this; + } + + public function encoding(string $encoding): static + { + if ($this->isMysql()) { + $this->options['encoding'] = $encoding; + } + + return $this; + } + + public function default(string $default): static + { + $this->options['default'] = $default; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Cidr.php b/src/Database/Migrations/Columns/Cidr.php new file mode 100644 index 00000000..1d97ca0a --- /dev/null +++ b/src/Database/Migrations/Columns/Cidr.php @@ -0,0 +1,26 @@ +options['default'] = $default; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Column.php b/src/Database/Migrations/Columns/Column.php index 74c41a7d..441f4067 100644 --- a/src/Database/Migrations/Columns/Column.php +++ b/src/Database/Migrations/Columns/Column.php @@ -4,10 +4,10 @@ namespace Phenix\Database\Migrations\Columns; +use Phinx\Db\Adapter\AdapterInterface; use Phinx\Db\Adapter\MysqlAdapter; -use Phinx\Db\Adapter\SQLiteAdapter; use Phinx\Db\Adapter\PostgresAdapter; -use Phinx\Db\Adapter\AdapterInterface; +use Phinx\Db\Adapter\SQLiteAdapter; use Phinx\Db\Adapter\SqlServerAdapter; abstract class Column @@ -62,6 +62,54 @@ public function first(): static return $this; } + public function collation(string $collation): static + { + if ($this->isMysql()) { + $this->options['collation'] = $collation; + } + + return $this; + } + + public function encoding(string $encoding): static + { + if ($this->isMysql()) { + $this->options['encoding'] = $encoding; + } + + return $this; + } + + public function timezone(bool $timezone = true): static + { + if ($this->isPostgres()) { + $this->options['timezone'] = $timezone; + } + + return $this; + } + + public function update(string $update): static + { + if ($this->isMysql()) { + $this->options['update'] = $update; + } + + return $this; + } + + public function length(int $length): static + { + return $this->limit($length); + } + + public function limit(int $limit): static + { + $this->options['limit'] = $limit; + + return $this; + } + public function setAdapter(AdapterInterface $adapter): static { $this->adapter = $adapter; diff --git a/src/Database/Migrations/Columns/Double.php b/src/Database/Migrations/Columns/Double.php new file mode 100644 index 00000000..39ee0554 --- /dev/null +++ b/src/Database/Migrations/Columns/Double.php @@ -0,0 +1,46 @@ +options['signed'] = $signed; + } + + public function getType(): string + { + return 'double'; + } + + public function default(float|int $default): static + { + $this->options['default'] = $default; + + return $this; + } + + public function unsigned(): static + { + if ($this->isMysql()) { + $this->options['signed'] = false; + } + + return $this; + } + + public function signed(): static + { + if ($this->isMysql()) { + $this->options['signed'] = true; + } + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Inet.php b/src/Database/Migrations/Columns/Inet.php new file mode 100644 index 00000000..44122adc --- /dev/null +++ b/src/Database/Migrations/Columns/Inet.php @@ -0,0 +1,26 @@ +options['default'] = $default; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Interval.php b/src/Database/Migrations/Columns/Interval.php new file mode 100644 index 00000000..74b7985e --- /dev/null +++ b/src/Database/Migrations/Columns/Interval.php @@ -0,0 +1,26 @@ +options['default'] = $default; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/JsonB.php b/src/Database/Migrations/Columns/JsonB.php new file mode 100644 index 00000000..fc125037 --- /dev/null +++ b/src/Database/Migrations/Columns/JsonB.php @@ -0,0 +1,26 @@ +options['default'] = $default; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/MacAddr.php b/src/Database/Migrations/Columns/MacAddr.php new file mode 100644 index 00000000..f6427a8c --- /dev/null +++ b/src/Database/Migrations/Columns/MacAddr.php @@ -0,0 +1,26 @@ +options['default'] = $default; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Set.php b/src/Database/Migrations/Columns/Set.php new file mode 100644 index 00000000..f408ba2a --- /dev/null +++ b/src/Database/Migrations/Columns/Set.php @@ -0,0 +1,53 @@ +options['values'] = $values; + } + + public function getType(): string + { + return 'set'; + } + + public function values(array $values): static + { + $this->options['values'] = $values; + + return $this; + } + + public function default(string|array $default): static + { + $this->options['default'] = $default; + + return $this; + } + + public function collation(string $collation): static + { + if ($this->isMysql()) { + $this->options['collation'] = $collation; + } + + return $this; + } + + public function encoding(string $encoding): static + { + if ($this->isMysql()) { + $this->options['encoding'] = $encoding; + } + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Time.php b/src/Database/Migrations/Columns/Time.php new file mode 100644 index 00000000..3392a100 --- /dev/null +++ b/src/Database/Migrations/Columns/Time.php @@ -0,0 +1,40 @@ +isPostgres()) { + $this->options['timezone'] = true; + } + } + + public function getType(): string + { + return 'time'; + } + + public function withTimezone(bool $timezone = true): static + { + if ($this->isPostgres()) { + $this->options['timezone'] = $timezone; + } + + return $this; + } + + public function default(string $default): static + { + $this->options['default'] = $default; + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Timestamp.php b/src/Database/Migrations/Columns/Timestamp.php index 8b33bd35..ef6dbe6d 100644 --- a/src/Database/Migrations/Columns/Timestamp.php +++ b/src/Database/Migrations/Columns/Timestamp.php @@ -28,9 +28,9 @@ public function default(string $value): static return $this; } - public function timezone(): static + public function timezone(bool $timezone = true): static { - $this->options['timezone'] = true; + $this->options['timezone'] = $timezone; return $this; } diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index 70b37c84..a03f07cd 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -6,18 +6,29 @@ use Phenix\Database\Migrations\Columns\BigInteger; use Phenix\Database\Migrations\Columns\Binary; +use Phenix\Database\Migrations\Columns\Bit; +use Phenix\Database\Migrations\Columns\Blob; use Phenix\Database\Migrations\Columns\Boolean; +use Phenix\Database\Migrations\Columns\Char; +use Phenix\Database\Migrations\Columns\Cidr; use Phenix\Database\Migrations\Columns\Column; use Phenix\Database\Migrations\Columns\Date; use Phenix\Database\Migrations\Columns\DateTime; use Phenix\Database\Migrations\Columns\Decimal; +use Phenix\Database\Migrations\Columns\Double; use Phenix\Database\Migrations\Columns\Enum; use Phenix\Database\Migrations\Columns\Floating; +use Phenix\Database\Migrations\Columns\Inet; use Phenix\Database\Migrations\Columns\Integer; +use Phenix\Database\Migrations\Columns\Interval; use Phenix\Database\Migrations\Columns\Json; +use Phenix\Database\Migrations\Columns\JsonB; +use Phenix\Database\Migrations\Columns\MacAddr; +use Phenix\Database\Migrations\Columns\Set; use Phenix\Database\Migrations\Columns\SmallInteger; use Phenix\Database\Migrations\Columns\Str; use Phenix\Database\Migrations\Columns\Text; +use Phenix\Database\Migrations\Columns\Time; use Phenix\Database\Migrations\Columns\Timestamp; use Phenix\Database\Migrations\Columns\UnsignedBigInteger; use Phenix\Database\Migrations\Columns\UnsignedDecimal; @@ -139,6 +150,61 @@ public function binary(string $name, int|null $limit = null): Binary return $this->addColumnWithAdapter(new Binary($name, $limit)); } + public function char(string $name, int $limit = 255): Char + { + return $this->addColumnWithAdapter(new Char($name, $limit)); + } + + public function time(string $name, bool $timezone = false): Time + { + return $this->addColumnWithAdapter(new Time($name, $timezone)); + } + + public function double(string $name, bool $signed = true): Double + { + return $this->addColumnWithAdapter(new Double($name, $signed)); + } + + public function blob(string $name, int|null $limit = null): Blob + { + return $this->addColumnWithAdapter(new Blob($name, $limit)); + } + + public function set(string $name, array $values): Set + { + return $this->addColumnWithAdapter(new Set($name, $values)); + } + + public function bit(string $name, int $limit = 1): Bit + { + return $this->addColumnWithAdapter(new Bit($name, $limit)); + } + + public function jsonb(string $name): JsonB + { + return $this->addColumnWithAdapter(new JsonB($name)); + } + + public function inet(string $name): Inet + { + return $this->addColumnWithAdapter(new Inet($name)); + } + + public function cidr(string $name): Cidr + { + return $this->addColumnWithAdapter(new Cidr($name)); + } + + public function macaddr(string $name): MacAddr + { + return $this->addColumnWithAdapter(new MacAddr($name)); + } + + public function interval(string $name): Interval + { + return $this->addColumnWithAdapter(new Interval($name)); + } + public function id(string $name = 'id'): UnsignedInteger { return $this->addColumnWithAdapter(new UnsignedInteger($name, null, true)); diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 8328a252..079e62f2 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -2,31 +2,42 @@ declare(strict_types=1); -use Phinx\Db\Table\Column; -use Phinx\Db\Adapter\MysqlAdapter; -use Phenix\Database\Migrations\Table; -use Phinx\Db\Adapter\PostgresAdapter; -use Phinx\Db\Adapter\AdapterInterface; -use Phenix\Database\Migrations\Columns\Str; -use Phenix\Database\Migrations\Columns\Date; -use Phenix\Database\Migrations\Columns\Enum; -use Phenix\Database\Migrations\Columns\Json; -use Phenix\Database\Migrations\Columns\Text; -use Phenix\Database\Migrations\Columns\Uuid; +use Phenix\Database\Migrations\Columns\BigInteger; use Phenix\Database\Migrations\Columns\Binary; +use Phenix\Database\Migrations\Columns\Bit; +use Phenix\Database\Migrations\Columns\Blob; use Phenix\Database\Migrations\Columns\Boolean; -use Phenix\Database\Migrations\Columns\Decimal; -use Phenix\Database\Migrations\Columns\Integer; +use Phenix\Database\Migrations\Columns\Char; +use Phenix\Database\Migrations\Columns\Cidr; +use Phenix\Database\Migrations\Columns\Date; use Phenix\Database\Migrations\Columns\DateTime; +use Phenix\Database\Migrations\Columns\Decimal; +use Phenix\Database\Migrations\Columns\Double; +use Phenix\Database\Migrations\Columns\Enum; use Phenix\Database\Migrations\Columns\Floating; -use Phenix\Database\Migrations\Columns\Timestamp; -use Phenix\Database\Migrations\Columns\BigInteger; +use Phenix\Database\Migrations\Columns\Inet; +use Phenix\Database\Migrations\Columns\Integer; +use Phenix\Database\Migrations\Columns\Interval; +use Phenix\Database\Migrations\Columns\Json; +use Phenix\Database\Migrations\Columns\JsonB; +use Phenix\Database\Migrations\Columns\MacAddr; +use Phenix\Database\Migrations\Columns\Set; use Phenix\Database\Migrations\Columns\SmallInteger; -use Phenix\Database\Migrations\Columns\UnsignedFloat; +use Phenix\Database\Migrations\Columns\Str; +use Phenix\Database\Migrations\Columns\Text; +use Phenix\Database\Migrations\Columns\Time; +use Phenix\Database\Migrations\Columns\Timestamp; +use Phenix\Database\Migrations\Columns\UnsignedBigInteger; use Phenix\Database\Migrations\Columns\UnsignedDecimal; +use Phenix\Database\Migrations\Columns\UnsignedFloat; use Phenix\Database\Migrations\Columns\UnsignedInteger; -use Phenix\Database\Migrations\Columns\UnsignedBigInteger; use Phenix\Database\Migrations\Columns\UnsignedSmallInteger; +use Phenix\Database\Migrations\Columns\Uuid; +use Phenix\Database\Migrations\Table; +use Phinx\Db\Adapter\AdapterInterface; +use Phinx\Db\Adapter\MysqlAdapter; +use Phinx\Db\Adapter\PostgresAdapter; +use Phinx\Db\Table\Column; beforeEach(function (): void { $this->mockAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); @@ -451,3 +462,162 @@ expect($column->isPostgres())->toBeTrue(); expect($column->isMysql())->toBeFalse(); }); + +it('can add char column with limit', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->char('code', 10)->comment('Product code'); + + expect($column)->toBeInstanceOf(Char::class); + expect($column->getName())->toBe('code'); + expect($column->getType())->toBe('char'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 10, + 'comment' => 'Product code', + ]); +}); + +it('can add time column', function (): void { + $table = new Table('events', adapter: $this->mockAdapter); + + $column = $table->time('start_time')->comment('Event start time'); + + expect($column)->toBeInstanceOf(Time::class); + expect($column->getName())->toBe('start_time'); + expect($column->getType())->toBe('time'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'comment' => 'Event start time', + ]); +}); + +it('can add double column', function (): void { + $table = new Table('measurements', adapter: $this->mockAdapter); + + $column = $table->double('value')->default(0.0)->comment('Measurement value'); + + expect($column)->toBeInstanceOf(Double::class); + expect($column->getName())->toBe('value'); + expect($column->getType())->toBe('double'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => true, + 'default' => 0.0, + 'comment' => 'Measurement value', + ]); +}); + +it('can add blob column', function (): void { + $table = new Table('files', adapter: $this->mockAdapter); + + $column = $table->blob('data')->comment('File data'); + + expect($column)->toBeInstanceOf(Blob::class); + expect($column->getName())->toBe('data'); + expect($column->getType())->toBe('blob'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'comment' => 'File data', + ]); +}); + +it('can add set column with values', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->set('permissions', ['read', 'write', 'execute'])->comment('User permissions'); + + expect($column)->toBeInstanceOf(Set::class); + expect($column->getName())->toBe('permissions'); + expect($column->getType())->toBe('set'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'values' => ['read', 'write', 'execute'], + 'comment' => 'User permissions', + ]); +}); + +it('can add bit column', function (): void { + $table = new Table('flags', adapter: $this->mockAdapter); + + $column = $table->bit('flags', 8)->comment('Status flags'); + + expect($column)->toBeInstanceOf(Bit::class); + expect($column->getName())->toBe('flags'); + expect($column->getType())->toBe('bit'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 8, + 'comment' => 'Status flags', + ]); +}); + +it('can add jsonb column (PostgreSQL)', function (): void { + $table = new Table('data', adapter: $this->mockAdapter); + + $column = $table->jsonb('metadata')->nullable()->comment('JSON metadata'); + + expect($column)->toBeInstanceOf(JsonB::class); + expect($column->getName())->toBe('metadata'); + expect($column->getType())->toBe('jsonb'); + expect($column->getOptions())->toBe([ + 'null' => true, + 'comment' => 'JSON metadata', + ]); +}); + +it('can add inet column (PostgreSQL)', function (): void { + $table = new Table('connections', adapter: $this->mockAdapter); + + $column = $table->inet('ip_address')->comment('IP address'); + + expect($column)->toBeInstanceOf(Inet::class); + expect($column->getName())->toBe('ip_address'); + expect($column->getType())->toBe('inet'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'comment' => 'IP address', + ]); +}); + +it('can add cidr column (PostgreSQL)', function (): void { + $table = new Table('networks', adapter: $this->mockAdapter); + + $column = $table->cidr('network')->comment('Network CIDR'); + + expect($column)->toBeInstanceOf(Cidr::class); + expect($column->getName())->toBe('network'); + expect($column->getType())->toBe('cidr'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'comment' => 'Network CIDR', + ]); +}); + +it('can add macaddr column (PostgreSQL)', function (): void { + $table = new Table('devices', adapter: $this->mockAdapter); + + $column = $table->macaddr('mac_address')->comment('MAC address'); + + expect($column)->toBeInstanceOf(MacAddr::class); + expect($column->getName())->toBe('mac_address'); + expect($column->getType())->toBe('macaddr'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'comment' => 'MAC address', + ]); +}); + +it('can add interval column (PostgreSQL)', function (): void { + $table = new Table('events', adapter: $this->mockAdapter); + + $column = $table->interval('duration')->comment('Event duration'); + + expect($column)->toBeInstanceOf(Interval::class); + expect($column->getName())->toBe('duration'); + expect($column->getType())->toBe('interval'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'comment' => 'Event duration', + ]); +}); From 9e051abb83056af8fa4284d1c65666e479287c94 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 31 Oct 2025 14:28:22 -0500 Subject: [PATCH 14/23] refactor(database): remove constructor from column classes to simplify instantiation --- src/Database/Migrations/Columns/Boolean.php | 6 ------ src/Database/Migrations/Columns/Cidr.php | 6 ------ src/Database/Migrations/Columns/Date.php | 6 ------ src/Database/Migrations/Columns/DateTime.php | 6 ------ src/Database/Migrations/Columns/Floating.php | 6 ------ src/Database/Migrations/Columns/Inet.php | 6 ------ src/Database/Migrations/Columns/Interval.php | 6 ------ src/Database/Migrations/Columns/Json.php | 6 ------ src/Database/Migrations/Columns/JsonB.php | 6 ------ src/Database/Migrations/Columns/MacAddr.php | 6 ------ src/Database/Migrations/Columns/Uuid.php | 6 ------ 11 files changed, 66 deletions(-) diff --git a/src/Database/Migrations/Columns/Boolean.php b/src/Database/Migrations/Columns/Boolean.php index b92ad4f1..c2606bec 100644 --- a/src/Database/Migrations/Columns/Boolean.php +++ b/src/Database/Migrations/Columns/Boolean.php @@ -6,12 +6,6 @@ class Boolean extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'boolean'; diff --git a/src/Database/Migrations/Columns/Cidr.php b/src/Database/Migrations/Columns/Cidr.php index 1d97ca0a..130a1228 100644 --- a/src/Database/Migrations/Columns/Cidr.php +++ b/src/Database/Migrations/Columns/Cidr.php @@ -6,12 +6,6 @@ class Cidr extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'cidr'; diff --git a/src/Database/Migrations/Columns/Date.php b/src/Database/Migrations/Columns/Date.php index 19f28fcd..60fe6b4c 100644 --- a/src/Database/Migrations/Columns/Date.php +++ b/src/Database/Migrations/Columns/Date.php @@ -6,12 +6,6 @@ class Date extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'date'; diff --git a/src/Database/Migrations/Columns/DateTime.php b/src/Database/Migrations/Columns/DateTime.php index fb3a2e3f..5b4f7ba2 100644 --- a/src/Database/Migrations/Columns/DateTime.php +++ b/src/Database/Migrations/Columns/DateTime.php @@ -6,12 +6,6 @@ class DateTime extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'datetime'; diff --git a/src/Database/Migrations/Columns/Floating.php b/src/Database/Migrations/Columns/Floating.php index 7e2bc73f..cb509a46 100644 --- a/src/Database/Migrations/Columns/Floating.php +++ b/src/Database/Migrations/Columns/Floating.php @@ -6,12 +6,6 @@ class Floating extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'float'; diff --git a/src/Database/Migrations/Columns/Inet.php b/src/Database/Migrations/Columns/Inet.php index 44122adc..ce56a00f 100644 --- a/src/Database/Migrations/Columns/Inet.php +++ b/src/Database/Migrations/Columns/Inet.php @@ -6,12 +6,6 @@ class Inet extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'inet'; diff --git a/src/Database/Migrations/Columns/Interval.php b/src/Database/Migrations/Columns/Interval.php index 74b7985e..c903827e 100644 --- a/src/Database/Migrations/Columns/Interval.php +++ b/src/Database/Migrations/Columns/Interval.php @@ -6,12 +6,6 @@ class Interval extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'interval'; diff --git a/src/Database/Migrations/Columns/Json.php b/src/Database/Migrations/Columns/Json.php index dc2202ab..b043cc98 100644 --- a/src/Database/Migrations/Columns/Json.php +++ b/src/Database/Migrations/Columns/Json.php @@ -6,12 +6,6 @@ class Json extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'json'; diff --git a/src/Database/Migrations/Columns/JsonB.php b/src/Database/Migrations/Columns/JsonB.php index fc125037..0f2ceb95 100644 --- a/src/Database/Migrations/Columns/JsonB.php +++ b/src/Database/Migrations/Columns/JsonB.php @@ -6,12 +6,6 @@ class JsonB extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'jsonb'; diff --git a/src/Database/Migrations/Columns/MacAddr.php b/src/Database/Migrations/Columns/MacAddr.php index f6427a8c..7b7d3ada 100644 --- a/src/Database/Migrations/Columns/MacAddr.php +++ b/src/Database/Migrations/Columns/MacAddr.php @@ -6,12 +6,6 @@ class MacAddr extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'macaddr'; diff --git a/src/Database/Migrations/Columns/Uuid.php b/src/Database/Migrations/Columns/Uuid.php index 807b8f53..f311eb70 100644 --- a/src/Database/Migrations/Columns/Uuid.php +++ b/src/Database/Migrations/Columns/Uuid.php @@ -6,12 +6,6 @@ class Uuid extends Column { - public function __construct( - protected string $name - ) { - parent::__construct($name); - } - public function getType(): string { return 'uuid'; From 196db15e0f020f764d5dd27b462133527e2d920f Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 31 Oct 2025 17:59:49 -0500 Subject: [PATCH 15/23] refactor: split table class --- .../Columns/Concerns/WithBinary.php | 27 +++ .../Columns/Concerns/WithConvenience.php | 27 +++ .../Columns/Concerns/WithDateTime.php | 39 ++++ .../Migrations/Columns/Concerns/WithJson.php | 21 ++ .../Columns/Concerns/WithNetwork.php | 27 +++ .../Columns/Concerns/WithNumeric.php | 75 ++++++ .../Columns/Concerns/WithSpecial.php | 33 +++ .../Migrations/Columns/Concerns/WithText.php | 27 +++ src/Database/Migrations/Table.php | 219 ++---------------- 9 files changed, 293 insertions(+), 202 deletions(-) create mode 100644 src/Database/Migrations/Columns/Concerns/WithBinary.php create mode 100644 src/Database/Migrations/Columns/Concerns/WithConvenience.php create mode 100644 src/Database/Migrations/Columns/Concerns/WithDateTime.php create mode 100644 src/Database/Migrations/Columns/Concerns/WithJson.php create mode 100644 src/Database/Migrations/Columns/Concerns/WithNetwork.php create mode 100644 src/Database/Migrations/Columns/Concerns/WithNumeric.php create mode 100644 src/Database/Migrations/Columns/Concerns/WithSpecial.php create mode 100644 src/Database/Migrations/Columns/Concerns/WithText.php diff --git a/src/Database/Migrations/Columns/Concerns/WithBinary.php b/src/Database/Migrations/Columns/Concerns/WithBinary.php new file mode 100644 index 00000000..46102210 --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/WithBinary.php @@ -0,0 +1,27 @@ +addColumnWithAdapter(new Binary($name, $limit)); + } + + public function blob(string $name, int|null $limit = null): Blob + { + return $this->addColumnWithAdapter(new Blob($name, $limit)); + } + + public function bit(string $name, int $limit = 1): Bit + { + return $this->addColumnWithAdapter(new Bit($name, $limit)); + } +} diff --git a/src/Database/Migrations/Columns/Concerns/WithConvenience.php b/src/Database/Migrations/Columns/Concerns/WithConvenience.php new file mode 100644 index 00000000..c76ed3da --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/WithConvenience.php @@ -0,0 +1,27 @@ +addColumnWithAdapter(new UnsignedInteger($name, null, true)); + } + + public function timestamps(bool $timezone = false): self + { + $createdAt = $this->addColumnWithAdapter(new Timestamp('created_at', $timezone)); + $createdAt->nullable()->currentTimestamp(); + + $updatedAt = $this->addColumnWithAdapter(new Timestamp('updated_at', $timezone)); + $updatedAt->nullable()->onUpdateCurrentTimestamp(); + + return $this; + } +} diff --git a/src/Database/Migrations/Columns/Concerns/WithDateTime.php b/src/Database/Migrations/Columns/Concerns/WithDateTime.php new file mode 100644 index 00000000..a18a0ff5 --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/WithDateTime.php @@ -0,0 +1,39 @@ +addColumnWithAdapter(new DateTime($name)); + } + + public function date(string $name): Date + { + return $this->addColumnWithAdapter(new Date($name)); + } + + public function time(string $name, bool $timezone = false): Time + { + return $this->addColumnWithAdapter(new Time($name, $timezone)); + } + + public function timestamp(string $name, bool $timezone = false): Timestamp + { + return $this->addColumnWithAdapter(new Timestamp($name, $timezone)); + } + + public function interval(string $name): Interval + { + return $this->addColumnWithAdapter(new Interval($name)); + } +} diff --git a/src/Database/Migrations/Columns/Concerns/WithJson.php b/src/Database/Migrations/Columns/Concerns/WithJson.php new file mode 100644 index 00000000..d31892d0 --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/WithJson.php @@ -0,0 +1,21 @@ +addColumnWithAdapter(new Json($name)); + } + + public function jsonb(string $name): JsonB + { + return $this->addColumnWithAdapter(new JsonB($name)); + } +} diff --git a/src/Database/Migrations/Columns/Concerns/WithNetwork.php b/src/Database/Migrations/Columns/Concerns/WithNetwork.php new file mode 100644 index 00000000..3138aaf0 --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/WithNetwork.php @@ -0,0 +1,27 @@ +addColumnWithAdapter(new Inet($name)); + } + + public function cidr(string $name): Cidr + { + return $this->addColumnWithAdapter(new Cidr($name)); + } + + public function macaddr(string $name): MacAddr + { + return $this->addColumnWithAdapter(new MacAddr($name)); + } +} diff --git a/src/Database/Migrations/Columns/Concerns/WithNumeric.php b/src/Database/Migrations/Columns/Concerns/WithNumeric.php new file mode 100644 index 00000000..3cdc1c7a --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/WithNumeric.php @@ -0,0 +1,75 @@ +addColumnWithAdapter(new Integer($name, $limit, $identity)); + } + + public function bigInteger(string $name, bool $identity = false): BigInteger + { + return $this->addColumnWithAdapter(new BigInteger($name, $identity)); + } + + public function smallInteger(string $name, bool $identity = false): SmallInteger + { + return $this->addColumnWithAdapter(new SmallInteger($name, $identity)); + } + + public function unsignedInteger(string $name, int|null $limit = null, bool $identity = false): UnsignedInteger + { + return $this->addColumnWithAdapter(new UnsignedInteger($name, $limit, $identity)); + } + + public function unsignedBigInteger(string $name, bool $identity = false): UnsignedBigInteger + { + return $this->addColumnWithAdapter(new UnsignedBigInteger($name, $identity)); + } + + public function unsignedSmallInteger(string $name, bool $identity = false): UnsignedSmallInteger + { + return $this->addColumnWithAdapter(new UnsignedSmallInteger($name, $identity)); + } + + public function decimal(string $name, int $precision = 10, int $scale = 2): Decimal + { + return $this->addColumnWithAdapter(new Decimal($name, $precision, $scale)); + } + + public function unsignedDecimal(string $name, int $precision = 10, int $scale = 2): UnsignedDecimal + { + return $this->addColumnWithAdapter(new UnsignedDecimal($name, $precision, $scale)); + } + + public function float(string $name): Floating + { + return $this->addColumnWithAdapter(new Floating($name)); + } + + public function unsignedFloat(string $name): UnsignedFloat + { + return $this->addColumnWithAdapter(new UnsignedFloat($name)); + } + + public function double(string $name, bool $signed = true): Double + { + return $this->addColumnWithAdapter(new Double($name, $signed)); + } +} diff --git a/src/Database/Migrations/Columns/Concerns/WithSpecial.php b/src/Database/Migrations/Columns/Concerns/WithSpecial.php new file mode 100644 index 00000000..77b614e4 --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/WithSpecial.php @@ -0,0 +1,33 @@ +addColumnWithAdapter(new Boolean($name)); + } + + public function uuid(string $name): Uuid + { + return $this->addColumnWithAdapter(new Uuid($name)); + } + + public function enum(string $name, array $values): Enum + { + return $this->addColumnWithAdapter(new Enum($name, $values)); + } + + public function set(string $name, array $values): Set + { + return $this->addColumnWithAdapter(new Set($name, $values)); + } +} diff --git a/src/Database/Migrations/Columns/Concerns/WithText.php b/src/Database/Migrations/Columns/Concerns/WithText.php new file mode 100644 index 00000000..55abd51d --- /dev/null +++ b/src/Database/Migrations/Columns/Concerns/WithText.php @@ -0,0 +1,27 @@ +addColumnWithAdapter(new Str($name, $limit)); + } + + public function text(string $name, int|null $limit = null): Text + { + return $this->addColumnWithAdapter(new Text($name, $limit)); + } + + public function char(string $name, int $limit = 255): Char + { + return $this->addColumnWithAdapter(new Char($name, $limit)); + } +} diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index a03f07cd..1651d3d7 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -4,42 +4,28 @@ namespace Phenix\Database\Migrations; -use Phenix\Database\Migrations\Columns\BigInteger; -use Phenix\Database\Migrations\Columns\Binary; -use Phenix\Database\Migrations\Columns\Bit; -use Phenix\Database\Migrations\Columns\Blob; -use Phenix\Database\Migrations\Columns\Boolean; -use Phenix\Database\Migrations\Columns\Char; -use Phenix\Database\Migrations\Columns\Cidr; use Phenix\Database\Migrations\Columns\Column; -use Phenix\Database\Migrations\Columns\Date; -use Phenix\Database\Migrations\Columns\DateTime; -use Phenix\Database\Migrations\Columns\Decimal; -use Phenix\Database\Migrations\Columns\Double; -use Phenix\Database\Migrations\Columns\Enum; -use Phenix\Database\Migrations\Columns\Floating; -use Phenix\Database\Migrations\Columns\Inet; -use Phenix\Database\Migrations\Columns\Integer; -use Phenix\Database\Migrations\Columns\Interval; -use Phenix\Database\Migrations\Columns\Json; -use Phenix\Database\Migrations\Columns\JsonB; -use Phenix\Database\Migrations\Columns\MacAddr; -use Phenix\Database\Migrations\Columns\Set; -use Phenix\Database\Migrations\Columns\SmallInteger; -use Phenix\Database\Migrations\Columns\Str; -use Phenix\Database\Migrations\Columns\Text; -use Phenix\Database\Migrations\Columns\Time; -use Phenix\Database\Migrations\Columns\Timestamp; -use Phenix\Database\Migrations\Columns\UnsignedBigInteger; -use Phenix\Database\Migrations\Columns\UnsignedDecimal; -use Phenix\Database\Migrations\Columns\UnsignedFloat; -use Phenix\Database\Migrations\Columns\UnsignedInteger; -use Phenix\Database\Migrations\Columns\UnsignedSmallInteger; -use Phenix\Database\Migrations\Columns\Uuid; +use Phenix\Database\Migrations\Columns\Concerns\WithBinary; +use Phenix\Database\Migrations\Columns\Concerns\WithConvenience; +use Phenix\Database\Migrations\Columns\Concerns\WithDateTime; +use Phenix\Database\Migrations\Columns\Concerns\WithJson; +use Phenix\Database\Migrations\Columns\Concerns\WithNetwork; +use Phenix\Database\Migrations\Columns\Concerns\WithNumeric; +use Phenix\Database\Migrations\Columns\Concerns\WithSpecial; +use Phenix\Database\Migrations\Columns\Concerns\WithText; use Phinx\Db\Table as PhinxTable; class Table extends PhinxTable { + use WithBinary; + use WithConvenience; + use WithDateTime; + use WithJson; + use WithNetwork; + use WithNumeric; + use WithSpecial; + use WithText; + /** * @var array */ @@ -50,177 +36,6 @@ public function getColumnBuilders(): array return $this->columns; } - public function string(string $name, int $limit = 255): Str - { - return $this->addColumnWithAdapter(new Str($name, $limit)); - } - - public function integer(string $name, int|null $limit = null, bool $identity = false): Integer - { - return $this->addColumnWithAdapter(new Integer($name, $limit, $identity)); - } - - public function bigInteger(string $name, bool $identity = false): BigInteger - { - return $this->addColumnWithAdapter(new BigInteger($name, $identity)); - } - - public function unsignedInteger(string $name, int|null $limit = null, bool $identity = false): UnsignedInteger - { - return $this->addColumnWithAdapter(new UnsignedInteger($name, $limit, $identity)); - } - - public function unsignedBigInteger(string $name, bool $identity = false): UnsignedBigInteger - { - return $this->addColumnWithAdapter(new UnsignedBigInteger($name, $identity)); - } - - public function smallInteger(string $name, bool $identity = false): SmallInteger - { - return $this->addColumnWithAdapter(new SmallInteger($name, $identity)); - } - - public function text(string $name, int|null $limit = null): Text - { - return $this->addColumnWithAdapter(new Text($name, $limit)); - } - - public function boolean(string $name): Boolean - { - return $this->addColumnWithAdapter(new Boolean($name)); - } - - public function decimal(string $name, int $precision = 10, int $scale = 2): Decimal - { - return $this->addColumnWithAdapter(new Decimal($name, $precision, $scale)); - } - - public function unsignedDecimal(string $name, int $precision = 10, int $scale = 2): UnsignedDecimal - { - return $this->addColumnWithAdapter(new UnsignedDecimal($name, $precision, $scale)); - } - - public function unsignedSmallInteger(string $name, bool $identity = false): UnsignedSmallInteger - { - return $this->addColumnWithAdapter(new UnsignedSmallInteger($name, $identity)); - } - - public function unsignedFloat(string $name): UnsignedFloat - { - return $this->addColumnWithAdapter(new UnsignedFloat($name)); - } - - public function dateTime(string $name): DateTime - { - return $this->addColumnWithAdapter(new DateTime($name)); - } - - public function timestamp(string $name, bool $timezone = false): Timestamp - { - return $this->addColumnWithAdapter(new Timestamp($name, $timezone)); - } - - public function json(string $name): Json - { - return $this->addColumnWithAdapter(new Json($name)); - } - - public function uuid(string $name): Uuid - { - return $this->addColumnWithAdapter(new Uuid($name)); - } - - public function enum(string $name, array $values): Enum - { - return $this->addColumnWithAdapter(new Enum($name, $values)); - } - - public function float(string $name): Floating - { - return $this->addColumnWithAdapter(new Floating($name)); - } - - public function date(string $name): Date - { - return $this->addColumnWithAdapter(new Date($name)); - } - - public function binary(string $name, int|null $limit = null): Binary - { - return $this->addColumnWithAdapter(new Binary($name, $limit)); - } - - public function char(string $name, int $limit = 255): Char - { - return $this->addColumnWithAdapter(new Char($name, $limit)); - } - - public function time(string $name, bool $timezone = false): Time - { - return $this->addColumnWithAdapter(new Time($name, $timezone)); - } - - public function double(string $name, bool $signed = true): Double - { - return $this->addColumnWithAdapter(new Double($name, $signed)); - } - - public function blob(string $name, int|null $limit = null): Blob - { - return $this->addColumnWithAdapter(new Blob($name, $limit)); - } - - public function set(string $name, array $values): Set - { - return $this->addColumnWithAdapter(new Set($name, $values)); - } - - public function bit(string $name, int $limit = 1): Bit - { - return $this->addColumnWithAdapter(new Bit($name, $limit)); - } - - public function jsonb(string $name): JsonB - { - return $this->addColumnWithAdapter(new JsonB($name)); - } - - public function inet(string $name): Inet - { - return $this->addColumnWithAdapter(new Inet($name)); - } - - public function cidr(string $name): Cidr - { - return $this->addColumnWithAdapter(new Cidr($name)); - } - - public function macaddr(string $name): MacAddr - { - return $this->addColumnWithAdapter(new MacAddr($name)); - } - - public function interval(string $name): Interval - { - return $this->addColumnWithAdapter(new Interval($name)); - } - - public function id(string $name = 'id'): UnsignedInteger - { - return $this->addColumnWithAdapter(new UnsignedInteger($name, null, true)); - } - - public function timestamps(bool $timezone = false): self - { - $createdAt = $this->addColumnWithAdapter(new Timestamp('created_at', $timezone)); - $createdAt->nullable()->currentTimestamp(); - - $updatedAt = $this->addColumnWithAdapter(new Timestamp('updated_at', $timezone)); - $updatedAt->nullable()->onUpdateCurrentTimestamp(); - - return $this; - } - public function __destruct() { foreach ($this->columns as $column) { From a37b6a8891fa4ba5320f8e4e83cdac7b263d8c20 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Sun, 2 Nov 2025 16:48:20 -0500 Subject: [PATCH 16/23] tests: add migration columns tests --- .../Database/Migrations/Columns/CidrTest.php | 36 +++++++ .../Database/Migrations/Columns/DateTest.php | 36 +++++++ .../Migrations/Columns/DateTimeTest.php | 36 +++++++ .../Migrations/Columns/DecimalTest.php | 74 ++++++++++++++ .../Migrations/Columns/DoubleTest.php | 94 ++++++++++++++++++ .../Database/Migrations/Columns/EnumTest.php | 44 +++++++++ .../Database/Migrations/Columns/InetTest.php | 36 +++++++ .../Migrations/Columns/IntervalTest.php | 36 +++++++ .../Database/Migrations/Columns/JsonBTest.php | 43 ++++++++ .../Database/Migrations/Columns/JsonTest.php | 36 +++++++ .../Migrations/Columns/MacAddrTest.php | 36 +++++++ .../Migrations/Columns/NumberTest.php | 42 ++++++++ .../Database/Migrations/Columns/SetTest.php | 98 +++++++++++++++++++ .../Migrations/Columns/SmallIntegerTest.php | 76 ++++++++++++++ .../Database/Migrations/Columns/StrTest.php | 86 ++++++++++++++++ .../Database/Migrations/Columns/TextTest.php | 63 ++++++++++++ .../Database/Migrations/Columns/TimeTest.php | 73 ++++++++++++++ .../Migrations/Columns/TimestampTest.php | 80 +++++++++++++++ .../Columns/UnsignedDecimalTest.php | 60 ++++++++++++ .../Columns/UnsignedSmallIntegerTest.php | 55 +++++++++++ .../Database/Migrations/Columns/UuidTest.php | 36 +++++++ 21 files changed, 1176 insertions(+) create mode 100644 tests/Unit/Database/Migrations/Columns/CidrTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/DateTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/DateTimeTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/DecimalTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/DoubleTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/EnumTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/InetTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/IntervalTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/JsonBTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/JsonTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/MacAddrTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/NumberTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/SetTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/SmallIntegerTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/StrTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/TextTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/TimeTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/TimestampTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/UnsignedDecimalTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/UnsignedSmallIntegerTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/UuidTest.php diff --git a/tests/Unit/Database/Migrations/Columns/CidrTest.php b/tests/Unit/Database/Migrations/Columns/CidrTest.php new file mode 100644 index 00000000..9f4b98b4 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/CidrTest.php @@ -0,0 +1,36 @@ +getName())->toBe('network'); + expect($column->getType())->toBe('cidr'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can set default value', function (): void { + $column = new Cidr('network'); + $column->default('192.168.0.0/24'); + + expect($column->getOptions()['default'])->toBe('192.168.0.0/24'); +}); + +it('can be nullable', function (): void { + $column = new Cidr('subnet'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Cidr('network'); + $column->comment('Network CIDR block'); + + expect($column->getOptions()['comment'])->toBe('Network CIDR block'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/DateTest.php b/tests/Unit/Database/Migrations/Columns/DateTest.php new file mode 100644 index 00000000..37539eb4 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/DateTest.php @@ -0,0 +1,36 @@ +getName())->toBe('birth_date'); + expect($column->getType())->toBe('date'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can set default value', function (): void { + $column = new Date('created_date'); + $column->default('2023-01-01'); + + expect($column->getOptions()['default'])->toBe('2023-01-01'); +}); + +it('can be nullable', function (): void { + $column = new Date('deleted_at'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Date('birth_date'); + $column->comment('User birth date'); + + expect($column->getOptions()['comment'])->toBe('User birth date'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/DateTimeTest.php b/tests/Unit/Database/Migrations/Columns/DateTimeTest.php new file mode 100644 index 00000000..aedc8075 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/DateTimeTest.php @@ -0,0 +1,36 @@ +getName())->toBe('created_at'); + expect($column->getType())->toBe('datetime'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can set default value', function (): void { + $column = new DateTime('published_at'); + $column->default('2023-01-01 12:00:00'); + + expect($column->getOptions()['default'])->toBe('2023-01-01 12:00:00'); +}); + +it('can be nullable', function (): void { + $column = new DateTime('deleted_at'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new DateTime('created_at'); + $column->comment('Creation timestamp'); + + expect($column->getOptions()['comment'])->toBe('Creation timestamp'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/DecimalTest.php b/tests/Unit/Database/Migrations/Columns/DecimalTest.php new file mode 100644 index 00000000..40f27630 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/DecimalTest.php @@ -0,0 +1,74 @@ +getName())->toBe('price'); + expect($column->getType())->toBe('decimal'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'precision' => 10, + 'scale' => 2, + 'signed' => true, + ]); +}); + +it('can create decimal column with custom precision and scale', function (): void { + $column = new Decimal('amount', 15, 4); + + expect($column->getOptions()['precision'])->toBe(15); + expect($column->getOptions()['scale'])->toBe(4); +}); + +it('can set default value', function (): void { + $column = new Decimal('price'); + $column->default(99.99); + + expect($column->getOptions()['default'])->toBe(99.99); +}); + +it('can set precision', function (): void { + $column = new Decimal('price'); + $column->precision(12); + + expect($column->getOptions()['precision'])->toBe(12); +}); + +it('can set scale', function (): void { + $column = new Decimal('price'); + $column->scale(4); + + expect($column->getOptions()['scale'])->toBe(4); +}); + +it('can be unsigned', function (): void { + $column = new Decimal('price'); + $column->unsigned(); + + expect($column->getOptions()['signed'])->toBeFalse(); +}); + +it('can be signed', function (): void { + $column = new Decimal('balance'); + $column->signed(); + + expect($column->getOptions()['signed'])->toBeTrue(); +}); + +it('can be nullable', function (): void { + $column = new Decimal('discount'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Decimal('price'); + $column->comment('Product price'); + + expect($column->getOptions()['comment'])->toBe('Product price'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/DoubleTest.php b/tests/Unit/Database/Migrations/Columns/DoubleTest.php new file mode 100644 index 00000000..0de51eac --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/DoubleTest.php @@ -0,0 +1,94 @@ +mockMysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mockPostgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); +}); + +it('can create double column with default signed', function (): void { + $column = new Double('value'); + + expect($column->getName())->toBe('value'); + expect($column->getType())->toBe('double'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => true, + ]); +}); + +it('can create double column as unsigned', function (): void { + $column = new Double('value', false); + + expect($column->getOptions()['signed'])->toBeFalse(); +}); + +it('can set default value as float', function (): void { + $column = new Double('temperature'); + $column->default(98.6); + + expect($column->getOptions()['default'])->toBe(98.6); +}); + +it('can set default value as integer', function (): void { + $column = new Double('count'); + $column->default(100); + + expect($column->getOptions()['default'])->toBe(100); +}); + +it('can set unsigned for mysql', function (): void { + $column = new Double('value'); + $column->setAdapter($this->mockMysqlAdapter); + $column->unsigned(); + + expect($column->getOptions()['signed'])->toBeFalse(); +}); + +it('ignores unsigned for non-mysql adapters', function (): void { + $column = new Double('value'); + $column->setAdapter($this->mockPostgresAdapter); + $column->unsigned(); + + expect($column->getOptions()['signed'])->toBeTrue(); +}); + +it('can set signed for mysql', function (): void { + $column = new Double('value', false); + $column->setAdapter($this->mockMysqlAdapter); + $column->signed(); + + expect($column->getOptions()['signed'])->toBeTrue(); +}); + +it('ignores signed for non-mysql adapters', function (): void { + $column = new Double('value', false); + $column->setAdapter($this->mockPostgresAdapter); + $column->signed(); + + expect($column->getOptions()['signed'])->toBeFalse(); +}); + +it('can be nullable', function (): void { + $column = new Double('measurement'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Double('value'); + $column->comment('Measurement value'); + + expect($column->getOptions()['comment'])->toBe('Measurement value'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/EnumTest.php b/tests/Unit/Database/Migrations/Columns/EnumTest.php new file mode 100644 index 00000000..c6cc6841 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/EnumTest.php @@ -0,0 +1,44 @@ +getName())->toBe('status'); + expect($column->getType())->toBe('enum'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'values' => ['active', 'inactive', 'pending'], + ]); +}); + +it('can set default value', function (): void { + $column = new Enum('status', ['active', 'inactive']); + $column->default('active'); + + expect($column->getOptions()['default'])->toBe('active'); +}); + +it('can update values', function (): void { + $column = new Enum('role', ['user', 'admin']); + $column->values(['user', 'admin', 'moderator']); + + expect($column->getOptions()['values'])->toBe(['user', 'admin', 'moderator']); +}); + +it('can be nullable', function (): void { + $column = new Enum('status', ['active', 'inactive']); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Enum('status', ['active', 'inactive']); + $column->comment('User status'); + + expect($column->getOptions()['comment'])->toBe('User status'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/InetTest.php b/tests/Unit/Database/Migrations/Columns/InetTest.php new file mode 100644 index 00000000..9b5a7248 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/InetTest.php @@ -0,0 +1,36 @@ +getName())->toBe('ip_address'); + expect($column->getType())->toBe('inet'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can set default value', function (): void { + $column = new Inet('ip_address'); + $column->default('192.168.1.1'); + + expect($column->getOptions()['default'])->toBe('192.168.1.1'); +}); + +it('can be nullable', function (): void { + $column = new Inet('client_ip'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Inet('ip_address'); + $column->comment('Client IP address'); + + expect($column->getOptions()['comment'])->toBe('Client IP address'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/IntervalTest.php b/tests/Unit/Database/Migrations/Columns/IntervalTest.php new file mode 100644 index 00000000..5e09ed57 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/IntervalTest.php @@ -0,0 +1,36 @@ +getName())->toBe('duration'); + expect($column->getType())->toBe('interval'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can set default value', function (): void { + $column = new Interval('duration'); + $column->default('1 hour'); + + expect($column->getOptions()['default'])->toBe('1 hour'); +}); + +it('can be nullable', function (): void { + $column = new Interval('processing_time'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Interval('duration'); + $column->comment('Event duration'); + + expect($column->getOptions()['comment'])->toBe('Event duration'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/JsonBTest.php b/tests/Unit/Database/Migrations/Columns/JsonBTest.php new file mode 100644 index 00000000..16b6ad12 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/JsonBTest.php @@ -0,0 +1,43 @@ +getName())->toBe('metadata'); + expect($column->getType())->toBe('jsonb'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can set default value as string', function (): void { + $column = new JsonB('settings'); + $column->default('{}'); + + expect($column->getOptions()['default'])->toBe('{}'); +}); + +it('can set default value as array', function (): void { + $column = new JsonB('config'); + $column->default(['key' => 'value']); + + expect($column->getOptions()['default'])->toBe(['key' => 'value']); +}); + +it('can be nullable', function (): void { + $column = new JsonB('data'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new JsonB('metadata'); + $column->comment('JSONB metadata'); + + expect($column->getOptions()['comment'])->toBe('JSONB metadata'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/JsonTest.php b/tests/Unit/Database/Migrations/Columns/JsonTest.php new file mode 100644 index 00000000..aaa750c7 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/JsonTest.php @@ -0,0 +1,36 @@ +getName())->toBe('metadata'); + expect($column->getType())->toBe('json'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can set default value', function (): void { + $column = new Json('settings'); + $column->default('{}'); + + expect($column->getOptions()['default'])->toBe('{}'); +}); + +it('can be nullable', function (): void { + $column = new Json('config'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Json('metadata'); + $column->comment('JSON metadata'); + + expect($column->getOptions()['comment'])->toBe('JSON metadata'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/MacAddrTest.php b/tests/Unit/Database/Migrations/Columns/MacAddrTest.php new file mode 100644 index 00000000..1e8502ba --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/MacAddrTest.php @@ -0,0 +1,36 @@ +getName())->toBe('mac_address'); + expect($column->getType())->toBe('macaddr'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can set default value', function (): void { + $column = new MacAddr('mac_address'); + $column->default('08:00:2b:01:02:03'); + + expect($column->getOptions()['default'])->toBe('08:00:2b:01:02:03'); +}); + +it('can be nullable', function (): void { + $column = new MacAddr('device_mac'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new MacAddr('mac_address'); + $column->comment('Device MAC address'); + + expect($column->getOptions()['comment'])->toBe('Device MAC address'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/NumberTest.php b/tests/Unit/Database/Migrations/Columns/NumberTest.php new file mode 100644 index 00000000..272d6b10 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/NumberTest.php @@ -0,0 +1,42 @@ +default(42); + + expect($column->getOptions()['default'])->toBe(42); +}); + +it('can set identity', function (): void { + $column = new TestNumber('test'); + $column->identity(); + + expect($column->getOptions()['identity'])->toBeTrue(); +}); + +it('can be nullable', function (): void { + $column = new TestNumber('test'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new TestNumber('test'); + $column->comment('Test number'); + + expect($column->getOptions()['comment'])->toBe('Test number'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/SetTest.php b/tests/Unit/Database/Migrations/Columns/SetTest.php new file mode 100644 index 00000000..87f396a1 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/SetTest.php @@ -0,0 +1,98 @@ +mockAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); + + $this->mockMysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mockPostgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); +}); + +it('can create set column with values', function (): void { + $column = new Set('permissions', ['read', 'write', 'execute']); + + expect($column->getName())->toBe('permissions'); + expect($column->getType())->toBe('set'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'values' => ['read', 'write', 'execute'], + ]); +}); + +it('can set default value as string', function (): void { + $column = new Set('status', ['active', 'inactive']); + $column->default('active'); + + expect($column->getOptions()['default'])->toBe('active'); +}); + +it('can set default value as array', function (): void { + $column = new Set('permissions', ['read', 'write', 'execute']); + $column->default(['read', 'write']); + + expect($column->getOptions()['default'])->toBe(['read', 'write']); +}); + +it('can update values', function (): void { + $column = new Set('permissions', ['read', 'write']); + $column->values(['read', 'write', 'execute', 'admin']); + + expect($column->getOptions()['values'])->toBe(['read', 'write', 'execute', 'admin']); +}); + +it('can set collation for mysql', function (): void { + $column = new Set('status', ['active', 'inactive']); + $column->setAdapter($this->mockMysqlAdapter); + $column->collation('utf8mb4_unicode_ci'); + + expect($column->getOptions()['collation'])->toBe('utf8mb4_unicode_ci'); +}); + +it('ignores collation for non-mysql adapters', function (): void { + $column = new Set('status', ['active', 'inactive']); + $column->setAdapter($this->mockPostgresAdapter); + $column->collation('utf8mb4_unicode_ci'); + + expect($column->getOptions())->not->toHaveKey('collation'); +}); + +it('can set encoding for mysql', function (): void { + $column = new Set('status', ['active', 'inactive']); + $column->setAdapter($this->mockMysqlAdapter); + $column->encoding('utf8mb4'); + + expect($column->getOptions()['encoding'])->toBe('utf8mb4'); +}); + +it('ignores encoding for non-mysql adapters', function (): void { + $column = new Set('status', ['active', 'inactive']); + $column->setAdapter($this->mockPostgresAdapter); + $column->encoding('utf8mb4'); + + expect($column->getOptions())->not->toHaveKey('encoding'); +}); + +it('can be nullable', function (): void { + $column = new Set('permissions', ['read', 'write']); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Set('permissions', ['read', 'write']); + $column->comment('User permissions'); + + expect($column->getOptions()['comment'])->toBe('User permissions'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/SmallIntegerTest.php b/tests/Unit/Database/Migrations/Columns/SmallIntegerTest.php new file mode 100644 index 00000000..0a8c05f4 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/SmallIntegerTest.php @@ -0,0 +1,76 @@ +getName())->toBe('status'); + expect($column->getType())->toBe('smallinteger'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can create small integer column with identity', function (): void { + $column = new SmallInteger('id', true); + + expect($column->getOptions())->toBe([ + 'null' => false, + 'identity' => true, + ]); +}); + +it('can create small integer column as unsigned', function (): void { + $column = new SmallInteger('count', false, false); + + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => false, + ]); +}); + +it('can set default value', function (): void { + $column = new SmallInteger('status'); + $column->default(1); + + expect($column->getOptions()['default'])->toBe(1); +}); + +it('can set identity', function (): void { + $column = new SmallInteger('id'); + $column->identity(); + + expect($column->getOptions()['identity'])->toBeTrue(); + expect($column->getOptions()['null'])->toBeFalse(); +}); + +it('can be unsigned', function (): void { + $column = new SmallInteger('count'); + $column->unsigned(); + + expect($column->getOptions()['signed'])->toBeFalse(); +}); + +it('can be signed', function (): void { + $column = new SmallInteger('balance'); + $column->signed(); + + expect($column->getOptions()['signed'])->toBeTrue(); +}); + +it('can be nullable', function (): void { + $column = new SmallInteger('priority'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new SmallInteger('status'); + $column->comment('Status code'); + + expect($column->getOptions()['comment'])->toBe('Status code'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/StrTest.php b/tests/Unit/Database/Migrations/Columns/StrTest.php new file mode 100644 index 00000000..88d29398 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/StrTest.php @@ -0,0 +1,86 @@ +mockAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); + + $this->mockMysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mockPostgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); +}); + +it('can create string column with default limit', function (): void { + $column = new Str('name'); + + expect($column->getName())->toBe('name'); + expect($column->getType())->toBe('string'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 255, + ]); +}); + +it('can create string column with custom limit', function (): void { + $column = new Str('username', 100); + + expect($column->getOptions()['limit'])->toBe(100); +}); + +it('can set default value', function (): void { + $column = new Str('status'); + $column->default('active'); + + expect($column->getOptions()['default'])->toBe('active'); +}); + +it('can set collation', function (): void { + $column = new Str('name'); + $column->collation('utf8mb4_unicode_ci'); + + expect($column->getOptions()['collation'])->toBe('utf8mb4_unicode_ci'); +}); + +it('can set encoding', function (): void { + $column = new Str('name'); + $column->encoding('utf8mb4'); + + expect($column->getOptions()['encoding'])->toBe('utf8mb4'); +}); + +it('can be nullable', function (): void { + $column = new Str('description'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Str('name'); + $column->comment('User name'); + + expect($column->getOptions()['comment'])->toBe('User name'); +}); + +it('can set limit after creation', function (): void { + $column = new Str('name'); + $column->limit(150); + + expect($column->getOptions()['limit'])->toBe(150); +}); + +it('can set length after creation', function (): void { + $column = new Str('name'); + $column->length(200); + + expect($column->getOptions()['limit'])->toBe(200); +}); diff --git a/tests/Unit/Database/Migrations/Columns/TextTest.php b/tests/Unit/Database/Migrations/Columns/TextTest.php new file mode 100644 index 00000000..72f38c7b --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/TextTest.php @@ -0,0 +1,63 @@ +getName())->toBe('content'); + expect($column->getType())->toBe('text'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can create text column with limit', function (): void { + $column = new Text('description', 1000); + + expect($column->getOptions()['limit'])->toBe(1000); +}); + +it('can set default value', function (): void { + $column = new Text('content'); + $column->default('Default content'); + + expect($column->getOptions()['default'])->toBe('Default content'); +}); + +it('can set collation', function (): void { + $column = new Text('content'); + $column->collation('utf8mb4_unicode_ci'); + + expect($column->getOptions()['collation'])->toBe('utf8mb4_unicode_ci'); +}); + +it('can set encoding', function (): void { + $column = new Text('content'); + $column->encoding('utf8mb4'); + + expect($column->getOptions()['encoding'])->toBe('utf8mb4'); +}); + +it('can be nullable', function (): void { + $column = new Text('notes'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Text('content'); + $column->comment('Post content'); + + expect($column->getOptions()['comment'])->toBe('Post content'); +}); + +it('can set limit after creation', function (): void { + $column = new Text('content'); + $column->limit(2000); + + expect($column->getOptions()['limit'])->toBe(2000); +}); diff --git a/tests/Unit/Database/Migrations/Columns/TimeTest.php b/tests/Unit/Database/Migrations/Columns/TimeTest.php new file mode 100644 index 00000000..b9594ac0 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/TimeTest.php @@ -0,0 +1,73 @@ +mockAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); + + $this->mockPostgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); +}); + +it('can create time column without timezone', function (): void { + $column = new Time('start_time'); + + expect($column->getName())->toBe('start_time'); + expect($column->getType())->toBe('time'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can create time column with timezone for postgres after setting adapter', function (): void { + $column = new Time('start_time', true); + $column->setAdapter($this->mockPostgresAdapter); + $column->withTimezone(true); + + expect($column->getOptions())->toBe([ + 'null' => false, + 'timezone' => true, + ]); +}); + +it('can set default value', function (): void { + $column = new Time('start_time'); + $column->default('09:00:00'); + + expect($column->getOptions()['default'])->toBe('09:00:00'); +}); + +it('can set timezone for postgres', function (): void { + $column = new Time('start_time'); + $column->setAdapter($this->mockPostgresAdapter); + $column->withTimezone(true); + + expect($column->getOptions()['timezone'])->toBeTrue(); +}); + +it('ignores timezone for non-postgres adapters', function (): void { + $column = new Time('start_time'); + $column->setAdapter($this->mockAdapter); + $column->withTimezone(true); + + expect($column->getOptions())->not->toHaveKey('timezone'); +}); + +it('can be nullable', function (): void { + $column = new Time('end_time'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Time('start_time'); + $column->comment('Event start time'); + + expect($column->getOptions()['comment'])->toBe('Event start time'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/TimestampTest.php b/tests/Unit/Database/Migrations/Columns/TimestampTest.php new file mode 100644 index 00000000..46adc96b --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/TimestampTest.php @@ -0,0 +1,80 @@ +getName())->toBe('created_at'); + expect($column->getType())->toBe('timestamp'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can create timestamp column with timezone', function (): void { + $column = new Timestamp('created_at', true); + + expect($column->getOptions())->toBe([ + 'null' => false, + 'timezone' => true, + ]); +}); + +it('can set default value', function (): void { + $column = new Timestamp('created_at'); + $column->default('2023-01-01 12:00:00'); + + expect($column->getOptions()['default'])->toBe('2023-01-01 12:00:00'); +}); + +it('can set timezone', function (): void { + $column = new Timestamp('created_at'); + $column->timezone(true); + + expect($column->getOptions()['timezone'])->toBeTrue(); +}); + +it('can disable timezone', function (): void { + $column = new Timestamp('created_at', true); + $column->timezone(false); + + expect($column->getOptions()['timezone'])->toBeFalse(); +}); + +it('can set update action', function (): void { + $column = new Timestamp('updated_at'); + $column->update('CURRENT_TIMESTAMP'); + + expect($column->getOptions()['update'])->toBe('CURRENT_TIMESTAMP'); +}); + +it('can use current timestamp as default', function (): void { + $column = new Timestamp('created_at'); + $column->currentTimestamp(); + + expect($column->getOptions()['default'])->toBe('CURRENT_TIMESTAMP'); +}); + +it('can use on update current timestamp', function (): void { + $column = new Timestamp('updated_at'); + $column->onUpdateCurrentTimestamp(); + + expect($column->getOptions()['update'])->toBe('CURRENT_TIMESTAMP'); +}); + +it('can be nullable', function (): void { + $column = new Timestamp('deleted_at'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Timestamp('created_at'); + $column->comment('Creation timestamp'); + + expect($column->getOptions()['comment'])->toBe('Creation timestamp'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/UnsignedDecimalTest.php b/tests/Unit/Database/Migrations/Columns/UnsignedDecimalTest.php new file mode 100644 index 00000000..5ea39e3e --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/UnsignedDecimalTest.php @@ -0,0 +1,60 @@ +getName())->toBe('price'); + expect($column->getType())->toBe('decimal'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'precision' => 10, + 'scale' => 2, + 'signed' => false, + ]); +}); + +it('can create unsigned decimal column with custom precision and scale', function (): void { + $column = new UnsignedDecimal('amount', 15, 4); + + expect($column->getOptions()['precision'])->toBe(15); + expect($column->getOptions()['scale'])->toBe(4); +}); + +it('can set default value', function (): void { + $column = new UnsignedDecimal('price'); + $column->default(99.99); + + expect($column->getOptions()['default'])->toBe(99.99); +}); + +it('can set precision', function (): void { + $column = new UnsignedDecimal('price'); + $column->precision(12); + + expect($column->getOptions()['precision'])->toBe(12); +}); + +it('can set scale', function (): void { + $column = new UnsignedDecimal('price'); + $column->scale(4); + + expect($column->getOptions()['scale'])->toBe(4); +}); + +it('can be nullable', function (): void { + $column = new UnsignedDecimal('discount'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new UnsignedDecimal('price'); + $column->comment('Product price'); + + expect($column->getOptions()['comment'])->toBe('Product price'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/UnsignedSmallIntegerTest.php b/tests/Unit/Database/Migrations/Columns/UnsignedSmallIntegerTest.php new file mode 100644 index 00000000..48a5a0ee --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/UnsignedSmallIntegerTest.php @@ -0,0 +1,55 @@ +getName())->toBe('count'); + expect($column->getType())->toBe('smallinteger'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => false, + ]); +}); + +it('can create unsigned small integer column with identity', function (): void { + $column = new UnsignedSmallInteger('id', true); + + expect($column->getOptions())->toBe([ + 'null' => false, + 'signed' => false, + 'identity' => true, + ]); +}); + +it('can set default value', function (): void { + $column = new UnsignedSmallInteger('status'); + $column->default(1); + + expect($column->getOptions()['default'])->toBe(1); +}); + +it('can set identity', function (): void { + $column = new UnsignedSmallInteger('id'); + $column->identity(); + + expect($column->getOptions()['identity'])->toBeTrue(); + expect($column->getOptions()['null'])->toBeFalse(); +}); + +it('can be nullable', function (): void { + $column = new UnsignedSmallInteger('priority'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new UnsignedSmallInteger('count'); + $column->comment('Item count'); + + expect($column->getOptions()['comment'])->toBe('Item count'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/UuidTest.php b/tests/Unit/Database/Migrations/Columns/UuidTest.php new file mode 100644 index 00000000..fa7f758c --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/UuidTest.php @@ -0,0 +1,36 @@ +getName())->toBe('uuid'); + expect($column->getType())->toBe('uuid'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can set default value', function (): void { + $column = new Uuid('identifier'); + $column->default('550e8400-e29b-41d4-a716-446655440000'); + + expect($column->getOptions()['default'])->toBe('550e8400-e29b-41d4-a716-446655440000'); +}); + +it('can be nullable', function (): void { + $column = new Uuid('external_id'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Uuid('uuid'); + $column->comment('Unique identifier'); + + expect($column->getOptions()['comment'])->toBe('Unique identifier'); +}); From 7550232435b82a52b0903a2264c8222651fa38d8 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Sun, 2 Nov 2025 17:01:26 -0500 Subject: [PATCH 17/23] refactor(database): change visibility of addColumnWithAdapter method to protected --- src/Database/Migrations/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index 1651d3d7..6ba66ebe 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -50,7 +50,7 @@ public function __destruct() * @param T $column * @return T */ - private function addColumnWithAdapter(Column $column): Column + protected function addColumnWithAdapter(Column $column): Column { $column->setAdapter($this->getAdapter()); From 5ddc36df37917e54770d65d5a3134e71b287ca17 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Sun, 2 Nov 2025 17:35:15 -0500 Subject: [PATCH 18/23] tests: bit, blob and char columns --- .../Database/Migrations/Columns/BitTest.php | 72 +++++++++++ .../Database/Migrations/Columns/BlobTest.php | 121 ++++++++++++++++++ .../Database/Migrations/Columns/CharTest.php | 97 ++++++++++++++ 3 files changed, 290 insertions(+) create mode 100644 tests/Unit/Database/Migrations/Columns/BitTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/BlobTest.php create mode 100644 tests/Unit/Database/Migrations/Columns/CharTest.php diff --git a/tests/Unit/Database/Migrations/Columns/BitTest.php b/tests/Unit/Database/Migrations/Columns/BitTest.php new file mode 100644 index 00000000..0712cf2b --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/BitTest.php @@ -0,0 +1,72 @@ +getName())->toBe('flags'); + expect($column->getType())->toBe('bit'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 1, + ]); +}); + +it('can create bit column with custom limit', function (): void { + $column = new Bit('permissions', 8); + + expect($column->getOptions()['limit'])->toBe(8); +}); + +it('can set limit after creation', function (): void { + $column = new Bit('flags'); + $column->limit(16); + + expect($column->getOptions()['limit'])->toBe(16); +}); + +it('throws exception when limit is less than 1', function (): void { + $column = new Bit('flags'); + + try { + $column->limit(0); + $this->fail('Expected InvalidArgumentException was not thrown'); + } catch (\InvalidArgumentException $e) { + expect($e->getMessage())->toBe('Bit limit must be between 1 and 64'); + } +}); + +it('throws exception when limit is greater than 64', function (): void { + $column = new Bit('flags'); + + try { + $column->limit(65); + $this->fail('Expected InvalidArgumentException was not thrown'); + } catch (\InvalidArgumentException $e) { + expect($e->getMessage())->toBe('Bit limit must be between 1 and 64'); + } +}); + +it('can set default value', function (): void { + $column = new Bit('flags'); + $column->default(1); + + expect($column->getOptions()['default'])->toBe(1); +}); + +it('can be nullable', function (): void { + $column = new Bit('flags'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Bit('flags'); + $column->comment('Status flags'); + + expect($column->getOptions()['comment'])->toBe('Status flags'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/BlobTest.php b/tests/Unit/Database/Migrations/Columns/BlobTest.php new file mode 100644 index 00000000..35ed48ca --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/BlobTest.php @@ -0,0 +1,121 @@ +mockAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); + + $this->mockMysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mockPostgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); +}); + +it('can create blob column without limit', function (): void { + $column = new Blob('data'); + + expect($column->getName())->toBe('data'); + expect($column->getType())->toBe('blob'); + expect($column->getOptions())->toBe([ + 'null' => false, + ]); +}); + +it('can create blob column with limit', function (): void { + $column = new Blob('file_data', 1024); + + expect($column->getOptions()['limit'])->toBe(1024); +}); + +it('can set limit after creation', function (): void { + $column = new Blob('data'); + $column->limit(2048); + + expect($column->getOptions()['limit'])->toBe(2048); +}); + +it('can set tiny blob for mysql', function (): void { + $column = new Blob('data'); + $column->setAdapter($this->mockMysqlAdapter); + $column->tiny(); + + expect($column->getOptions()['limit'])->toBe(MysqlAdapter::BLOB_TINY); +}); + +it('ignores tiny blob for non-mysql adapters', function (): void { + $column = new Blob('data'); + $column->setAdapter($this->mockPostgresAdapter); + $column->tiny(); + + expect($column->getOptions())->not->toHaveKey('limit'); +}); + +it('can set regular blob for mysql', function (): void { + $column = new Blob('data'); + $column->setAdapter($this->mockMysqlAdapter); + $column->regular(); + + expect($column->getOptions()['limit'])->toBe(MysqlAdapter::BLOB_REGULAR); +}); + +it('ignores regular blob for non-mysql adapters', function (): void { + $column = new Blob('data'); + $column->setAdapter($this->mockPostgresAdapter); + $column->regular(); + + expect($column->getOptions())->not->toHaveKey('limit'); +}); + +it('can set medium blob for mysql', function (): void { + $column = new Blob('data'); + $column->setAdapter($this->mockMysqlAdapter); + $column->medium(); + + expect($column->getOptions()['limit'])->toBe(MysqlAdapter::BLOB_MEDIUM); +}); + +it('ignores medium blob for non-mysql adapters', function (): void { + $column = new Blob('data'); + $column->setAdapter($this->mockPostgresAdapter); + $column->medium(); + + expect($column->getOptions())->not->toHaveKey('limit'); +}); + +it('can set long blob for mysql', function (): void { + $column = new Blob('data'); + $column->setAdapter($this->mockMysqlAdapter); + $column->long(); + + expect($column->getOptions()['limit'])->toBe(MysqlAdapter::BLOB_LONG); +}); + +it('ignores long blob for non-mysql adapters', function (): void { + $column = new Blob('data'); + $column->setAdapter($this->mockPostgresAdapter); + $column->long(); + + expect($column->getOptions())->not->toHaveKey('limit'); +}); + +it('can be nullable', function (): void { + $column = new Blob('data'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Blob('data'); + $column->comment('Binary data'); + + expect($column->getOptions()['comment'])->toBe('Binary data'); +}); diff --git a/tests/Unit/Database/Migrations/Columns/CharTest.php b/tests/Unit/Database/Migrations/Columns/CharTest.php new file mode 100644 index 00000000..5e0c4c02 --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/CharTest.php @@ -0,0 +1,97 @@ +mockAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); + + $this->mockMysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mockPostgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); +}); + +it('can create char column with default limit', function (): void { + $column = new Char('code'); + + expect($column->getName())->toBe('code'); + expect($column->getType())->toBe('char'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 255, + ]); +}); + +it('can create char column with custom limit', function (): void { + $column = new Char('status', 10); + + expect($column->getOptions()['limit'])->toBe(10); +}); + +it('can set limit after creation', function (): void { + $column = new Char('code'); + $column->limit(50); + + expect($column->getOptions()['limit'])->toBe(50); +}); + +it('can set default value', function (): void { + $column = new Char('status'); + $column->default('A'); + + expect($column->getOptions()['default'])->toBe('A'); +}); + +it('can set collation for mysql', function (): void { + $column = new Char('code'); + $column->setAdapter($this->mockMysqlAdapter); + $column->collation('utf8mb4_unicode_ci'); + + expect($column->getOptions()['collation'])->toBe('utf8mb4_unicode_ci'); +}); + +it('ignores collation for non-mysql adapters', function (): void { + $column = new Char('code'); + $column->setAdapter($this->mockPostgresAdapter); + $column->collation('utf8mb4_unicode_ci'); + + expect($column->getOptions())->not->toHaveKey('collation'); +}); + +it('can set encoding for mysql', function (): void { + $column = new Char('code'); + $column->setAdapter($this->mockMysqlAdapter); + $column->encoding('utf8mb4'); + + expect($column->getOptions()['encoding'])->toBe('utf8mb4'); +}); + +it('ignores encoding for non-mysql adapters', function (): void { + $column = new Char('code'); + $column->setAdapter($this->mockPostgresAdapter); + $column->encoding('utf8mb4'); + + expect($column->getOptions())->not->toHaveKey('encoding'); +}); + +it('can be nullable', function (): void { + $column = new Char('code'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can have comment', function (): void { + $column = new Char('code'); + $column->comment('Status code'); + + expect($column->getOptions()['comment'])->toBe('Status code'); +}); From 3c17d2e31573d47f6d4052879e4286850668c757 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Sun, 2 Nov 2025 17:58:17 -0500 Subject: [PATCH 19/23] tests: add column positioning and collation features for MySQL and PostgreSQL --- tests/Unit/Database/Migrations/TableTest.php | 382 +++++++++++++++++++ 1 file changed, 382 insertions(+) diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 079e62f2..c83df9ec 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -621,3 +621,385 @@ 'comment' => 'Event duration', ]); }); + +it('can use after method to position column', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->string('email')->after('username'); + + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 255, + 'after' => 'username', + ]); +}); + +it('can use first method to position column at beginning', function (): void { + $mysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $mysqlAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('users', adapter: $mysqlAdapter); + + $column = $table->string('id')->setAdapter($mysqlAdapter)->first(); + + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 255, + 'after' => MysqlAdapter::FIRST, + ]); +}); + +it('can set collation for MySQL columns', function (): void { + $mysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $mysqlAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('users', adapter: $mysqlAdapter); + + $column = $table->string('name')->setAdapter($mysqlAdapter)->collation('utf8mb4_unicode_ci'); + + expect($column->isMysql())->toBeTrue(); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 255, + 'collation' => 'utf8mb4_unicode_ci', + ]); +}); + +it('sets collation for non-MySQL adapters (Str class behavior)', function (): void { + $postgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $postgresAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $postgresAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $postgresAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('users', adapter: $postgresAdapter); + + $column = $table->string('name'); + $column->setAdapter($postgresAdapter); + $column->collation('utf8mb4_unicode_ci'); + + expect($column->isPostgres())->toBeTrue(); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 255, + 'collation' => 'utf8mb4_unicode_ci', + ]); +}); + +it('can set encoding for MySQL columns', function (): void { + $mysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $mysqlAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('users', adapter: $mysqlAdapter); + + $column = $table->string('name'); + $column->setAdapter($mysqlAdapter); + $column->encoding('utf8mb4'); + + expect($column->isMysql())->toBeTrue(); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 255, + 'encoding' => 'utf8mb4', + ]); +}); + +it('sets encoding for non-MySQL adapters (Str class behavior)', function (): void { + $postgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $postgresAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $postgresAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $postgresAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('users', adapter: $postgresAdapter); + + $column = $table->string('name'); + $column->setAdapter($postgresAdapter); + $column->encoding('utf8mb4'); + + expect($column->isPostgres())->toBeTrue(); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 255, + 'encoding' => 'utf8mb4', + ]); +}); + +it('can set timezone for PostgreSQL columns', function (): void { + $postgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $postgresAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $postgresAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $postgresAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('events', adapter: $postgresAdapter); + + $column = $table->timestamp('created_at'); + $column->setAdapter($postgresAdapter); + $column->timezone(true); + + expect($column->isPostgres())->toBeTrue(); + expect($column->getOptions())->toBe([ + 'null' => false, + 'timezone' => true, + ]); +}); + +it('can set timezone to false for PostgreSQL columns', function (): void { + $postgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $postgresAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $postgresAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $postgresAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('events', adapter: $postgresAdapter); + + $column = $table->timestamp('created_at'); + $column->setAdapter($postgresAdapter); + $column->timezone(false); + + expect($column->isPostgres())->toBeTrue(); + expect($column->getOptions())->toBe([ + 'null' => false, + 'timezone' => false, + ]); +}); + +it('sets timezone for non-PostgreSQL adapters (Timestamp class behavior)', function (): void { + $mysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $mysqlAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('events', adapter: $mysqlAdapter); + + $column = $table->timestamp('created_at'); + $column->setAdapter($mysqlAdapter); + $column->timezone(true); + + expect($column->isMysql())->toBeTrue(); + expect($column->getOptions())->toBe([ + 'null' => false, + 'timezone' => true, + ]); +}); + +it('can set update trigger for MySQL columns', function (): void { + $mysqlAdapter = $this->getMockBuilder(MysqlAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $mysqlAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $mysqlAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('users', adapter: $mysqlAdapter); + + $column = $table->timestamp('updated_at'); + $column->setAdapter($mysqlAdapter); + $column->update('CURRENT_TIMESTAMP'); + + expect($column->isMysql())->toBeTrue(); + expect($column->getOptions())->toBe([ + 'null' => false, + 'update' => 'CURRENT_TIMESTAMP', + ]); +}); + +it('sets update trigger for non-MySQL adapters (Timestamp class behavior)', function (): void { + $postgresAdapter = $this->getMockBuilder(PostgresAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $postgresAdapter->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + + $postgresAdapter->expects($this->any()) + ->method('getColumnTypes') + ->willReturn(['string', 'integer', 'boolean', 'text', 'datetime', 'timestamp']); + + $postgresAdapter->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options): Column { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); + + return $column; + }); + + $table = new Table('users', adapter: $postgresAdapter); + + $column = $table->timestamp('updated_at'); + $column->setAdapter($postgresAdapter); + $column->update('CURRENT_TIMESTAMP'); + + expect($column->isPostgres())->toBeTrue(); + expect($column->getOptions())->toBe([ + 'null' => false, + 'update' => 'CURRENT_TIMESTAMP', + ]); +}); From 8ad4226597fc2c7e6e605fd4a291823261a0412a Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 10:03:27 -0500 Subject: [PATCH 20/23] tests: add migration test for returning new table instance --- tests/Unit/Database/Migrations/TableTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index c83df9ec..2f33c274 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Phenix\Database\Migration; use Phenix\Database\Migrations\Columns\BigInteger; use Phenix\Database\Migrations\Columns\Binary; use Phenix\Database\Migrations\Columns\Bit; @@ -1003,3 +1004,10 @@ 'update' => 'CURRENT_TIMESTAMP', ]); }); + +it('returns new table for migrations', function (): void { + $migration = new class ('local', 1) extends Migration {}; + $migration->setAdapter($this->mockAdapter); + + expect($migration->table('users'))->toBeInstanceOf(Table::class); +}); From 284674af5689cf0404ea939eaa864c864b2bb8e6 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 10:06:50 -0500 Subject: [PATCH 21/23] tests: enhance adapter change test for column types in TableTest --- tests/Unit/Database/Migrations/TableTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 2f33c274..77ac48f6 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -453,6 +453,9 @@ $column->setAdapter($mysqlAdapter); expect($column->isMysql())->toBeTrue(); + expect($column->isPostgres())->toBeFalse(); + expect($column->isSQLite())->toBeFalse(); + expect($column->isSqlServer())->toBeFalse(); $postgresAdapter = $this->getMockBuilder(PostgresAdapter::class) ->disableOriginalConstructor() From 2dc11c637f0abf16a636336628c424b6db0f93f7 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 10:58:44 -0500 Subject: [PATCH 22/23] feat(database): add ULID column support and corresponding tests --- .../Columns/Concerns/WithSpecial.php | 6 +++ src/Database/Migrations/Columns/Ulid.php | 37 +++++++++++++ .../Database/Migrations/Columns/UlidTest.php | 52 +++++++++++++++++++ tests/Unit/Database/Migrations/TableTest.php | 16 ++++++ 4 files changed, 111 insertions(+) create mode 100644 src/Database/Migrations/Columns/Ulid.php create mode 100644 tests/Unit/Database/Migrations/Columns/UlidTest.php diff --git a/src/Database/Migrations/Columns/Concerns/WithSpecial.php b/src/Database/Migrations/Columns/Concerns/WithSpecial.php index 77b614e4..106a3e2e 100644 --- a/src/Database/Migrations/Columns/Concerns/WithSpecial.php +++ b/src/Database/Migrations/Columns/Concerns/WithSpecial.php @@ -7,6 +7,7 @@ use Phenix\Database\Migrations\Columns\Boolean; use Phenix\Database\Migrations\Columns\Enum; use Phenix\Database\Migrations\Columns\Set; +use Phenix\Database\Migrations\Columns\Ulid; use Phenix\Database\Migrations\Columns\Uuid; trait WithSpecial @@ -21,6 +22,11 @@ public function uuid(string $name): Uuid return $this->addColumnWithAdapter(new Uuid($name)); } + public function ulid(string $name): Ulid + { + return $this->addColumnWithAdapter(new Ulid($name)); + } + public function enum(string $name, array $values): Enum { return $this->addColumnWithAdapter(new Enum($name, $values)); diff --git a/src/Database/Migrations/Columns/Ulid.php b/src/Database/Migrations/Columns/Ulid.php new file mode 100644 index 00000000..102bf7f6 --- /dev/null +++ b/src/Database/Migrations/Columns/Ulid.php @@ -0,0 +1,37 @@ +options['limit'] = 26; + } + + public function getType(): string + { + return 'string'; + } + + public function default(string $value): static + { + $this->options['default'] = $value; + + return $this; + } + + public function limit(int $limit): static + { + return $this; + } + + public function length(int $length): static + { + return $this; + } +} diff --git a/tests/Unit/Database/Migrations/Columns/UlidTest.php b/tests/Unit/Database/Migrations/Columns/UlidTest.php new file mode 100644 index 00000000..2d4c38eb --- /dev/null +++ b/tests/Unit/Database/Migrations/Columns/UlidTest.php @@ -0,0 +1,52 @@ +getName())->toBe('ulid_field'); + expect($column->getType())->toBe('string'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 26, + ]); +}); + +it('can be nullable', function (): void { + $column = new Ulid('ulid_field'); + $column->nullable(); + + expect($column->getOptions()['null'])->toBeTrue(); +}); + +it('can set default value', function (): void { + $defaultUlid = '01ARZ3NDEKTSV4RRFFQ69G5FAV'; + $column = new Ulid('ulid_field'); + $column->default($defaultUlid); + + expect($column->getOptions()['default'])->toBe($defaultUlid); +}); + +it('can have comment', function (): void { + $column = new Ulid('ulid_field'); + $column->comment('User identifier'); + + expect($column->getOptions()['comment'])->toBe('User identifier'); +}); + +it('maintains fixed length of 26 characters when limit is called', function (): void { + $column = new Ulid('ulid_field'); + $column->limit(50); + + expect($column->getOptions()['limit'])->toBe(26); +}); + +it('maintains fixed length of 26 characters when length is called', function (): void { + $column = new Ulid('ulid_field'); + $column->length(100); + + expect($column->getOptions()['limit'])->toBe(26); +}); diff --git a/tests/Unit/Database/Migrations/TableTest.php b/tests/Unit/Database/Migrations/TableTest.php index 77ac48f6..51a739d1 100644 --- a/tests/Unit/Database/Migrations/TableTest.php +++ b/tests/Unit/Database/Migrations/TableTest.php @@ -28,6 +28,7 @@ use Phenix\Database\Migrations\Columns\Text; use Phenix\Database\Migrations\Columns\Time; use Phenix\Database\Migrations\Columns\Timestamp; +use Phenix\Database\Migrations\Columns\Ulid; use Phenix\Database\Migrations\Columns\UnsignedBigInteger; use Phenix\Database\Migrations\Columns\UnsignedDecimal; use Phenix\Database\Migrations\Columns\UnsignedFloat; @@ -269,6 +270,21 @@ ]); }); +it('can add ulid column', function (): void { + $table = new Table('users', adapter: $this->mockAdapter); + + $column = $table->ulid('ulid')->comment('ULID identifier'); + + expect($column)->toBeInstanceOf(Ulid::class); + expect($column->getName())->toBe('ulid'); + expect($column->getType())->toBe('string'); + expect($column->getOptions())->toBe([ + 'null' => false, + 'limit' => 26, + 'comment' => 'ULID identifier', + ]); +}); + it('can add enum column with values', function (): void { $table = new Table('users', adapter: $this->mockAdapter); From acb2a75ebafe3bf6e4f40c577a2417fd17a77aa6 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 4 Nov 2025 11:22:06 -0500 Subject: [PATCH 23/23] refactor(Table): remove destructor for column saving logic --- src/Database/Migrations/Table.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Database/Migrations/Table.php b/src/Database/Migrations/Table.php index 6ba66ebe..cf3024c3 100644 --- a/src/Database/Migrations/Table.php +++ b/src/Database/Migrations/Table.php @@ -36,15 +36,6 @@ public function getColumnBuilders(): array return $this->columns; } - public function __destruct() - { - foreach ($this->columns as $column) { - $this->addColumn($column->getName(), $column->getType(), $column->getOptions()); - } - - $this->save(); - } - /** * @template T of Column * @param T $column