Skip to content

Commit

Permalink
Typecast refactoring (#295)
Browse files Browse the repository at this point in the history
* Typecast refactoring

* Fix test issues

* Fix test issues

* Update

* Rename `$columnSchema` to `$column`

* $info['type'] is always string

* Update

* Update

* Keep methods order

* Update

* Add EOLs

* Add tests

* Add tests

* Remove strtolower($dbType) MySQL always returns type in lower case

* Revert strtolower($dbType)

* Split `match (true)` in `if`
  • Loading branch information
Tigrov committed Jul 16, 2023
1 parent 9db293e commit 04a6a66
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 66 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Expand Up @@ -31,8 +31,10 @@ composer.phar

# phpunit itself is not needed
phpunit.phar
# local phpunit config

# local phpunit config and cache
/phpunit.xml
/.phpunit.result.cache

# ignore dev installed apps and extensions
/apps
Expand Down
4 changes: 2 additions & 2 deletions CHANGELOG.md
Expand Up @@ -2,8 +2,8 @@

## 1.0.1 under development

- no changes in this release.
- Enh #295: Typecast refactoring (@Tigrov)

## 1.0.0 April 12, 2023

- Initial release.
- Initial release.
12 changes: 4 additions & 8 deletions src/ColumnSchema.php
Expand Up @@ -74,18 +74,14 @@ public function phpTypecast(mixed $value): mixed
*/
public function dbTypecast(mixed $value): mixed
{
if ($value === null) {
return null;
}

if ($value instanceof ExpressionInterface) {
if ($value === null || $value instanceof ExpressionInterface) {
return $value;
}

if ($this->getDbType() === SchemaInterface::TYPE_JSON) {
return new JsonExpression($value, $this->getType());
if ($this->getType() === SchemaInterface::TYPE_JSON) {
return new JsonExpression($value, $this->getDbType());
}

return $this->typecast($value);
return parent::dbTypecast($value);
}
}
106 changes: 55 additions & 51 deletions src/Schema.php
Expand Up @@ -15,7 +15,6 @@
use Yiisoft\Db\Exception\NotSupportedException;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Helper\DbArrayHelper;
use Yiisoft\Db\Schema\Builder\AbstractColumn;
use Yiisoft\Db\Schema\Builder\ColumnInterface;
use Yiisoft\Db\Schema\ColumnSchemaInterface;
use Yiisoft\Db\Schema\TableSchemaInterface;
Expand Down Expand Up @@ -475,7 +474,7 @@ protected function getCreateTableSql(TableSchemaInterface $table): string
*/
protected function loadColumnSchema(array $info): ColumnSchemaInterface
{
$dbType = $info['type'] ?? '';
$dbType = $info['type'];

$column = $this->createColumnSchema($info['field']);

Expand All @@ -488,13 +487,6 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface
$column->unsigned(stripos($dbType, 'unsigned') !== false);
$column->type(self::TYPE_STRING);

$extra = $info['extra'];

if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
$extra = strtoupper(substr($extra, 18));
}
$column->extra(trim($extra));

if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $dbType, $matches)) {
$type = strtolower($matches[1]);

Expand Down Expand Up @@ -533,55 +525,67 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface
}
}

$column->phpType($this->getColumnPhpType($column));
// Chapter 2: crutches for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747}
$extra = $info['extra'];
if (
empty($extra)
&& !empty($info['extra_default_value'])
&& !str_starts_with($info['extra_default_value'], '\'')
&& in_array($column->getType(), [
self::TYPE_CHAR, self::TYPE_STRING, self::TYPE_TEXT,
self::TYPE_DATETIME, self::TYPE_TIMESTAMP, self::TYPE_TIME, self::TYPE_DATE,
], true)
) {
$extra = 'DEFAULT_GENERATED';
}

if (!$column->isPrimaryKey()) {
// Chapter 2: crutches for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747}
/** @psalm-var string $columnCategory */
$columnCategory = $this->createColumn(
$column->getType(),
$column->getSize()
)->getCategoryMap()[$column->getType()] ?? '';
$defaultValue = $info['extra_default_value'] ?? '';

if (
empty($info['extra']) &&
!empty($defaultValue) &&
in_array($columnCategory, [
AbstractColumn::TYPE_CATEGORY_STRING,
AbstractColumn::TYPE_CATEGORY_TIME,
], true)
&& !str_starts_with($defaultValue, '\'')
) {
$info['extra'] = 'DEFAULT_GENERATED';
}
$column->extra($extra);
$column->phpType($this->getColumnPhpType($column));
$column->defaultValue($this->normalizeDefaultValue($info['default'], $column));

/**
* When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
* as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
*
* See details here: https://mariadb.com/kb/en/library/now/#description
*/
if (
in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], true)
&& preg_match('/^current_timestamp(?:\((\d*)\))?$/i', (string) $info['default'], $matches)
) {
$column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
? '(' . $matches[1] . ')' : '')));
} elseif (!empty($info['extra']) && !empty($info['default'])) {
$column->defaultValue(new Expression($info['default']));
} elseif (isset($type) && $type === 'bit' && $column->getType() !== self::TYPE_BOOLEAN) {
$column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
} else {
$column->defaultValue($column->phpTypecast($info['default']));
}
} elseif ($info['default'] !== null) {
$column->defaultValue($column->phpTypecast($info['default']));
if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
$column->extra(trim(strtoupper(substr($extra, 18))));
}

return $column;
}

