Skip to content

Commit

Permalink
ColumnSchema classes for performance of typecasting (#277)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov authored May 30, 2024
1 parent dc65f9e commit 42340b5
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 100 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions src/Column/BinaryColumnSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Mssql\Column;

use Yiisoft\Db\Command\ParamInterface;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Schema\Column\BinaryColumnSchema as BaseBinaryColumnSchema;

use function bin2hex;
use function is_string;

final class BinaryColumnSchema extends BaseBinaryColumnSchema
{
public function dbTypecast(mixed $value): mixed
{
if ($this->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);
}
}
54 changes: 0 additions & 54 deletions src/ColumnSchema.php

This file was deleted.

4 changes: 2 additions & 2 deletions src/DMLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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;
}
Expand Down
95 changes: 53 additions & 42 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down
52 changes: 50 additions & 2 deletions tests/ColumnSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)),
);
}
}
26 changes: 26 additions & 0 deletions tests/Provider/ColumnSchemaProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Mssql\Tests\Provider;

use Yiisoft\Db\Mssql\Column\BinaryColumnSchema;

class ColumnSchemaProvider extends \Yiisoft\Db\Tests\Provider\ColumnSchemaProvider
{
public static function predefinedTypes(): array
{
$values = parent::predefinedTypes();
$values['binary'][0] = BinaryColumnSchema::class;

return $values;
}

public static function dbTypecastColumns(): array
{
$values = parent::dbTypecastColumns();
$values['binary'][0] = BinaryColumnSchema::class;

return $values;
}
}

0 comments on commit 42340b5

Please sign in to comment.