diff --git a/CHANGELOG.md b/CHANGELOG.md index 1730fb3b..27eb497a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Enh #370: Refactor `Schema::normalizeDefaultValue()` method and move it to `ColumnFactory` class (@Tigrov) - New #373: Override `QueryBuilder::prepareBinary()` method (@Tigrov) - Chg #375: Update `QueryBuilder` constructor (@Tigrov) +- Enh #374: Use `ColumnDefinitionBuilder` to generate table column SQL representation (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/Column.php b/src/Column.php index e59fc023..3ac53f99 100644 --- a/src/Column.php +++ b/src/Column.php @@ -21,6 +21,10 @@ * * Provides a fluent interface, which means that the methods can be chained together to create a column schema with * many properties in a single line of code. + * + * @psalm-suppress DeprecatedClass + * + * @deprecated Use {@see StringColumnSchema} or other column classes instead. Will be removed in 2.0.0. */ final class Column extends AbstractColumn { diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php index 8532fa65..64419845 100644 --- a/src/Column/ColumnDefinitionBuilder.php +++ b/src/Column/ColumnDefinitionBuilder.php @@ -8,10 +8,10 @@ use Yiisoft\Db\QueryBuilder\AbstractColumnDefinitionBuilder; use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; +use function version_compare; + final class ColumnDefinitionBuilder extends AbstractColumnDefinitionBuilder { - protected const GENERATE_UUID_EXPRESSION = 'gen_random_uuid()'; - protected const TYPES_WITH_SIZE = [ 'bit', 'bit varying', @@ -47,6 +47,12 @@ public function build(ColumnSchemaInterface $column): string . $this->buildExtra($column); } + public function buildAlter(ColumnSchemaInterface $column): string + { + return $this->buildType($column) + . $this->buildExtra($column); + } + protected function buildType(ColumnSchemaInterface $column): string { if ($column instanceof \Yiisoft\Db\Schema\Column\ArrayColumnSchema) { @@ -84,4 +90,16 @@ protected function getDbType(ColumnSchemaInterface $column): string default => 'varchar', }; } + + protected function getDefaultUuidExpression(): string + { + $serverVersion = $this->queryBuilder->getServerInfo()->getVersion(); + + if (version_compare($serverVersion, '13', '<')) { + return "uuid_in(overlay(overlay(md5(now()::text || random()::text) placing '4' from 13) placing" + . ' to_hex(floor(4 * random() + 8)::int)::text from 17)::cstring)'; + } + + return 'gen_random_uuid()'; + } } diff --git a/src/DDLQueryBuilder.php b/src/DDLQueryBuilder.php index de4442fc..090caa69 100644 --- a/src/DDLQueryBuilder.php +++ b/src/DDLQueryBuilder.php @@ -8,11 +8,12 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\QueryBuilder\AbstractDDLQueryBuilder; use Yiisoft\Db\Schema\Builder\ColumnInterface; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; use function array_diff; -use function array_unshift; use function explode; use function implode; +use function is_string; use function preg_match; use function preg_replace; use function str_contains; @@ -27,7 +28,7 @@ public function addDefaultValue(string $table, string $name, string $column, mix throw new NotSupportedException(__METHOD__ . ' is not supported by PostgreSQL.'); } - public function alterColumn(string $table, string $column, ColumnInterface|string $type): string + public function alterColumn(string $table, string $column, ColumnInterface|ColumnSchemaInterface|string $type): string { $columnName = $this->quoter->quoteColumnName($column); $tableName = $this->quoter->quoteTableName($table); @@ -40,47 +41,42 @@ public function alterColumn(string $table, string $column, ColumnInterface|strin * @link https://github.com/yiisoft/yii2/issues/4492 * @link https://www.postgresql.org/docs/9.1/static/sql-altertable.html */ - if (preg_match('/^(DROP|SET|RESET|USING)\s+/i', $type)) { - return "ALTER TABLE $tableName ALTER COLUMN $columnName $type"; + if (is_string($type)) { + if (preg_match('/^(DROP|SET|RESET|USING)\s+/i', $type) === 1) { + return "ALTER TABLE $tableName ALTER COLUMN $columnName $type"; + } + + $type = $this->schema->getColumnFactory()->fromDefinition($type); } - /** @psalm-suppress DeprecatedMethod */ - $type = 'TYPE ' . $this->queryBuilder->getColumnType($type); - $multiAlterStatement = []; - $constraintPrefix = preg_replace('/[^a-z0-9_]/i', '', $table . '_' . $column); + $columnDefinitionBuilder = $this->queryBuilder->getColumnDefinitionBuilder(); - if (preg_match('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', $type, $matches)) { - $type = preg_replace('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', '', $type); - $multiAlterStatement[] = "ALTER COLUMN $columnName SET DEFAULT $matches[1]"; - } + $multiAlterStatement = ["ALTER COLUMN $columnName TYPE " . $columnDefinitionBuilder->buildAlter($type)]; - $type = preg_replace('/\s+NOT\s+NULL/i', '', $type, -1, $count); + if ($type->hasDefaultValue()) { + $defaultValue = $type->dbTypecast($type->getDefaultValue()); + $defaultValue = $this->queryBuilder->prepareValue($defaultValue); - if ($count > 0) { - $multiAlterStatement[] = "ALTER COLUMN $columnName SET NOT NULL"; - } else { - /** remove extra null if any */ - $type = preg_replace('/\s+NULL/i', '', $type, -1, $count); - if ($count > 0) { - $multiAlterStatement[] = "ALTER COLUMN $columnName DROP NOT NULL"; - } + $multiAlterStatement[] = "ALTER COLUMN $columnName SET DEFAULT $defaultValue"; } - if (preg_match('/\s+CHECK\s+\((.+)\)/i', $type, $matches)) { - $type = preg_replace('/\s+CHECK\s+\((.+)\)/i', '', $type); - $multiAlterStatement[] = "ADD CONSTRAINT {$constraintPrefix}_check CHECK ($matches[1])"; - } + match ($type->isNotNull()) { + true => $multiAlterStatement[] = "ALTER COLUMN $columnName SET NOT NULL", + false => $multiAlterStatement[] = "ALTER COLUMN $columnName DROP NOT NULL", + default => null, + }; - $type = preg_replace('/\s+UNIQUE/i', '', $type, -1, $count); + $check = $type->getCheck(); + if (!empty($check)) { + $constraintPrefix = preg_replace('/\W/', '', $table . '_' . $column); + $multiAlterStatement[] = "ADD CONSTRAINT {$constraintPrefix}_check CHECK ($check)"; + } - if ($count > 0) { + if ($type->isUnique()) { $multiAlterStatement[] = "ADD UNIQUE ($columnName)"; } - /** add what's left at the beginning */ - array_unshift($multiAlterStatement, "ALTER COLUMN $columnName $type"); - - return 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $multiAlterStatement); + return "ALTER TABLE $tableName " . implode(', ', $multiAlterStatement); } /** diff --git a/src/Schema.php b/src/Schema.php index a2f6f1e7..d9847071 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -103,6 +103,7 @@ final class Schema extends AbstractPdoSchema /** @deprecated Use {@see ColumnBuilder} instead. Will be removed in 2.0. */ public function createColumn(string $type, array|int|string $length = null): ColumnInterface { + /** @psalm-suppress DeprecatedClass */ return new Column($type, $length); } diff --git a/tests/ColumnSchemaBuilderTest.php b/tests/ColumnSchemaBuilderTest.php deleted file mode 100644 index 38c0d6ac..00000000 --- a/tests/ColumnSchemaBuilderTest.php +++ /dev/null @@ -1,26 +0,0 @@ - 5)', 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ADD CONSTRAINT foo1_bar_check CHECK (char_length(bar) > 5)'], + ['string(30) UNIQUE', 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(30), ADD UNIQUE ("bar")'], + ['varchar(255) USING bar::varchar', 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255) USING bar::varchar'], + ['varchar(255) using cast("bar" as varchar)', 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255) using cast("bar" as varchar)'], + [ColumnBuilder::string(), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255)'], + [ColumnBuilder::string()->notNull(), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ALTER COLUMN "bar" SET NOT NULL'], + [ColumnBuilder::string()->null(), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ALTER COLUMN "bar" DROP NOT NULL'], + [ColumnBuilder::string()->defaultValue(null), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ALTER COLUMN "bar" SET DEFAULT NULL'], + [ColumnBuilder::string()->defaultValue(''), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ALTER COLUMN "bar" SET DEFAULT \'\''], + [ColumnBuilder::string()->null()->defaultValue('xxx'), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ALTER COLUMN "bar" SET DEFAULT \'xxx\', ALTER COLUMN "bar" DROP NOT NULL'], + [ColumnBuilder::timestamp()->defaultValue(new Expression('now()')), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE timestamp(0), ALTER COLUMN "bar" SET DEFAULT now()'], + [ColumnBuilder::string()->check('char_length(bar) > 5'), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ADD CONSTRAINT foo1_bar_check CHECK (char_length(bar) > 5)'], + [ColumnBuilder::string(30)->unique(), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(30), ADD UNIQUE ("bar")'], + [ColumnBuilder::string()->extra('USING bar::varchar'), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255) USING bar::varchar'], + [ColumnBuilder::string()->extra('using cast("bar" as varchar)'), 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255) using cast("bar" as varchar)'], + ]; + } + public static function buildCondition(): array { $buildCondition = parent::buildCondition(); @@ -591,6 +623,19 @@ public static function buildColumnDefinition(): array $values['scale(2)'][0] = 'numeric(10,2)'; $values['integer(8)->scale(2)'][0] = 'integer'; + $db = self::getDb(); + $serverVersion = self::getDb()->getServerInfo()->getVersion(); + $db->close(); + + if (version_compare($serverVersion, '13', '<')) { + $uuidExpression = "uuid_in(overlay(overlay(md5(now()::text || random()::text) placing '4' from 13) placing" + . ' to_hex(floor(4 * random() + 8)::int)::text from 17)::cstring)'; + + $values[PseudoType::UUID_PK][0] = "uuid PRIMARY KEY DEFAULT $uuidExpression"; + $values[PseudoType::UUID_PK_SEQ][0] = "uuid PRIMARY KEY DEFAULT $uuidExpression"; + $values['uuidPrimaryKey()'][0] = "uuid PRIMARY KEY DEFAULT $uuidExpression"; + } + return $values; } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index acd6ce97..5f99ad88 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -6,7 +6,6 @@ use PHPUnit\Framework\Attributes\DataProviderExternal; use Throwable; -use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\IntegrityException; @@ -14,7 +13,6 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\ExpressionInterface; -use Yiisoft\Db\Pgsql\Column; use Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; @@ -35,6 +33,11 @@ final class QueryBuilderTest extends CommonQueryBuilderTest { use TestTrait; + public function getBuildColumnDefinitionProvider(): array + { + return QueryBuilderProvider::buildColumnDefinition(); + } + protected PdoConnectionInterface $db; /** @@ -57,171 +60,10 @@ public function testAddDefaultValue(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - */ - public function testAlterColumn(): void + #[DataProviderExternal(QueryBuilderProvider::class, 'alterColumn')] + public function testAlterColumn(string|ColumnSchemaInterface $type, string $expected): void { - $db = $this->getConnection(); - - $qb = $db->getQueryBuilder(); - - $this->assertSame( - <<alterColumn('foo1', 'bar', 'varchar(255)'), - ); - - $this->assertSame( - <<alterColumn('foo1', 'bar', 'SET NOT null'), - ); - - $this->assertSame( - <<alterColumn('foo1', 'bar', 'drop default'), - ); - - $this->assertSame( - <<alterColumn('foo1', 'bar', 'reset xyz'), - ); - - $this->assertSame( - <<alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::STRING, 255))->asString() - ), - ); - - $this->assertSame( - <<alterColumn('foo1', 'bar', 'varchar(255) USING bar::varchar'), - ); - - $this->assertSame( - <<alterColumn('foo1', 'bar', 'varchar(255) using cast("bar" as varchar)'), - ); - - $this->assertSame( - <<alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::STRING, 255))->notNull()->asString() - ), - ); - - $this->assertSame( - <<alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::STRING, 255))->null()->asString() - ), - ); - - $this->assertSame( - <<alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::STRING, 255))->null()->defaultValue('xxx')->asString() - ), - ); - - $this->assertSame( - << 5) - SQL, - $qb->alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::STRING, 255))->check('char_length(bar) > 5')->asString() - ), - ); - - $this->assertSame( - <<alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::STRING, 255))->defaultValue('')->asString() - ), - ); - - $this->assertSame( - <<alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::STRING, 255))->defaultValue('AbCdE')->asString() - ), - ); - - $this->assertSame( - <<alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::TIMESTAMP)) - ->defaultExpression('CURRENT_TIMESTAMP') - ->asString() - ), - ); - - $this->assertSame( - <<alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::STRING, 30))->unique()->asString() - ), - ); - - $this->assertSame( - <<alterColumn( - 'foo1', - 'bar', - (new Column(ColumnType::STRING, 30))->unique() - ), - ); - - $db->close(); + parent::testAlterColumn($type, $expected); } /** @@ -370,11 +212,11 @@ public function testCreateTable(): void $this->assertSame( <<createTable(