diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index db39d788b..de0d19ece 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -88,7 +88,7 @@ public function dbTypecast(mixed $value): mixed ? str_pad(decbin($value), (int) $this->getSize(), '0', STR_PAD_LEFT) : (string) $value, - default => $this->typecast($value), + default => parent::dbTypecast($value), }; } @@ -191,4 +191,12 @@ public function sequenceName(string|null $sequenceName): void { $this->sequenceName = $sequenceName; } + + public function hasTimezone(): bool + { + $dbType = (string) $this->getDbType(); + + return str_ends_with($dbType, 'tz') + || str_ends_with($dbType, ' with time zone'); + } } diff --git a/src/Schema.php b/src/Schema.php index 4b1b6a809..b0a193cea 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -51,7 +51,7 @@ * is_autoinc: bool, * sequence_name: string|null, * enum_values: string|null, - * numeric_precision: int|null, + * precision: int|null, * numeric_scale: int|null, * size: string|null, * is_pkey: bool|null, @@ -148,7 +148,7 @@ final class Schema extends AbstractPdoSchema 'timestamp' => self::TYPE_TIMESTAMP, 'timestamp with time zone' => self::TYPE_TIMESTAMP, 'timestamptz' => self::TYPE_TIMESTAMP, - 'abstime' => self::TYPE_TIMESTAMP, + 'abstime' => self::TYPE_TIMESTAMP, // shouldn't be used. it's pg internal! 'tsquery' => self::TYPE_STRING, 'tsvector' => self::TYPE_STRING, 'txid_snapshot' => self::TYPE_STRING, @@ -699,10 +699,10 @@ protected function findColumns(TableSchemaInterface $table): bool ',') ELSE NULL END AS enum_values, - information_schema._pg_numeric_precision( - COALESCE(td.oid, tb.oid, a.atttypid), - information_schema._pg_truetypmod(a, t) - ) AS numeric_precision, + COALESCE( + information_schema._pg_numeric_precision(COALESCE(td.oid, tb.oid, a.atttypid), information_schema._pg_truetypmod(a, t)), + information_schema._pg_datetime_precision(COALESCE(td.oid, tb.oid, a.atttypid), information_schema._pg_truetypmod(a, t)) + ) AS precision, information_schema._pg_numeric_scale( COALESCE(td.oid, tb.oid, a.atttypid), information_schema._pg_truetypmod(a, t) @@ -787,7 +787,7 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface : null); $column->unsigned(false); // has no meaning in PG $column->primaryKey((bool) $info['is_pkey']); - $column->precision($info['numeric_precision']); + $column->precision($info['precision']); $column->scale($info['numeric_scale']); $column->size($info['size'] === null ? null : (int) $info['size']); $column->dimension($info['dimension']); @@ -813,6 +813,7 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface $column->type($this->typeMap[(string) $column->getDbType()] ?? self::TYPE_STRING); $column->phpType($this->getColumnPhpType($column)); + $column->dateTimeFormat($this->getDateTimeFormat($column)); $column->defaultValue($this->normalizeDefaultValue($defaultValue, $column)); return $column; @@ -856,16 +857,13 @@ private function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaIn return $defaultValue === 'true'; } - if ( - in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME], true) - && in_array(strtoupper($defaultValue), ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'], true) - ) { - return new Expression($defaultValue); - } - - $value = preg_replace("/^B?['(](.*?)[)'](?:::[^:]+)?$/s", '$1', $defaultValue); + $value = preg_replace("/^B?['(](.*?)[)'](?:::[^:]+)?$/s", '$1', $defaultValue, 1); $value = str_replace("''", "'", $value); + if ($column->getDateTimeFormat() !== null) { + return date_create_immutable($value) ?: new Expression($defaultValue); + } + if ($column->getType() === self::TYPE_BINARY && str_starts_with($value, '\\x')) { return hex2bin(substr($value, 2)); } diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 2c8195acb..468b0febc 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Tests; +use DateTimeImmutable; use JsonException; use PHPUnit\Framework\TestCase; use Throwable; @@ -48,6 +49,11 @@ public function testPhpTypeCast(): void 'char_col3' => null, 'float_col' => 1.234, 'blob_col' => "\x10\x11\x12", + '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'), + 'time_col' => new DateTimeImmutable('14:50:23'), 'bool_col' => false, 'bit_col' => 0b0110_0100, // 100 'varbit_col' => 0b1_1100_1000, // 456 @@ -70,6 +76,12 @@ public function testPhpTypeCast(): void $charColPhpTypeCast = $tableSchema->getColumn('char_col')?->phpTypecast($query['char_col']); $floatColPhpTypeCast = $tableSchema->getColumn('float_col')?->phpTypecast($query['float_col']); $blobColPhpTypeCast = $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']); + $timeColPhpType = $tableSchema->getColumn('time_col')?->phpTypecast($query['time_col']); + $tsDefaultPhpType = $tableSchema->getColumn('ts_default')?->phpTypecast($query['ts_default']); $boolColPhpTypeCast = $tableSchema->getColumn('bool_col')?->phpTypecast($query['bool_col']); $bitColPhpTypeCast = $tableSchema->getColumn('bit_col')?->phpTypecast($query['bit_col']); $varbitColPhpTypeCast = $tableSchema->getColumn('varbit_col')?->phpTypecast($query['varbit_col']); @@ -86,6 +98,12 @@ public function testPhpTypeCast(): void $this->assertSame(str_repeat('x', 100), $charColPhpTypeCast); $this->assertSame(1.234, $floatColPhpTypeCast); $this->assertSame("\x10\x11\x12", stream_get_contents($blobColPhpTypeCast)); + $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->assertEquals(new DateTimeImmutable('14:50:23'), $timeColPhpType); + $this->assertInstanceOf(DateTimeImmutable::class, $tsDefaultPhpType); $this->assertFalse($boolColPhpTypeCast); $this->assertSame(0b0110_0100, $bitColPhpTypeCast); $this->assertSame(0b1_1100_1000, $varbitColPhpTypeCast); diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index 62ee902c0..23ebf01c0 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Tests\Provider; +use DateTimeImmutable; use Yiisoft\Db\Expression\Expression; final class SchemaProvider extends \Yiisoft\Db\Tests\Provider\SchemaProvider @@ -169,18 +170,75 @@ public static function columns(): array 'scale' => 2, 'defaultValue' => 33.22, ], - 'time' => [ + 'timestamp_col' => [ 'type' => 'timestamp', 'dbType' => 'timestamp', - 'phpType' => 'string', + 'phpType' => 'DateTimeInterface', 'primaryKey' => false, 'allowNull' => false, 'autoIncrement' => false, 'enumValues' => null, 'size' => null, - 'precision' => null, + 'precision' => 0, + 'scale' => null, + 'defaultValue' => new DateTimeImmutable('2002-01-01 00:00:00'), + 'dateTimeFormat' => 'Y-m-d H:i:s', + ], + 'timestamp_col2' => [ + 'type' => 'timestamp', + 'dbType' => 'timestamp', + 'phpType' => 'DateTimeInterface', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => 6, + 'scale' => null, + 'defaultValue' => null, + 'dateTimeFormat' => 'Y-m-d H:i:s.u', + ], + 'timestamptz_col' => [ + 'type' => 'timestamp', + 'dbType' => 'timestamptz', + 'phpType' => 'DateTimeInterface', + 'primaryKey' => false, + 'allowNull' => false, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => 2, + 'scale' => null, + 'defaultValue' => new DateTimeImmutable('2023-06-11 15:24:11.12+0200'), + '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' => null, + 'precision' => 0, 'scale' => null, - 'defaultValue' => '2002-01-01 00:00:00', + 'defaultValue' => new DateTimeImmutable('2023-06-11'), + 'dateTimeFormat' => 'Y-m-d', + ], + 'time_col' => [ + 'type' => 'time', + 'dbType' => 'time', + 'phpType' => 'DateTimeInterface', + 'primaryKey' => false, + 'allowNull' => false, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => 6, + 'scale' => null, + 'defaultValue' => new DateTimeImmutable('15:24:11.123456'), + 'dateTimeFormat' => 'H:i:s.u', ], 'bool_col' => [ 'type' => 'boolean', @@ -211,13 +269,13 @@ public static function columns(): array 'ts_default' => [ 'type' => 'timestamp', 'dbType' => 'timestamp', - 'phpType' => 'string', + 'phpType' => 'DateTimeInterface', 'primaryKey' => false, 'allowNull' => false, 'autoIncrement' => false, 'enumValues' => null, 'size' => null, - 'precision' => null, + 'precision' => 6, 'scale' => null, 'defaultValue' => new Expression('now()'), ], diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index 97df69307..780b5da0c 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -149,7 +149,11 @@ CREATE TABLE "type" ( float_col2 double precision DEFAULT '1.23', blob_col bytea DEFAULT 'a binary value', numeric_col decimal(5,2) DEFAULT '33.22', - time timestamp NOT NULL DEFAULT '2002-01-01 00:00:00', + timestamp_col timestamp(0) NOT NULL DEFAULT '2002-01-01 00:00:00', + timestamp_col2 timestamp, + timestamptz_col timestamptz(2) NOT NULL DEFAULT '2023-06-11 15:24:11.12+0200', + date_col date NOT NULL DEFAULT '2023-06-11', + time_col time NOT NULL DEFAULT '15:24:11.123456', bool_col boolean NOT NULL, bool_col2 boolean DEFAULT TRUE, ts_default TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,