Skip to content

Use DateTimeColumn #323

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 5 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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
30 changes: 30 additions & 0 deletions src/Column/ColumnBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,36 @@ public static function boolean(): BooleanColumn
return new BooleanColumn(ColumnType::BOOLEAN);
}

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);
}

public static function json(): JsonColumn
{
return new JsonColumn(ColumnType::JSON);
Expand Down
6 changes: 4 additions & 2 deletions src/Column/ColumnDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,12 @@ protected function getDbType(ColumnInterface $column): string
ColumnType::TEXT => 'clob',
ColumnType::BINARY => 'blob',
ColumnType::UUID => 'raw(16)',
ColumnType::DATETIME => 'timestamp',
ColumnType::TIMESTAMP => 'timestamp',
ColumnType::DATE => 'date',
ColumnType::DATETIME => 'timestamp',
ColumnType::DATETIMETZ => 'timestamp' . ($size !== null ? "($size)" : '') . ' with time zone',
ColumnType::TIME => 'interval day(0) to second',
ColumnType::TIMETZ => 'interval day(0) to second',
ColumnType::DATE => 'date',
ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON =>
version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '>=')
? 'json'
Expand Down
29 changes: 25 additions & 4 deletions src/Column/ColumnFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
namespace Yiisoft\Db\Oracle\Column;

use Yiisoft\Db\Constant\ColumnType;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Schema\Column\AbstractColumnFactory;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function date_create_immutable;
use function preg_match;
use function rtrim;
use function strcasecmp;

