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
40 changes: 18 additions & 22 deletions src/ColumnSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,14 @@ final class ColumnSchema extends AbstractColumnSchema
*/
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);
return match (true) {
$value === null
=> null,
$this->getType() === SchemaInterface::TYPE_JSON
=> json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR),
default
=> parent::phpTypecast($value),
};
}

/**
Expand All @@ -74,18 +73,15 @@ public function phpTypecast(mixed $value): mixed
*/
public function dbTypecast(mixed $value): mixed
{
if ($value === null) {
return null;
}

if ($value instanceof ExpressionInterface) {
return $value;
}

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

return $this->typecast($value);
return match (true) {
$value === null
=> null,
$value instanceof ExpressionInterface
=> $value,
$this->getType() === SchemaInterface::TYPE_JSON
=> new JsonExpression($value, $this->getDbType()),
default
=> parent::dbTypecast($value),
};
}
}
101 changes: 53 additions & 48 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -488,13 +488,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 +526,67 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface
}
}

$column->phpType($this->getColumnPhpType($column));

if (!$column->isPrimaryKey()) {
// Chapter 2: crutches for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747}
/** @psalm-var string $columnCategory */
$columnCategory = $this->createColumn(
// 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($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';
}
)->getCategoryMap()[$column->getType()] ?? null, [
AbstractColumn::TYPE_CATEGORY_STRING,
AbstractColumn::TYPE_CATEGORY_TIME,
], true)
) {
$extra = 'DEFAULT_GENERATED';
}

/**
* 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']));
$column->extra($extra);
$column->phpType($this->getColumnPhpType($column));
$column->defaultValue($this->normalizeDefaultValue($info['default'], $column));

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 $columnSchema The column schema object.
*
* @return mixed The normalized default value.
*/
private function normalizeDefaultValue(?string $defaultValue, ColumnSchemaInterface $columnSchema): mixed
{
return match (true) {
$defaultValue === null
=> null,
$columnSchema->isPrimaryKey()
=> $columnSchema->phpTypecast($defaultValue),
in_array($columnSchema->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($columnSchema->getExtra())
&& !empty($defaultValue)
=> new Expression($defaultValue),
str_starts_with(strtolower((string) $columnSchema->getDbType()), 'bit')
=> $columnSchema->phpTypecast(bindec(trim($defaultValue, "b'"))),
default
=> $columnSchema->phpTypecast($defaultValue),
};
}

/**
* Loads all check constraints for the given table.
*
Expand Down
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