/**
* Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
*
* @param string|null $defaultValue The default value retrieved from the database.
* @param ColumnSchemaInterface $column The column schema object.
*
* @return mixed The normalized default value.
*/
private function normalizeDefaultValue(?string $defaultValue, ColumnSchemaInterface $column): mixed
{
if ($defaultValue === null) {
return null;
}

if ($column->isPrimaryKey()) {
return $column->phpTypecast($defaultValue);
}

if (
in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], true)
&& preg_match('/^current_timestamp(?:\((\d*)\))?$/i', $defaultValue, $matches) === 1
) {
return new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : ''));
}

if (!empty($column->getExtra()) && !empty($defaultValue)) {
return new Expression($defaultValue);
}

if (str_starts_with(strtolower((string) $column->getDbType()), 'bit')) {
return $column->phpTypecast(bindec(trim($defaultValue, "b'")));
}

return $column->phpTypecast($defaultValue);
}

/**
* Loads all check constraints for the given table.
*
Expand Down
50 changes: 50 additions & 0 deletions tests/ColumnSchemaTest.php
Expand Up @@ -62,4 +62,54 @@ public function testPhpTypeCastJson(): void

$this->assertSame(['a' => 1], $columnSchema->phpTypeCast('{"a":1}'));
}

public function testPhpTypeCast(): void
{
$db = $this->getConnection(true);

$command = $db->createCommand();
$schema = $db->getSchema();
$tableSchema = $schema->getTableSchema('type');

$command->insert(
'type',
[
'int_col' => 1,
'char_col' => str_repeat('x', 100),
'char_col3' => null,
'float_col' => 1.234,
'blob_col' => "\x10\x11\x12",
'time' => '2023-07-11 14:50:23',
'bool_col' => false,
'bit_col' => 0b0110_0100, // 100
'json_col' => [['a' => 1, 'b' => null, 'c' => [1, 3, 5]]],
]
);
$command->execute();
$query = (new Query($db))->from('type')->one();

$this->assertNotNull($tableSchema);

$intColPhpType = $tableSchema->getColumn('int_col')?->phpTypecast($query['int_col']);
$charColPhpType = $tableSchema->getColumn('char_col')?->phpTypecast($query['char_col']);
$charCol3PhpType = $tableSchema->getColumn('char_col')?->phpTypecast($query['char_col3']);
$floatColPhpType = $tableSchema->getColumn('float_col')?->phpTypecast($query['float_col']);
$blobColPhpType = $tableSchema->getColumn('blob_col')?->phpTypecast($query['blob_col']);
$timePhpType = $tableSchema->getColumn('time')?->phpTypecast($query['time']);
$boolColPhpType = $tableSchema->getColumn('bool_col')?->phpTypecast($query['bool_col']);
$bitColPhpType = $tableSchema->getColumn('bit_col')?->phpTypecast($query['bit_col']);
$jsonColPhpType = $tableSchema->getColumn('json_col')?->phpTypecast($query['json_col']);

$this->assertSame(1, $intColPhpType);
$this->assertSame(str_repeat('x', 100), $charColPhpType);
$this->assertNull($charCol3PhpType);
$this->assertSame(1.234, $floatColPhpType);
$this->assertSame("\x10\x11\x12", $blobColPhpType);
$this->assertSame('2023-07-11 14:50:23', $timePhpType);
$this->assertFalse($boolColPhpType);
$this->assertSame(0b0110_0100, $bitColPhpType);
$this->assertSame([['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], $jsonColPhpType);

$db->close();
}
}
6 changes: 3 additions & 3 deletions tests/Provider/SchemaProvider.php
Expand Up @@ -303,7 +303,7 @@ public static function columnsTypeBit(): array
'size' => 1,
'precision' => 1,
'scale' => null,
'defaultValue' => null,
'defaultValue' => false,
],
'bit_col_2' => [
'type' => 'boolean',
Expand All @@ -329,7 +329,7 @@ public static function columnsTypeBit(): array
'size' => 32,
'precision' => 32,
'scale' => null,
'defaultValue' => 0,
'defaultValue' => null,
],
'bit_col_4' => [
'type' => 'integer',
Expand All @@ -355,7 +355,7 @@ public static function columnsTypeBit(): array
'size' => 64,
'precision' => 64,
'scale' => null,
'defaultValue' => 0,
'defaultValue' => null,
],
'bit_col_6' => [
'type' => 'bigint',
Expand Down
1 change: 1 addition & 0 deletions tests/SchemaTest.php
Expand Up @@ -61,6 +61,7 @@ public function testAlternativeDisplayOfDefaultCurrentTimestampInMariaDB(): void
'key' => '',
'default' => 'current_timestamp()',
'extra' => '',
'extra_default_value' => 'current_timestamp()',
'privileges' => 'select,insert,update,references',
'comment' => '',
]]);
Expand Down
2 changes: 1 addition & 1 deletion tests/Support/Fixture/mysql.sql
Expand Up @@ -167,7 +167,7 @@ CREATE TABLE `type` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `type_bit` (
`bit_col_1` BIT(1) NOT NULL,
`bit_col_1` BIT(1) NOT NULL DEFAULT b'0',
`bit_col_2` BIT(1) DEFAULT b'1',
`bit_col_3` BIT(32) NOT NULL,
`bit_col_4` BIT(32) DEFAULT b'10000010',
Expand Down

0 comments on commit 04a6a66

Please sign in to comment.