final class ColumnFactory extends AbstractColumnFactory
{
private const DATETIME_REGEX = "/^(?:TIMESTAMP|DATE|INTERVAL|to_timestamp(?:_tz)?\(|to_date\(|to_dsinterval\()\s*'(?:\d )?([^']+)/";

/**
* The mapping from physical column types (keys) to abstract column types (values).
*
Expand All @@ -39,9 +44,9 @@ final class ColumnFactory extends AbstractColumnFactory
'binary_double' => ColumnType::DOUBLE, // 64 bit
'float' => ColumnType::DOUBLE, // 126 bit
'date' => ColumnType::DATE,
'timestamp' => ColumnType::TIMESTAMP,
'timestamp with time zone' => ColumnType::TIMESTAMP,
'timestamp with local time zone' => ColumnType::TIMESTAMP,
'timestamp' => ColumnType::DATETIME,
'timestamp with time zone' => ColumnType::DATETIMETZ,
'timestamp with local time zone' => ColumnType::DATETIME,
'interval day to second' => ColumnType::STRING,
'interval year to month' => ColumnType::STRING,
'json' => ColumnType::JSON,
Expand Down Expand Up @@ -91,13 +96,29 @@ protected function getColumnClass(string $type, array $info = []): string
return match ($type) {
ColumnType::BINARY => BinaryColumn::class,
ColumnType::BOOLEAN => BooleanColumn::class,
ColumnType::DATETIME => DateTimeColumn::class,
ColumnType::DATETIMETZ => DateTimeColumn::class,
ColumnType::TIME => DateTimeColumn::class,
ColumnType::TIMETZ => DateTimeColumn::class,
ColumnType::DATE => DateTimeColumn::class,
ColumnType::JSON => JsonColumn::class,
default => parent::getColumnClass($type, $info),
};
}

protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnInterface $column): mixed
{
return parent::normalizeNotNullDefaultValue(rtrim($defaultValue), $column);
$value = parent::normalizeNotNullDefaultValue(rtrim($defaultValue), $column);

if ($column instanceof DateTimeColumn
&& $value instanceof Expression
&& preg_match(self::DATETIME_REGEX, (string) $value, $matches) === 1
) {
return date_create_immutable($matches[1]) !== false
? $column->phpTypecast($matches[1])
: new Expression($matches[1]);
}

return $value;
}
}
66 changes: 66 additions & 0 deletions src/Column/DateTimeColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle\Column;

use DateTimeImmutable;
use Yiisoft\Db\Constant\ColumnType;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Expression\ExpressionInterface;

use function explode;
use function is_string;
use function str_replace;

final class DateTimeColumn extends \Yiisoft\Db\Schema\Column\DateTimeColumn
{
public function dbTypecast(mixed $value): string|ExpressionInterface|null
{
$value = parent::dbTypecast($value);

if (!is_string($value)) {
return $value;
}

$value = str_replace(["'", '"', "\000", "\032"], '', $value);

Check warning on line 26 in src/Column/DateTimeColumn.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "UnwrapStrReplace": @@ @@ if (!is_string($value)) { return $value; } - $value = str_replace(["'", '"', "\x00", "\x1a"], '', $value); + $value = $value; return match ($this->getType()) { ColumnType::TIMESTAMP, ColumnType::DATETIME, ColumnType::DATETIMETZ => new Expression("TIMESTAMP '{$value}'"), ColumnType::TIME, ColumnType::TIMETZ => new Expression("INTERVAL '{$value}' DAY(0) TO SECOND" . (($size = $this->getSize()) !== null ? "({$size})" : '')),

return match ($this->getType()) {
ColumnType::TIMESTAMP, ColumnType::DATETIME, ColumnType::DATETIMETZ => new Expression("TIMESTAMP '$value'"),
ColumnType::TIME, ColumnType::TIMETZ => new Expression(
"INTERVAL '$value' DAY(0) TO SECOND" . (($size = $this->getSize()) !== null ? "($size)" : '')
),
ColumnType::DATE => new Expression("DATE '$value'"),
default => $value,
};
}

public function phpTypecast(mixed $value): DateTimeImmutable|null
{
if (is_string($value) && match ($this->getType()) {
ColumnType::TIME, ColumnType::TIMETZ => true,
default => false,
}) {
$value = explode(' ', $value, 2)[1] ?? $value;
}

return parent::phpTypecast($value);
}

protected function getFormat(): string
{
return $this->format ??= match ($this->getType()) {

Check warning on line 52 in src/Column/DateTimeColumn.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "AssignCoalesce": @@ @@ } protected function getFormat(): string { - return $this->format ??= match ($this->getType()) { + return $this->format = match ($this->getType()) { ColumnType::TIME, ColumnType::TIMETZ => '0 H:i:s' . $this->getMillisecondsFormat(), default => parent::getFormat(), };
ColumnType::TIME, ColumnType::TIMETZ => '0 H:i:s' . $this->getMillisecondsFormat(),
default => parent::getFormat(),
};
}

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

use Throwable;
use Yiisoft\Db\Connection\ServerInfoInterface;
use Yiisoft\Db\Driver\Pdo\AbstractPdoConnection;
use Yiisoft\Db\Driver\Pdo\PdoCommandInterface;
use Yiisoft\Db\Exception\Exception;
Expand Down Expand Up @@ -92,4 +93,9 @@ public function getSchema(): SchemaInterface
{
return $this->schema ??= new Schema($this, $this->schemaCache, strtoupper($this->driver->getUsername()));
}

public function getServerInfo(): ServerInfoInterface
{
return $this->serverInfo ??= new ServerInfo($this);
}
}
16 changes: 15 additions & 1 deletion src/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,21 @@ final class Driver extends AbstractPdoDriver
public function createConnection(): PDO
{
$this->attributes += [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
return parent::createConnection();

$pdo = parent::createConnection();

$pdo->exec(
<<<SQL
ALTER SESSION SET
NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SSXFF'
NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SSXFFTZH:TZM'
NLS_TIME_FORMAT = 'HH24:MI:SSXFF'
NLS_TIME_TZ_FORMAT = 'HH24:MI:SSXFFTZH:TZM'
NLS_DATE_FORMAT = 'YYYY-MM-DD'
SQL
);

return $pdo;
}

public function getDriverName(): string
Expand Down
14 changes: 12 additions & 2 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ protected function loadResultColumn(array $metadata): ColumnInterface|null
default => $columnInfo['size'] = $metadata['len'],
};

if ($dbType === 'timestamp with local time zone') {
$columnInfo['dbTimezone'] = $this->db->getServerInfo()->getTimezone();
}

$columnInfo['notNull'] = in_array('not_null', $metadata['flags'], true);

/** @psalm-suppress MixedArgumentTypeCoercion */
Expand Down Expand Up @@ -517,7 +521,7 @@ private function loadColumn(array $info): ColumnInterface
default => null,
};

return $this->db->getColumnFactory()->fromDbType($dbType, [
$columnInfo = [
'autoIncrement' => $info['identity_column'] === 'YES',
'check' => $info['check'],
'comment' => $info['column_comment'],
Expand All @@ -530,7 +534,13 @@ private function loadColumn(array $info): ColumnInterface
'size' => $info['size'] !== null ? (int) $info['size'] : null,
'table' => $info['table'],
'unique' => $info['constraint_type'] === 'U',
]);
];

if ($dbType === 'timestamp with local time zone') {
$columnInfo['dbTimezone'] = $this->db->getServerInfo()->getTimezone();
}

return $this->db->getColumnFactory()->fromDbType($dbType, $columnInfo);
}

/**
Expand Down
24 changes: 24 additions & 0 deletions src/ServerInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle;

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 SESSIONTIMEZONE FROM DUAL')->queryScalar();
}

return $this->timezone;
}
}
Loading
Loading