Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ColumnSchema classes for performance of typecasting #236

Merged
merged 12 commits into from
May 30, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from `$table, $columns, $rows` to `$table, $rows, $columns = []` (@Tigrov)
- Enh #260: Support `Traversable` values for `DMLQueryBuilder::batchInsert()` method with empty columns (@Tigrov)
- Enh #255: Implement `SqlParser` and `ExpressionBuilder` driver classes (@Tigrov)
- Enh #236: Implement `ColumnSchemaInterface` classes according to the data type of database table columns
for type casting performance. Related with yiisoft/db#752 (@Tigrov)
- Chg #272: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov)

## 1.3.0 March 21, 2024
Expand Down
30 changes: 30 additions & 0 deletions src/Column/BinaryColumnSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle\Column;

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

use function is_string;

final class BinaryColumnSchema extends BaseBinaryColumnSchema
{
public function dbTypecast(mixed $value): mixed
{
if ($this->getDbType() === 'BLOB') {
if ($value instanceof ParamInterface && is_string($value->getValue())) {
/** @var string */
$value = $value->getValue();
}

if (is_string($value)) {
return new Expression('TO_BLOB(UTL_RAW.CAST_TO_RAW(:value))', ['value' => $value]);
}
}

return parent::dbTypecast($value);
}
}
58 changes: 0 additions & 58 deletions src/ColumnSchema.php

This file was deleted.

4 changes: 3 additions & 1 deletion src/DMLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Yiisoft\Db\Query\QueryInterface;
use Yiisoft\Db\QueryBuilder\AbstractDMLQueryBuilder;

use function array_key_first;
use function array_map;
use function implode;
use function count;
Expand Down Expand Up @@ -145,7 +146,8 @@ protected function prepareInsertValues(string $table, array|QueryInterface $colu
if (!empty($tableSchema->getPrimaryKey())) {
$columns = $tableSchema->getPrimaryKey();
} else {
$columns = [current($tableSchema->getColumns())->getName()];
/** @var list<string> $columns */
$columns = [array_key_first($tableSchema->getColumns())];
}

foreach ($columns as $name) {
Expand Down
58 changes: 39 additions & 19 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
use Yiisoft\Db\Exception\NotSupportedException;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Helper\DbArrayHelper;
use Yiisoft\Db\Oracle\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_change_key_case;
Expand Down Expand Up @@ -75,7 +76,7 @@
*
* @var string[]
*/
private const TYPE_MAP = [

Check failure on line 79 in src/Schema.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

MissingClassConstType

src/Schema.php:79:19: MissingClassConstType: Class constant "Yiisoft\Db\Oracle\Schema::TYPE_MAP" should have a declared type. (see https://psalm.dev/359)
'char' => self::TYPE_CHAR,
'nchar' => self::TYPE_CHAR,
'varchar2' => self::TYPE_STRING,
Expand Down Expand Up @@ -405,14 +406,14 @@
return false;
}

/** @psalm-var string[][] $columns */
foreach ($columns as $column) {
/** @psalm-var ColumnInfoArray $column */
$column = array_change_key_case($column);
/** @psalm-var string[][] $info */
foreach ($columns as $info) {
/** @psalm-var ColumnInfoArray $info */
$info = array_change_key_case($info);

$c = $this->createColumnSchema($column);
$column = $this->loadColumnSchema($info);

$table->column($c->getName(), $c);
$table->column($info['column_name'], $column);
}

return true;
Expand Down Expand Up @@ -448,28 +449,43 @@
}

/**
* Creates ColumnSchema instance.
* Loads the column information into a {@see ColumnSchemaInterface} object.
*
* @psalm-param ColumnInfoArray $info
* @param array $info The column information.
*
* @return ColumnSchemaInterface The column schema object.
*
* @psalm-param ColumnInfoArray $info The column information.
*/
protected function createColumnSchema(array $info): ColumnSchemaInterface
private function loadColumnSchema(array $info): ColumnSchemaInterface
{
$column = new ColumnSchema($info['column_name']);
$dbType = $info['data_type'];
$type = $this->extractColumnType($dbType, $info);

$column = $this->createColumnSchema($type);
$column->name($info['column_name']);
$column->allowNull($info['nullable'] === 'Y');
$column->comment($info['column_comment']);
$column->primaryKey((bool) $info['is_pk']);
$column->autoIncrement($info['identity_column'] === 'YES');
$column->size((int) $info['data_length']);
$column->precision($info['data_precision'] !== null ? (int) $info['data_precision'] : null);
$column->scale($info['data_scale'] !== null ? (int) $info['data_scale'] : null);
$column->dbType($info['data_type']);
$column->type($this->extractColumnType($column));
$column->phpType($this->getColumnPhpType($column));
$column->dbType($dbType);
$column->defaultValue($this->normalizeDefaultValue($info['data_default'], $column));

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);
}

/**
* Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
*
Expand Down Expand Up @@ -645,16 +661,20 @@
/**
* Extracts the data type for the given column.
*
* @param ColumnSchemaInterface $column The column schema object.
* @param string $dbType The database data type
* @param array $info Column information.
* @psalm-param ColumnInfoArray $info
*
* @return string The abstract column type.
*/
private function extractColumnType(ColumnSchemaInterface $column): string
private function extractColumnType(string $dbType, array $info): string
{
$dbType = strtolower((string) $column->getDbType());
$dbType = strtolower($dbType);

if ($dbType === 'number') {
return match ($column->getScale()) {
$scale = $info['data_scale'] !== null ? (int) $info['data_scale'] : null;

return match ($scale) {
null => self::TYPE_DOUBLE,
0 => self::TYPE_INTEGER,
default => self::TYPE_DECIMAL,
Expand All @@ -663,7 +683,7 @@

$dbType = preg_replace('/\([^)]+\)/', '', $dbType);

if ($dbType === 'interval day to second' && $column->getPrecision() > 0) {
if ($dbType === 'interval day to second' && $info['data_precision'] > 0) {
return self::TYPE_STRING;
}

Expand Down
46 changes: 44 additions & 2 deletions tests/ColumnSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@

namespace Yiisoft\Db\Oracle\Tests;

use PHPUnit\Framework\TestCase;
use PDO;
use Yiisoft\Db\Command\Param;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Oracle\Column\BinaryColumnSchema;
use Yiisoft\Db\Oracle\Tests\Support\TestTrait;
use Yiisoft\Db\Query\Query;
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 oracle
*/
final class ColumnSchemaTest extends TestCase
final class ColumnSchemaTest extends CommonColumnSchemaTest
{
use TestTrait;

Expand Down Expand Up @@ -62,4 +68,40 @@ 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'));
}

/** @dataProvider \Yiisoft\Db\Oracle\Tests\Provider\ColumnSchemaProvider::predefinedTypes */
public function testPredefinedType(string $className, string $type, string $phpType)
{
parent::testPredefinedType($className, $type, $phpType);
}

/** @dataProvider \Yiisoft\Db\Oracle\Tests\Provider\ColumnSchemaProvider::dbTypecastColumns */
public function testDbTypecastColumns(string $className, array $values)
{
parent::testDbTypecastColumns($className, $values);
}

public function testBinaryColumnSchema()
{
$binaryCol = new BinaryColumnSchema();
$binaryCol->dbType('BLOB');

$this->assertInstanceOf(Expression::class, $binaryCol->dbTypecast("\x10\x11\x12"));
$this->assertInstanceOf(
Expression::class,
$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\Oracle\Tests\Provider;

use Yiisoft\Db\Oracle\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;
}
}
Loading