From 5171ad7eb28470580310e8408e2746728e881668 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Tue, 15 Oct 2024 13:11:17 +0700 Subject: [PATCH] Add `ColumnDefinitionBuilder` (#322) --- CHANGELOG.md | 7 +- src/Column/ColumnDefinitionBuilder.php | 89 ++++++++++++++++++++++++ src/Column/ColumnFactory.php | 17 ++++- src/Connection.php | 7 -- src/DDLQueryBuilder.php | 1 + src/QueryBuilder.php | 6 +- src/Schema.php | 10 ++- tests/ConnectionTest.php | 8 --- tests/Provider/ColumnBuilderProvider.php | 13 ++-- tests/Provider/ColumnFactoryProvider.php | 14 +++- tests/Provider/QueryBuilderProvider.php | 80 +++++++++++++++++++++ tests/QueryBuilderTest.php | 7 ++ tests/SchemaTest.php | 8 +++ tests/Type/RowversionTest.php | 2 +- 14 files changed, 237 insertions(+), 32 deletions(-) create mode 100644 src/Column/ColumnDefinitionBuilder.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab083503..a99cb15e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.0.0 under development -- Enh #277: Implement `ColumnSchemaInterface` classes according to the data type of database table columns +- New #277: Implement `ColumnSchemaInterface` classes according to the data type of database table columns for type casting performance. Related with yiisoft/db#752 (@Tigrov) - Enh #293: Implement `SqlParser` and `ExpressionBuilder` driver classes (@Tigrov) - Chg #306: Remove parameter `$withColumn` from `Quoter::getTableNameParts()` method (@Tigrov) @@ -10,10 +10,11 @@ - Enh #312: Refactor `bit` type (@Tigrov) - Enh #315: Refactor PHP type of `ColumnSchemaInterface` instances (@Tigrov) - Enh #317: Raise minimum PHP version to `^8.1` with minor refactoring (@Tigrov) -- Enh #316: Implement `ColumnFactory` class (@Tigrov) +- New #316: Implement `ColumnFactory` class (@Tigrov) - Enh #319: Separate column type constants (@Tigrov) -- Enh #320: Realize `ColumnBuilder` class (@Tigrov) +- New #320: Realize `ColumnBuilder` class (@Tigrov) - Enh #321: Update according changes in `ColumnSchemaInterface` (@Tigrov) +- New #322: Add `ColumnDefinitionBuilder` class (@Tigrov) ## 1.2.0 March 21, 2024 diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php new file mode 100644 index 000000000..d0f40d090 --- /dev/null +++ b/src/Column/ColumnDefinitionBuilder.php @@ -0,0 +1,89 @@ +buildType($column) + . $this->buildAutoIncrement($column) + . $this->buildPrimaryKey($column) + . $this->buildUnique($column) + . $this->buildNotNull($column) + . $this->buildDefault($column) + . $this->buildCheck($column) + . $this->buildReferences($column) + . $this->buildExtra($column); + } + + protected function getDbType(ColumnSchemaInterface $column): string + { + /** @psalm-suppress DocblockTypeContradiction */ + return match ($column->getType()) { + ColumnType::BOOLEAN => 'bit', + ColumnType::BIT => match (true) { + ($size = $column->getSize()) === null => 'bigint', + $size === 1 => 'bit', + $size <= 8 => 'tinyint', + $size <= 16 => 'smallint', + $size <= 32 => 'int', + $size <= 64 => 'bigint', + default => 'varbinary(' . ceil($size / 8) . ')', + }, + ColumnType::TINYINT => 'tinyint', + ColumnType::SMALLINT => 'smallint', + ColumnType::INTEGER => 'int', + ColumnType::BIGINT => 'bigint', + ColumnType::FLOAT => 'real', + ColumnType::DOUBLE => 'float(53)', + ColumnType::DECIMAL => 'decimal', + ColumnType::MONEY => 'money', + ColumnType::CHAR => 'nchar', + ColumnType::STRING => 'nvarchar', + ColumnType::TEXT => 'nvarchar(max)', + ColumnType::BINARY => 'varbinary(max)', + ColumnType::UUID => 'uniqueidentifier', + ColumnType::DATETIME => 'datetime2', + ColumnType::TIMESTAMP => 'datetime2', + ColumnType::DATE => 'date', + ColumnType::TIME => 'time', + ColumnType::ARRAY => 'json', + ColumnType::STRUCTURED => 'json', + ColumnType::JSON => 'json', + default => 'varchar', + }; + } +} diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index e95fd8c58..a09b02a9b 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -5,6 +5,8 @@ namespace Yiisoft\Db\Mssql\Column; use Yiisoft\Db\Constant\ColumnType; +use Yiisoft\Db\Constant\PseudoType; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Schema\Column\AbstractColumnFactory; use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; @@ -58,9 +60,9 @@ final class ColumnFactory extends AbstractColumnFactory /** * Other data types 'cursor' type can't be used with tables */ - 'timestamp' => ColumnType::TIMESTAMP, + 'timestamp' => ColumnType::BINARY, 'hierarchyid' => ColumnType::STRING, - 'uniqueidentifier' => ColumnType::STRING, + 'uniqueidentifier' => ColumnType::UUID, 'sql_variant' => ColumnType::STRING, 'xml' => ColumnType::STRING, 'table' => ColumnType::STRING, @@ -71,6 +73,17 @@ protected function getType(string $dbType, array $info = []): string return self::TYPE_MAP[$dbType] ?? ColumnType::STRING; } + public function fromPseudoType(string $pseudoType, array $info = []): ColumnSchemaInterface + { + if ($pseudoType === PseudoType::UUID_PK_SEQ) { + return ColumnBuilder::uuidPrimaryKey() + ->defaultValue(new Expression('newsequentialid()')) + ->load($info); + } + + return parent::fromPseudoType($pseudoType, $info); + } + public function fromType(string $type, array $info = []): ColumnSchemaInterface { if ($type === ColumnType::BINARY) { diff --git a/src/Connection.php b/src/Connection.php index 6320efbb8..79cca8b61 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -6,11 +6,9 @@ use Yiisoft\Db\Driver\Pdo\AbstractPdoConnection; use Yiisoft\Db\Driver\Pdo\PdoCommandInterface; -use Yiisoft\Db\Mssql\Column\ColumnFactory; use Yiisoft\Db\Query\BatchQueryResultInterface; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; -use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Transaction\TransactionInterface; @@ -51,11 +49,6 @@ public function createTransaction(): TransactionInterface return new Transaction($this); } - public function getColumnFactory(): ColumnFactoryInterface - { - return new ColumnFactory(); - } - public function getQueryBuilder(): QueryBuilderInterface { if ($this->queryBuilder === null) { diff --git a/src/DDLQueryBuilder.php b/src/DDLQueryBuilder.php index ad610ec0f..e446077cf 100644 --- a/src/DDLQueryBuilder.php +++ b/src/DDLQueryBuilder.php @@ -84,6 +84,7 @@ public function alterColumn(string $table, string $column, ColumnInterface|strin } } + /** @psalm-suppress DeprecatedMethod */ return implode("\n", [ $this->dropConstraintsForColumn($table, $column, 'D'), "ALTER TABLE $tableName ALTER COLUMN $columnName {$this->queryBuilder->getColumnType($type)}", diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 89bb9cbea..5d7396269 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -6,6 +6,7 @@ use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Constant\PseudoType; +use Yiisoft\Db\Mssql\Column\ColumnDefinitionBuilder; use Yiisoft\Db\QueryBuilder\AbstractQueryBuilder; use Yiisoft\Db\Schema\Builder\ColumnInterface; use Yiisoft\Db\Schema\QuoterInterface; @@ -52,12 +53,15 @@ public function __construct(QuoterInterface $quoter, SchemaInterface $schema) $ddlBuilder = new DDLQueryBuilder($this, $quoter, $schema); $dmlBuilder = new DMLQueryBuilder($this, $quoter, $schema); $dqlBuilder = new DQLQueryBuilder($this, $quoter); + $columnDefinitionBuilder = new ColumnDefinitionBuilder($this); - parent::__construct($quoter, $schema, $ddlBuilder, $dmlBuilder, $dqlBuilder); + parent::__construct($quoter, $schema, $ddlBuilder, $dmlBuilder, $dqlBuilder, $columnDefinitionBuilder); } + /** @deprecated Use {@see buildColumnDefinition()}. Will be removed in version 2.0. */ public function getColumnType(ColumnInterface|string $type): string { + /** @psalm-suppress DeprecatedMethod */ $columnType = parent::getColumnType($type); /** remove unsupported keywords*/ diff --git a/src/Schema.php b/src/Schema.php index 46ca44b29..fb6899bca 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -14,7 +14,9 @@ use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Helper\DbArrayHelper; +use Yiisoft\Db\Mssql\Column\ColumnFactory; use Yiisoft\Db\Schema\Builder\ColumnInterface; +use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; use Yiisoft\Db\Schema\TableSchemaInterface; @@ -64,11 +66,17 @@ final class Schema extends AbstractPdoSchema */ protected string|null $defaultSchema = 'dbo'; + /** @deprecated Use {@see ColumnBuilder} instead. Will be removed in 2.0. */ public function createColumn(string $type, array|int|string|null $length = null): ColumnInterface { return new Column($type, $length); } + public function getColumnFactory(): ColumnFactoryInterface + { + return new ColumnFactory(); + } + /** * Resolves the table name and schema name (if any). * @@ -358,7 +366,7 @@ protected function loadTableDefaultValues(string $tableName): array */ private function loadColumnSchema(array $info): ColumnSchemaInterface { - $columnFactory = $this->db->getColumnFactory(); + $columnFactory = $this->getColumnFactory(); $dbType = $info['data_type']; /** @psalm-var ColumnArray $info */ diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index f92ffbef1..f35853b4c 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -10,7 +10,6 @@ use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; -use Yiisoft\Db\Mssql\Column\ColumnFactory; use Yiisoft\Db\Mssql\Tests\Support\TestTrait; use Yiisoft\Db\Tests\Common\CommonConnectionTest; use Yiisoft\Db\Transaction\TransactionInterface; @@ -88,11 +87,4 @@ public function testSettingDefaultAttributes(): void $this->assertSame(PDO::ERRMODE_EXCEPTION, $db->getActivePDO()?->getAttribute(PDO::ATTR_ERRMODE)); } - - public function testGetColumnFactory(): void - { - $db = $this->getConnection(); - - $this->assertInstanceOf(ColumnFactory::class, $db->getColumnFactory()); - } } diff --git a/tests/Provider/ColumnBuilderProvider.php b/tests/Provider/ColumnBuilderProvider.php index 52e0e36d4..8db2054b7 100644 --- a/tests/Provider/ColumnBuilderProvider.php +++ b/tests/Provider/ColumnBuilderProvider.php @@ -4,18 +4,17 @@ namespace Yiisoft\Db\Mssql\Tests\Provider; -use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Mssql\Column\BinaryColumnSchema; class ColumnBuilderProvider extends \Yiisoft\Db\Tests\Provider\ColumnBuilderProvider { public static function buildingMethods(): array { - return [ - // building method, args, expected instance of, expected type, expected column method results - ...parent::buildingMethods(), - ['binary', [], BinaryColumnSchema::class, ColumnType::BINARY], - ['binary', [8], BinaryColumnSchema::class, ColumnType::BINARY, ['getSize' => 8]], - ]; + $values = parent::buildingMethods(); + + $values['binary()'][2] = BinaryColumnSchema::class; + $values['binary(8)'][2] = BinaryColumnSchema::class; + + return $values; } } diff --git a/tests/Provider/ColumnFactoryProvider.php b/tests/Provider/ColumnFactoryProvider.php index 5f0d83507..b1326a315 100644 --- a/tests/Provider/ColumnFactoryProvider.php +++ b/tests/Provider/ColumnFactoryProvider.php @@ -5,6 +5,7 @@ namespace Yiisoft\Db\Mssql\Tests\Provider; use Yiisoft\Db\Constant\ColumnType; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Mssql\Column\BinaryColumnSchema; use Yiisoft\Db\Schema\Column\BooleanColumnSchema; use Yiisoft\Db\Schema\Column\DoubleColumnSchema; @@ -13,6 +14,15 @@ final class ColumnFactoryProvider extends \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider { + public static function pseudoTypes(): array + { + $values = parent::pseudoTypes(); + + $values['uuid_pk_seq'][3]['getDefaultValue'] = new Expression('newsequentialid()'); + + return $values; + } + public static function dbTypes(): array { return [ @@ -44,9 +54,9 @@ public static function dbTypes(): array ['binary', ColumnType::BINARY, BinaryColumnSchema::class], ['varbinary', ColumnType::BINARY, BinaryColumnSchema::class], ['image', ColumnType::BINARY, BinaryColumnSchema::class], - ['timestamp', ColumnType::TIMESTAMP, StringColumnSchema::class], + ['timestamp', ColumnType::BINARY, BinaryColumnSchema::class], ['hierarchyid', ColumnType::STRING, StringColumnSchema::class], - ['uniqueidentifier', ColumnType::STRING, StringColumnSchema::class], + ['uniqueidentifier', ColumnType::UUID, StringColumnSchema::class], ['sql_variant', ColumnType::STRING, StringColumnSchema::class], ['xml', ColumnType::STRING, StringColumnSchema::class], ['table', ColumnType::STRING, StringColumnSchema::class], diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index b0712e500..da0b2067a 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Mssql\Tests\Provider; +use Yiisoft\Db\Constant\PseudoType; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Mssql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; @@ -337,4 +338,83 @@ public static function upsert(): array return $upsert; } + + public static function buildColumnDefinition(): array + { + $values = parent::buildColumnDefinition(); + + $values[PseudoType::PK][0] = 'int IDENTITY PRIMARY KEY'; + $values[PseudoType::UPK][0] = 'int IDENTITY PRIMARY KEY'; + $values[PseudoType::BIGPK][0] = 'bigint IDENTITY PRIMARY KEY'; + $values[PseudoType::UBIGPK][0] = 'bigint IDENTITY PRIMARY KEY'; + $values[PseudoType::UUID_PK][0] = 'uniqueidentifier PRIMARY KEY DEFAULT newid()'; + $values[PseudoType::UUID_PK_SEQ][0] = 'uniqueidentifier PRIMARY KEY DEFAULT newsequentialid()'; + $values['STRING'][0] = 'nvarchar'; + $values['STRING(100)'][0] = 'nvarchar(100)'; + $values['primaryKey()'][0] = 'int IDENTITY PRIMARY KEY'; + $values['primaryKey(false)'][0] = 'int PRIMARY KEY'; + $values['smallPrimaryKey()'][0] = 'smallint IDENTITY PRIMARY KEY'; + $values['smallPrimaryKey(false)'][0] = 'smallint PRIMARY KEY'; + $values['bigPrimaryKey()'][0] = 'bigint IDENTITY PRIMARY KEY'; + $values['bigPrimaryKey(false)'][0] = 'bigint PRIMARY KEY'; + $values['uuidPrimaryKey()'][0] = 'uniqueidentifier PRIMARY KEY DEFAULT newid()'; + $values['uuidPrimaryKey(false)'][0] = 'uniqueidentifier PRIMARY KEY'; + $values['boolean()'][0] = 'bit'; + $values['boolean(100)'][0] = 'bit'; + $values['bit()'][0] = 'bigint'; + $values['bit(1)'][0] = 'bit'; + $values['bit(8)'][0] = 'tinyint'; + $values['bit(1000)'][0] = 'varbinary(125)'; + $values['tinyint(2)'][0] = 'tinyint'; + $values['smallint(4)'][0] = 'smallint'; + $values['integer()'][0] = 'int'; + $values['integer(8)'][0] = 'int'; + $values['bigint(15)'][0] = 'bigint'; + $values['float()'][0] = 'real'; + $values['float(10)'][0] = 'real'; + $values['float(10,2)'][0] = 'real'; + $values['double()'][0] = 'float(53)'; + $values['double(10)'][0] = 'float(53)'; + $values['double(10,2)'][0] = 'float(53)'; + $values['char()'][0] = 'nchar(1)'; + $values['char(10)'][0] = 'nchar(10)'; + $values['char(null)'][0] = 'nchar'; + $values['string()'][0] = 'nvarchar(255)'; + $values['string(100)'][0] = 'nvarchar(100)'; + $values['string(null)'][0] = 'nvarchar'; + $values['text()'][0] = 'nvarchar(max)'; + $values['text(1000)'][0] = 'nvarchar(max)'; + $values['binary()'][0] = 'varbinary(max)'; + $values['binary(1000)'][0] = 'varbinary(max)'; + $values['datetime()'][0] = 'datetime2(0)'; + $values['datetime(6)'][0] = 'datetime2(6)'; + $values['datetime(null)'][0] = 'datetime2'; + $values['timestamp()'][0] = 'datetime2(0)'; + $values['timestamp(6)'][0] = 'datetime2(6)'; + $values['timestamp(null)'][0] = 'datetime2'; + $values['uuid()'][0] = 'uniqueidentifier'; + $values["extra('bar')"][0] = 'nvarchar(255) bar'; + $values["extra('')"][0] = 'nvarchar(255)'; + $values["check('value > 5')"][0] = 'nvarchar(255) CHECK (value > 5)'; + $values["check('')"][0] = 'nvarchar(255)'; + $values['check(null)'][0] = 'nvarchar(255)'; + $values["comment('comment')"][0] = 'nvarchar(255)'; + $values["comment('')"][0] = 'nvarchar(255)'; + $values['comment(null)'][0] = 'nvarchar(255)'; + $values["defaultValue('value')"][0] = "nvarchar(255) DEFAULT 'value'"; + $values["defaultValue('')"][0] = "nvarchar(255) DEFAULT ''"; + $values['defaultValue(null)'][0] = 'nvarchar(255)'; + $values['defaultValue($expression)'][0] = 'nvarchar(255) DEFAULT expression'; + $values["integer()->defaultValue('')"][0] = 'int'; + $values['notNull()'][0] = 'nvarchar(255) NOT NULL'; + $values['integer()->primaryKey()'][0] = 'int PRIMARY KEY'; + $values['size(10)'][0] = 'nvarchar(10)'; + $values['unique()'][0] = 'nvarchar(255) UNIQUE'; + $values['unsigned()'][0] = 'int'; + $values['integer(8)->scale(2)'][0] = 'int'; + $values['reference($reference)'][0] = 'int REFERENCES [ref_table] ([id]) ON DELETE CASCADE ON UPDATE CASCADE'; + $values['reference($referenceWithSchema)'][0] = 'int REFERENCES [ref_schema].[ref_table] ([id]) ON DELETE CASCADE ON UPDATE CASCADE'; + + return $values; + } } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 3015dc329..a2cc69e9f 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -18,6 +18,7 @@ use Yiisoft\Db\Mssql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Query\QueryInterface; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; use Yiisoft\Db\Tests\Common\CommonQueryBuilderTest; use function json_encode; @@ -1147,4 +1148,10 @@ public function testSelectScalar(array|bool|float|int|string $columns, string $e { parent::testSelectScalar($columns, $expected); } + + /** @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\QueryBuilderProvider::buildColumnDefinition() */ + public function testBuildColumnDefinition(string $expected, ColumnSchemaInterface|string $column): void + { + parent::testBuildColumnDefinition($expected, $column); + } } diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 3be139c2e..b6945f98a 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -11,6 +11,7 @@ use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Mssql\Column\ColumnFactory; use Yiisoft\Db\Mssql\Schema; use Yiisoft\Db\Mssql\Tests\Support\TestTrait; use Yiisoft\Db\Schema\SchemaInterface; @@ -197,4 +198,11 @@ public function testNegativeDefaultValues(): void $db->close(); } + + public function testGetColumnFactory(): void + { + $db = $this->getConnection(); + + $this->assertInstanceOf(ColumnFactory::class, $db->getSchema()->getColumnFactory()); + } } diff --git a/tests/Type/RowversionTest.php b/tests/Type/RowversionTest.php index 97aaf0fc1..90240aaab 100644 --- a/tests/Type/RowversionTest.php +++ b/tests/Type/RowversionTest.php @@ -38,7 +38,7 @@ public function testValue(): void $tableSchema = $db->getTableSchema('rowversion'); $this->assertSame('timestamp', $tableSchema?->getColumn('Myrowversion')->getDbType()); - $this->assertSame('string', $tableSchema?->getColumn('Myrowversion')->getPhpType()); + $this->assertSame('mixed', $tableSchema?->getColumn('Myrowversion')->getPhpType()); $this->assertNull($tableSchema?->getColumn('Myrowversion')->getDefaultValue()); $command = $db->createCommand();