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

Typecast refactoring #295

Merged
merged 16 commits into from
Jul 16, 2023
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
}
}
101 changes: 48 additions & 53 deletions src/Schema.php
Original file line number Diff line number Diff line change
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 All @@ -33,7 +32,6 @@
use function preg_match;
use function serialize;
use function stripos;
use function strtolower;
use function trim;

/**
Expand Down Expand Up @@ -475,7 +473,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,15 +486,8 @@ 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]);
$type = $matches[1];
Tigrov marked this conversation as resolved.
Show resolved Hide resolved

if (isset($this->typeMap[$type])) {
$column->type($this->typeMap[$type]);
Expand Down Expand Up @@ -533,55 +524,59 @@ 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
{
return match (true) {
$defaultValue === null => null,

$column->isPrimaryKey() => $column->phpTypecast($defaultValue),

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
=> new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : '')),

!empty($column->getExtra()) && !empty($defaultValue) => new Expression($defaultValue),

str_starts_with((string) $column->getDbType(), 'bit')
=> $column->phpTypecast(bindec(trim($defaultValue, "b'"))),

default => $column->phpTypecast($defaultValue),
};
}

/**
* Loads all check constraints for the given table.
*
Expand Down
50 changes: 50 additions & 0 deletions tests/ColumnSchemaTest.php
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ public static function columnsTypeBit(): array
'size' => 1,
'precision' => 1,
'scale' => null,
'defaultValue' => null,
'defaultValue' => false,
darkdef marked this conversation as resolved.
Show resolved Hide resolved
],
'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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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',
darkdef marked this conversation as resolved.
Show resolved Hide resolved
`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
Loading