Skip to content

Use DateTimeColumn #393

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

Merged
merged 4 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
- Bug #388: Set empty `comment` and `extra` properties to `null` when loading table columns (@Tigrov)
- Enh #389, #390: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov)
- New #387: Realize `Schema::loadResultColumn()` method (@Tigrov)
- New #393: Use `DateTimeColumn` class for datetime column types (@Tigrov)

## 1.2.0 March 21, 2024

Expand Down
31 changes: 31 additions & 0 deletions src/Column/ColumnBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@

namespace Yiisoft\Db\Mysql\Column;

use Yiisoft\Db\Constant\ColumnType;

final class ColumnBuilder extends \Yiisoft\Db\Schema\Column\ColumnBuilder
{
public static function timestamp(int|null $size = 0): DateTimeColumn
{
return new DateTimeColumn(ColumnType::TIMESTAMP, size: $size);
}

public static function datetime(int|null $size = 0): DateTimeColumn
{
return new DateTimeColumn(ColumnType::DATETIME, size: $size);
}

public static function datetimeWithTimezone(int|null $size = 0): DateTimeColumn
{
return new DateTimeColumn(ColumnType::DATETIMETZ, size: $size);
}

public static function time(int|null $size = 0): DateTimeColumn
{
return new DateTimeColumn(ColumnType::TIME, size: $size);
}

public static function timeWithTimezone(int|null $size = 0): DateTimeColumn
{
return new DateTimeColumn(ColumnType::TIMETZ, size: $size);
}

public static function date(): DateTimeColumn
{
return new DateTimeColumn(ColumnType::DATE);
}
}
10 changes: 6 additions & 4 deletions src/Column/ColumnDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ final class ColumnDefinitionBuilder extends AbstractColumnDefinitionBuilder
'varbinary',
'blob',
'year',
'time',
'datetime',
'timestamp',
'datetime',
'time',
];

protected const TYPES_WITH_SCALE = [
Expand Down Expand Up @@ -87,10 +87,12 @@ protected function getDbType(ColumnInterface $column): string
ColumnType::TEXT => 'text',
ColumnType::BINARY => 'blob',
ColumnType::UUID => 'binary(16)',
ColumnType::DATETIME => 'datetime',
ColumnType::TIMESTAMP => 'timestamp',
ColumnType::DATE => 'date',
ColumnType::DATETIME => 'datetime',
ColumnType::DATETIMETZ => 'datetime',
ColumnType::TIME => 'time',
ColumnType::TIMETZ => 'time',
ColumnType::DATE => 'date',
ColumnType::ARRAY => 'json',
ColumnType::STRUCTURED => 'json',
ColumnType::JSON => 'json',
Expand Down
21 changes: 17 additions & 4 deletions src/Column/ColumnFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,27 @@ final class ColumnFactory extends AbstractColumnFactory
'tinyblob' => ColumnType::BINARY,
'mediumblob' => ColumnType::BINARY,
'longblob' => ColumnType::BINARY,
'year' => ColumnType::DATE,
'date' => ColumnType::DATE,
'time' => ColumnType::TIME,
'datetime' => ColumnType::DATETIME,
'year' => ColumnType::SMALLINT,
'timestamp' => ColumnType::TIMESTAMP,
'datetime' => ColumnType::DATETIME,
'time' => ColumnType::TIME,
'date' => ColumnType::DATE,
'json' => ColumnType::JSON,
];

protected function getColumnClass(string $type, array $info = []): string
{
return match ($type) {
ColumnType::TIMESTAMP => DateTimeColumn::class,
ColumnType::DATETIME => DateTimeColumn::class,
ColumnType::DATETIMETZ => DateTimeColumn::class,
ColumnType::TIME => DateTimeColumn::class,
ColumnType::TIMETZ => DateTimeColumn::class,
ColumnType::DATE => DateTimeColumn::class,
default => parent::getColumnClass($type, $info),
};
}

