From 022dbbb2efb6df30251c57ba19161807bad8cf62 Mon Sep 17 00:00:00 2001 From: "aidan.casey" Date: Thu, 22 Oct 2026 21:02:41 -0400 Subject: [PATCH 1/6] initial columns testing --- .../core/src/Exceptions/ExceptionTester.php | 2 +- packages/database/src/Columns/Column.php | 18 ++++++ .../database/src/Columns/HasConstraints.php | 38 ++++++++++++ packages/database/src/Columns/Integer.php | 50 +++++++++++++++ packages/database/src/Columns/IsColumn.php | 61 +++++++++++++++++++ packages/database/src/Columns/Uuid.php | 9 +++ 6 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 packages/database/src/Columns/Column.php create mode 100644 packages/database/src/Columns/HasConstraints.php create mode 100644 packages/database/src/Columns/Integer.php create mode 100644 packages/database/src/Columns/IsColumn.php create mode 100644 packages/database/src/Columns/Uuid.php diff --git a/packages/core/src/Exceptions/ExceptionTester.php b/packages/core/src/Exceptions/ExceptionTester.php index a8e72ad894..a27ee15dfa 100644 --- a/packages/core/src/Exceptions/ExceptionTester.php +++ b/packages/core/src/Exceptions/ExceptionTester.php @@ -92,7 +92,7 @@ private function findRecordedProcessings(string|object $exception): array return true; } - return class_exists($exception) && is_a($reported, $exception, allow_string: true); + return class_exists($exception) && $reported instanceof $exception; }); } diff --git a/packages/database/src/Columns/Column.php b/packages/database/src/Columns/Column.php new file mode 100644 index 0000000000..59e637c8f5 --- /dev/null +++ b/packages/database/src/Columns/Column.php @@ -0,0 +1,18 @@ +primary = $primary; + + return $column; + } + + public function isPrimary(): bool + { + return $this->primary; + } + + public function unique(bool $unique = true): self + { + $column = clone $this; + + $column->unique = $unique; + + return $column; + } + + public function isUnique(): bool + { + return $this->unique; + } +} diff --git a/packages/database/src/Columns/Integer.php b/packages/database/src/Columns/Integer.php new file mode 100644 index 0000000000..b1db6d4003 --- /dev/null +++ b/packages/database/src/Columns/Integer.php @@ -0,0 +1,50 @@ +autoIncrement = $autoIncrement; + + return $column; + } + + public function shouldAutoIncrement(): bool + { + return $this->autoIncrement; + } + + public function unsigned(bool $unsigned = true): self + { + $column = clone $this; + + $column->unsigned = $unsigned; + + return $column; + } + + public function isUnsigned(): bool + { + return $this->unsigned; + } +} + +return [ + new Uuid('id'), + new Integer('client_id') + ->nullable() + ->unsigned() + ->default(0) + ->autoIncrement(), +]; diff --git a/packages/database/src/Columns/IsColumn.php b/packages/database/src/Columns/IsColumn.php new file mode 100644 index 0000000000..eb3d28604b --- /dev/null +++ b/packages/database/src/Columns/IsColumn.php @@ -0,0 +1,61 @@ +name = $name; + + return $column; + } + + public function getName(): string + { + return $this->name; + } + + public function nullable(bool $nullable = true): static + { + $column = clone $this; + + $column->nullable = $nullable; + + return $column; + } + + public function isNullable(): bool + { + return $this->nullable; + } + + public function default(mixed $default): static + { + $column = clone $this; + + $column->default = $default; + + return $column; + } + + public function hasDefault(): bool + { + return isset($this->default); + } + + public function getDefault(): mixed + { + return $this->default; + } +} diff --git a/packages/database/src/Columns/Uuid.php b/packages/database/src/Columns/Uuid.php new file mode 100644 index 0000000000..1f50523d59 --- /dev/null +++ b/packages/database/src/Columns/Uuid.php @@ -0,0 +1,9 @@ + Date: Thu, 22 Oct 2026 21:08:11 -0400 Subject: [PATCH 2/6] initial columns testing --- packages/database/src/Columns/Integer.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/database/src/Columns/Integer.php b/packages/database/src/Columns/Integer.php index b1db6d4003..ea0ff4f5dd 100644 --- a/packages/database/src/Columns/Integer.php +++ b/packages/database/src/Columns/Integer.php @@ -39,12 +39,3 @@ public function isUnsigned(): bool return $this->unsigned; } } - -return [ - new Uuid('id'), - new Integer('client_id') - ->nullable() - ->unsigned() - ->default(0) - ->autoIncrement(), -]; From f2f33a59c27a119eeb41d0b4387f1203e0e52b5c Mon Sep 17 00:00:00 2001 From: "aidan.casey" Date: Fri, 23 Oct 2026 00:10:00 -0400 Subject: [PATCH 3/6] Adds compilers --- packages/database/src/Columns/CharColumn.php | 25 ++ .../src/Columns/Concerns/IsColumn.php | 83 ++++++ packages/database/src/Columns/DateColumn.php | 25 ++ .../database/src/Columns/DateTimeColumn.php | 58 +++++ packages/database/src/Columns/FloatColumn.php | 25 ++ .../database/src/Columns/HasConstraints.php | 38 --- .../{Integer.php => IntegerColumn.php} | 22 +- packages/database/src/Columns/IsColumn.php | 61 ----- packages/database/src/Columns/TextColumn.php | 25 ++ packages/database/src/Columns/Uuid.php | 9 - packages/database/src/Columns/UuidColumn.php | 25 ++ .../database/src/Columns/VarcharColumn.php | 25 ++ .../database/src/Compilers/ColumnCompiler.php | 10 + .../Dialects/MySQL/MySQLColumnCompiler.php | 136 ++++++++++ .../Postgres/PostgresColumnCompiler.php | 14 + .../Dialects/Sqlite/SqliteColumnCompiler.php | 14 + packages/database/src/Uuid.php | 2 +- .../MySQL/MySQLColumnCompilerTest.php | 243 ++++++++++++++++++ 18 files changed, 719 insertions(+), 121 deletions(-) create mode 100644 packages/database/src/Columns/CharColumn.php create mode 100644 packages/database/src/Columns/Concerns/IsColumn.php create mode 100644 packages/database/src/Columns/DateColumn.php create mode 100644 packages/database/src/Columns/DateTimeColumn.php create mode 100644 packages/database/src/Columns/FloatColumn.php delete mode 100644 packages/database/src/Columns/HasConstraints.php rename packages/database/src/Columns/{Integer.php => IntegerColumn.php} (60%) delete mode 100644 packages/database/src/Columns/IsColumn.php create mode 100644 packages/database/src/Columns/TextColumn.php delete mode 100644 packages/database/src/Columns/Uuid.php create mode 100644 packages/database/src/Columns/UuidColumn.php create mode 100644 packages/database/src/Columns/VarcharColumn.php create mode 100644 packages/database/src/Compilers/ColumnCompiler.php create mode 100644 packages/database/src/Compilers/Dialects/MySQL/MySQLColumnCompiler.php create mode 100644 packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php create mode 100644 packages/database/src/Compilers/Dialects/Sqlite/SqliteColumnCompiler.php create mode 100644 packages/database/tests/Compilers/Dialects/MySQL/MySQLColumnCompilerTest.php diff --git a/packages/database/src/Columns/CharColumn.php b/packages/database/src/Columns/CharColumn.php new file mode 100644 index 0000000000..5673a76f94 --- /dev/null +++ b/packages/database/src/Columns/CharColumn.php @@ -0,0 +1,25 @@ + $length, + ]); + } + + public function getLength(): int + { + return $this->length; + } +} diff --git a/packages/database/src/Columns/Concerns/IsColumn.php b/packages/database/src/Columns/Concerns/IsColumn.php new file mode 100644 index 0000000000..dc49cf4651 --- /dev/null +++ b/packages/database/src/Columns/Concerns/IsColumn.php @@ -0,0 +1,83 @@ + $name, + ]); + } + + public function getName(): string + { + return $this->name; + } + + public function primaryKey(): static + { + return clone($this, [ + 'primaryKey' => true, + ]); + } + + public function isPrimaryKey(): bool + { + return $this->primaryKey; + } + + public function nullable(bool $nullable = true): static + { + return clone($this, [ + 'nullable' => $nullable, + ]); + } + + public function isNullable(): bool + { + return $this->nullable; + } + + public function default(mixed $default): static + { + return clone($this, [ + 'default' => $default, + ]); + } + + public function hasDefault(): bool + { + return isset($this->default); + } + + public function getDefault(): mixed + { + return $this->default; + } + + public function unique(bool $unique = true): self + { + return clone($this, [ + 'unique' => $unique, + ]); + } + + public function isUnique(): bool + { + return $this->unique; + } +} diff --git a/packages/database/src/Columns/DateColumn.php b/packages/database/src/Columns/DateColumn.php new file mode 100644 index 0000000000..e4ad1a5814 --- /dev/null +++ b/packages/database/src/Columns/DateColumn.php @@ -0,0 +1,25 @@ + true, + ]); + } + + public function usesCurrent(): bool + { + return $this->current; + } +} diff --git a/packages/database/src/Columns/DateTimeColumn.php b/packages/database/src/Columns/DateTimeColumn.php new file mode 100644 index 0000000000..1d74cee59b --- /dev/null +++ b/packages/database/src/Columns/DateTimeColumn.php @@ -0,0 +1,58 @@ + 6) { + throw new InvalidArgumentException( + 'Precision must be between 0 and 6.', + ); + } + + return clone($this, [ + 'precision' => $precision, + ]); + } + + public function getPrecision(): int + { + return $this->precision; + } + + public function current(): self + { + return clone($this, [ + 'current' => true, + ]); + } + + public function usesCurrent(): bool + { + return $this->current; + } + + public function currentOnUpdate(): self + { + return clone($this, [ + 'currentOnUpdate' => true, + ]); + } + + public function usesCurrentOnUpdate(): bool + { + return $this->currentOnUpdate; + } +} diff --git a/packages/database/src/Columns/FloatColumn.php b/packages/database/src/Columns/FloatColumn.php new file mode 100644 index 0000000000..b3fc0496fc --- /dev/null +++ b/packages/database/src/Columns/FloatColumn.php @@ -0,0 +1,25 @@ + true, + ]); + } + + public function isUnsigned(): bool + { + return $this->unsigned; + } +} diff --git a/packages/database/src/Columns/HasConstraints.php b/packages/database/src/Columns/HasConstraints.php deleted file mode 100644 index 630576c13e..0000000000 --- a/packages/database/src/Columns/HasConstraints.php +++ /dev/null @@ -1,38 +0,0 @@ -primary = $primary; - - return $column; - } - - public function isPrimary(): bool - { - return $this->primary; - } - - public function unique(bool $unique = true): self - { - $column = clone $this; - - $column->unique = $unique; - - return $column; - } - - public function isUnique(): bool - { - return $this->unique; - } -} diff --git a/packages/database/src/Columns/Integer.php b/packages/database/src/Columns/IntegerColumn.php similarity index 60% rename from packages/database/src/Columns/Integer.php rename to packages/database/src/Columns/IntegerColumn.php index ea0ff4f5dd..cfa4b319e9 100644 --- a/packages/database/src/Columns/Integer.php +++ b/packages/database/src/Columns/IntegerColumn.php @@ -2,10 +2,12 @@ namespace Tempest\Database\Columns; -final class Integer implements Column +use Tempest\Database\Columns\Concerns\HasConstraints; +use Tempest\Database\Columns\Concerns\IsColumn; + +final class IntegerColumn implements Column { use IsColumn; - use HasConstraints; private bool $autoIncrement = false; @@ -13,11 +15,9 @@ final class Integer implements Column public function autoIncrement(bool $autoIncrement = true): self { - $column = clone $this; - - $column->autoIncrement = $autoIncrement; - - return $column; + return clone($this, [ + 'autoIncrement' => $autoIncrement, + ]); } public function shouldAutoIncrement(): bool @@ -27,11 +27,9 @@ public function shouldAutoIncrement(): bool public function unsigned(bool $unsigned = true): self { - $column = clone $this; - - $column->unsigned = $unsigned; - - return $column; + return clone($this, [ + 'unsigned' => $unsigned, + ]); } public function isUnsigned(): bool diff --git a/packages/database/src/Columns/IsColumn.php b/packages/database/src/Columns/IsColumn.php deleted file mode 100644 index eb3d28604b..0000000000 --- a/packages/database/src/Columns/IsColumn.php +++ /dev/null @@ -1,61 +0,0 @@ -name = $name; - - return $column; - } - - public function getName(): string - { - return $this->name; - } - - public function nullable(bool $nullable = true): static - { - $column = clone $this; - - $column->nullable = $nullable; - - return $column; - } - - public function isNullable(): bool - { - return $this->nullable; - } - - public function default(mixed $default): static - { - $column = clone $this; - - $column->default = $default; - - return $column; - } - - public function hasDefault(): bool - { - return isset($this->default); - } - - public function getDefault(): mixed - { - return $this->default; - } -} diff --git a/packages/database/src/Columns/TextColumn.php b/packages/database/src/Columns/TextColumn.php new file mode 100644 index 0000000000..8ccc973f13 --- /dev/null +++ b/packages/database/src/Columns/TextColumn.php @@ -0,0 +1,25 @@ + $length, + ]); + } + + public function getLength(): int + { + return $this->length; + } +} diff --git a/packages/database/src/Columns/Uuid.php b/packages/database/src/Columns/Uuid.php deleted file mode 100644 index 1f50523d59..0000000000 --- a/packages/database/src/Columns/Uuid.php +++ /dev/null @@ -1,9 +0,0 @@ - true, + ]); + } + + public function usesGenerated(): bool + { + return $this->generated; + } +} diff --git a/packages/database/src/Columns/VarcharColumn.php b/packages/database/src/Columns/VarcharColumn.php new file mode 100644 index 0000000000..f4524ba9c0 --- /dev/null +++ b/packages/database/src/Columns/VarcharColumn.php @@ -0,0 +1,25 @@ + $length, + ]); + } + + public function getLength(): int + { + return $this->length; + } +} diff --git a/packages/database/src/Compilers/ColumnCompiler.php b/packages/database/src/Compilers/ColumnCompiler.php new file mode 100644 index 0000000000..c2ff7ca1f4 --- /dev/null +++ b/packages/database/src/Compilers/ColumnCompiler.php @@ -0,0 +1,10 @@ +quote($column->getName()), + $this->compileType($column), + $this->compileNullability($column), + $this->compileDefault($column), + $this->compileModifiers($column), + ]; + + return implode(' ', array_filter($parts)); + } + + private function compileType(Column $column): string + { + return match (true) { + $column instanceof CharColumn => "CHAR({$column->getLength()})", + $column instanceof VarcharColumn => "VARCHAR({$column->getLength()})", + $column instanceof TextColumn => $this->compileTextType($column), + $column instanceof IntegerColumn => 'INT' . ($column->isUnsigned() ? ' UNSIGNED' : ''), + $column instanceof FloatColumn => 'FLOAT', + $column instanceof DateColumn => 'DATE', + $column instanceof DateTimeColumn => "DATETIME({$column->getPrecision()})", + $column instanceof UuidColumn => 'CHAR(36)', + default => throw new \LogicException( + 'Unsupported column: ' . $column::class, + ), + }; + } + + private function compileTextType(TextColumn $column): string + { + return match (true) { + $column->getLength() <= 255 => 'TINYTEXT', + $column->getLength() <= 65_535 => 'TEXT', + $column->getLength() <= 16_777_215 => 'MEDIUMTEXT', + $column->getLength() <= 4_294_967_295 => 'LONGTEXT', + default => new \LogicException('Unsupported text length'), + }; + } + + private function compileNullability(Column $column): string + { + return $column->isNullable() ? 'NULL' : 'NOT NULL'; + } + + private function compileDefault(Column $column): string + { + if ($column instanceof DateColumn && $column->usesCurrent()) { + return 'DEFAULT (CURRENT_DATE)'; + } + + if ($column instanceof DateTimeColumn && $column->usesCurrent()) { + $precision = $column->getPrecision(); + + return "DEFAULT CURRENT_TIMESTAMP($precision)"; + } + + if ($column instanceof UuidColumn && $column->usesGenerated()) { + return 'DEFAULT (UUID())'; + } + + if (! $column->hasDefault()) { + return ''; + } + + $value = $this->literal($column->getDefault()); + + if ($column instanceof TextColumn) { + return "DEFAULT ($value)"; + } + + return "DEFAULT $value"; + } + + private function compileModifiers(Column $column): string + { + $parts = []; + + if ($column instanceof IntegerColumn && $column->shouldAutoIncrement()) { + $parts[] = 'AUTO_INCREMENT'; + } + + if ($column instanceof DateTimeColumn && $column->usesCurrentOnUpdate()) { + $precision = $column->getPrecision(); + + $parts[] = "ON UPDATE CURRENT_TIMESTAMP($precision)"; + } + + if ($column->isUnique()) { + $parts[] = 'UNIQUE'; + } + + if ($column->isPrimaryKey()) { + $parts[] = 'PRIMARY KEY'; + } + + return implode(' ', $parts); + } + + private function quote(string $value): string + { + return '`' . str_replace('`', '``', $value) . '`'; + } + + private function literal(mixed $value): string + { + return match (true) { + $value === null => 'NULL', + is_bool($value) => $value ? '1' : '0', + is_int($value), is_float($value) => (string) $value, + is_string($value) => "'" . str_replace("'", "''", $value) . "'", + default => throw new InvalidArgumentException( + 'Unsupported default value.', + ), + }; + } +} diff --git a/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php b/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php new file mode 100644 index 0000000000..ec0d3dbf25 --- /dev/null +++ b/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php @@ -0,0 +1,14 @@ +assertEquals('`test` CHAR(1) NOT NULL', $compiler->compile($column)); + } + + public function test_char_compilation_with_length() + { + $column = new CharColumn('test')->length(10); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`test` CHAR(10) NOT NULL', $compiler->compile($column)); + } + + public function test_date_compilation() + { + $column = new DateColumn('birthday'); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`birthday` DATE NOT NULL', $compiler->compile($column)); + } + + public function test_datetime_compilation() + { + $column = new DateTimeColumn('created_at'); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`created_at` DATETIME(0) NOT NULL', $compiler->compile($column)); + } + + public function test_datetime_compilation_with_precision() + { + $column = new DateTimeColumn('created_at')->precision(6); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`created_at` DATETIME(6) NOT NULL', $compiler->compile($column)); + } + + public function test_datetime_compilation_with_current() + { + $column = new DateTimeColumn('created_at')->current(); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`created_at` DATETIME(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0)', $compiler->compile($column)); + } + + public function test_datetime_compilation_with_current_on_update() + { + $column = new DateTimeColumn('created_at')->currentOnUpdate(); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`created_at` DATETIME(0) NOT NULL ON UPDATE CURRENT_TIMESTAMP(0)', $compiler->compile($column)); + } + + public function test_float_compilation() + { + $column = new FloatColumn('temperature'); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`temperature` FLOAT NOT NULL', $compiler->compile($column)); + } + + public function test_integer_compilation() + { + $column = new IntegerColumn('age'); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`age` INT NOT NULL', $compiler->compile($column)); + } + + public function test_tiny_text_compilation() + { + $column = new TextColumn('description')->length(255); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`description` TINYTEXT NOT NULL', $compiler->compile($column)); + } + + public function test_text_compilation() + { + $column = new TextColumn('description')->length(65_535); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`description` TEXT NOT NULL', $compiler->compile($column)); + } + + public function test_medium_text_compilation() + { + $column = new TextColumn('description')->length(16_777_215); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`description` MEDIUMTEXT NOT NULL', $compiler->compile($column)); + } + + public function test_long_text_compilation() + { + $column = new TextColumn('description')->length(4_294_967_295); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`description` LONGTEXT NOT NULL', $compiler->compile($column)); + } + + public function test_uuid_compilation() + { + $column = new UuidColumn('uuid'); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`uuid` CHAR(36) NOT NULL', $compiler->compile($column)); + } + + public function test_uuid_compilation_with_generated_uuid() + { + $column = new UuidColumn('uuid')->generated(); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`uuid` CHAR(36) NOT NULL DEFAULT (UUID())', $compiler->compile($column)); + } + + public function test_varchar_compilation() + { + $column = new VarcharColumn('note'); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`note` VARCHAR(255) NOT NULL', $compiler->compile($column)); + } + + public function test_varchar_compilation_with_length() + { + $column = new VarcharColumn('note')->length(100); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`note` VARCHAR(100) NOT NULL', $compiler->compile($column)); + } + + public function test_primary_key_constraint_compilation() + { + $column = new IntegerColumn('id')->primaryKey(); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`id` INT NOT NULL PRIMARY KEY', $compiler->compile($column)); + } + + public function test_unique_constraint_compilation() + { + $column = new VarcharColumn('name')->unique(); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`name` VARCHAR(255) NOT NULL UNIQUE', $compiler->compile($column)); + } + + public function test_nullability_constraint_compilation() + { + $default = new VarcharColumn('name'); + $column1 = new VarcharColumn('name')->nullable(); + $column2 = new VarcharColumn('name')->nullable(false); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`name` VARCHAR(255) NOT NULL', $compiler->compile($default)); + $this->assertEquals('`name` VARCHAR(255) NULL', $compiler->compile($column1)); + $this->assertEquals('`name` VARCHAR(255) NOT NULL', $compiler->compile($column2)); + } + + public function test_auto_increment_attribute_compilation() + { + $column = new IntegerColumn('id')->autoIncrement(); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`id` INT NOT NULL AUTO_INCREMENT', $compiler->compile($column)); + } + + public function test_default_value_compilation() + { + $column = new VarcharColumn('name')->default('Jim Halpert'); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals('`name` VARCHAR(255) NOT NULL DEFAULT \'Jim Halpert\'', $compiler->compile($column)); + } + + public function test_default_value_escaping() + { + $column = new VarcharColumn('name')->default("O'Reilly"); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals( + "`name` VARCHAR(255) NOT NULL DEFAULT 'O''Reilly'", + $compiler->compile($column), + ); + } + + public function test_column_name_escaping() + { + $column = new VarcharColumn('user`name'); + + $compiler = new MySQLColumnCompiler(); + + $this->assertEquals( + '`user``name` VARCHAR(255) NOT NULL', + $compiler->compile($column), + ); + } +} From 5f51b05d46a0543cbf19843818d552510039e4a7 Mon Sep 17 00:00:00 2001 From: "aidan.casey" Date: Fri, 23 Oct 2026 00:14:04 -0400 Subject: [PATCH 4/6] Revert inadvertant change. --- packages/database/src/Uuid.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/database/src/Uuid.php b/packages/database/src/Uuid.php index a5acdf7b15..2087295e04 100644 --- a/packages/database/src/Uuid.php +++ b/packages/database/src/Uuid.php @@ -14,7 +14,7 @@ * ```php * final class User * { - * #[UuidColumn] + * #[Uuid] * public PrimaryKey $uuid; * * public function __construct( From 745a804050531b2ac9fc2c385650de347134515e Mon Sep 17 00:00:00 2001 From: "aidan.casey" Date: Fri, 23 Oct 2026 22:36:37 -0400 Subject: [PATCH 5/6] Adds table compilation --- .../Dialects/MySQL/MySQLTableCompiler.php | 50 ++++ .../Postgres/PostgresColumnCompiler.php | 121 +++++++- .../Postgres/PostgresTableCompiler.php | 50 ++++ .../database/src/Compilers/TableCompiler.php | 10 + packages/database/src/Indexes/Index.php | 15 + .../src/TableDefinitions/AlterTable.php | 8 + .../src/TableDefinitions/CreateTable.php | 43 +++ .../src/TableDefinitions/DropTable.php | 10 + .../src/TableDefinitions/TableDefinition.php | 8 + .../MySQL/MySQLTableCompilerTestCase.php | 39 +++ .../Postgres/PostgresColumnCompilerTest.php | 259 ++++++++++++++++++ .../PostgresTableCompilerTestCase.php | 39 +++ 12 files changed, 651 insertions(+), 1 deletion(-) create mode 100644 packages/database/src/Compilers/Dialects/MySQL/MySQLTableCompiler.php create mode 100644 packages/database/src/Compilers/Dialects/Postgres/PostgresTableCompiler.php create mode 100644 packages/database/src/Compilers/TableCompiler.php create mode 100644 packages/database/src/Indexes/Index.php create mode 100644 packages/database/src/TableDefinitions/AlterTable.php create mode 100644 packages/database/src/TableDefinitions/CreateTable.php create mode 100644 packages/database/src/TableDefinitions/DropTable.php create mode 100644 packages/database/src/TableDefinitions/TableDefinition.php create mode 100644 packages/database/tests/Compilers/Dialects/MySQL/MySQLTableCompilerTestCase.php create mode 100644 packages/database/tests/Compilers/Dialects/Postgres/PostgresColumnCompilerTest.php create mode 100644 packages/database/tests/Compilers/Dialects/Postgres/PostgresTableCompilerTestCase.php diff --git a/packages/database/src/Compilers/Dialects/MySQL/MySQLTableCompiler.php b/packages/database/src/Compilers/Dialects/MySQL/MySQLTableCompiler.php new file mode 100644 index 0000000000..7194dcf34c --- /dev/null +++ b/packages/database/src/Compilers/Dialects/MySQL/MySQLTableCompiler.php @@ -0,0 +1,50 @@ + $this->compileCreateTable($tableDefinition), + }; + } + + private function compileCreateTable(CreateTable $createTable): string + { + if (empty($createTable->columns)) { + // @todo(aidan-casey): Real exceptions. + throw new \InvalidArgumentException('Table must have at least one column'); + } + + // @todo(aidan-casey): Guard against duplicate column names. + + $columns = array_map( + fn (Column $column) => $this->columnCompiler->compile($column), + $createTable->columns + ); + + $columns = implode(",\n", $columns); + $columns = ' ' . str_replace("\n", "\n ", $columns); + + return <<quote($createTable->name)} ( + {$columns} + ); + SQL; + } + + private function quote(string $value): string + { + return '`' . str_replace('`', '``', $value) . '`'; + } +} \ No newline at end of file diff --git a/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php b/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php index ec0d3dbf25..5f4d471cba 100644 --- a/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php +++ b/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php @@ -2,13 +2,132 @@ namespace Tempest\Database\Compilers\Dialects\Postgres; +use InvalidArgumentException; +use Tempest\Database\Columns\CharColumn; use Tempest\Database\Columns\Column; +use Tempest\Database\Columns\DateColumn; +use Tempest\Database\Columns\DateTimeColumn; +use Tempest\Database\Columns\FloatColumn; +use Tempest\Database\Columns\IntegerColumn; +use Tempest\Database\Columns\TextColumn; +use Tempest\Database\Columns\UuidColumn; +use Tempest\Database\Columns\VarcharColumn; use Tempest\Database\Compilers\ColumnCompiler; +use UnexpectedValueException; final class PostgresColumnCompiler implements ColumnCompiler { public function compile(Column $column): string { - // TODO: Implement compile() method. + $parts = [ + $this->quote($column->getName()), + $this->compileType($column), + $this->compileNullability($column), + $this->compileDefault($column), + $this->compileModifiers($column), + ]; + + return implode(' ', array_filter($parts)); + } + + private function compileType(Column $column): string + { + return match (true) { + $column instanceof CharColumn => "CHAR({$column->getLength()})", + $column instanceof VarcharColumn => "VARCHAR({$column->getLength()})", + $column instanceof TextColumn => 'TEXT', + $column instanceof IntegerColumn => 'INT', + $column instanceof FloatColumn => 'REAL', + $column instanceof DateColumn => 'DATE', + $column instanceof DateTimeColumn => "TIMESTAMP({$column->getPrecision()})", + $column instanceof UuidColumn => 'UUID', + default => throw new \LogicException( + 'Unsupported column: ' . $column::class, + ), + }; + } + + private function compileNullability(Column $column): string + { + return $column->isNullable() ? 'NULL' : 'NOT NULL'; + } + + private function compileDefault(Column $column): string + { + if ($column instanceof DateColumn && $column->usesCurrent()) { + return 'DEFAULT (CURRENT_DATE)'; + } + + if ($column instanceof DateTimeColumn && $column->usesCurrent()) { + $precision = $column->getPrecision(); + + return "DEFAULT CURRENT_TIMESTAMP($precision)"; + } + + if ($column instanceof UuidColumn && $column->usesGenerated()) { + return 'DEFAULT gen_random_uuid()'; + } + + if (! $column->hasDefault()) { + return ''; + } + + $value = $this->literal($column->getDefault()); + + if ($column instanceof TextColumn) { + return "DEFAULT ($value)"; + } + + return "DEFAULT $value"; + } + + private function compileModifiers(Column $column): string + { + $parts = []; + + if ($column instanceof IntegerColumn && $column->shouldAutoIncrement()) { + $parts[] = 'GENERATED BY DEFAULT AS IDENTITY'; + } + + if ($column instanceof IntegerColumn && $column->isUnsigned()) { + $name = $this->quote($column->getName()); + + $parts[] = "CHECK ($name >= 0)"; + } + + // @todo(aidan-casey): Eventually we can move this to a trigger. + if ($column instanceof DateTimeColumn && $column->usesCurrentOnUpdate()) { + throw new UnexpectedValueException( + 'The "currentOnUpdate" method is not supported by the Postgres dialect.' + ); + } + + if ($column->isUnique()) { + $parts[] = 'UNIQUE'; + } + + if ($column->isPrimaryKey()) { + $parts[] = 'PRIMARY KEY'; + } + + return implode(' ', $parts); + } + + private function quote(string $value): string + { + return '"' . str_replace('"', '""', $value) . '"'; + } + + private function literal(mixed $value): string + { + return match (true) { + $value === null => 'NULL', + is_bool($value) => $value ? 'TRUE' : 'FALSE', + is_int($value), is_float($value) => (string) $value, + is_string($value) => "'" . str_replace("'", "''", $value) . "'", + default => throw new InvalidArgumentException( + 'Unsupported default value.', + ), + }; } } diff --git a/packages/database/src/Compilers/Dialects/Postgres/PostgresTableCompiler.php b/packages/database/src/Compilers/Dialects/Postgres/PostgresTableCompiler.php new file mode 100644 index 0000000000..c82f78de65 --- /dev/null +++ b/packages/database/src/Compilers/Dialects/Postgres/PostgresTableCompiler.php @@ -0,0 +1,50 @@ + $this->compileCreateTable($tableDefinition), + }; + } + + private function compileCreateTable(CreateTable $createTable): string + { + if (empty($createTable->columns)) { + // @todo(aidan-casey): Real exceptions. + throw new \InvalidArgumentException('Table must have at least one column'); + } + + // @todo(aidan-casey): Guard against duplicate column names. + + $columns = array_map( + fn (Column $column) => $this->columnCompiler->compile($column), + $createTable->columns + ); + + $columns = implode(",\n", $columns); + $columns = ' ' . str_replace("\n", "\n ", $columns); + + return <<quote($createTable->name)} ( + {$columns} + ); + SQL; + } + + private function quote(string $value): string + { + return '"' . str_replace('`', '``', $value) . '"'; + } +} \ No newline at end of file diff --git a/packages/database/src/Compilers/TableCompiler.php b/packages/database/src/Compilers/TableCompiler.php new file mode 100644 index 0000000000..877769f9f7 --- /dev/null +++ b/packages/database/src/Compilers/TableCompiler.php @@ -0,0 +1,10 @@ + $columns + */ + public function __construct( + public string $name, + public array $columns, + public bool $unique = false, + ) {} +} \ No newline at end of file diff --git a/packages/database/src/TableDefinitions/AlterTable.php b/packages/database/src/TableDefinitions/AlterTable.php new file mode 100644 index 0000000000..a372593a4a --- /dev/null +++ b/packages/database/src/TableDefinitions/AlterTable.php @@ -0,0 +1,8 @@ + $columns + * @param list $indexes + */ + public function __construct( + public string $name, + public array $columns = [], + public array $indexes = [], + ) {} + + public function addColumn(Column $column): self + { + return $this->addColumns($column); + } + + public function addColumns(Column ...$columns): self + { + return clone($this, [ + 'columns' => [...$this->columns, ...$columns], + ]); + } + + public function addIndex(Index $index): self + { + return $this->addIndexes($index); + } + + public function addIndexes(Index ...$indexes): self + { + return clone($this, [ + 'columns' => [...$this->columns, ...$indexes], + ]); + } +} \ No newline at end of file diff --git a/packages/database/src/TableDefinitions/DropTable.php b/packages/database/src/TableDefinitions/DropTable.php new file mode 100644 index 0000000000..956cf61e0d --- /dev/null +++ b/packages/database/src/TableDefinitions/DropTable.php @@ -0,0 +1,10 @@ +addColumns( + new IntegerColumn('id') + ->unsigned() + ->autoIncrement() + ->primaryKey(), + new TextColumn('first_name'), + new TextColumn('middle_name')->nullable(), + new TextColumn('last_name'), + ); + + $this->assertEquals( + <<compile($createTable) + ); + } +} \ No newline at end of file diff --git a/packages/database/tests/Compilers/Dialects/Postgres/PostgresColumnCompilerTest.php b/packages/database/tests/Compilers/Dialects/Postgres/PostgresColumnCompilerTest.php new file mode 100644 index 0000000000..5cc8eccf96 --- /dev/null +++ b/packages/database/tests/Compilers/Dialects/Postgres/PostgresColumnCompilerTest.php @@ -0,0 +1,259 @@ +assertEquals('"test" CHAR(1) NOT NULL', $compiler->compile($column)); + } + + public function test_char_compilation_with_length() + { + $column = new CharColumn('test')->length(10); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"test" CHAR(10) NOT NULL', $compiler->compile($column)); + } + + public function test_date_compilation() + { + $column = new DateColumn('birthday'); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"birthday" DATE NOT NULL', $compiler->compile($column)); + } + + public function test_datetime_compilation() + { + $column = new DateTimeColumn('created_at'); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"created_at" TIMESTAMP(0) NOT NULL', $compiler->compile($column)); + } + + public function test_datetime_compilation_with_precision() + { + $column = new DateTimeColumn('created_at')->precision(6); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"created_at" TIMESTAMP(6) NOT NULL', $compiler->compile($column)); + } + + public function test_datetime_compilation_with_current() + { + $column = new DateTimeColumn('created_at')->current(); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"created_at" TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0)', $compiler->compile($column)); + } + + public function test_datetime_compilation_with_current_on_update() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('The "currentOnUpdate" method is not supported by the Postgres dialect.'); + + $column = new DateTimeColumn('created_at')->currentOnUpdate(); + + new PostgresColumnCompiler()->compile($column); + } + + public function test_float_compilation() + { + $column = new FloatColumn('temperature'); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"temperature" REAL NOT NULL', $compiler->compile($column)); + } + + public function test_integer_compilation() + { + $column = new IntegerColumn('age'); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"age" INT NOT NULL', $compiler->compile($column)); + } + + public function test_unsigned_integer_compilation() + { + $column = new IntegerColumn('age')->unsigned(); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals( + '"age" INT NOT NULL CHECK ("age" >= 0)', + $compiler->compile($column), + ); + } + + public function test_tiny_text_compilation() + { + $column = new TextColumn('description')->length(255); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"description" TEXT NOT NULL', $compiler->compile($column)); + } + + public function test_text_compilation() + { + $column = new TextColumn('description')->length(65_535); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"description" TEXT NOT NULL', $compiler->compile($column)); + } + + public function test_medium_text_compilation() + { + $column = new TextColumn('description')->length(16_777_215); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"description" TEXT NOT NULL', $compiler->compile($column)); + } + + public function test_long_text_compilation() + { + $column = new TextColumn('description')->length(4_294_967_295); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"description" TEXT NOT NULL', $compiler->compile($column)); + } + + public function test_uuid_compilation() + { + $column = new UuidColumn('uuid'); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"uuid" UUID NOT NULL', $compiler->compile($column)); + } + + public function test_uuid_compilation_with_generated_uuid() + { + $column = new UuidColumn('uuid')->generated(); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"uuid" UUID NOT NULL DEFAULT gen_random_uuid()', $compiler->compile($column)); + } + + public function test_varchar_compilation() + { + $column = new VarcharColumn('note'); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"note" VARCHAR(255) NOT NULL', $compiler->compile($column)); + } + + public function test_varchar_compilation_with_length() + { + $column = new VarcharColumn('note')->length(100); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"note" VARCHAR(100) NOT NULL', $compiler->compile($column)); + } + + public function test_primary_key_constraint_compilation() + { + $column = new IntegerColumn('id')->primaryKey(); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals( + '"id" INT NOT NULL PRIMARY KEY', + $compiler->compile($column) + ); + } + + public function test_unique_constraint_compilation() + { + $column = new VarcharColumn('name')->unique(); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"name" VARCHAR(255) NOT NULL UNIQUE', $compiler->compile($column)); + } + + public function test_nullability_constraint_compilation() + { + $default = new VarcharColumn('name'); + $column1 = new VarcharColumn('name')->nullable(); + $column2 = new VarcharColumn('name')->nullable(false); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"name" VARCHAR(255) NOT NULL', $compiler->compile($default)); + $this->assertEquals('"name" VARCHAR(255) NULL', $compiler->compile($column1)); + $this->assertEquals('"name" VARCHAR(255) NOT NULL', $compiler->compile($column2)); + } + + public function test_auto_increment_attribute_compilation() + { + $column = new IntegerColumn('id')->autoIncrement(); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"id" INT NOT NULL GENERATED BY DEFAULT AS IDENTITY', $compiler->compile($column)); + } + + public function test_auto_increment_primary_key_compilation() + { + $column = new IntegerColumn('id')->autoIncrement()->primaryKey(); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals( + '"id" INT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY', + $compiler->compile($column), + ); + } + + public function test_default_value_compilation() + { + $column = new VarcharColumn('name')->default('Jim Halpert'); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals('"name" VARCHAR(255) NOT NULL DEFAULT \'Jim Halpert\'', $compiler->compile($column)); + } + + public function test_default_value_escaping() + { + $column = new VarcharColumn('name')->default("O'Reilly"); + + $compiler = new PostgresColumnCompiler(); + + $this->assertEquals( + "\"name\" VARCHAR(255) NOT NULL DEFAULT 'O''Reilly'", + $compiler->compile($column), + ); + } +} \ No newline at end of file diff --git a/packages/database/tests/Compilers/Dialects/Postgres/PostgresTableCompilerTestCase.php b/packages/database/tests/Compilers/Dialects/Postgres/PostgresTableCompilerTestCase.php new file mode 100644 index 0000000000..2e073b7170 --- /dev/null +++ b/packages/database/tests/Compilers/Dialects/Postgres/PostgresTableCompilerTestCase.php @@ -0,0 +1,39 @@ +addColumns( + new IntegerColumn('id') + ->unsigned() + ->autoIncrement() + ->primaryKey(), + new TextColumn('first_name'), + new TextColumn('middle_name')->nullable(), + new TextColumn('last_name'), + ); + + $this->assertEquals( + <<= 0) PRIMARY KEY, + "first_name" TEXT NOT NULL, + "middle_name" TEXT NULL, + "last_name" TEXT NOT NULL + ); + SQL, + $compiler->compile($createTable) + ); + } +} \ No newline at end of file From 7a6dc3bf3c55d2f7a2c28dd407565f2990f50570 Mon Sep 17 00:00:00 2001 From: "aidan.casey" Date: Fri, 23 Oct 2026 23:04:58 -0400 Subject: [PATCH 6/6] Updates column objects for consistency. --- packages/database/src/Columns/CharColumn.php | 8 +--- packages/database/src/Columns/Column.php | 12 +++--- .../src/Columns/Concerns/IsColumn.php | 40 +++--------------- packages/database/src/Columns/DateColumn.php | 8 +--- .../database/src/Columns/DateTimeColumn.php | 22 ++-------- packages/database/src/Columns/FloatColumn.php | 8 +--- .../database/src/Columns/IntegerColumn.php | 15 +------ packages/database/src/Columns/TextColumn.php | 8 +--- packages/database/src/Columns/UuidColumn.php | 8 +--- .../database/src/Columns/VarcharColumn.php | 8 +--- .../Dialects/MySQL/MySQLColumnCompiler.php | 42 +++++++++---------- .../Postgres/PostgresColumnCompiler.php | 34 +++++++-------- 12 files changed, 60 insertions(+), 153 deletions(-) diff --git a/packages/database/src/Columns/CharColumn.php b/packages/database/src/Columns/CharColumn.php index 5673a76f94..c7d00f5eeb 100644 --- a/packages/database/src/Columns/CharColumn.php +++ b/packages/database/src/Columns/CharColumn.php @@ -2,14 +2,13 @@ namespace Tempest\Database\Columns; -use Tempest\Database\Columns\Concerns\HasConstraints; use Tempest\Database\Columns\Concerns\IsColumn; final class CharColumn implements Column { use IsColumn; - private int $length = 1; + private(set) int $length = 1; public function length(int $length): self { @@ -17,9 +16,4 @@ public function length(int $length): self 'length' => $length, ]); } - - public function getLength(): int - { - return $this->length; - } } diff --git a/packages/database/src/Columns/Column.php b/packages/database/src/Columns/Column.php index 59e637c8f5..bc983085b8 100644 --- a/packages/database/src/Columns/Column.php +++ b/packages/database/src/Columns/Column.php @@ -4,15 +4,15 @@ interface Column { - public function name(string $name): static; + public string $name { get; } + public bool $nullable { get; } + public mixed $default { get; } + public bool $unique { get; } + public bool $primaryKey { get; } - public function getName(): string; + public function name(string $name): static; public function nullable(bool $nullable = true): static; - public function isNullable(): bool; - public function default(mixed $default): static; - - public function getDefault(): mixed; } diff --git a/packages/database/src/Columns/Concerns/IsColumn.php b/packages/database/src/Columns/Concerns/IsColumn.php index dc49cf4651..e8e4acfc05 100644 --- a/packages/database/src/Columns/Concerns/IsColumn.php +++ b/packages/database/src/Columns/Concerns/IsColumn.php @@ -4,16 +4,16 @@ trait IsColumn { - private bool $primaryKey = false; + private(set) bool $primaryKey = false; - private bool $nullable = false; + private(set) bool $nullable = false; - private mixed $default; + private(set) mixed $default; - private bool $unique = false; + private(set) bool $unique = false; public function __construct( - private string $name, + private(set) string $name, ) {} public function name(string $name): static @@ -23,11 +23,6 @@ public function name(string $name): static ]); } - public function getName(): string - { - return $this->name; - } - public function primaryKey(): static { return clone($this, [ @@ -35,11 +30,6 @@ public function primaryKey(): static ]); } - public function isPrimaryKey(): bool - { - return $this->primaryKey; - } - public function nullable(bool $nullable = true): static { return clone($this, [ @@ -47,11 +37,6 @@ public function nullable(bool $nullable = true): static ]); } - public function isNullable(): bool - { - return $this->nullable; - } - public function default(mixed $default): static { return clone($this, [ @@ -59,25 +44,10 @@ public function default(mixed $default): static ]); } - public function hasDefault(): bool - { - return isset($this->default); - } - - public function getDefault(): mixed - { - return $this->default; - } - public function unique(bool $unique = true): self { return clone($this, [ 'unique' => $unique, ]); } - - public function isUnique(): bool - { - return $this->unique; - } } diff --git a/packages/database/src/Columns/DateColumn.php b/packages/database/src/Columns/DateColumn.php index e4ad1a5814..02cca38b9d 100644 --- a/packages/database/src/Columns/DateColumn.php +++ b/packages/database/src/Columns/DateColumn.php @@ -2,14 +2,13 @@ namespace Tempest\Database\Columns; -use Tempest\Database\Columns\Concerns\HasConstraints; use Tempest\Database\Columns\Concerns\IsColumn; final class DateColumn implements Column { use IsColumn; - private bool $current = false; + private(set) bool $current = false; public function useCurrent(): self { @@ -17,9 +16,4 @@ public function useCurrent(): self 'current' => true, ]); } - - public function usesCurrent(): bool - { - return $this->current; - } } diff --git a/packages/database/src/Columns/DateTimeColumn.php b/packages/database/src/Columns/DateTimeColumn.php index 1d74cee59b..64583a0f5b 100644 --- a/packages/database/src/Columns/DateTimeColumn.php +++ b/packages/database/src/Columns/DateTimeColumn.php @@ -3,16 +3,15 @@ namespace Tempest\Database\Columns; use InvalidArgumentException; -use Tempest\Database\Columns\Concerns\HasConstraints; use Tempest\Database\Columns\Concerns\IsColumn; final class DateTimeColumn implements Column { use IsColumn; - private int $precision = 0; - private bool $current = false; - private bool $currentOnUpdate = false; + private(set) int $precision = 0; + private(set) bool $current = false; + private(set) bool $currentOnUpdate = false; public function precision(int $precision): self { @@ -27,11 +26,6 @@ public function precision(int $precision): self ]); } - public function getPrecision(): int - { - return $this->precision; - } - public function current(): self { return clone($this, [ @@ -39,20 +33,10 @@ public function current(): self ]); } - public function usesCurrent(): bool - { - return $this->current; - } - public function currentOnUpdate(): self { return clone($this, [ 'currentOnUpdate' => true, ]); } - - public function usesCurrentOnUpdate(): bool - { - return $this->currentOnUpdate; - } } diff --git a/packages/database/src/Columns/FloatColumn.php b/packages/database/src/Columns/FloatColumn.php index b3fc0496fc..9d15dc3ce5 100644 --- a/packages/database/src/Columns/FloatColumn.php +++ b/packages/database/src/Columns/FloatColumn.php @@ -2,14 +2,13 @@ namespace Tempest\Database\Columns; -use Tempest\Database\Columns\Concerns\HasConstraints; use Tempest\Database\Columns\Concerns\IsColumn; final class FloatColumn implements Column { use IsColumn; - private bool $unsigned = false; + private(set) bool $unsigned = false; public function unsigned(): self { @@ -17,9 +16,4 @@ public function unsigned(): self 'unsigned' => true, ]); } - - public function isUnsigned(): bool - { - return $this->unsigned; - } } diff --git a/packages/database/src/Columns/IntegerColumn.php b/packages/database/src/Columns/IntegerColumn.php index cfa4b319e9..737695da25 100644 --- a/packages/database/src/Columns/IntegerColumn.php +++ b/packages/database/src/Columns/IntegerColumn.php @@ -2,16 +2,15 @@ namespace Tempest\Database\Columns; -use Tempest\Database\Columns\Concerns\HasConstraints; use Tempest\Database\Columns\Concerns\IsColumn; final class IntegerColumn implements Column { use IsColumn; - private bool $autoIncrement = false; + private(set) bool $autoIncrement = false; - private bool $unsigned = false; + private(set) bool $unsigned = false; public function autoIncrement(bool $autoIncrement = true): self { @@ -20,20 +19,10 @@ public function autoIncrement(bool $autoIncrement = true): self ]); } - public function shouldAutoIncrement(): bool - { - return $this->autoIncrement; - } - public function unsigned(bool $unsigned = true): self { return clone($this, [ 'unsigned' => $unsigned, ]); } - - public function isUnsigned(): bool - { - return $this->unsigned; - } } diff --git a/packages/database/src/Columns/TextColumn.php b/packages/database/src/Columns/TextColumn.php index 8ccc973f13..5215bd6c00 100644 --- a/packages/database/src/Columns/TextColumn.php +++ b/packages/database/src/Columns/TextColumn.php @@ -2,14 +2,13 @@ namespace Tempest\Database\Columns; -use Tempest\Database\Columns\Concerns\HasConstraints; use Tempest\Database\Columns\Concerns\IsColumn; final class TextColumn implements Column { use IsColumn; - private int $length = 65_535; + private(set) int $length = 65_535; public function length(int $length): self { @@ -17,9 +16,4 @@ public function length(int $length): self 'length' => $length, ]); } - - public function getLength(): int - { - return $this->length; - } } diff --git a/packages/database/src/Columns/UuidColumn.php b/packages/database/src/Columns/UuidColumn.php index a74a8b15ff..bc9e518f3f 100644 --- a/packages/database/src/Columns/UuidColumn.php +++ b/packages/database/src/Columns/UuidColumn.php @@ -2,14 +2,13 @@ namespace Tempest\Database\Columns; -use Tempest\Database\Columns\Concerns\HasConstraints; use Tempest\Database\Columns\Concerns\IsColumn; final class UuidColumn implements Column { use IsColumn; - private bool $generated = false; + private(set) bool $generated = false; public function generated(): self { @@ -17,9 +16,4 @@ public function generated(): self 'generated' => true, ]); } - - public function usesGenerated(): bool - { - return $this->generated; - } } diff --git a/packages/database/src/Columns/VarcharColumn.php b/packages/database/src/Columns/VarcharColumn.php index f4524ba9c0..574815db56 100644 --- a/packages/database/src/Columns/VarcharColumn.php +++ b/packages/database/src/Columns/VarcharColumn.php @@ -2,14 +2,13 @@ namespace Tempest\Database\Columns; -use Tempest\Database\Columns\Concerns\HasConstraints; use Tempest\Database\Columns\Concerns\IsColumn; final class VarcharColumn implements Column { use IsColumn; - private int $length = 255; + private(set) int $length = 255; public function length(int $length): self { @@ -17,9 +16,4 @@ public function length(int $length): self 'length' => $length, ]); } - - public function getLength(): int - { - return $this->length; - } } diff --git a/packages/database/src/Compilers/Dialects/MySQL/MySQLColumnCompiler.php b/packages/database/src/Compilers/Dialects/MySQL/MySQLColumnCompiler.php index f663605cbd..6c6bf54ce4 100644 --- a/packages/database/src/Compilers/Dialects/MySQL/MySQLColumnCompiler.php +++ b/packages/database/src/Compilers/Dialects/MySQL/MySQLColumnCompiler.php @@ -19,7 +19,7 @@ final class MySQLColumnCompiler implements ColumnCompiler public function compile(Column $column): string { $parts = [ - $this->quote($column->getName()), + $this->quote($column->name), $this->compileType($column), $this->compileNullability($column), $this->compileDefault($column), @@ -32,13 +32,13 @@ public function compile(Column $column): string private function compileType(Column $column): string { return match (true) { - $column instanceof CharColumn => "CHAR({$column->getLength()})", - $column instanceof VarcharColumn => "VARCHAR({$column->getLength()})", + $column instanceof CharColumn => "CHAR({$column->length})", + $column instanceof VarcharColumn => "VARCHAR({$column->length})", $column instanceof TextColumn => $this->compileTextType($column), - $column instanceof IntegerColumn => 'INT' . ($column->isUnsigned() ? ' UNSIGNED' : ''), + $column instanceof IntegerColumn => 'INT' . ($column->unsigned ? ' UNSIGNED' : ''), $column instanceof FloatColumn => 'FLOAT', $column instanceof DateColumn => 'DATE', - $column instanceof DateTimeColumn => "DATETIME({$column->getPrecision()})", + $column instanceof DateTimeColumn => "DATETIME({$column->precision})", $column instanceof UuidColumn => 'CHAR(36)', default => throw new \LogicException( 'Unsupported column: ' . $column::class, @@ -49,40 +49,40 @@ private function compileType(Column $column): string private function compileTextType(TextColumn $column): string { return match (true) { - $column->getLength() <= 255 => 'TINYTEXT', - $column->getLength() <= 65_535 => 'TEXT', - $column->getLength() <= 16_777_215 => 'MEDIUMTEXT', - $column->getLength() <= 4_294_967_295 => 'LONGTEXT', + $column->length <= 255 => 'TINYTEXT', + $column->length <= 65_535 => 'TEXT', + $column->length <= 16_777_215 => 'MEDIUMTEXT', + $column->length <= 4_294_967_295 => 'LONGTEXT', default => new \LogicException('Unsupported text length'), }; } private function compileNullability(Column $column): string { - return $column->isNullable() ? 'NULL' : 'NOT NULL'; + return $column->nullable ? 'NULL' : 'NOT NULL'; } private function compileDefault(Column $column): string { - if ($column instanceof DateColumn && $column->usesCurrent()) { + if ($column instanceof DateColumn && $column->current) { return 'DEFAULT (CURRENT_DATE)'; } - if ($column instanceof DateTimeColumn && $column->usesCurrent()) { - $precision = $column->getPrecision(); + if ($column instanceof DateTimeColumn && $column->current) { + $precision = $column->precision; return "DEFAULT CURRENT_TIMESTAMP($precision)"; } - if ($column instanceof UuidColumn && $column->usesGenerated()) { + if ($column instanceof UuidColumn && $column->generated) { return 'DEFAULT (UUID())'; } - if (! $column->hasDefault()) { + if (! isset($column->default)) { return ''; } - $value = $this->literal($column->getDefault()); + $value = $this->literal($column->default); if ($column instanceof TextColumn) { return "DEFAULT ($value)"; @@ -95,21 +95,21 @@ private function compileModifiers(Column $column): string { $parts = []; - if ($column instanceof IntegerColumn && $column->shouldAutoIncrement()) { + if ($column instanceof IntegerColumn && $column->autoIncrement) { $parts[] = 'AUTO_INCREMENT'; } - if ($column instanceof DateTimeColumn && $column->usesCurrentOnUpdate()) { - $precision = $column->getPrecision(); + if ($column instanceof DateTimeColumn && $column->currentOnUpdate) { + $precision = $column->precision; $parts[] = "ON UPDATE CURRENT_TIMESTAMP($precision)"; } - if ($column->isUnique()) { + if ($column->unique) { $parts[] = 'UNIQUE'; } - if ($column->isPrimaryKey()) { + if ($column->primaryKey) { $parts[] = 'PRIMARY KEY'; } diff --git a/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php b/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php index 5f4d471cba..41644c8b45 100644 --- a/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php +++ b/packages/database/src/Compilers/Dialects/Postgres/PostgresColumnCompiler.php @@ -20,7 +20,7 @@ final class PostgresColumnCompiler implements ColumnCompiler public function compile(Column $column): string { $parts = [ - $this->quote($column->getName()), + $this->quote($column->name), $this->compileType($column), $this->compileNullability($column), $this->compileDefault($column), @@ -33,13 +33,13 @@ public function compile(Column $column): string private function compileType(Column $column): string { return match (true) { - $column instanceof CharColumn => "CHAR({$column->getLength()})", - $column instanceof VarcharColumn => "VARCHAR({$column->getLength()})", + $column instanceof CharColumn => "CHAR({$column->length})", + $column instanceof VarcharColumn => "VARCHAR({$column->length})", $column instanceof TextColumn => 'TEXT', $column instanceof IntegerColumn => 'INT', $column instanceof FloatColumn => 'REAL', $column instanceof DateColumn => 'DATE', - $column instanceof DateTimeColumn => "TIMESTAMP({$column->getPrecision()})", + $column instanceof DateTimeColumn => "TIMESTAMP({$column->precision})", $column instanceof UuidColumn => 'UUID', default => throw new \LogicException( 'Unsupported column: ' . $column::class, @@ -49,30 +49,30 @@ private function compileType(Column $column): string private function compileNullability(Column $column): string { - return $column->isNullable() ? 'NULL' : 'NOT NULL'; + return $column->nullable ? 'NULL' : 'NOT NULL'; } private function compileDefault(Column $column): string { - if ($column instanceof DateColumn && $column->usesCurrent()) { + if ($column instanceof DateColumn && $column->current) { return 'DEFAULT (CURRENT_DATE)'; } - if ($column instanceof DateTimeColumn && $column->usesCurrent()) { - $precision = $column->getPrecision(); + if ($column instanceof DateTimeColumn && $column->current) { + $precision = $column->precision; return "DEFAULT CURRENT_TIMESTAMP($precision)"; } - if ($column instanceof UuidColumn && $column->usesGenerated()) { + if ($column instanceof UuidColumn && $column->generated) { return 'DEFAULT gen_random_uuid()'; } - if (! $column->hasDefault()) { + if (! isset($column->default)) { return ''; } - $value = $this->literal($column->getDefault()); + $value = $this->literal($column->default); if ($column instanceof TextColumn) { return "DEFAULT ($value)"; @@ -85,28 +85,28 @@ private function compileModifiers(Column $column): string { $parts = []; - if ($column instanceof IntegerColumn && $column->shouldAutoIncrement()) { + if ($column instanceof IntegerColumn && $column->autoIncrement) { $parts[] = 'GENERATED BY DEFAULT AS IDENTITY'; } - if ($column instanceof IntegerColumn && $column->isUnsigned()) { - $name = $this->quote($column->getName()); + if ($column instanceof IntegerColumn && $column->unsigned) { + $name = $this->quote($column->name); $parts[] = "CHECK ($name >= 0)"; } // @todo(aidan-casey): Eventually we can move this to a trigger. - if ($column instanceof DateTimeColumn && $column->usesCurrentOnUpdate()) { + if ($column instanceof DateTimeColumn && $column->currentOnUpdate) { throw new UnexpectedValueException( 'The "currentOnUpdate" method is not supported by the Postgres dialect.' ); } - if ($column->isUnique()) { + if ($column->unique) { $parts[] = 'UNIQUE'; } - if ($column->isPrimaryKey()) { + if ($column->primaryKey) { $parts[] = 'PRIMARY KEY'; }