From a8529daabcb60ca57fa982d10b298a56864697a0 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Thu, 30 May 2024 15:55:15 +0700 Subject: [PATCH] ColumnSchema classes for performance of typecasting (#236) --- CHANGELOG.md | 2 + src/Column/BinaryColumnSchema.php | 30 +++++++++++++ src/ColumnSchema.php | 58 ------------------------- src/DMLQueryBuilder.php | 4 +- src/Schema.php | 58 +++++++++++++++++-------- tests/ColumnSchemaTest.php | 46 +++++++++++++++++++- tests/Provider/ColumnSchemaProvider.php | 26 +++++++++++ 7 files changed, 144 insertions(+), 80 deletions(-) create mode 100644 src/Column/BinaryColumnSchema.php delete mode 100644 src/ColumnSchema.php create mode 100644 tests/Provider/ColumnSchemaProvider.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a9bbe5..1a4161c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/Column/BinaryColumnSchema.php b/src/Column/BinaryColumnSchema.php new file mode 100644 index 0000000..3ca984c --- /dev/null +++ b/src/Column/BinaryColumnSchema.php @@ -0,0 +1,30 @@ +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); + } +} diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php deleted file mode 100644 index b0da373..0000000 --- a/src/ColumnSchema.php +++ /dev/null @@ -1,58 +0,0 @@ -name('id'); - * $column->allowNull(false); - * $column->dbType('number'); - * $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() === 'BLOB') { - if ($value instanceof ParamInterface && is_string($value->getValue())) { - $value = (string) $value->getValue(); - } - - if (is_string($value)) { - /** @var non-empty-string $placeholder */ - $placeholder = uniqid('exp_' . preg_replace('/[^a-z0-9]/i', '', $this->getName())); - return new Expression('TO_BLOB(UTL_RAW.CAST_TO_RAW(:' . $placeholder . '))', [$placeholder => $value]); - } - } - - return parent::dbTypecast($value); - } -} diff --git a/src/DMLQueryBuilder.php b/src/DMLQueryBuilder.php index a04164f..639eef6 100644 --- a/src/DMLQueryBuilder.php +++ b/src/DMLQueryBuilder.php @@ -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; @@ -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 $columns */ + $columns = [array_key_first($tableSchema->getColumns())]; } foreach ($columns as $name) { diff --git a/src/Schema.php b/src/Schema.php index 8344c80..f92dc5f 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -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; @@ -405,14 +406,14 @@ protected function findColumns(TableSchemaInterface $table): bool 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; @@ -448,13 +449,21 @@ protected function getTableSequenceName(string $tableName): string|null } /** - * 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']); @@ -462,14 +471,21 @@ protected function createColumnSchema(array $info): ColumnSchemaInterface $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. * @@ -645,16 +661,20 @@ public function findUniqueIndexes(TableSchemaInterface $table): array /** * 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, @@ -663,7 +683,7 @@ private function extractColumnType(ColumnSchemaInterface $column): string $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; } diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 5626260..6977bd6 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -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; @@ -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)), + ); + } } diff --git a/tests/Provider/ColumnSchemaProvider.php b/tests/Provider/ColumnSchemaProvider.php new file mode 100644 index 0000000..02545ec --- /dev/null +++ b/tests/Provider/ColumnSchemaProvider.php @@ -0,0 +1,26 @@ +