diff --git a/CHANGELOG.md b/CHANGELOG.md index 846332f69..e49c672f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 2.0.0 under development +- Enh #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) - Chg #308: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) diff --git a/src/Column/BinaryColumnSchema.php b/src/Column/BinaryColumnSchema.php new file mode 100644 index 000000000..f42194a1c --- /dev/null +++ b/src/Column/BinaryColumnSchema.php @@ -0,0 +1,31 @@ +getDbType() === 'varbinary') { + if ($value instanceof ParamInterface && is_string($value->getValue())) { + /** @psalm-var string */ + $value = $value->getValue(); + } + + if (is_string($value)) { + return new Expression('CONVERT(VARBINARY(MAX), ' . ('0x' . bin2hex($value)) . ')'); + } + } + + return parent::dbTypecast($value); + } +} diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php deleted file mode 100644 index 97e21993d..000000000 --- a/src/ColumnSchema.php +++ /dev/null @@ -1,54 +0,0 @@ -name('id'); - * $column->allowNull(false); - * $column->dbType('int'); - * $column->phpType('integer'); - * $column->type('integer'); - * $column->defaultValue(0); - * $column->autoIncrement(true); - * $column->primaryKey(true); - * ``` - */ -final class ColumnSchema extends AbstractColumnSchema -{ - public function dbTypecast(mixed $value): mixed - { - if ($this->getType() === SchemaInterface::TYPE_BINARY && $this->getDbType() === 'varbinary') { - if ($value instanceof ParamInterface && is_string($value->getValue())) { - $value = (string) $value->getValue(); - } - if (is_string($value)) { - return new Expression('CONVERT(VARBINARY(MAX), ' . ('0x' . bin2hex($value)) . ')'); - } - } - - return parent::dbTypecast($value); - } -} diff --git a/src/DMLQueryBuilder.php b/src/DMLQueryBuilder.php index 4d68e3d62..732a35060 100644 --- a/src/DMLQueryBuilder.php +++ b/src/DMLQueryBuilder.php @@ -43,7 +43,7 @@ public function insertWithReturningPks(string $table, QueryInterface|array $colu $insertedCols = []; $returnColumns = array_intersect_key($tableSchema?->getColumns() ?? [], array_flip($primaryKeys)); - foreach ($returnColumns as $returnColumn) { + foreach ($returnColumns as $columnName => $returnColumn) { $dbType = $returnColumn->getDbType(); if (in_array($dbType, ['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary'], true)) { @@ -52,7 +52,7 @@ public function insertWithReturningPks(string $table, QueryInterface|array $colu $dbType = $returnColumn->isAllowNull() ? 'varbinary(8)' : 'binary(8)'; } - $quotedName = $this->quoter->quoteColumnName($returnColumn->getName()); + $quotedName = $this->quoter->quoteColumnName($columnName); $createdCols[] = $quotedName . ' ' . (string) $dbType . ' ' . ($returnColumn->isAllowNull() ? 'NULL' : ''); $insertedCols[] = 'INSERTED.' . $quotedName; } diff --git a/src/Schema.php b/src/Schema.php index 5695c59e9..d511c4424 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -14,8 +14,9 @@ use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Helper\DbArrayHelper; +use Yiisoft\Db\Mssql\Column\BinaryColumnSchema; use Yiisoft\Db\Schema\Builder\ColumnInterface; -use Yiisoft\Db\Schema\ColumnSchemaInterface; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; use Yiisoft\Db\Schema\TableSchemaInterface; use function array_map; @@ -38,7 +39,10 @@ * column_default: string|null, * is_identity: string, * is_computed: string, - * comment: null|string + * comment: null|string, + * size?: int, + * precision?: int, + * scale?: int, * } * @psalm-type ConstraintArray = array< * array-key, @@ -406,64 +410,71 @@ protected function loadTableDefaultValues(string $tableName): array return is_array($tableDefault) ? $tableDefault : []; } - /** - * Creates a column schema for the database. - * - * This method may be overridden by child classes to create a DBMS-specific column schema. - * - * @param string $name Name of the column. - */ - protected function createColumnSchema(string $name): ColumnSchema - { - return new ColumnSchema($name); - } - /** * Loads the column information into a {@see ColumnSchemaInterface} object. * * @psalm-param ColumnArray $info The column information. */ - protected function loadColumnSchema(array $info): ColumnSchemaInterface + private function loadColumnSchema(array $info): ColumnSchemaInterface { $dbType = $info['data_type']; - - $column = $this->createColumnSchema($info['column_name']); + $type = $this->getColumnType($dbType, $info); + $isUnsigned = stripos($dbType, 'unsigned') !== false; + /** @psalm-var ColumnArray $info */ + $column = $this->createColumnSchema($type, unsigned: $isUnsigned); + $column->name($info['column_name']); + $column->size($info['size'] ?? null); + $column->precision($info['precision'] ?? null); + $column->scale($info['scale'] ?? null); $column->allowNull($info['is_nullable'] === 'YES'); $column->dbType($dbType); $column->enumValues([]); // MSSQL has only vague equivalents to enum. $column->primaryKey(false); // The primary key will be determined in the `findColumns()` method. $column->autoIncrement($info['is_identity'] === '1'); $column->computed($info['is_computed'] === '1'); - $column->unsigned(stripos($dbType, 'unsigned') !== false); $column->comment($info['comment'] ?? ''); - $column->type(self::TYPE_STRING); + $column->defaultValue($this->normalizeDefaultValue($info['column_default'], $column)); - if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $dbType, $matches)) { - $type = $matches[1]; + return $column; + } - if (isset(self::TYPE_MAP[$type])) { - $column->type(self::TYPE_MAP[$type]); - } + /** + * Get the abstract data type for the database data type. + * + * @param string $dbType The database data type + * @param array $info Column information. + * + * @return string The abstract data type. + */ + private function getColumnType(string $dbType, array &$info): string + { + preg_match('/^(\w*)(?:\(([^)]+)\))?/', $dbType, $matches); + $dbType = strtolower($matches[1]); - if ($type === 'bit') { - $column->type(self::TYPE_BOOLEAN); - } + if ($dbType === 'bit') { + return self::TYPE_BOOLEAN; + } - if (!empty($matches[2])) { - $values = explode(',', $matches[2]); - $column->precision((int) $values[0]); - $column->size((int) $values[0]); + if (!empty($matches[2])) { + $values = explode(',', $matches[2], 2); + $info['size'] = (int) $values[0]; + $info['precision'] = (int) $values[0]; - if (isset($values[1])) { - $column->scale((int) $values[1]); - } + if (isset($values[1])) { + $info['scale'] = (int) $values[1]; } } - $column->phpType($this->getColumnPhpType($column)); - $column->defaultValue($this->normalizeDefaultValue($info['column_default'], $column)); + return self::TYPE_MAP[$dbType] ?? self::TYPE_STRING; + } - return $column; + protected function createColumnSchemaFromPhpType(string $phpType, string $type): ColumnSchemaInterface + { + if ($phpType === self::PHP_TYPE_RESOURCE) { + return new BinaryColumnSchema($type, $phpType); + } + + return parent::createColumnSchemaFromPhpType($phpType, $type); } /** @@ -474,7 +485,7 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface * * @return mixed The normalized default value. */ - private function normalizeDefaultValue(?string $defaultValue, ColumnSchemaInterface $column): mixed + private function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaInterface $column): mixed { if ( $defaultValue === null @@ -567,10 +578,10 @@ protected function findColumns(TableSchemaInterface $table): bool return false; } - foreach ($columns as $column) { - $column = $this->loadColumnSchema($column); + foreach ($columns as $info) { + $column = $this->loadColumnSchema($info); foreach ($table->getPrimaryKey() as $primaryKey) { - if (strcasecmp($column->getName(), $primaryKey) === 0) { + if (strcasecmp($info['column_name'], $primaryKey) === 0) { $column->primaryKey(true); break; } @@ -580,7 +591,7 @@ protected function findColumns(TableSchemaInterface $table): bool $table->sequenceName(''); } - $table->column($column->getName(), $column); + $table->column($info['column_name'], $column); } return true; diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 9642c05ad..e80773b35 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -4,16 +4,24 @@ namespace Yiisoft\Db\Mssql\Tests; -use PHPUnit\Framework\TestCase; +use PDO; +use Yiisoft\Db\Command\Param; +use Yiisoft\Db\Expression\Expression; +use Yiisoft\Db\Mssql\Column\BinaryColumnSchema; use Yiisoft\Db\Mssql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; +use Yiisoft\Db\Schema\Column\BooleanColumnSchema; +use Yiisoft\Db\Schema\Column\DoubleColumnSchema; +use Yiisoft\Db\Schema\Column\IntegerColumnSchema; +use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Tests\Common\CommonColumnSchemaTest; use function str_repeat; /** * @group mssql */ -final class ColumnSchemaTest extends TestCase +final class ColumnSchemaTest extends CommonColumnSchemaTest { use TestTrait; @@ -60,4 +68,44 @@ public function testPhpTypeCast(): void $db->close(); } + + public function testColumnSchemaInstance() + { + $db = $this->getConnection(true); + $schema = $db->getSchema(); + $tableSchema = $schema->getTableSchema('type'); + + $this->assertInstanceOf(IntegerColumnSchema::class, $tableSchema->getColumn('int_col')); + $this->assertInstanceOf(StringColumnSchema::class, $tableSchema->getColumn('char_col')); + $this->assertInstanceOf(DoubleColumnSchema::class, $tableSchema->getColumn('float_col')); + $this->assertInstanceOf(BinaryColumnSchema::class, $tableSchema->getColumn('blob_col')); + $this->assertInstanceOf(BooleanColumnSchema::class, $tableSchema->getColumn('bool_col')); + } + + /** @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\ColumnSchemaProvider::predefinedTypes */ + public function testPredefinedType(string $className, string $type, string $phpType) + { + parent::testPredefinedType($className, $type, $phpType); + } + + /** @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\ColumnSchemaProvider::dbTypecastColumns */ + public function testDbTypecastColumns(string $className, array $values) + { + parent::testDbTypecastColumns($className, $values); + } + + public function testBinaryColumnSchema() + { + $binaryCol = new BinaryColumnSchema(); + $binaryCol->dbType('varbinary'); + + $this->assertEquals( + new Expression('CONVERT(VARBINARY(MAX), 0x101112)'), + $binaryCol->dbTypecast("\x10\x11\x12"), + ); + $this->assertEquals( + new Expression('CONVERT(VARBINARY(MAX), 0x101112)'), + $binaryCol->dbTypecast(new Param("\x10\x11\x12", PDO::PARAM_LOB)), + ); + } } diff --git a/tests/Provider/ColumnSchemaProvider.php b/tests/Provider/ColumnSchemaProvider.php new file mode 100644 index 000000000..b78dc4aa4 --- /dev/null +++ b/tests/Provider/ColumnSchemaProvider.php @@ -0,0 +1,26 @@ +