protected function getType(string $dbType, array $info = []): string
{
if ($dbType === 'bit' && isset($info['size']) && $info['size'] === 1) {
Expand Down
39 changes: 39 additions & 0 deletions src/Column/DateTimeColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Mysql\Column;

use Yiisoft\Db\Constant\ColumnType;

/**
* Represents the metadata for a datetime column.
*
* > [!WARNING]
* > MySQL DBMS converts `TIMESTAMP` column type values from database session time zone to UTC for storage, and back
* > from UTC to the session time zone when retrieve the values.
*
* `TIMESTAMP` database type does not store time zone offset and require to convert datetime values to the database
* session time zone before insert and back to the PHP time zone after retrieve the values. This will be done in the
* {@see dbTypecast()} and {@see phpTypecast()} methods and guarantees that the values are stored in the database
* in the correct time zone.
*
* To avoid possible time zone issues with the datetime values conversion, it is recommended to set the PHP and database
* time zones to UTC.
*/
final class DateTimeColumn extends \Yiisoft\Db\Schema\Column\DateTimeColumn
{
protected function getFormat(): string
{
return $this->format ??= match ($this->getType()) {
ColumnType::DATETIMETZ => 'Y-m-d H:i:s' . $this->getMillisecondsFormat(),
ColumnType::TIMETZ => 'H:i:s' . $this->getMillisecondsFormat(),
default => parent::getFormat(),
};
}

protected function shouldConvertTimezone(): bool
{
return $this->shouldConvertTimezone ??= !empty($this->dbTimezone) && $this->getType() !== ColumnType::DATE;
}
}
6 changes: 6 additions & 0 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Psr\Log\LogLevel;
use Throwable;
use Yiisoft\Db\Connection\ServerInfoInterface;
use Yiisoft\Db\Driver\Pdo\AbstractPdoConnection;
use Yiisoft\Db\Driver\Pdo\PdoCommandInterface;
use Yiisoft\Db\Mysql\Column\ColumnFactory;
Expand Down Expand Up @@ -84,4 +85,9 @@ public function getSchema(): SchemaInterface
{
return $this->schema ??= new Schema($this, $this->schemaCache);
}

public function getServerInfo(): ServerInfoInterface
{
return $this->serverInfo ??= new ServerInfo($this);
}
}
13 changes: 10 additions & 3 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use function str_starts_with;
use function strtolower;
use function substr;
use function substr_compare;
use function trim;

use const PHP_INT_SIZE;
Expand Down Expand Up @@ -424,6 +425,7 @@ protected function loadResultColumn(array $metadata): ColumnInterface|null
'float', 'double', 'decimal' => $columnInfo['scale'] = $metadata['precision'],
'bigint' => $metadata['len'] === 20 ? $columnInfo['unsigned'] = true : null,
'int' => $metadata['len'] === 10 && PHP_INT_SIZE !== 8 ? $columnInfo['unsigned'] = true : null,
'timestamp' => $columnInfo['dbTimezone'] = $this->db->getServerInfo()->getTimezone(),
default => null,
};

Expand All @@ -444,8 +446,7 @@ protected function loadResultColumn(array $metadata): ColumnInterface|null
private function loadColumn(array $info): ColumnInterface
{
$extra = trim(str_ireplace('auto_increment', '', $info['extra'], $autoIncrement));

$column = $this->db->getColumnFactory()->fromDefinition($info['column_type'], [
$columnInfo = [
'autoIncrement' => $autoIncrement > 0,
'comment' => $info['column_comment'] === '' ? null : $info['column_comment'],
'defaultValueRaw' => $info['column_default'],
Expand All @@ -456,7 +457,13 @@ private function loadColumn(array $info): ColumnInterface
'schema' => $info['schema'],
'table' => $info['table'],
'unique' => $info['column_key'] === 'UNI',
]);
];

if (substr_compare($info['column_type'], 'timestamp', 0, 9, true) === 0) {
$columnInfo['dbTimezone'] = $this->db->getServerInfo()->getTimezone();
}

$column = $this->db->getColumnFactory()->fromDefinition($info['column_type'], $columnInfo);

if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
$extra = trim(substr($extra, 18));
Expand Down
26 changes: 26 additions & 0 deletions src/ServerInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Mysql;

use Yiisoft\Db\Driver\Pdo\PdoServerInfo;

final class ServerInfo extends PdoServerInfo
{
/** @psalm-suppress PropertyNotSetInConstructor */
private string $timezone;

public function getTimezone(bool $refresh = false): string
{
/** @psalm-suppress TypeDoesNotContainType */
if (!isset($this->timezone) || $refresh) {
/** @var string */
$this->timezone = $this->db->createCommand(
"SELECT LPAD(TIME_FORMAT(TIMEDIFF(NOW(), UTC_TIMESTAMP), '%H:%i'), 6, '+')"
)->queryScalar();
}

return $this->timezone;
}
}
13 changes: 13 additions & 0 deletions tests/ColumnBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Yiisoft\Db\Mysql\Tests;

use PHPUnit\Framework\Attributes\DataProviderExternal;
use Yiisoft\Db\Mysql\Column\ColumnBuilder;
use Yiisoft\Db\Mysql\Tests\Provider\ColumnBuilderProvider;
use Yiisoft\Db\Mysql\Tests\Support\TestTrait;
use Yiisoft\Db\Tests\AbstractColumnBuilderTest;

Expand All @@ -19,4 +21,15 @@ public function getColumnBuilderClass(): string
{
return ColumnBuilder::class;
}

#[DataProviderExternal(ColumnBuilderProvider::class, 'buildingMethods')]
public function testBuildingMethods(
string $buildingMethod,
array $args,
string $expectedInstanceOf,
string $expectedType,
array $expectedMethodResults = [],
): void {
parent::testBuildingMethods($buildingMethod, $args, $expectedInstanceOf, $expectedType, $expectedMethodResults);
}
}
Loading