diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml_ similarity index 100% rename from .github/workflows/rector.yml rename to .github/workflows/rector.yml_ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c698b70..a41abec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - Enh #353: Update `bit` type according to main PR yiisoft/db#860 (@Tigrov) - Enh #354: Refactor PHP type of `ColumnSchemaInterface` instances (@Tigrov) - Enh #356: Raise minimum PHP version to `^8.1` with minor refactoring (@Tigrov) -- New #355, #368: Implement `ColumnFactory` class (@Tigrov) +- New #355, #368, #370: Implement `ColumnFactory` class (@Tigrov) - Enh #359: Separate column type constants (@Tigrov) - Enh #359: Remove `Schema::TYPE_ARRAY` and `Schema::TYPE_STRUCTURED` constants (@Tigrov) - New #360: Realize `ColumnBuilder` class (@Tigrov) @@ -19,6 +19,7 @@ - New #364: Add `ColumnDefinitionBuilder` class (@Tigrov) - Enh #365: Refactor `Dsn` class (@Tigrov) - Enh #366: Use constructor to create columns and initialize properties (@Tigrov) +- Enh #370: Refactor `Schema::normalizeDefaultValue()` method and move it to `ColumnFactory` class (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index c73d4370..f03b4f62 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -6,9 +6,14 @@ use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Constraint\ForeignKeyConstraint; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Schema\Column\AbstractColumnFactory; use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; +use function preg_replace; +use function str_starts_with; +use function substr; + use const PHP_INT_SIZE; /** @@ -114,6 +119,26 @@ final class ColumnFactory extends AbstractColumnFactory 'jsonb' => ColumnType::JSON, ]; + public function fromType(string $type, array $info = []): ColumnSchemaInterface + { + $column = parent::fromType($type, $info); + + if ($column instanceof StructuredColumnSchema) { + /** @psalm-var array|null $defaultValue */ + $defaultValue = $column->getDefaultValue(); + + if (is_array($defaultValue)) { + foreach ($column->getColumns() as $structuredColumnName => $structuredColumn) { + if (isset($defaultValue[$structuredColumnName])) { + $structuredColumn->defaultValue($defaultValue[$structuredColumnName]); + } + } + } + } + + return $column; + } + protected function getColumnClass(string $type, array $info = []): string { return match ($type) { @@ -131,4 +156,21 @@ protected function getColumnClass(string $type, array $info = []): string default => parent::getColumnClass($type, $info), }; } + + protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnSchemaInterface $column): mixed + { + $value = preg_replace("/::[^:']+$/", '$1', $defaultValue); + + if (str_starts_with($value, "B'") && $value[-1] === "'") { + return $column->phpTypecast(substr($value, 2, -1)); + } + + $value = parent::normalizeNotNullDefaultValue($value, $column); + + if ($value instanceof Expression) { + return new Expression($defaultValue); + } + + return $value; + } } diff --git a/src/Column/SequenceColumnSchemaTrait.php b/src/Column/SequenceColumnSchemaTrait.php index 3beab37b..1a9795fd 100644 --- a/src/Column/SequenceColumnSchemaTrait.php +++ b/src/Column/SequenceColumnSchemaTrait.php @@ -9,7 +9,7 @@ trait SequenceColumnSchemaTrait /** * @var string|null Name of an associated sequence if column is auto incremental. */ - private string|null $sequenceName = null; + protected string|null $sequenceName = null; public function getSequenceName(): string|null { diff --git a/src/Schema.php b/src/Schema.php index dd31aee1..ad7a269e 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -16,14 +16,12 @@ use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; -use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Helper\DbArrayHelper; use Yiisoft\Db\Pgsql\Column\ColumnFactory; use Yiisoft\Db\Pgsql\Column\SequenceColumnSchemaInterface; use Yiisoft\Db\Schema\Builder\ColumnInterface; use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; -use Yiisoft\Db\Schema\Column\StructuredColumnSchema; use Yiisoft\Db\Schema\TableSchemaInterface; use function array_change_key_case; @@ -34,7 +32,6 @@ use function in_array; use function is_string; use function preg_match; -use function preg_replace; use function str_replace; use function str_starts_with; use function substr; @@ -760,92 +757,36 @@ private function loadColumnSchema(array $info): ColumnSchemaInterface $columnInfo['columns'] = $structured->getColumns(); } - $column = $columnFactory->fromType(ColumnType::STRUCTURED, $columnInfo); - } else { - $column = $columnFactory->fromDbType($dbType, $columnInfo); + $columnInfo['type'] = ColumnType::STRUCTURED; } $dimension = (int) $info['dimension']; if ($dimension > 0) { - $columnInfo['column'] = $column; + $columnInfo['column'] = $columnFactory->fromDbType($dbType, $columnInfo); $columnInfo['dimension'] = $dimension; + $columnInfo['defaultValueRaw'] = $info['column_default']; - $column = $columnFactory->fromType(ColumnType::ARRAY, $columnInfo); + return $columnFactory->fromType(ColumnType::ARRAY, $columnInfo); } + $defaultValue = $info['column_default']; + /** * pg_get_serial_sequence() doesn't track DEFAULT value change. * GENERATED BY IDENTITY columns always have a null default value. */ - $defaultValue = $info['column_default']; - - if ($column instanceof SequenceColumnSchemaInterface) { - if ( - $defaultValue !== null - && preg_match("/^nextval\('([^']+)/", $defaultValue, $matches) === 1 - ) { - $column->sequenceName($matches[1]); - } elseif ($info['sequence_name'] !== null) { - $column->sequenceName($this->resolveTableName($info['sequence_name'])->getFullName()); - } - } - - $column->defaultValue($this->normalizeDefaultValue($defaultValue, $column)); - - if ($column instanceof StructuredColumnSchema) { - /** @psalm-var array|null $defaultValue */ - $defaultValue = $column->getDefaultValue(); - - if (is_array($defaultValue)) { - foreach ($column->getColumns() as $structuredColumnName => $structuredColumn) { - if (isset($defaultValue[$structuredColumnName])) { - $structuredColumn->defaultValue($defaultValue[$structuredColumnName]); - } - } - } - } - - return $column; - } - - /** - * Converts column's default value according to {@see ColumnSchema::phpType} 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. - */ - private function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaInterface $column): mixed - { - if ( - $defaultValue === null - || $column->isPrimaryKey() - || str_starts_with($defaultValue, 'NULL::') - ) { - return null; + if ($defaultValue !== null && preg_match("/^nextval\('([^']+)/", $defaultValue, $matches) === 1) { + $defaultValue = null; + $columnInfo['sequenceName'] = $matches[1]; + } elseif ($info['sequence_name'] !== null) { + $columnInfo['sequenceName'] = $this->resolveTableName($info['sequence_name'])->getFullName(); } - if ($column->getType() === ColumnType::BOOLEAN && in_array($defaultValue, ['true', 'false'], true)) { - return $defaultValue === 'true'; - } - - if ( - in_array($column->getType(), [ColumnType::TIMESTAMP, ColumnType::DATE, ColumnType::TIME], true) - && in_array(strtoupper($defaultValue), ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'], true) - ) { - return new Expression($defaultValue); - } - - $value = preg_replace("/^B?['(](.*?)[)'](?:::[^:]+)?$/s", '$1', $defaultValue); - $value = str_replace("''", "'", $value); - - if ($column->getType() === ColumnType::BINARY && str_starts_with($value, '\\x')) { - return hex2bin(substr($value, 2)); - } + $columnInfo['defaultValueRaw'] = $defaultValue; - return $column->phpTypecast($value); + /** @psalm-suppress InvalidArgument */ + return $columnFactory->fromDbType($dbType, $columnInfo); } /** diff --git a/tests/ColumnFactoryTest.php b/tests/ColumnFactoryTest.php index 10d0ee82..ec52392a 100644 --- a/tests/ColumnFactoryTest.php +++ b/tests/ColumnFactoryTest.php @@ -4,8 +4,10 @@ namespace Yiisoft\Db\Pgsql\Tests; +use PHPUnit\Framework\Attributes\DataProviderExternal; use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Pgsql\Column\ArrayColumnSchema; +use Yiisoft\Db\Pgsql\Tests\Provider\ColumnFactoryProvider; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Tests\AbstractColumnFactoryTest; @@ -16,7 +18,7 @@ final class ColumnFactoryTest extends AbstractColumnFactoryTest { use TestTrait; - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnFactoryProvider::dbTypes */ + #[DataProviderExternal(ColumnFactoryProvider::class, 'dbTypes')] public function testFromDbType(string $dbType, string $expectedType, string $expectedInstanceOf): void { parent::testFromDbType($dbType, $expectedType, $expectedInstanceOf); @@ -35,7 +37,7 @@ public function testFromDbType(string $dbType, string $expectedType, string $exp $db->close(); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnFactoryProvider::definitions */ + #[DataProviderExternal(ColumnFactoryProvider::class, 'definitions')] public function testFromDefinition( string $definition, string $expectedType, @@ -45,7 +47,7 @@ public function testFromDefinition( parent::testFromDefinition($definition, $expectedType, $expectedInstanceOf, $expectedMethodResults); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnFactoryProvider::pseudoTypes */ + #[DataProviderExternal(ColumnFactoryProvider::class, 'pseudoTypes')] public function testFromPseudoType( string $pseudoType, string $expectedType, @@ -55,7 +57,7 @@ public function testFromPseudoType( parent::testFromPseudoType($pseudoType, $expectedType, $expectedInstanceOf, $expectedMethodResults); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnFactoryProvider::types */ + #[DataProviderExternal(ColumnFactoryProvider::class, 'types')] public function testFromType(string $type, string $expectedType, string $expectedInstanceOf): void { parent::testFromType($type, $expectedType, $expectedInstanceOf); @@ -72,4 +74,10 @@ public function testFromType(string $type, string $expectedType, string $expecte $db->close(); } + + #[DataProviderExternal(ColumnFactoryProvider::class, 'defaultValueRaw')] + public function testFromTypeDefaultValueRaw(string $type, string|null $defaultValueRaw, mixed $expected): void + { + parent::testFromTypeDefaultValueRaw($type, $defaultValueRaw, $expected); + } } diff --git a/tests/Provider/ColumnFactoryProvider.php b/tests/Provider/ColumnFactoryProvider.php index 0e7dfb77..d1c96a50 100644 --- a/tests/Provider/ColumnFactoryProvider.php +++ b/tests/Provider/ColumnFactoryProvider.php @@ -5,6 +5,7 @@ namespace Yiisoft\Db\Pgsql\Tests\Provider; use Yiisoft\Db\Constant\ColumnType; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Pgsql\Column\BinaryColumnSchema; use Yiisoft\Db\Pgsql\Column\BitColumnSchema; use Yiisoft\Db\Pgsql\Column\BooleanColumnSchema; @@ -100,4 +101,21 @@ public static function pseudoTypes(): array return $result; } + + public static function defaultValueRaw(): array + { + $defaultValueRaw = parent::defaultValueRaw(); + + $defaultValueRaw[] = [ColumnType::TEXT, 'NULL::"text"', null]; + $defaultValueRaw[] = [ColumnType::TEXT, '(NULL)::"text"', null]; + $defaultValueRaw[] = [ColumnType::TEXT, "'str''ing'::\"text\"", "str'ing"]; + $defaultValueRaw[] = [ColumnType::TEXT, "'str::ing'::\"text\"", 'str::ing']; + $defaultValueRaw[] = [ColumnType::INTEGER, '(-1)::"int"', -1]; + $defaultValueRaw[] = [ColumnType::BIT, "B'1011'::\"bit\"", 0b1011]; + $defaultValueRaw[] = [ColumnType::STRING, "'\\x737472696e67'", '\\x737472696e67']; + $defaultValueRaw[] = [ColumnType::BINARY, "'\\x737472696e67'::bytea", 'string']; + $defaultValueRaw[] = [ColumnType::BINARY, '(1 + 2)::int', new Expression('(1 + 2)::int')]; + + return $defaultValueRaw; + } }