diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 6768c49..49b56ec 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -9,6 +9,7 @@ use Yiisoft\Db\Schema\AbstractColumnSchema; use Yiisoft\Db\Schema\SchemaInterface; +use function explode; use function is_string; use function preg_replace; use function uniqid; @@ -54,4 +55,18 @@ public function dbTypecast(mixed $value): mixed return parent::dbTypecast($value); } + + public function phpTypecast(mixed $value): mixed + { + if (is_string($value) && $this->getType() === SchemaInterface::TYPE_TIME) { + $value = explode(' ', $value)[1] ?? $value; + } + + return parent::phpTypecast($value); + } + + public function hasTimezone(): bool + { + return str_ends_with((string) $this->getDbType(), ' WITH TIME ZONE'); + } } diff --git a/src/Connection.php b/src/Connection.php index 144dd7d..9e7c786 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -97,4 +97,27 @@ public function getSchema(): SchemaInterface return $this->schema; } + + /** + * Initializes the DB connection and sets default date and time format. + * + * This method is invoked right after the DB connection is established. + * + * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`, if {@see getEmulatePrepare()} is `true`. + */ + protected function initConnection(): void + { + parent::initConnection(); + + $this->pdo->exec( + <<dbType($info['data_type']); $column->type($this->extractColumnType($column)); $column->phpType($this->getColumnPhpType($column)); + $column->dateTimeFormat($this->getDateTimeFormat($column)); $column->defaultValue($this->normalizeDefaultValue($info['data_default'], $column)); return $column; @@ -453,8 +454,11 @@ private function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaIn return null; } - if ($column->getType() === self::TYPE_TIMESTAMP && $defaultValue === 'CURRENT_TIMESTAMP') { - return new Expression($defaultValue); + if ($column->getDateTimeFormat() !== null) { + $dateTimeRegex = "/^(?:TIMESTAMP|DATE|INTERVAL|to_timestamp(?:_tz)?\(|to_date\(|to_dsinterval\()?\s*'(?:\d )?([^']+)'.*/"; + $value = preg_replace($dateTimeRegex, '$1', $defaultValue, 1); + + return date_create_immutable($value) ?: new Expression($defaultValue); } if (preg_match("/^'(.*)'$/s", $defaultValue, $matches) === 1) { @@ -624,6 +628,7 @@ private function extractColumnType(ColumnSchemaInterface $column): string 'BLOB' => self::TYPE_BINARY, 'CLOB' => self::TYPE_TEXT, 'TIMESTAMP' => self::TYPE_TIMESTAMP, + 'DATE' => self::TYPE_DATE, default => self::TYPE_STRING, }; } @@ -782,4 +787,27 @@ protected function getCacheTag(): string { return md5(serialize(array_merge([self::class], $this->generateCacheKey()))); } + + protected function getDateTimeFormat(ColumnSchemaInterface $column): string|null + { + return match ($column->getType()) { + self::TYPE_TIMESTAMP => 'Y-m-d H:i:s' + . $this->getMillisecondsFormat($column) + . ($column->hasTimezone() ? 'P' : ''), + self::TYPE_DATE => 'Y-m-d', + self::TYPE_TIME => '0 H:i:s' . $this->getMillisecondsFormat($column), + default => null, + }; + } + + protected function getMillisecondsFormat(ColumnSchemaInterface $column): string + { + $precision = $column->getScale(); + + return match (true) { + $precision > 3 => '.u', + $precision > 0 => '.v', + default => '', + }; + } } diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 800794b..7a96c0f 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -4,8 +4,8 @@ namespace Yiisoft\Db\Oracle\Tests; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; -use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Oracle\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; @@ -34,7 +34,10 @@ public function testPhpTypeCast(): void 'char_col3' => null, 'float_col' => 1.234, 'blob_col' => "\x10\x11\x12", - 'timestamp_col' => new Expression("TIMESTAMP '2023-07-11 14:50:23'"), + 'timestamp_col' => '2023-07-11 14:50:23', + 'timestamp_col2' => new DateTimeImmutable('2023-07-11 14:50:23.123456 +02:00'), + 'timestamptz_col' => new DateTimeImmutable('2023-07-11 14:50:23.12 -2:30'), + 'date_col' => new DateTimeImmutable('2023-07-11'), 'bool_col' => false, 'bit_col' => 0b0110_0110, // 102 ] @@ -49,6 +52,11 @@ public function testPhpTypeCast(): void $charCol3PhpType = $tableSchema->getColumn('char_col3')?->phpTypecast($query['char_col3']); $floatColPhpType = $tableSchema->getColumn('float_col')?->phpTypecast($query['float_col']); $blobColPhpType = $tableSchema->getColumn('blob_col')?->phpTypecast($query['blob_col']); + $timestampColPhpType = $tableSchema->getColumn('timestamp_col')?->phpTypecast($query['timestamp_col']); + $timestampCol2PhpType = $tableSchema->getColumn('timestamp_col2')?->phpTypecast($query['timestamp_col2']); + $timestamptzColPhpType = $tableSchema->getColumn('timestamptz_col')?->phpTypecast($query['timestamptz_col']); + $dateColPhpType = $tableSchema->getColumn('date_col')?->phpTypecast($query['date_col']); + $tsDefaultPhpType = $tableSchema->getColumn('ts_default')?->phpTypecast($query['ts_default']); $boolColPhpType = $tableSchema->getColumn('bool_col')?->phpTypecast($query['bool_col']); $bitColPhpType = $tableSchema->getColumn('bit_col')?->phpTypecast($query['bit_col']); @@ -57,6 +65,11 @@ public function testPhpTypeCast(): void $this->assertNull($charCol3PhpType); $this->assertSame(1.234, $floatColPhpType); $this->assertSame("\x10\x11\x12", stream_get_contents($blobColPhpType)); + $this->assertEquals(new DateTimeImmutable('2023-07-11 14:50:23'), $timestampColPhpType); + $this->assertEquals(new DateTimeImmutable('2023-07-11 14:50:23.123456 +02:00'), $timestampCol2PhpType); + $this->assertEquals(new DateTimeImmutable('2023-07-11 14:50:23.12 -2:30'), $timestamptzColPhpType); + $this->assertEquals(new DateTimeImmutable('2023-07-11'), $dateColPhpType); + $this->assertInstanceOf(DateTimeImmutable::class, $tsDefaultPhpType); $this->assertEquals(false, $boolColPhpType); $this->assertEquals(0b0110_0110, $bitColPhpType); diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index 1475831..af22878 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Oracle\Tests\Provider; +use DateTimeImmutable; use Yiisoft\Db\Constraint\CheckConstraint; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Tests\Support\AnyValue; @@ -173,16 +174,59 @@ public static function columns(): array ], 'timestamp_col' => [ 'type' => 'timestamp', - 'dbType' => 'TIMESTAMP(6)', - 'phpType' => 'string', + 'dbType' => 'TIMESTAMP(0)', + 'phpType' => 'DateTimeInterface', 'primaryKey' => false, 'allowNull' => false, 'autoIncrement' => false, 'enumValues' => null, + 'size' => 7, + 'precision' => null, + 'scale' => 0, + 'defaultValue' => new DateTimeImmutable('2002-01-01 00:00:00'), + 'dateTimeFormat' => 'Y-m-d H:i:s', + ], + 'timestamp_col2' => [ + 'type' => 'timestamp', + 'dbType' => 'TIMESTAMP(6)', + 'phpType' => 'DateTimeInterface', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, 'size' => 11, 'precision' => null, 'scale' => 6, - 'defaultValue' => "to_timestamp('2002-01-01 00:00:00', 'yyyy-mm-dd hh24:mi:ss')", + 'defaultValue' => null, + 'dateTimeFormat' => 'Y-m-d H:i:s.u', + ], + 'timestamptz_col' => [ + 'type' => 'timestamp', + 'dbType' => 'TIMESTAMP(2) WITH TIME ZONE', + 'phpType' => 'DateTimeInterface', + 'primaryKey' => false, + 'allowNull' => false, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => 13, + 'precision' => null, + 'scale' => 2, + 'defaultValue' => new DateTimeImmutable('2023-06-11 15:24:11.12 +02:00'), + 'dateTimeFormat' => 'Y-m-d H:i:s.vP', + ], + 'date_col' => [ + 'type' => 'date', + 'dbType' => 'DATE', + 'phpType' => 'DateTimeInterface', + 'primaryKey' => false, + 'allowNull' => false, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => 7, + 'precision' => null, + 'scale' => null, + 'defaultValue' => new DateTimeImmutable('2023-06-11'), + 'dateTimeFormat' => 'Y-m-d', ], 'bool_col' => [ 'type' => 'string', @@ -213,7 +257,7 @@ public static function columns(): array 'ts_default' => [ 'type' => 'timestamp', 'dbType' => 'TIMESTAMP(6)', - 'phpType' => 'string', + 'phpType' => 'DateTimeInterface', 'primaryKey' => false, 'allowNull' => false, 'autoIncrement' => false, @@ -222,6 +266,7 @@ public static function columns(): array 'precision' => null, 'scale' => 6, 'defaultValue' => new Expression('CURRENT_TIMESTAMP'), + 'dateTimeFormat' => 'Y-m-d H:i:s.u', ], 'bit_col' => [ 'type' => 'string', diff --git a/tests/Support/Fixture/oci.sql b/tests/Support/Fixture/oci.sql index 08e7b61..cdf8efb 100644 --- a/tests/Support/Fixture/oci.sql +++ b/tests/Support/Fixture/oci.sql @@ -174,7 +174,10 @@ CREATE TABLE "type" ( "float_col2" double precision DEFAULT 1.23, "blob_col" blob DEFAULT NULL, "numeric_col" decimal(5,2) DEFAULT 33.22, - "timestamp_col" timestamp DEFAULT to_timestamp('2002-01-01 00:00:00', 'yyyy-mm-dd hh24:mi:ss') NOT NULL, + "timestamp_col" timestamp(0) DEFAULT to_timestamp('2002-01-01 00:00:00', 'yyyy-mm-dd hh24:mi:ss') NOT NULL, + "timestamp_col2" timestamp, + "timestamptz_col" timestamp(2) with time zone DEFAULT TIMESTAMP '2023-06-11 15:24:11.12 +02:00' NOT NULL, + "date_col" date DEFAULT DATE '2023-06-11' NOT NULL, "bool_col" char NOT NULL check ("bool_col" in (0,1)), "bool_col2" char DEFAULT 1 check("bool_col2" in (0,1)), "ts_default" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,