From 0d9db1826dbfd17c9a3fd5f22d5a597ae6e243ad Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Thu, 30 May 2024 15:55:10 +0700 Subject: [PATCH] ColumnSchema classes for performance of typecasting (#273) --- CHANGELOG.md | 2 + src/ColumnSchema.php | 85 ------------------------------- src/Schema.php | 101 +++++++++++++++++++------------------ tests/ColumnSchemaTest.php | 29 +++++++---- 4 files changed, 73 insertions(+), 144 deletions(-) delete mode 100644 src/ColumnSchema.php diff --git a/CHANGELOG.md b/CHANGELOG.md index daf70f0b..0db1647a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 2.0.0 under development - Enh #289: Implement `SqlParser` and `ExpressionBuilder` driver classes (@Tigrov) +- Enh #273: Implement `ColumnSchemaInterface` classes according to the data type of database table columns + for type casting performance. Related with yiisoft/db#752 (@Tigrov) - Chg #307: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) ## 1.2.0 March 21, 2024 diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php deleted file mode 100644 index 45a886a3..00000000 --- a/src/ColumnSchema.php +++ /dev/null @@ -1,85 +0,0 @@ -name('id'); - * $column->allowNull(false); - * $column->dbType('integer'); - * $column->phpType('integer'); - * $column->type('integer'); - * $column->defaultValue(0); - * $column->autoIncrement(true); - * $column->primaryKey(true); - * ``` - */ -final class ColumnSchema extends AbstractColumnSchema -{ - /** - * Converts a value from its PHP representation to a database-specific representation. - * - * If the value is null or an {@see Expression}, it won't be converted. - * - * @param mixed $value The value to be converted. - * - * @return mixed The converted value. - */ - public function dbTypecast(mixed $value): mixed - { - if ($value === null || $value instanceof ExpressionInterface) { - return $value; - } - - if ($this->getType() === SchemaInterface::TYPE_JSON) { - return new JsonExpression($value, $this->getDbType()); - } - - return parent::dbTypecast($value); - } - - /** - * Converts the input value according to {@see phpType} after retrieval from the database. - * - * If the value is null or an {@see Expression}, it won't be converted. - * - * @param mixed $value The value to be converted. - * - * @throws JsonException - * @return mixed The converted value. - */ - public function phpTypecast(mixed $value): mixed - { - if ($value === null) { - return null; - } - - if ($this->getType() === SchemaInterface::TYPE_JSON) { - return json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR); - } - - return parent::phpTypecast($value); - } -} diff --git a/src/Schema.php b/src/Schema.php index 5400c489..fbc2073c 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -17,7 +17,7 @@ use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Helper\DbArrayHelper; 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_column; @@ -67,7 +67,10 @@ * type:string, * notnull:string, * dflt_value:string|null, - * pk:string + * pk:string, + * size?: int, + * precision?: int, + * scale?: int, * } */ final class Schema extends AbstractPdoSchema @@ -370,10 +373,10 @@ protected function findColumns(TableSchemaInterface $table): bool } $column = $this->loadColumnSchema($info); - $table->column($column->getName(), $column); + $table->column($info['name'], $column); if ($column->isPrimaryKey()) { - $table->primaryKey($column->getName()); + $table->primaryKey($info['name']); } } @@ -469,49 +472,63 @@ public function getSchemaDefaultValues(string $schema = '', bool $refresh = fals * * @return ColumnSchemaInterface The column schema object. * - * @psalm-param array{cid:string, name:string, type:string, notnull:string, dflt_value:string|null, pk:string} $info + * @psalm-param ColumnInfo $info */ - protected function loadColumnSchema(array $info): ColumnSchemaInterface + private function loadColumnSchema(array $info): ColumnSchemaInterface { - $column = $this->createColumnSchema($info['name']); + $dbType = strtolower($info['type']); + $type = $this->getColumnType($dbType, $info); + $isUnsigned = str_contains($dbType, 'unsigned'); + /** @psalm-var ColumnInfo $info */ + $column = $this->createColumnSchema($type, unsigned: $isUnsigned); + $column->name($info['name']); + $column->size($info['size'] ?? null); + $column->precision($info['precision'] ?? null); + $column->scale($info['scale'] ?? null); $column->allowNull(!$info['notnull']); - $column->primaryKey($info['pk'] != '0'); - $column->dbType(strtolower($info['type'])); - $column->unsigned(str_contains($column->getDbType() ?? '', 'unsigned')); - $column->type(self::TYPE_STRING); + $column->primaryKey((bool) $info['pk']); + $column->dbType($dbType); + $column->defaultValue($this->normalizeDefaultValue($info['dflt_value'], $column)); - if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType() ?? '', $matches)) { - $type = strtolower($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 (!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]; + } - if (($type === 'tinyint' || $type === 'bit') && $column->getSize() === 1) { - $column->type(self::TYPE_BOOLEAN); - } elseif ($type === 'bit') { - if ($column->getSize() > 32) { - $column->type(self::TYPE_BIGINT); - } elseif ($column->getSize() === 32) { - $column->type(self::TYPE_INTEGER); - } - } + if (($dbType === 'tinyint' || $dbType === 'bit') && $info['size'] === 1) { + return self::TYPE_BOOLEAN; } - } - $column->phpType($this->getColumnPhpType($column)); - $column->defaultValue($this->normalizeDefaultValue($info['dflt_value'], $column)); + if ($dbType === 'bit') { + return match (true) { + $info['size'] === 32 => self::TYPE_INTEGER, + $info['size'] > 32 => self::TYPE_BIGINT, + default => self::TYPE_SMALLINT, + }; + } + } - return $column; + return self::TYPE_MAP[$dbType] ?? self::TYPE_STRING; } /** @@ -627,18 +644,6 @@ private function loadTableConstraints(string $tableName, string $returnType): Co return $result[$returnType]; } - /** - * 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. - */ - private function createColumnSchema(string $name): ColumnSchemaInterface - { - return new ColumnSchema($name); - } - /** * @throws Exception * @throws InvalidConfigException diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 65187aa5..e942084b 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -5,20 +5,23 @@ namespace Yiisoft\Db\Sqlite\Tests; use PDO; -use PHPUnit\Framework\TestCase; use Yiisoft\Db\Command\Param; -use Yiisoft\Db\Expression\JsonExpression; -use Yiisoft\Db\Sqlite\ColumnSchema; -use Yiisoft\Db\Schema\SchemaInterface; +use Yiisoft\Db\Schema\Column\BinaryColumnSchema; +use Yiisoft\Db\Schema\Column\BooleanColumnSchema; +use Yiisoft\Db\Schema\Column\DoubleColumnSchema; +use Yiisoft\Db\Schema\Column\IntegerColumnSchema; +use Yiisoft\Db\Schema\Column\JsonColumnSchema; +use Yiisoft\Db\Schema\Column\StringColumnSchema; use Yiisoft\Db\Sqlite\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; +use Yiisoft\Db\Tests\Common\CommonColumnSchemaTest; use function str_repeat; /** * @group sqlite */ -final class ColumnSchemaTest extends TestCase +final class ColumnSchemaTest extends CommonColumnSchemaTest { use TestTrait; @@ -75,13 +78,17 @@ public function testPhpTypeCast(): void $db->close(); } - public function testTypeCastJson(): void + public function testColumnSchemaInstance() { - $columnSchema = new ColumnSchema('json_col'); - $columnSchema->dbType(SchemaInterface::TYPE_JSON); - $columnSchema->type(SchemaInterface::TYPE_JSON); + $db = $this->getConnection(true); + $schema = $db->getSchema(); + $tableSchema = $schema->getTableSchema('type'); - $this->assertSame(['a' => 1], $columnSchema->phpTypeCast('{"a":1}')); - $this->assertEquals(new JsonExpression(['a' => 1], SchemaInterface::TYPE_JSON), $columnSchema->dbTypeCast(['a' => 1])); + $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')); + $this->assertInstanceOf(JsonColumnSchema::class, $tableSchema->getColumn('json_col')); } }