Skip to content

Commit 04a6a66

Browse files
authored
Typecast refactoring (#295)
* 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`
1 parent 9db293e commit 04a6a66

File tree

8 files changed

+119
-66
lines changed

8 files changed

+119
-66
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ composer.phar
3131

3232
# phpunit itself is not needed
3333
phpunit.phar
34-
# local phpunit config
34+
35+
# local phpunit config and cache
3536
/phpunit.xml
37+
/.phpunit.result.cache
3638

3739
# ignore dev installed apps and extensions
3840
/apps

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
## 1.0.1 under development
44

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

77
## 1.0.0 April 12, 2023
88

9-
- Initial release.
9+
- Initial release.

src/ColumnSchema.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,14 @@ public function phpTypecast(mixed $value): mixed
7474
*/
7575
public function dbTypecast(mixed $value): mixed
7676
{
77-
if ($value === null) {
78-
return null;
79-
}
80-
81-
if ($value instanceof ExpressionInterface) {
77+
if ($value === null || $value instanceof ExpressionInterface) {
8278
return $value;
8379
}
8480

85-
if ($this->getDbType() === SchemaInterface::TYPE_JSON) {
86-
return new JsonExpression($value, $this->getType());
81+
if ($this->getType() === SchemaInterface::TYPE_JSON) {
82+
return new JsonExpression($value, $this->getDbType());
8783
}
8884

89-
return $this->typecast($value);
85+
return parent::dbTypecast($value);
9086
}
9187
}

src/Schema.php

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Yiisoft\Db\Exception\NotSupportedException;
1616
use Yiisoft\Db\Expression\Expression;
1717
use Yiisoft\Db\Helper\DbArrayHelper;
18-
use Yiisoft\Db\Schema\Builder\AbstractColumn;
1918
use Yiisoft\Db\Schema\Builder\ColumnInterface;
2019
use Yiisoft\Db\Schema\ColumnSchemaInterface;
2120
use Yiisoft\Db\Schema\TableSchemaInterface;
@@ -475,7 +474,7 @@ protected function getCreateTableSql(TableSchemaInterface $table): string
475474
*/
476475
protected function loadColumnSchema(array $info): ColumnSchemaInterface
477476
{
478-
$dbType = $info['type'] ?? '';
477+
$dbType = $info['type'];
479478

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

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

491-
$extra = $info['extra'];
492-
493-
if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
494-
$extra = strtoupper(substr($extra, 18));
495-
}
496-
$column->extra(trim($extra));
497-
498490
if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $dbType, $matches)) {
499491
$type = strtolower($matches[1]);
500492

@@ -533,55 +525,67 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface
533525
}
534526
}
535527

536-
$column->phpType($this->getColumnPhpType($column));
528+
// Chapter 2: crutches for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747}
529+
$extra = $info['extra'];
530+
if (
531+
empty($extra)
532+
&& !empty($info['extra_default_value'])
533+
&& !str_starts_with($info['extra_default_value'], '\'')
534+
&& in_array($column->getType(), [
535+
self::TYPE_CHAR, self::TYPE_STRING, self::TYPE_TEXT,
536+
self::TYPE_DATETIME, self::TYPE_TIMESTAMP, self::TYPE_TIME, self::TYPE_DATE,
537+
], true)
538+
) {
539+
$extra = 'DEFAULT_GENERATED';
540+
}
537541

538-
if (!$column->isPrimaryKey()) {
539-
// Chapter 2: crutches for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747}
540-
/** @psalm-var string $columnCategory */
541-
$columnCategory = $this->createColumn(
542-
$column->getType(),
543-
$column->getSize()
544-
)->getCategoryMap()[$column->getType()] ?? '';
545-
$defaultValue = $info['extra_default_value'] ?? '';
546-
547-
if (
548-
empty($info['extra']) &&
549-
!empty($defaultValue) &&
550-
in_array($columnCategory, [
551-
AbstractColumn::TYPE_CATEGORY_STRING,
552-
AbstractColumn::TYPE_CATEGORY_TIME,
553-
], true)
554-
&& !str_starts_with($defaultValue, '\'')
555-
) {
556-
$info['extra'] = 'DEFAULT_GENERATED';
557-
}
542+
$column->extra($extra);
543+
$column->phpType($this->getColumnPhpType($column));
544+
$column->defaultValue($this->normalizeDefaultValue($info['default'], $column));
558545

559-
/**
560-
* When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
561-
* as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
562-
*
563-
* See details here: https://mariadb.com/kb/en/library/now/#description
564-
*/
565-
if (
566-
in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], true)
567-
&& preg_match('/^current_timestamp(?:\((\d*)\))?$/i', (string) $info['default'], $matches)
568-
) {
569-
$column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
570-
? '(' . $matches[1] . ')' : '')));
571-
} elseif (!empty($info['extra']) && !empty($info['default'])) {
572-
$column->defaultValue(new Expression($info['default']));
573-
} elseif (isset($type) && $type === 'bit' && $column->getType() !== self::TYPE_BOOLEAN) {
574-
$column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
575-
} else {
576-
$column->defaultValue($column->phpTypecast($info['default']));
577-
}
578-
} elseif ($info['default'] !== null) {
579-
$column->defaultValue($column->phpTypecast($info['default']));
546+
if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
547+
$column->extra(trim(strtoupper(substr($extra, 18))));
580548
}
581549

582550
return $column;
583551
}
584552

553+
/**
554+
* Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
555+
*
556+
* @param string|null $defaultValue The default value retrieved from the database.
557+
* @param ColumnSchemaInterface $column The column schema object.
558+
*
559+
* @return mixed The normalized default value.
560+
*/
561+
private function normalizeDefaultValue(?string $defaultValue, ColumnSchemaInterface $column): mixed
562+
{
563+
if ($defaultValue === null) {
564+
return null;
565+
}
566+
567+
if ($column->isPrimaryKey()) {
568+
return $column->phpTypecast($defaultValue);
569+
}
570+
571+
if (
572+
in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], true)
573+
&& preg_match('/^current_timestamp(?:\((\d*)\))?$/i', $defaultValue, $matches) === 1
574+
) {
575+
return new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : ''));
576+
}
577+
578+
if (!empty($column->getExtra()) && !empty($defaultValue)) {
579+
return new Expression($defaultValue);
580+
}
581+
582+
if (str_starts_with(strtolower((string) $column->getDbType()), 'bit')) {
583+
return $column->phpTypecast(bindec(trim($defaultValue, "b'")));
584+
}
585+
586+
return $column->phpTypecast($defaultValue);
587+
}
588+
585589
/**
586590
* Loads all check constraints for the given table.
587591
*

tests/ColumnSchemaTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,54 @@ public function testPhpTypeCastJson(): void
6262

6363
$this->assertSame(['a' => 1], $columnSchema->phpTypeCast('{"a":1}'));
6464
}
65+
66+
public function testPhpTypeCast(): void
67+
{
68+
$db = $this->getConnection(true);
69+
70+
$command = $db->createCommand();
71+
$schema = $db->getSchema();
72+
$tableSchema = $schema->getTableSchema('type');
73+
74+
$command->insert(
75+
'type',
76+
[
77+
'int_col' => 1,
78+
'char_col' => str_repeat('x', 100),
79+
'char_col3' => null,
80+
'float_col' => 1.234,
81+
'blob_col' => "\x10\x11\x12",
82+
'time' => '2023-07-11 14:50:23',
83+
'bool_col' => false,
84+
'bit_col' => 0b0110_0100, // 100
85+
'json_col' => [['a' => 1, 'b' => null, 'c' => [1, 3, 5]]],
86+
]
87+
);
88+
$command->execute();
89+
$query = (new Query($db))->from('type')->one();
90+
91+
$this->assertNotNull($tableSchema);
92+
93+
$intColPhpType = $tableSchema->getColumn('int_col')?->phpTypecast($query['int_col']);
94+
$charColPhpType = $tableSchema->getColumn('char_col')?->phpTypecast($query['char_col']);
95+
$charCol3PhpType = $tableSchema->getColumn('char_col')?->phpTypecast($query['char_col3']);
96+
$floatColPhpType = $tableSchema->getColumn('float_col')?->phpTypecast($query['float_col']);
97+
$blobColPhpType = $tableSchema->getColumn('blob_col')?->phpTypecast($query['blob_col']);
98+
$timePhpType = $tableSchema->getColumn('time')?->phpTypecast($query['time']);
99+
$boolColPhpType = $tableSchema->getColumn('bool_col')?->phpTypecast($query['bool_col']);
100+
$bitColPhpType = $tableSchema->getColumn('bit_col')?->phpTypecast($query['bit_col']);
101+
$jsonColPhpType = $tableSchema->getColumn('json_col')?->phpTypecast($query['json_col']);
102+
103+
$this->assertSame(1, $intColPhpType);
104+
$this->assertSame(str_repeat('x', 100), $charColPhpType);
105+
$this->assertNull($charCol3PhpType);
106+
$this->assertSame(1.234, $floatColPhpType);
107+
$this->assertSame("\x10\x11\x12", $blobColPhpType);
108+
$this->assertSame('2023-07-11 14:50:23', $timePhpType);
109+
$this->assertFalse($boolColPhpType);
110+
$this->assertSame(0b0110_0100, $bitColPhpType);
111+
$this->assertSame([['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], $jsonColPhpType);
112+
113+
$db->close();
114+
}
65115
}

tests/Provider/SchemaProvider.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ public static function columnsTypeBit(): array
303303
'size' => 1,
304304
'precision' => 1,
305305
'scale' => null,
306-
'defaultValue' => null,
306+
'defaultValue' => false,
307307
],
308308
'bit_col_2' => [
309309
'type' => 'boolean',
@@ -329,7 +329,7 @@ public static function columnsTypeBit(): array
329329
'size' => 32,
330330
'precision' => 32,
331331
'scale' => null,
332-
'defaultValue' => 0,
332+
'defaultValue' => null,
333333
],
334334
'bit_col_4' => [
335335
'type' => 'integer',
@@ -355,7 +355,7 @@ public static function columnsTypeBit(): array
355355
'size' => 64,
356356
'precision' => 64,
357357
'scale' => null,
358-
'defaultValue' => 0,
358+
'defaultValue' => null,
359359
],
360360
'bit_col_6' => [
361361
'type' => 'bigint',

tests/SchemaTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public function testAlternativeDisplayOfDefaultCurrentTimestampInMariaDB(): void
6161
'key' => '',
6262
'default' => 'current_timestamp()',
6363
'extra' => '',
64+
'extra_default_value' => 'current_timestamp()',
6465
'privileges' => 'select,insert,update,references',
6566
'comment' => '',
6667
]]);

tests/Support/Fixture/mysql.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ CREATE TABLE `type` (
167167
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
168168

169169
CREATE TABLE `type_bit` (
170-
`bit_col_1` BIT(1) NOT NULL,
170+
`bit_col_1` BIT(1) NOT NULL DEFAULT b'0',
171171
`bit_col_2` BIT(1) DEFAULT b'1',
172172
`bit_col_3` BIT(32) NOT NULL,
173173
`bit_col_4` BIT(32) DEFAULT b'10000010',

0 commit comments

Comments
 (0)