From 86fc92e267e477df49c15ee54da5486cdcf212c6 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Sat, 16 Nov 2024 09:27:53 +0700 Subject: [PATCH] Refactor `normalizeDefaultValue()` (#898) --- CHANGELOG.md | 2 +- src/Schema/Column/AbstractColumnFactory.php | 74 +++++++++++++++++++- src/Schema/Column/ColumnFactoryInterface.php | 1 + tests/AbstractColumnFactoryTest.php | 46 ++++++++++-- tests/Provider/ColumnFactoryProvider.php | 30 ++++++++ 5 files changed, 144 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 155d1ce9b..113b61433 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ - Enh #865: Raise minimum PHP version to `^8.1` with minor refactoring (@Tigrov, @vjik) - Enh #798: Allow `QueryInterface::one()` and `QueryInterface::all()` to return objects (@darkdef, @Tigrov) - Enh #872: Use `#[\SensitiveParameter]` attribute to mark sensitive parameters (@heap-s) -- New #864, #897: Realize column factory (@Tigrov) +- New #864, #897, #898: Realize column factory (@Tigrov) - Enh #875: Ignore "Packets out of order..." warnings in `AbstractPdoCommand::internalExecute()` method (@Tigrov) - Enh #877: Separate column type constants (@Tigrov) - Enh #878: Realize `ColumnBuilder` class (@Tigrov) diff --git a/src/Schema/Column/AbstractColumnFactory.php b/src/Schema/Column/AbstractColumnFactory.php index 4fc4627bb..05fa0b537 100644 --- a/src/Schema/Column/AbstractColumnFactory.php +++ b/src/Schema/Column/AbstractColumnFactory.php @@ -6,8 +6,15 @@ use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Constant\PseudoType; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Syntax\ColumnDefinitionParser; +use function array_diff_key; +use function is_numeric; +use function preg_match; +use function str_replace; +use function substr; + use const PHP_INT_SIZE; /** @@ -89,12 +96,22 @@ public function fromType(string $type, array $info = []): ColumnSchemaInterface unset($info['type']); if ($type === ColumnType::ARRAY && empty($info['column']) && !empty($info['dbType'])) { - $info['column'] = $this->fromDbType($info['dbType'], $info); + /** @psalm-suppress ArgumentTypeCoercion */ + $info['column'] = $this->fromDbType( + $info['dbType'], + array_diff_key($info, ['dimension' => 1, 'defaultValueRaw' => 1]) + ); } $columnClass = $this->getColumnClass($type, $info); - return new $columnClass($type, ...$info); + $column = new $columnClass($type, ...$info); + + if (isset($info['defaultValueRaw'])) { + $column->defaultValue($this->normalizeDefaultValue($info['defaultValueRaw'], $column)); + } + + return $column; } /** @@ -210,4 +227,57 @@ protected function isType(string $type): bool default => false, }; } + + /** + * Converts column's default value according to {@see ColumnSchemaInterface::getPhpType()} after retrieval from the + * database. + * + * @param string|null $defaultValue The default value retrieved from the database. + * @param ColumnSchemaInterface $column The column schema object. + * + * @return mixed The normalized default value. + */ + protected function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaInterface $column): mixed + { + if ( + $defaultValue === null + || $defaultValue === '' + || $column->isPrimaryKey() + || $column->isComputed() + || preg_match('/^\(?NULL\b/i', $defaultValue) === 1 + ) { + return null; + } + + return $this->normalizeNotNullDefaultValue($defaultValue, $column); + } + + /** + * Converts a not null default value according to {@see ColumnSchemaInterface::getPhpType()}. + */ + protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnSchemaInterface $column): mixed + { + $value = $defaultValue; + + if ($value[0] === '(' && $value[-1] === ')') { + $value = substr($value, 1, -1); + } + + if (is_numeric($value)) { + return $column->phpTypecast($value); + } + + if ($value[0] === "'" && $value[-1] === "'") { + $value = substr($value, 1, -1); + $value = str_replace("''", "'", $value); + + return $column->phpTypecast($value); + } + + return match ($value) { + 'true' => true, + 'false' => false, + default => new Expression($defaultValue), + }; + } } diff --git a/src/Schema/Column/ColumnFactoryInterface.php b/src/Schema/Column/ColumnFactoryInterface.php index 57d9251ec..4a30ee5a3 100644 --- a/src/Schema/Column/ColumnFactoryInterface.php +++ b/src/Schema/Column/ColumnFactoryInterface.php @@ -21,6 +21,7 @@ * computed?: bool, * dbType?: string|null, * defaultValue?: mixed, + * defaultValueRaw?: string|null, * dimension?: positive-int, * enumValues?: array|null, * extra?: string|null, diff --git a/tests/AbstractColumnFactoryTest.php b/tests/AbstractColumnFactoryTest.php index 908f4b88e..1393658b5 100644 --- a/tests/AbstractColumnFactoryTest.php +++ b/tests/AbstractColumnFactoryTest.php @@ -4,16 +4,19 @@ namespace Yiisoft\Db\Tests; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; +use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Schema\Column\StringColumnSchema; use Yiisoft\Db\Tests\Provider\ColumnBuilderProvider; +use Yiisoft\Db\Tests\Provider\ColumnFactoryProvider; use Yiisoft\Db\Tests\Support\TestTrait; abstract class AbstractColumnFactoryTest extends TestCase { use TestTrait; - /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::types */ + #[DataProviderExternal(ColumnFactoryProvider::class, 'types')] public function testFromDbType(string $dbType, string $expectedType, string $expectedInstanceOf): void { $db = $this->getConnection(); @@ -28,9 +31,7 @@ public function testFromDbType(string $dbType, string $expectedType, string $exp $db->close(); } - /** - * @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::definitions - */ + #[DataProviderExternal(ColumnFactoryProvider::class, 'definitions')] public function testFromDefinition( string $definition, string $expectedType, @@ -57,7 +58,7 @@ public function testFromDefinition( $db->close(); } - /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::pseudoTypes */ + #[DataProviderExternal(ColumnFactoryProvider::class, 'pseudoTypes')] public function testFromPseudoType( string $pseudoType, string $expectedType, @@ -84,7 +85,7 @@ public function testFromPseudoType( $db->close(); } - /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::types */ + #[DataProviderExternal(ColumnFactoryProvider::class, 'types')] public function testFromType(string $type, string $expectedType, string $expectedInstanceOf): void { $db = $this->getConnection(); @@ -112,4 +113,37 @@ public function testFromDefinitionWithExtra(): void $db->close(); } + + #[DataProviderExternal(ColumnFactoryProvider::class, 'defaultValueRaw')] + public function testFromTypeDefaultValueRaw(string $type, string|null $defaultValueRaw, mixed $expected): void + { + $db = $this->getConnection(); + $columnFactory = $db->getSchema()->getColumnFactory(); + + $column = $columnFactory->fromType($type, ['defaultValueRaw' => $defaultValueRaw]); + + if (is_scalar($expected)) { + $this->assertSame($expected, $column->getDefaultValue()); + } else { + $this->assertEquals($expected, $column->getDefaultValue()); + } + + $db->close(); + } + + public function testNullDefaultValueRaw(): void + { + $db = $this->getConnection(); + $columnFactory = $db->getSchema()->getColumnFactory(); + + $column = $columnFactory->fromType(ColumnType::INTEGER, ['defaultValueRaw' => '1', 'primaryKey' => true]); + + $this->assertNull($column->getDefaultValue()); + + $column = $columnFactory->fromType(ColumnType::INTEGER, ['defaultValueRaw' => '1', 'computed' => true]); + + $this->assertNull($column->getDefaultValue()); + + $db->close(); + } } diff --git a/tests/Provider/ColumnFactoryProvider.php b/tests/Provider/ColumnFactoryProvider.php index 77ff08a8a..1b05828b2 100644 --- a/tests/Provider/ColumnFactoryProvider.php +++ b/tests/Provider/ColumnFactoryProvider.php @@ -6,6 +6,7 @@ use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Constant\PseudoType; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Schema\Column\BigIntColumnSchema; use Yiisoft\Db\Schema\Column\BinaryColumnSchema; use Yiisoft\Db\Schema\Column\BooleanColumnSchema; @@ -69,4 +70,33 @@ public static function types(): array 'json' => [ColumnType::JSON, ColumnType::JSON, JsonColumnSchema::class], ]; } + + public static function defaultValueRaw(): array + { + return [ + // type, default value, expected value + 'null' => [ColumnType::STRING, null, null], + '(null)' => [ColumnType::STRING, '(null)', null], + 'NULL' => [ColumnType::STRING, 'NULL', null], + '(NULL)' => [ColumnType::STRING, '(NULL)', null], + '' => [ColumnType::STRING, '', null], + '(0)' => [ColumnType::INTEGER, '(0)', 0], + '-1' => [ColumnType::INTEGER, '-1', -1], + '(-1)' => [ColumnType::INTEGER, '(-1)', -1], + '0.0' => [ColumnType::DOUBLE, '0.0', 0.0], + '(0.0)' => [ColumnType::DOUBLE, '(0.0)', 0.0], + '-1.1' => [ColumnType::DOUBLE, '-1.1', -1.1], + '(-1.1)' => [ColumnType::DOUBLE, '(-1.1)', -1.1], + 'true' => [ColumnType::BOOLEAN, 'true', true], + 'false' => [ColumnType::BOOLEAN, 'false', false], + '1' => [ColumnType::BOOLEAN, '1', true], + '0' => [ColumnType::BOOLEAN, '0', false], + "''" => [ColumnType::STRING, "''", ''], + "('')" => [ColumnType::STRING, "('')", ''], + "'str''ing'" => [ColumnType::STRING, "'str''ing'", "str'ing"], + "('str''ing')" => [ColumnType::STRING, "('str''ing')", "str'ing"], + 'CURRENT_TIMESTAMP' => [ColumnType::TIMESTAMP, 'CURRENT_TIMESTAMP', new Expression('CURRENT_TIMESTAMP')], + '(now())' => [ColumnType::TIMESTAMP, '(now())', new Expression('(now())')], + ]; + } }