diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 90717a3d..0cf11376 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -1173,7 +1173,7 @@ count = 1 [[issues]] file = "src/Adapter/Profiler/Profiler.php" code = "invalid-property-assignment-value" -message = '''Invalid type for property `$profiles`: expected `array`, but got `array`.''' +message = '''Invalid type for property `$profiles`: expected `array`, but got `non-empty-array`.''' count = 1 [[issues]] @@ -1194,24 +1194,6 @@ code = "missing-property-type" message = "Property `$profiles` is missing a type hint." count = 1 -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "possibly-null-operand" -message = "Right operand in arithmetic operation might be `null` (type `float|null`)." -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "possibly-undefined-int-array-index" -message = '''Possibly undefined array key `int` accessed on `array`.''' -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "possibly-undefined-string-array-index" -message = '''Possibly undefined array key accessed on `array{'elapse': float|null, 'end': float, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}|array{'end': float}`.''' -count = 1 - [[issues]] file = "src/Adapter/Profiler/Profiler.php" code = "unhandled-thrown-type" @@ -2076,6 +2058,12 @@ code = "class-must-be-final" message = 'Class `PhpDb\RowGateway\Exception\RuntimeException` should be declared `final`.' count = 1 +[[issues]] +file = "src/RowGateway/Feature/FeatureSet.php" +code = "unused-property" +message = "Property `$magicSpecifications` is never used." +count = 1 + [[issues]] file = "src/RowGateway/RowGateway.php" code = "class-must-be-final" @@ -2142,12 +2130,6 @@ code = "invalid-iterator" message = "The expression provided to `foreach` is not iterable. It resolved to type `mixed`, which is not iterable." count = 1 -[[issues]] -file = "src/Sql/AbstractSql.php" -code = "invalid-member-selector" -message = "Invalid type `array-key` used as a member selector; a string is expected." -count = 1 - [[issues]] file = "src/Sql/AbstractSql.php" code = "invalid-operand" @@ -2187,7 +2169,7 @@ count = 1 [[issues]] file = "src/Sql/AbstractSql.php" code = "less-specific-nested-argument-type" -message = "Argument type mismatch for argument #2 of `vsprintf`: expected `array`, but provided type `array{}|non-empty-list` is less specific." +message = "Argument type mismatch for argument #2 of `vsprintf`: expected `array`, but provided type `list` is less specific." count = 1 [[issues]] @@ -2376,6 +2358,12 @@ code = "string-member-selector" message = "This member selector uses a non-literal string type (`non-empty-string`); its specific value cannot be statically determined." count = 1 +[[issues]] +file = "src/Sql/AbstractSql.php" +code = "string-member-selector" +message = "This member selector uses a non-literal string type (`string`); its specific value cannot be statically determined." +count = 1 + [[issues]] file = "src/Sql/AbstractSql.php" code = "unhandled-thrown-type" @@ -3219,7 +3207,13 @@ count = 1 [[issues]] file = "src/Sql/Ddl/CreateTable.php" code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('combinedBy')` accessed on `array|string>`." +message = "Possibly undefined array key `string('combinedBy')` accessed on `array>`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTable.php" +code = "possibly-undefined-string-array-index" +message = "Possibly undefined array key `string('combinedBy')` accessed on `array`." count = 1 [[issues]] @@ -3900,12 +3894,6 @@ code = "class-must-be-final" message = 'Class `PhpDb\Sql\Platform\Platform` should be declared `final`.' count = 1 -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "imprecise-type" -message = "Type `array` in return type of `getDecorators` is imprecise, equivalent to `array`." -count = 1 - [[issues]] file = "src/Sql/Platform/Platform.php" code = "invalid-iterator" @@ -4251,7 +4239,7 @@ count = 1 [[issues]] file = "src/Sql/Predicate/PredicateSet.php" code = "less-specific-nested-argument-type" -message = "Argument type mismatch for argument #2 of `implode`: expected `array|null`, but provided type `array{}|non-empty-list` is less specific." +message = "Argument type mismatch for argument #2 of `implode`: expected `array|null`, but provided type `list` is less specific." count = 1 [[issues]] @@ -4263,7 +4251,7 @@ count = 1 [[issues]] file = "src/Sql/Predicate/PredicateSet.php" code = "less-specific-nested-return-statement" -message = '''Returned type `array{'spec': string, 'values': array{}|list}` is less specific than the declared return type `array{'spec': string, 'values': array}` for function `PhpDb\Sql\Predicate\PredicateSet::getExpressionData` due to nested 'mixed'.''' +message = '''Returned type `array{'spec': string, 'values': list}` is less specific than the declared return type `array{'spec': string, 'values': array}` for function `PhpDb\Sql\Predicate\PredicateSet::getExpressionData` due to nested 'mixed'.''' count = 1 [[issues]] @@ -4518,12 +4506,6 @@ code = "invalid-property-assignment-value" message = 'Invalid type for property `$joins`: expected `PhpDb\Sql\Join`, but got `null`.' count = 1 -[[issues]] -file = "src/Sql/Select.php" -code = "invalid-property-assignment-value" -message = "Invalid type for property `$specifications`: expected `array>|array`, but got `array|string>`." -count = 1 - [[issues]] file = "src/Sql/Select.php" code = "invalid-property-assignment-value" @@ -5218,36 +5200,6 @@ count = 1 file = "src/TableGateway/AbstractTableGateway.php" code = "impossible-condition" message = "This condition (type `false`) will always evaluate to false." -count = 7 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$deleteState['table']` of type `string` can never be `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$insertState['table']` of type `string` can never be `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$this->table` of type `string` can never be `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$this->table` of type `string` can never be `object`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$updateState['table']` of type `string` can never be `array`." count = 1 [[issues]] @@ -5294,9 +5246,9 @@ count = 1 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-clone" -message = "Invalid `clone` operation on non-cloneable type `never`." -count = 1 +code = "invalid-array-access" +message = 'Cannot access array index on object `PhpDb\Sql\TableIdentifier` that does not implement `ArrayAccess`.' +count = 3 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" @@ -5328,6 +5280,12 @@ code = "invalid-return-statement" message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getSql`: expected `PhpDb\Sql\Sql`, but found `PhpDb\Sql\Sql|null`.' count = 1 +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getTable`: expected `PhpDb\Sql\TableIdentifier|array|string`, but found `PhpDb\Sql\TableIdentifier|array|null|string`.' +count = 1 + [[issues]] file = "src/TableGateway/AbstractTableGateway.php" code = "missing-api-or-internal" @@ -5430,24 +5388,6 @@ code = "mixed-return-statement" message = 'Could not infer a precise return type for function `PhpDb\TableGateway\AbstractTableGateway::executeUpdate`. Saw type `mixed`.' count = 1 -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "no-value" -message = "Argument #1 passed to function `array_values` has type `never`, meaning it cannot produce a value." -count = 3 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "no-value" -message = "Argument #1 passed to function `count` has type `never`, meaning it cannot produce a value." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "no-value" -message = "Argument #1 passed to function `reset` has type `never`, meaning it cannot produce a value." -count = 1 - [[issues]] file = "src/TableGateway/AbstractTableGateway.php" code = "nullable-return-statement" @@ -5472,6 +5412,12 @@ code = "nullable-return-statement" message = 'Function `PhpDb\TableGateway\AbstractTableGateway::getSql` is declared to return `PhpDb\Sql\Sql` but possibly returns a nullable value (inferred as `PhpDb\Sql\Sql|null`).' count = 1 +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "nullable-return-statement" +message = 'Function `PhpDb\TableGateway\AbstractTableGateway::getTable` is declared to return `PhpDb\Sql\TableIdentifier|array|string` but possibly returns a nullable value (inferred as `PhpDb\Sql\TableIdentifier|array|null|string`).' +count = 1 + [[issues]] file = "src/TableGateway/AbstractTableGateway.php" code = "overly-wide-return-type" @@ -5498,15 +5444,15 @@ count = 1 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" -code = "redundant-comparison" -message = "Redundant `===` comparison: left-hand side is never identical to right-hand side." -count = 2 +code = "possibly-undefined-string-array-index" +message = "Possibly undefined array key `string('table')` accessed on `array`." +count = 3 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" -code = "redundant-logical-operation" -message = "Redundant `&&` operation: left operand is always falsy and right operand is not evaluated." -count = 2 +code = "redundant-comparison" +message = "Redundant `===` comparison: left-hand side is never identical to right-hand side." +count = 1 [[issues]] file = "src/TableGateway/Exception/ExceptionInterface.php" @@ -5527,27 +5473,39 @@ message = 'Class `PhpDb\TableGateway\Exception\RuntimeException` should be decla count = 1 [[issues]] -file = "src/TableGateway/TableGateway.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\TableGateway` should be declared `final`.' +file = "src/TableGateway/Feature/MetadataFeature.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 1 [[issues]] -file = "src/TableGateway/TableGateway.php" -code = "imprecise-type" -message = "Type `array` in parameter `$features` is imprecise, equivalent to `array`." +file = "src/TableGateway/Feature/MetadataFeature.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/TableGateway/Feature/RowGatewayFeature.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/TableGateway/Feature/SequenceFeature.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 1 [[issues]] file = "src/TableGateway/TableGateway.php" -code = "imprecise-type" -message = "Type `array` in parameter `$table` is imprecise, equivalent to `array`." +code = "class-must-be-final" +message = 'Class `PhpDb\TableGateway\TableGateway` should be declared `final`.' count = 1 [[issues]] file = "src/TableGateway/TableGateway.php" -code = "invalid-property-assignment-value" -message = 'Invalid type for property `$table`: expected `string`, but got `PhpDb\Sql\TableIdentifier|array|string`.' +code = "imprecise-type" +message = "Type `array` in parameter `$table` is imprecise, equivalent to `array`." count = 1 [[issues]] @@ -5567,28 +5525,3 @@ file = "src/TableGateway/TableGatewayInterface.php" code = "missing-api-or-internal" message = 'Interface `PhpDb\TableGateway\TableGatewayInterface` is missing an `@api` or `@internal` annotation.' count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "nullable-return-statement" -message = 'Function `PhpDb\TableGateway\AbstractTableGateway::getTable` is declared to return `PhpDb\Sql\TableIdentifier|array|string` but possibly returns a nullable value (inferred as `PhpDb\Sql\TableIdentifier|array|null|string`).' -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getTable`: expected `PhpDb\Sql\TableIdentifier|array|string`, but found `PhpDb\Sql\TableIdentifier|array|null|string`.' -count = 1 - - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-array-access" -message = 'Cannot access array index on object `PhpDb\Sql\TableIdentifier` that does not implement `ArrayAccess`.' -count = 3 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('table')` accessed on `array`." -count = 3 diff --git a/docs/book/sql-ddl/columns.md b/docs/book/sql-ddl/columns.md index 2d60e9ae..b34c9b47 100644 --- a/docs/book/sql-ddl/columns.md +++ b/docs/book/sql-ddl/columns.md @@ -86,6 +86,10 @@ $column->setDecimal(3); // Change scale - `setDecimal(int $decimal): self` - Set scale - `getDecimal(): int` - Get scale +Digits and scale are both optional. Without digits the column renders as a bare `DECIMAL`, +leaving the precision to the database default. A scale without digits cannot be rendered and +throws `PhpDb\Sql\Exception\InvalidArgumentException`. + ### Floating Floating-point numbers. @@ -102,6 +106,10 @@ $column->setDecimal(4); **Constructor:** `__construct($name, $digits, $decimal)` +Digits and scale are both optional and render the same way as for `Decimal`: without digits the +column renders as a bare `FLOAT`, and a scale without digits throws +`PhpDb\Sql\Exception\InvalidArgumentException`. + > The class is named `Floating` rather than `Float` because `float` is a reserved > keyword in PHP. @@ -142,6 +150,9 @@ __construct( - `setDefault(string|int|null $default): static` - `setOption(string $name, mixed $value): static` +A length is required. SQL-92 and MySQL both reject a bare `VARCHAR`, so rendering a `Varchar` +without a length throws `PhpDb\Sql\Exception\InvalidArgumentException` naming the column. + ### Char Fixed-length character string. @@ -153,6 +164,9 @@ $column = new Char('country_code', 2); // ISO country codes $column = new Char('status', 1); // Single character status ``` +The length is optional. Without one the column renders as a bare `CHAR`, which databases treat as +`CHAR(1)`. + **Constructor:** ```php @@ -203,6 +217,9 @@ use PhpDb\Sql\Ddl\Column\Binary; $column = new Binary('hash', 32); // 32-byte hash ``` +The length is optional. Without one the column renders as a bare `BINARY`, which databases treat as +`BINARY(1)`. + **Constructor:** ```php @@ -225,6 +242,9 @@ use PhpDb\Sql\Ddl\Column\Varbinary; $column = new Varbinary('file_data', 65535); ``` +A length is required. As with `Varchar`, rendering a `Varbinary` without a length throws +`PhpDb\Sql\Exception\InvalidArgumentException` naming the column. + **Constructor:** ```php diff --git a/lint-baseline.toml b/lint-baseline.toml index ff32f48d..263478e5 100644 --- a/lint-baseline.toml +++ b/lint-baseline.toml @@ -1284,18 +1284,6 @@ code = "assertion-style" message = "Inconsistent assertions style." count = 10 -[[issues]] -file = "test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php" -code = "no-redundant-variable" -message = "Variable `$container` is assigned but never used." -count = 1 - -[[issues]] -file = "test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php" -code = "no-redundant-variable" -message = "Variable `$pluginManagerConfig` is assigned but never used." -count = 1 - [[issues]] file = "test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php" code = "prefer-test-attribute" @@ -4918,13 +4906,7 @@ count = 1 file = "test/unit/Sql/Ddl/Column/VarcharTest.php" code = "assertion-style" message = "Inconsistent assertions style." -count = 9 - -[[issues]] -file = "test/unit/Sql/Ddl/Column/VarcharTest.php" -code = "prefer-test-attribute" -message = "Use `#[Test]` attribute instead of `test` prefix on method `testGetExpressionDataWithNullLength`." -count = 1 +count = 7 [[issues]] file = "test/unit/Sql/Ddl/Column/VarcharTest.php" @@ -4948,7 +4930,7 @@ count = 1 file = "test/unit/Sql/Ddl/Column/VarcharTest.php" code = "strict-assertions" message = "Use strict assertions in PHPUnit tests." -count = 4 +count = 3 [[issues]] file = "test/unit/Sql/Ddl/Constraint/AbstractConstraintTest.php" diff --git a/src/Metadata/Source/AbstractSource.php b/src/Metadata/Source/AbstractSource.php index 1c6c333e..7552e872 100644 --- a/src/Metadata/Source/AbstractSource.php +++ b/src/Metadata/Source/AbstractSource.php @@ -693,15 +693,17 @@ protected function loadTriggerData(string $schema): void * Prepare data hierarchy * * The by-reference walk builds arbitrary depths of the hierarchy, which - * cannot be expressed against the MetadataData shape. + * cannot be expressed against the MetadataData shape. The reference is + * re-typed as a plain array so the analyzer does not re-derive the shape + * on every iteration, which does not terminate in useful time on Mago + * 1.47.5 and later. * - * @mago-expect analysis:possibly-undefined-string-array-index - * @mago-expect analysis:possibly-undefined-int-array-index - * @mago-expect analysis:possibly-null-array-access + * @mago-expect analysis:mixed-assignment * @mago-expect lint:no-isset */ protected function prepareDataHierarchy(string $type, string ...$keys): void { + /** @var array $data */ $data = &$this->data; foreach ([$type, ...$keys] as $key) { if (! isset($data[$key])) { diff --git a/src/Sql/Ddl/Column/AbstractLengthColumn.php b/src/Sql/Ddl/Column/AbstractLengthColumn.php index 3eeb65b4..eae34660 100644 --- a/src/Sql/Ddl/Column/AbstractLengthColumn.php +++ b/src/Sql/Ddl/Column/AbstractLengthColumn.php @@ -6,12 +6,19 @@ use Override; use PhpDb\Sql\Argument\Literal; +use PhpDb\Sql\Exception\InvalidArgumentException; use function array_splice; +use function sprintf; +use function strlen; +use function substr; abstract class AbstractLengthColumn extends Column { - protected string $specification = '%s %s(%s)'; + /** + * Whether the type is invalid without a length, as VARCHAR and VARBINARY are in SQL-92 and MySQL. + */ + protected bool $lengthRequired = false; protected ?int $length = null; @@ -27,16 +34,38 @@ public function __construct( parent::__construct($name, $nullable, $default, $options); } - /** @inheritDoc */ + /** + * Renders the length in parentheses directly after the type when one is set. Without a length + * the column renders bare, unless the type requires one. + * + * @inheritDoc + * @throws InvalidArgumentException When the type requires a length and none is set. + */ #[Override] public function getExpressionData(): array { + $lengthExpression = $this->getLengthExpression(); + $hasLength = '' !== $lengthExpression && '0' !== $lengthExpression; + + if (! $hasLength && $this->lengthRequired) { + throw new InvalidArgumentException(sprintf( + 'Column "%s" of type %s requires a length', + $this->name, + $this->type, + )); + } + $expressionData = parent::getExpressionData(); - if ($this->getLengthExpression() !== '' && $this->getLengthExpression() !== '0') { - array_splice($expressionData['values'], 2, 0, [new Literal($this->getLengthExpression())]); + if (! $hasLength) { + return $expressionData; } + $attributes = substr($expressionData['spec'], strlen($this->specification)); + + $expressionData['spec'] = "{$this->specification}(%s){$attributes}"; + array_splice($expressionData['values'], 2, 0, [new Literal($lengthExpression)]); + return $expressionData; } diff --git a/src/Sql/Ddl/Column/AbstractPrecisionColumn.php b/src/Sql/Ddl/Column/AbstractPrecisionColumn.php index 2930f209..5d37f29b 100644 --- a/src/Sql/Ddl/Column/AbstractPrecisionColumn.php +++ b/src/Sql/Ddl/Column/AbstractPrecisionColumn.php @@ -5,6 +5,9 @@ namespace PhpDb\Sql\Ddl\Column; use Override; +use PhpDb\Sql\Exception\InvalidArgumentException; + +use function sprintf; abstract class AbstractPrecisionColumn extends AbstractLengthColumn { @@ -45,13 +48,24 @@ public function setDigits(?int $digits): static return $this->setLength($digits); } + /** + * @throws InvalidArgumentException When a decimal scale is set without digits. + */ #[Override] protected function getLengthExpression(): string { - if (null !== $this->decimal) { - return "{$this->length},{$this->decimal}"; + if (null === $this->decimal) { + return (string) $this->length; + } + + if (null === $this->length) { + throw new InvalidArgumentException(sprintf( + 'Column "%s" of type %s has a decimal scale but no digits', + $this->name, + $this->type, + )); } - return (string) $this->length; + return "{$this->length},{$this->decimal}"; } } diff --git a/src/Sql/Ddl/Column/Blob.php b/src/Sql/Ddl/Column/Blob.php index 9980211f..929d6cbe 100644 --- a/src/Sql/Ddl/Column/Blob.php +++ b/src/Sql/Ddl/Column/Blob.php @@ -4,10 +4,19 @@ namespace PhpDb\Sql\Ddl\Column; +use Override; + class Blob extends AbstractLengthColumn { - protected string $specification = '%s %s'; - /** @var string Change type to blob */ protected string $type = 'BLOB'; + + /** + * BLOB takes no length, so a configured length is kept but never rendered. + */ + #[Override] + protected function getLengthExpression(): string + { + return ''; + } } diff --git a/src/Sql/Ddl/Column/Text.php b/src/Sql/Ddl/Column/Text.php index af608c3d..ec989744 100644 --- a/src/Sql/Ddl/Column/Text.php +++ b/src/Sql/Ddl/Column/Text.php @@ -4,9 +4,18 @@ namespace PhpDb\Sql\Ddl\Column; +use Override; + class Text extends AbstractLengthColumn { - protected string $specification = '%s %s'; - protected string $type = 'TEXT'; + + /** + * TEXT takes no length, so a configured length is kept but never rendered. + */ + #[Override] + protected function getLengthExpression(): string + { + return ''; + } } diff --git a/src/Sql/Ddl/Column/Varbinary.php b/src/Sql/Ddl/Column/Varbinary.php index 93cc54c1..8d98f08d 100644 --- a/src/Sql/Ddl/Column/Varbinary.php +++ b/src/Sql/Ddl/Column/Varbinary.php @@ -6,5 +6,7 @@ class Varbinary extends AbstractLengthColumn { + protected bool $lengthRequired = true; + protected string $type = 'VARBINARY'; } diff --git a/src/Sql/Ddl/Column/Varchar.php b/src/Sql/Ddl/Column/Varchar.php index 8c1c23f7..62681e7a 100644 --- a/src/Sql/Ddl/Column/Varchar.php +++ b/src/Sql/Ddl/Column/Varchar.php @@ -6,5 +6,7 @@ class Varchar extends AbstractLengthColumn { + protected bool $lengthRequired = true; + protected string $type = 'VARCHAR'; } diff --git a/src/TableGateway/AbstractTableGateway.php b/src/TableGateway/AbstractTableGateway.php index 6c0b7da7..0d0c2f51 100644 --- a/src/TableGateway/AbstractTableGateway.php +++ b/src/TableGateway/AbstractTableGateway.php @@ -347,8 +347,8 @@ protected function executeSelect(Select $select): ResultSetInterface if ( isset($selectState['columns']) - && [Select::SQL_STAR] === $selectState['columns'] - && [] !== $this->columns + && [Select::SQL_STAR] === $selectState['columns'] + && [] !== $this->columns ) { $select->columns($this->columns); } @@ -430,8 +430,8 @@ public function __clone(): void $this->table = clone $this->table; } elseif ( is_array($this->table) - && count($this->table) === 1 - && is_object(reset($this->table)) + && count($this->table) === 1 + && is_object(reset($this->table)) ) { foreach ($this->table as &$tableObject) { $tableObject = clone $tableObject; diff --git a/test/unit/Sql/Ddl/Column/AbstractLengthColumnTest.php b/test/unit/Sql/Ddl/Column/AbstractLengthColumnTest.php index 5fe37436..373c48a4 100644 --- a/test/unit/Sql/Ddl/Column/AbstractLengthColumnTest.php +++ b/test/unit/Sql/Ddl/Column/AbstractLengthColumnTest.php @@ -8,14 +8,71 @@ use PhpDb\Sql\Argument\Literal; use PhpDb\Sql\Ddl\Column\AbstractLengthColumn; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; #[CoversMethod(AbstractLengthColumn::class, 'setLength')] #[CoversMethod(AbstractLengthColumn::class, 'getLength')] +#[CoversMethod(AbstractLengthColumn::class, 'getLengthExpression')] #[CoversMethod(AbstractLengthColumn::class, 'getExpressionData')] +#[Group('unit')] final class AbstractLengthColumnTest extends TestCase { + use ColumnAssertionsTrait; + + /** + * @throws Exception + */ + #[Test] + public function getExpressionDataOmitsLengthPlaceholderWhenLengthIsNotSet(): void + { + $column = $this->getMockBuilder(AbstractLengthColumn::class) + ->setConstructorArgs(['foo']) + ->onlyMethods([]) + ->getMock(); + + $expressionData = $column->getExpressionData(); + + static::assertSame('%s %s NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + new Identifier('foo'), + new Literal('INTEGER'), + ], + $expressionData['values'], + ); + } + + /** + * @throws Exception + */ + #[Test] + public function rendersLengthDirectlyAfterType(): void + { + $column = $this->getMockBuilder(AbstractLengthColumn::class) + ->setConstructorArgs(['foo', 4, true]) + ->onlyMethods([]) + ->getMock(); + + static::assertColumnRenders('"foo" INTEGER(4) NULL DEFAULT NULL', $column); + } + + /** + * @throws Exception + */ + #[Test] + public function rendersWithoutParenthesesWhenLengthIsNotSet(): void + { + $column = $this->getMockBuilder(AbstractLengthColumn::class) + ->setConstructorArgs(['foo']) + ->onlyMethods([]) + ->getMock(); + + static::assertColumnRenders('"foo" INTEGER NOT NULL', $column); + } + /** * @throws Exception */ diff --git a/test/unit/Sql/Ddl/Column/AbstractPrecisionColumnTest.php b/test/unit/Sql/Ddl/Column/AbstractPrecisionColumnTest.php index 459d7748..743fc301 100644 --- a/test/unit/Sql/Ddl/Column/AbstractPrecisionColumnTest.php +++ b/test/unit/Sql/Ddl/Column/AbstractPrecisionColumnTest.php @@ -6,7 +6,10 @@ use PhpDb\Sql\Argument; use PhpDb\Sql\Ddl\Column\AbstractPrecisionColumn; +use PhpDb\Sql\Exception\InvalidArgumentException; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; @@ -14,9 +17,58 @@ #[CoversMethod(AbstractPrecisionColumn::class, 'getDigits')] #[CoversMethod(AbstractPrecisionColumn::class, 'setDecimal')] #[CoversMethod(AbstractPrecisionColumn::class, 'getDecimal')] +#[CoversMethod(AbstractPrecisionColumn::class, 'getLengthExpression')] #[CoversMethod(AbstractPrecisionColumn::class, 'getExpressionData')] +#[Group('unit')] final class AbstractPrecisionColumnTest extends TestCase { + /** + * @throws Exception + */ + #[Test] + public function getExpressionDataOmitsLengthPlaceholderWhenPrecisionIsNotSet(): void + { + $column = $this->getMockBuilder(AbstractPrecisionColumn::class) + ->setConstructorArgs(['foo']) + ->onlyMethods([]) + ->getMock(); + + $expressionData = $column->getExpressionData(); + + static::assertSame('%s %s NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + Argument::identifier('foo'), + Argument::literal('INTEGER'), + ], + $expressionData['values'], + ); + } + + /** + * @throws Exception + */ + #[Test] + public function getExpressionDataUsesDigitsOnlyWhenDecimalIsNotSet(): void + { + $column = $this->getMockBuilder(AbstractPrecisionColumn::class) + ->setConstructorArgs(['foo', 10]) + ->onlyMethods([]) + ->getMock(); + + $expressionData = $column->getExpressionData(); + + static::assertSame('%s %s(%s) NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + Argument::identifier('foo'), + Argument::literal('INTEGER'), + Argument::literal('10'), + ], + $expressionData['values'], + ); + } + /** * @throws Exception */ @@ -91,4 +143,21 @@ public function testSetDigits(): void self::assertSame($column, $column->setDigits(12)); self::assertEquals(12, $column->getDigits()); } + + /** + * @throws Exception + */ + #[Test] + public function throwsWhenDecimalIsSetWithoutDigits(): void + { + $column = $this->getMockBuilder(AbstractPrecisionColumn::class) + ->setConstructorArgs(['foo', null, 2]) + ->onlyMethods([]) + ->getMock(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Column "foo" of type INTEGER has a decimal scale but no digits'); + + $column->getExpressionData(); + } } diff --git a/test/unit/Sql/Ddl/Column/BinaryTest.php b/test/unit/Sql/Ddl/Column/BinaryTest.php index 26e29fa5..6677118d 100644 --- a/test/unit/Sql/Ddl/Column/BinaryTest.php +++ b/test/unit/Sql/Ddl/Column/BinaryTest.php @@ -5,13 +5,49 @@ namespace PhpDbTest\Sql\Ddl\Column; use PhpDb\Sql\Argument; +use PhpDb\Sql\Ddl\Column\AbstractLengthColumn; use PhpDb\Sql\Ddl\Column\Binary; 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(Binary::class, 'getExpressionData')] +#[CoversMethod(AbstractLengthColumn::class, 'getExpressionData')] +#[Group('unit')] final class BinaryTest extends TestCase { + use ColumnAssertionsTrait; + + #[Test] + public function getExpressionDataOmitsLengthPlaceholderWhenLengthIsNotSet(): void + { + $expressionData = (new Binary('hash'))->getExpressionData(); + + static::assertSame('%s %s NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + Argument::identifier('hash'), + Argument::literal('BINARY'), + ], + $expressionData['values'], + ); + } + + #[Test] + public function rendersLengthDirectlyAfterType(): void + { + static::assertColumnRenders('"hash" BINARY(32) NOT NULL', new Binary('hash', 32)); + } + + #[Test] + #[DataProvider('missingLengthProvider')] + public function rendersWithoutParenthesesWhenLengthIsNotSet(?int $length): void + { + static::assertColumnRenders('"hash" BINARY NOT NULL', new Binary('hash', $length)); + } + public function testGetExpressionData(): void { $column = new Binary('foo', 10_000_000); diff --git a/test/unit/Sql/Ddl/Column/BlobTest.php b/test/unit/Sql/Ddl/Column/BlobTest.php index 0cf51bc2..5e6dfee3 100644 --- a/test/unit/Sql/Ddl/Column/BlobTest.php +++ b/test/unit/Sql/Ddl/Column/BlobTest.php @@ -8,11 +8,38 @@ use PhpDb\Sql\Argument\Literal; use PhpDb\Sql\Ddl\Column\Blob; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(Blob::class, 'getExpressionData')] +#[CoversMethod(Blob::class, 'getLengthExpression')] +#[Group('unit')] final class BlobTest extends TestCase { + use ColumnAssertionsTrait; + + #[Test] + public function getExpressionDataOmitsConfiguredLength(): void + { + $expressionData = (new Blob('foo', 16_777_215))->getExpressionData(); + + static::assertSame('%s %s NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + new Identifier('foo'), + new Literal('BLOB'), + ], + $expressionData['values'], + ); + } + + #[Test] + public function rendersWithoutConfiguredLength(): void + { + static::assertColumnRenders('"foo" BLOB NOT NULL', new Blob('foo', 16_777_215)); + } + public function testGetExpressionData(): void { $column = new Blob('foo'); diff --git a/test/unit/Sql/Ddl/Column/CharTest.php b/test/unit/Sql/Ddl/Column/CharTest.php index cfcc1790..d6ff2f43 100644 --- a/test/unit/Sql/Ddl/Column/CharTest.php +++ b/test/unit/Sql/Ddl/Column/CharTest.php @@ -6,13 +6,49 @@ use PhpDb\Sql\Argument\Identifier; use PhpDb\Sql\Argument\Literal; +use PhpDb\Sql\Ddl\Column\AbstractLengthColumn; use PhpDb\Sql\Ddl\Column\Char; 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(Char::class, 'getExpressionData')] +#[CoversMethod(AbstractLengthColumn::class, 'getExpressionData')] +#[Group('unit')] final class CharTest extends TestCase { + use ColumnAssertionsTrait; + + #[Test] + public function getExpressionDataOmitsLengthPlaceholderWhenLengthIsNotSet(): void + { + $expressionData = (new Char('code'))->getExpressionData(); + + static::assertSame('%s %s NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + new Identifier('code'), + new Literal('CHAR'), + ], + $expressionData['values'], + ); + } + + #[Test] + public function rendersLengthDirectlyAfterType(): void + { + static::assertColumnRenders('"code" CHAR(2) NOT NULL', new Char('code', 2)); + } + + #[Test] + #[DataProvider('missingLengthProvider')] + public function rendersWithoutParenthesesWhenLengthIsNotSet(?int $length): void + { + static::assertColumnRenders('"code" CHAR NOT NULL', new Char('code', $length)); + } + public function testGetExpressionData(): void { $column = new Char('foo', 20); diff --git a/test/unit/Sql/Ddl/Column/ColumnAssertionsTrait.php b/test/unit/Sql/Ddl/Column/ColumnAssertionsTrait.php new file mode 100644 index 00000000..57de9e25 --- /dev/null +++ b/test/unit/Sql/Ddl/Column/ColumnAssertionsTrait.php @@ -0,0 +1,33 @@ + + */ + public static function missingLengthProvider(): array + { + return [ + 'null length' => [null], + 'zero length' => [0], + ]; + } + + /** + * Asserts what a column renders to inside CREATE TABLE on the default SQL-92 platform. + */ + private static function assertColumnRenders(string $expected, ColumnInterface $column): void + { + $createTable = new CreateTable('t'); + $createTable->addColumn($column); + + self::assertSame("CREATE TABLE \"t\" ( \n {$expected} \n)", $createTable->getSqlString()); + } +} diff --git a/test/unit/Sql/Ddl/Column/DecimalTest.php b/test/unit/Sql/Ddl/Column/DecimalTest.php index bf89a47b..5f9aaba8 100644 --- a/test/unit/Sql/Ddl/Column/DecimalTest.php +++ b/test/unit/Sql/Ddl/Column/DecimalTest.php @@ -5,9 +5,13 @@ namespace PhpDbTest\Sql\Ddl\Column; use PhpDb\Sql\Argument; +use PhpDb\Sql\Ddl\Column\AbstractLengthColumn; use PhpDb\Sql\Ddl\Column\AbstractPrecisionColumn; use PhpDb\Sql\Ddl\Column\Decimal; +use PhpDb\Sql\Exception\InvalidArgumentException; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(Decimal::class, 'getExpressionData')] @@ -17,8 +21,45 @@ #[CoversMethod(AbstractPrecisionColumn::class, 'setDecimal')] #[CoversMethod(AbstractPrecisionColumn::class, 'getDecimal')] #[CoversMethod(AbstractPrecisionColumn::class, 'getLengthExpression')] +#[CoversMethod(AbstractLengthColumn::class, 'getExpressionData')] +#[Group('unit')] final class DecimalTest extends TestCase { + use ColumnAssertionsTrait; + + #[Test] + public function getExpressionDataOmitsLengthPlaceholderWhenPrecisionIsNotSet(): void + { + $expressionData = (new Decimal('amount'))->getExpressionData(); + + static::assertSame('%s %s NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + Argument::identifier('amount'), + Argument::literal('DECIMAL'), + ], + $expressionData['values'], + ); + } + + #[Test] + public function rendersDigitsAndDecimalDirectlyAfterType(): void + { + static::assertColumnRenders('"price" DECIMAL(10,2) NOT NULL', new Decimal('price', 10, 2)); + } + + #[Test] + public function rendersDigitsOnlyWhenDecimalIsNotSet(): void + { + static::assertColumnRenders('"price" DECIMAL(10) NOT NULL', new Decimal('price', 10)); + } + + #[Test] + public function rendersWithoutParenthesesWhenPrecisionIsNotSet(): void + { + static::assertColumnRenders('"price" DECIMAL NOT NULL', new Decimal('price')); + } + public function testConstructorSetsDigitsAndDecimal(): void { $column = new Decimal('price', 10, 2); @@ -83,4 +124,15 @@ public function testSetDigitsAndGetDigits(): void self::assertSame($column, $result); // Fluent interface self::assertEquals(15, $column->getDigits()); } + + #[Test] + public function throwsWhenDecimalIsSetWithoutDigits(): void + { + $column = new Decimal('price', null, 2); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Column "price" of type DECIMAL has a decimal scale but no digits'); + + $column->getExpressionData(); + } } diff --git a/test/unit/Sql/Ddl/Column/DoubleTest.php b/test/unit/Sql/Ddl/Column/DoubleTest.php index 7901a81a..548b1b2c 100644 --- a/test/unit/Sql/Ddl/Column/DoubleTest.php +++ b/test/unit/Sql/Ddl/Column/DoubleTest.php @@ -5,13 +5,41 @@ namespace PhpDbTest\Sql\Ddl\Column; use PhpDb\Sql\Argument; +use PhpDb\Sql\Ddl\Column\AbstractLengthColumn; +use PhpDb\Sql\Ddl\Column\AbstractPrecisionColumn; use PhpDb\Sql\Ddl\Column\Double; +use PhpDb\Sql\Exception\InvalidArgumentException; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(Double::class, 'getExpressionData')] +#[CoversMethod(AbstractPrecisionColumn::class, 'getLengthExpression')] +#[CoversMethod(AbstractLengthColumn::class, 'getExpressionData')] +#[Group('unit')] final class DoubleTest extends TestCase { + use ColumnAssertionsTrait; + + #[Test] + public function rendersDigitsAndDecimalDirectlyAfterType(): void + { + static::assertColumnRenders('"f" DOUBLE(10,5) NOT NULL', new Double('f', 10, 5)); + } + + #[Test] + public function rendersDigitsOnlyWhenDecimalIsNotSet(): void + { + static::assertColumnRenders('"f" DOUBLE(10) NOT NULL', new Double('f', 10)); + } + + #[Test] + public function rendersWithoutParenthesesWhenPrecisionIsNotSet(): void + { + static::assertColumnRenders('"f" DOUBLE NOT NULL', new Double('f')); + } + public function testGetExpressionData(): void { $column = new Double('foo', 10, 5); @@ -28,4 +56,15 @@ public function testGetExpressionData(): void $expressionData['values'], ); } + + #[Test] + public function throwsWhenDecimalIsSetWithoutDigits(): void + { + $column = new Double('f', null, 2); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Column "f" of type DOUBLE has a decimal scale but no digits'); + + $column->getExpressionData(); + } } diff --git a/test/unit/Sql/Ddl/Column/FloatingTest.php b/test/unit/Sql/Ddl/Column/FloatingTest.php index 14f439d2..cfafef7b 100644 --- a/test/unit/Sql/Ddl/Column/FloatingTest.php +++ b/test/unit/Sql/Ddl/Column/FloatingTest.php @@ -5,13 +5,41 @@ namespace PhpDbTest\Sql\Ddl\Column; use PhpDb\Sql\Argument; +use PhpDb\Sql\Ddl\Column\AbstractLengthColumn; +use PhpDb\Sql\Ddl\Column\AbstractPrecisionColumn; use PhpDb\Sql\Ddl\Column\Floating; +use PhpDb\Sql\Exception\InvalidArgumentException; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(Floating::class, 'getExpressionData')] +#[CoversMethod(AbstractPrecisionColumn::class, 'getLengthExpression')] +#[CoversMethod(AbstractLengthColumn::class, 'getExpressionData')] +#[Group('unit')] final class FloatingTest extends TestCase { + use ColumnAssertionsTrait; + + #[Test] + public function rendersDigitsAndDecimalDirectlyAfterType(): void + { + static::assertColumnRenders('"f" FLOAT(10,5) NOT NULL', new Floating('f', 10, 5)); + } + + #[Test] + public function rendersDigitsOnlyWhenDecimalIsNotSet(): void + { + static::assertColumnRenders('"f" FLOAT(10) NOT NULL', new Floating('f', 10)); + } + + #[Test] + public function rendersWithoutParenthesesWhenPrecisionIsNotSet(): void + { + static::assertColumnRenders('"f" FLOAT NOT NULL', new Floating('f')); + } + public function testGetExpressionData(): void { $column = new Floating('foo', 10, 5); @@ -28,4 +56,15 @@ public function testGetExpressionData(): void $expressionData['values'], ); } + + #[Test] + public function throwsWhenDecimalIsSetWithoutDigits(): void + { + $column = new Floating('f', null, 2); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Column "f" of type FLOAT has a decimal scale but no digits'); + + $column->getExpressionData(); + } } diff --git a/test/unit/Sql/Ddl/Column/TextTest.php b/test/unit/Sql/Ddl/Column/TextTest.php index 548a7100..f718c3b9 100644 --- a/test/unit/Sql/Ddl/Column/TextTest.php +++ b/test/unit/Sql/Ddl/Column/TextTest.php @@ -7,11 +7,38 @@ use PhpDb\Sql\Argument; use PhpDb\Sql\Ddl\Column\Text; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(Text::class, 'getExpressionData')] +#[CoversMethod(Text::class, 'getLengthExpression')] +#[Group('unit')] final class TextTest extends TestCase { + use ColumnAssertionsTrait; + + #[Test] + public function getExpressionDataOmitsConfiguredLength(): void + { + $expressionData = (new Text('foo', 65_535))->getExpressionData(); + + static::assertSame('%s %s NOT NULL', $expressionData['spec']); + static::assertEquals( + [ + Argument::identifier('foo'), + Argument::literal('TEXT'), + ], + $expressionData['values'], + ); + } + + #[Test] + public function rendersWithoutConfiguredLength(): void + { + static::assertColumnRenders('"foo" TEXT NOT NULL', new Text('foo', 65_535)); + } + public function testGetExpressionData(): void { $column = new Text('foo'); diff --git a/test/unit/Sql/Ddl/Column/VarbinaryTest.php b/test/unit/Sql/Ddl/Column/VarbinaryTest.php index 33a28336..bd660b58 100644 --- a/test/unit/Sql/Ddl/Column/VarbinaryTest.php +++ b/test/unit/Sql/Ddl/Column/VarbinaryTest.php @@ -5,13 +5,28 @@ namespace PhpDbTest\Sql\Ddl\Column; use PhpDb\Sql\Argument; +use PhpDb\Sql\Ddl\Column\AbstractLengthColumn; use PhpDb\Sql\Ddl\Column\Varbinary; +use PhpDb\Sql\Exception\InvalidArgumentException; 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(Varbinary::class, 'getExpressionData')] +#[CoversMethod(AbstractLengthColumn::class, 'getExpressionData')] +#[Group('unit')] final class VarbinaryTest extends TestCase { + use ColumnAssertionsTrait; + + #[Test] + public function rendersLengthDirectlyAfterType(): void + { + static::assertColumnRenders('"data" VARBINARY(20) NOT NULL', new Varbinary('data', 20)); + } + public function testGetExpressionData(): void { $column = new Varbinary('foo', 20); @@ -28,4 +43,16 @@ public function testGetExpressionData(): void $expressionData['values'], ); } + + #[Test] + #[DataProvider('missingLengthProvider')] + public function throwsWhenRenderedWithoutLength(?int $length): void + { + $column = new Varbinary('data', $length); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Column "data" of type VARBINARY requires a length'); + + $column->getExpressionData(); + } } diff --git a/test/unit/Sql/Ddl/Column/VarcharTest.php b/test/unit/Sql/Ddl/Column/VarcharTest.php index 869a0d3a..5f9ffcc9 100644 --- a/test/unit/Sql/Ddl/Column/VarcharTest.php +++ b/test/unit/Sql/Ddl/Column/VarcharTest.php @@ -7,7 +7,11 @@ use PhpDb\Sql\Argument; use PhpDb\Sql\Ddl\Column\AbstractLengthColumn; use PhpDb\Sql\Ddl\Column\Varchar; +use PhpDb\Sql\Exception\InvalidArgumentException; 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(Varchar::class, 'getExpressionData')] @@ -16,8 +20,23 @@ #[CoversMethod(AbstractLengthColumn::class, 'getLength')] #[CoversMethod(AbstractLengthColumn::class, 'getLengthExpression')] #[CoversMethod(AbstractLengthColumn::class, 'getExpressionData')] +#[Group('unit')] final class VarcharTest extends TestCase { + use ColumnAssertionsTrait; + + #[Test] + public function rendersLengthBeforeNullabilityAndDefault(): void + { + static::assertColumnRenders('"name" VARCHAR(20) NULL DEFAULT NULL', new Varchar('name', 20, true)); + } + + #[Test] + public function rendersLengthDirectlyAfterType(): void + { + static::assertColumnRenders('"name" VARCHAR(20) NOT NULL', new Varchar('name', 20)); + } + public function testGetExpressionData(): void { $column = new Varchar('foo', 20); @@ -50,31 +69,6 @@ public function testGetExpressionData(): void ); } - public function testGetExpressionDataWithNullLength(): void - { - $column = new Varchar('name'); - - $expressionData = $column->getExpressionData(); - - // When length is null, getLengthExpression() returns empty string - // The condition in getExpressionData checks: getLengthExpression() !== '' && !== '0' - // Empty string fails the first check, so length value is NOT added - // But specification still has (%s) placeholder - need to verify actual behavior - $spec = $expressionData['spec']; - $values = $expressionData['values']; - - // The specification format is defined in AbstractLengthColumn as '%s %s(%s)' - // But when length value is not added, we need to check if placeholder remains - self::assertEquals('%s %s(%s) NOT NULL', $spec); - self::assertEquals( - [ - Argument::identifier('name'), - Argument::literal('VARCHAR'), - ], - $values, - ); - } - public function testInheritanceFromAbstractLengthColumn(): void { $column = new Varchar('test'); @@ -89,4 +83,16 @@ public function testSetLengthAndGetLength(): void self::assertSame($column, $result); // Fluent interface self::assertEquals(100, $column->getLength()); } + + #[Test] + #[DataProvider('missingLengthProvider')] + public function throwsWhenRenderedWithoutLength(?int $length): void + { + $column = new Varchar('name', $length); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Column "name" of type VARCHAR requires a length'); + + $column->getExpressionData(); + } }