diff --git a/docs/book/sql-ddl/columns.md b/docs/book/sql-ddl/columns.md index b34c9b47..7e5ab598 100644 --- a/docs/book/sql-ddl/columns.md +++ b/docs/book/sql-ddl/columns.md @@ -14,11 +14,16 @@ use PhpDb\Sql\Ddl\Column\Integer; $column = new Integer('user_id'); $column = new Integer('count', false, 0); // NOT NULL with default 0 -// With display length (platform-specific) +// With display width (platform-specific) $column = new Integer('user_id'); -$column->setOption('length', 11); +$column->setOption('length', 11); // INTEGER(11) ``` +The `length` option is a display width rendered directly after the type, as `INTEGER(11)`. It +accepts an `int` or a string of digits. MySQL deprecated integer display widths in 8.0.17, and the +MySQL platform decorator drops the attribute (see phpdb-mysql#81); other platforms render it as +given. + **Constructor:** ```php diff --git a/src/Sql/Ddl/Column/Column.php b/src/Sql/Ddl/Column/Column.php index 4fec18d3..7e1c41de 100644 --- a/src/Sql/Ddl/Column/Column.php +++ b/src/Sql/Ddl/Column/Column.php @@ -130,7 +130,7 @@ public function setNullable(bool $nullable): static return $this; } - public function setOption(string $name, bool|string $value): static + public function setOption(string $name, bool|int|string $value): static { $this->options[$name] = $value; return $this; diff --git a/src/Sql/Ddl/Column/Integer.php b/src/Sql/Ddl/Column/Integer.php index 4c517515..c816d4e6 100644 --- a/src/Sql/Ddl/Column/Integer.php +++ b/src/Sql/Ddl/Column/Integer.php @@ -5,20 +5,61 @@ namespace PhpDb\Sql\Ddl\Column; use Override; +use PhpDb\Sql\Argument\Literal; +use PhpDb\Sql\Exception\InvalidArgumentException; + +use function array_splice; +use function ctype_digit; +use function is_int; +use function is_string; +use function sprintf; +use function strlen; +use function substr; class Integer extends Column { - /** @inheritDoc */ + /** + * Renders the display width from the "length" option in parentheses directly after the type, + * ahead of the nullability and default clauses. + * + * @inheritDoc + * @throws InvalidArgumentException When the "length" option is not a non-negative integer. + */ #[Override] public function getExpressionData(): array { $expressionData = parent::getExpressionData(); $options = $this->getOptions(); - if (isset($options['length'])) { - $expressionData['spec'] .= " ({$options['length']})"; + if (! isset($options['length'])) { + return $expressionData; } + $displayWidth = $this->normaliseDisplayWidth($options['length']); + $attributes = substr($expressionData['spec'], strlen($this->specification)); + + $expressionData['spec'] = "{$this->specification}(%s){$attributes}"; + array_splice($expressionData['values'], offset: 2, length: 0, replacement: [new Literal($displayWidth)]); + return $expressionData; } + + /** + * @throws InvalidArgumentException When the value is not a non-negative integer. + */ + private function normaliseDisplayWidth(mixed $length): string + { + if (is_int($length) && $length >= 0) { + return (string) $length; + } + + if (is_string($length) && ctype_digit($length)) { + return $length; + } + + throw new InvalidArgumentException(sprintf( + 'Column "%s" length option must be a non-negative integer', + $this->name, + )); + } } diff --git a/test/unit/Sql/Ddl/Column/BigIntegerTest.php b/test/unit/Sql/Ddl/Column/BigIntegerTest.php index 55b330e1..3fc7959b 100644 --- a/test/unit/Sql/Ddl/Column/BigIntegerTest.php +++ b/test/unit/Sql/Ddl/Column/BigIntegerTest.php @@ -7,13 +7,28 @@ use PhpDb\Sql\Argument; use PhpDb\Sql\Ddl\Column\BigInteger; use PhpDb\Sql\Ddl\Column\Column; +use PhpDb\Sql\Ddl\Column\Integer; +use PhpDb\Sql\Ddl\CreateTable; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(BigInteger::class, '__construct')] #[CoversMethod(Column::class, 'getExpressionData')] +#[CoversMethod(Integer::class, 'getExpressionData')] +#[Group('unit')] final class BigIntegerTest extends TestCase { + #[Test] + public function rendersLengthDirectlyAfterType(): void + { + $createTable = new CreateTable('t'); + $createTable->addColumn(new BigInteger('i', false, null, ['length' => 20])); + + static::assertSame("CREATE TABLE \"t\" ( \n \"i\" BIGINT(20) NOT NULL \n)", $createTable->getSqlString()); + } + public function testGetExpressionData(): void { $column = new BigInteger('foo'); diff --git a/test/unit/Sql/Ddl/Column/ColumnTest.php b/test/unit/Sql/Ddl/Column/ColumnTest.php index 2db3fa68..0e496f88 100644 --- a/test/unit/Sql/Ddl/Column/ColumnTest.php +++ b/test/unit/Sql/Ddl/Column/ColumnTest.php @@ -11,6 +11,7 @@ use PhpDb\Sql\Ddl\Constraint\PrimaryKey; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(Column::class, '__construct')] @@ -28,6 +29,17 @@ #[Group('unit')] final class ColumnTest extends TestCase { + #[Test] + public function setOptionAcceptsIntegerValue(): void + { + $column = new Column(); + + $result = $column->setOption('length', 11); + + static::assertSame($column, $result); + static::assertSame(['length' => 11], $column->getOptions()); + } + public function testAddConstraintAppendsConstraintToColumn(): void { $column = new Column('id'); diff --git a/test/unit/Sql/Ddl/Column/IntegerTest.php b/test/unit/Sql/Ddl/Column/IntegerTest.php index 0404d88b..dd26f090 100644 --- a/test/unit/Sql/Ddl/Column/IntegerTest.php +++ b/test/unit/Sql/Ddl/Column/IntegerTest.php @@ -6,18 +6,91 @@ use PhpDb\Sql\Argument; use PhpDb\Sql\Ddl\Column\Column; +use PhpDb\Sql\Ddl\Column\ColumnInterface; use PhpDb\Sql\Ddl\Column\Integer; use PhpDb\Sql\Ddl\Constraint\PrimaryKey; +use PhpDb\Sql\Ddl\CreateTable; +use PhpDb\Sql\Exception\InvalidArgumentException; +use PhpDbTest\TestAsset\TrustingSql92Platform; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(Integer::class, '__construct')] #[CoversMethod(Integer::class, 'getExpressionData')] +#[CoversMethod(Integer::class, 'normaliseDisplayWidth')] #[CoversMethod(Column::class, 'getExpressionData')] #[Group('unit')] final class IntegerTest extends TestCase { + /** + * @return array + */ + public static function invalidLengthProvider(): array + { + return [ + 'boolean' => [true], + 'negative integer' => [-1], + 'non-digit string' => ['abc'], + ]; + } + + /** + * Asserts what a column renders to inside CREATE TABLE, with values quoted. + */ + private static function assertColumnRenders(string $expected, ColumnInterface $column): void + { + $createTable = new CreateTable('t'); + $createTable->addColumn($column); + + static::assertSame( + "CREATE TABLE \"t\" ( \n {$expected} \n)", + $createTable->getSqlString(new TrustingSql92Platform()), + ); + } + + #[Test] + public function getExpressionDataPlacesLengthDirectlyAfterType(): void + { + $expressionData = (new Integer('i', false, null, ['length' => 11]))->getExpressionData(); + + static::assertSame('%s %s(%s) NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + Argument::identifier('i'), + Argument::literal('INTEGER'), + Argument::literal('11'), + ], + $expressionData['values'], + ); + } + + #[Test] + public function rendersLengthBeforeNullabilityAndDefault(): void + { + static::assertColumnRenders( + '"i" INTEGER(11) NULL DEFAULT \'7\'', + new Integer('i', true, 7, ['length' => 11]), + ); + } + + #[Test] + public function rendersLengthDirectlyAfterType(): void + { + static::assertColumnRenders('"i" INTEGER(11) NOT NULL', new Integer('i', false, null, ['length' => 11])); + } + + #[Test] + public function rendersLengthSetAsIntegerOption(): void + { + $column = new Integer('i'); + $column->setOption('length', 11); + + static::assertColumnRenders('"i" INTEGER(11) NOT NULL', $column); + } + public function testGetExpressionData(): void { $column = new Integer('foo'); @@ -64,7 +137,15 @@ public function testGetExpressionDataIncludesLengthWhenOptionSet(): void $expressionData = $column->getExpressionData(); - self::assertStringContainsString('(11)', $expressionData['spec']); + static::assertSame('%s %s(%s) NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + Argument::identifier('id'), + Argument::literal('INTEGER'), + Argument::literal('11'), + ], + $expressionData['values'], + ); } public function testObjectConstruction(): void @@ -72,4 +153,16 @@ public function testObjectConstruction(): void $integer = new Integer('foo'); self::assertEquals('foo', $integer->getName()); } + + #[Test] + #[DataProvider('invalidLengthProvider')] + public function throwsWhenLengthOptionIsNotANonNegativeInteger(bool|int|string $length): void + { + $column = new Integer('i', false, null, ['length' => $length]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Column "i" length option must be a non-negative integer'); + + $column->getExpressionData(); + } } diff --git a/test/unit/Sql/Ddl/Column/SmallIntegerTest.php b/test/unit/Sql/Ddl/Column/SmallIntegerTest.php index 62593edb..1218b4da 100644 --- a/test/unit/Sql/Ddl/Column/SmallIntegerTest.php +++ b/test/unit/Sql/Ddl/Column/SmallIntegerTest.php @@ -6,14 +6,29 @@ use PhpDb\Sql\Argument; use PhpDb\Sql\Ddl\Column\Column; +use PhpDb\Sql\Ddl\Column\Integer; use PhpDb\Sql\Ddl\Column\SmallInteger; +use PhpDb\Sql\Ddl\CreateTable; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(SmallInteger::class, '__construct')] #[CoversMethod(Column::class, 'getExpressionData')] +#[CoversMethod(Integer::class, 'getExpressionData')] +#[Group('unit')] final class SmallIntegerTest extends TestCase { + #[Test] + public function rendersLengthDirectlyAfterType(): void + { + $createTable = new CreateTable('t'); + $createTable->addColumn(new SmallInteger('i', false, null, ['length' => 6])); + + static::assertSame("CREATE TABLE \"t\" ( \n \"i\" SMALLINT(6) NOT NULL \n)", $createTable->getSqlString()); + } + public function testGetExpressionData(): void { $column = new SmallInteger('foo');