diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ba7d0d..55fa4d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - Enh #315: Remove `getCacheKey()` and `getCacheTag()` methods from `Schema` class (@Tigrov) - Enh #319: Support `boolean` type (@Tigrov) - Enh #318, #320: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov) +- New #316: Realize `Schema::loadResultColumn()` method (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/Schema.php b/src/Schema.php index 249dd585..3ebbcb8e 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -7,6 +7,7 @@ use Throwable; use Yiisoft\Db\Cache\SchemaCache; use Yiisoft\Db\Connection\ConnectionInterface; +use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Constant\ReferentialAction; use Yiisoft\Db\Constraint\CheckConstraint; use Yiisoft\Db\Constraint\Constraint; @@ -25,6 +26,7 @@ use function array_map; use function array_reverse; use function implode; +use function in_array; use function is_array; use function preg_replace; use function strtolower; @@ -175,6 +177,70 @@ protected function findTableNames(string $schema = ''): array return $names; } + /** + * @param array{ + * "oci:decl_type": int|string, + * native_type: string, + * pdo_type: int, + * scale: int, + * table?: string, + * flags: string[], + * name: string, + * len: int, + * precision: int, + * } $metadata + * + * @psalm-suppress MoreSpecificImplementedParamType + */ + protected function loadResultColumn(array $metadata): ColumnInterface|null + { + if (empty($metadata['oci:decl_type'])) { + return null; + } + + $dbType = match ($metadata['oci:decl_type']) { + 119 => 'json', + 'TIMESTAMP WITH TIMEZONE' => 'timestamp with time zone', + 'TIMESTAMP WITH LOCAL TIMEZONE' => 'timestamp with local time zone', + default => strtolower((string) $metadata['oci:decl_type']), + }; + + $columnInfo = ['fromResult' => true]; + + if (!empty($metadata['table'])) { + $columnInfo['table'] = $metadata['table']; + $columnInfo['name'] = $metadata['name']; + } elseif (!empty($metadata['name'])) { + $columnInfo['name'] = $metadata['name']; + } + + if ($metadata['pdo_type'] === 3) { + $columnInfo['type'] = ColumnType::BINARY; + } + + if (!empty($metadata['precision'])) { + $columnInfo['size'] = $metadata['precision']; + } + + /** @psalm-suppress PossiblyUndefinedArrayOffset, InvalidArrayOffset */ + match ($dbType) { + 'timestamp', + 'timestamp with time zone', + 'timestamp with local time zone' => $columnInfo['size'] = $metadata['scale'], + 'interval day to second', + 'interval year to month' => + [$columnInfo['size'], $columnInfo['scale']] = [$metadata['scale'], $metadata['precision']], + 'number' => $metadata['scale'] !== -127 ? $columnInfo['scale'] = $metadata['scale'] : null, + 'float' => null, + default => $columnInfo['size'] = $metadata['len'], + }; + + $columnInfo['notNull'] = in_array('not_null', $metadata['flags'], true); + + /** @psalm-suppress MixedArgumentTypeCoercion */ + return $this->db->getColumnFactory()->fromDbType($dbType, $columnInfo); + } + /** * @throws Exception * @throws InvalidConfigException diff --git a/tests/ColumnTest.php b/tests/ColumnTest.php index dc169137..557da9af 100644 --- a/tests/ColumnTest.php +++ b/tests/ColumnTest.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Oracle\Column\BinaryColumn; use Yiisoft\Db\Oracle\Column\JsonColumn; +use Yiisoft\Db\Oracle\Connection; use Yiisoft\Db\Oracle\Tests\Provider\ColumnProvider; use Yiisoft\Db\Oracle\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; @@ -29,6 +30,135 @@ final class ColumnTest extends AbstractColumnTest { use TestTrait; + private function insertTypeValues(Connection $db): void + { + $db->createCommand()->insert( + 'type', + [ + 'int_col' => 1, + 'char_col' => str_repeat('x', 100), + 'char_col3' => null, + 'float_col' => 1.234, + 'blob_col' => "\x10\x11\x12", + 'timestamp_col' => new Expression("TIMESTAMP '2023-07-11 14:50:23'"), + 'bool_col' => false, + 'bit_col' => 0b0110_0110, // 102 + 'json_col' => [['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], + ] + )->execute(); + } + + private function assertResultValues(array $result, string $varsion): void + { + $this->assertSame(1, $result['int_col']); + $this->assertSame(str_repeat('x', 100), $result['char_col']); + $this->assertNull($result['char_col3']); + $this->assertSame(1.234, $result['float_col']); + $this->assertSame("\x10\x11\x12", stream_get_contents($result['blob_col'])); + $this->assertEquals(false, $result['bool_col']); + $this->assertSame(0b0110_0110, $result['bit_col']); + + if (version_compare($varsion, '21', '>=')) { + $this->assertSame([['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], $result['json_col']); + } else { + $this->assertSame('[{"a":1,"b":null,"c":[1,3,5]}]', stream_get_contents($result['json_col'])); + } + } + + public function testQueryWithTypecasting(): void + { + $db = $this->getConnection(); + $varsion = $db->getServerInfo()->getVersion(); + $db->close(); + + if (version_compare($varsion, '21', '>=')) { + $this->fixture = 'oci21.sql'; + } + + $db = $this->getConnection(true); + + $this->insertTypeValues($db); + + $query = (new Query($db))->from('type')->withTypecasting(); + + $result = $query->one(); + + $this->assertResultValues($result, $varsion); + + $result = $query->all(); + + $this->assertResultValues($result[0], $varsion); + + $db->close(); + } + + public function testCommandWithPhpTypecasting(): void + { + $db = $this->getConnection(); + $varsion = $db->getServerInfo()->getVersion(); + $db->close(); + + if (version_compare($varsion, '21', '>=')) { + $this->fixture = 'oci21.sql'; + } + + $db = $this->getConnection(true); + + $this->insertTypeValues($db); + + $command = $db->createCommand('SELECT * FROM "type"'); + + $result = $command->withPhpTypecasting()->queryOne(); + + $this->assertResultValues($result, $varsion); + + $result = $command->withPhpTypecasting()->queryAll(); + + $this->assertResultValues($result[0], $varsion); + + $db->close(); + } + + public function testSelectWithPhpTypecasting(): void + { + $db = $this->getConnection(); + + $sql = "SELECT null, 1, 2.5, 'string' FROM DUAL"; + + $expected = [ + 'NULL' => null, + 1 => 1.0, + '2.5' => 2.5, + "'STRING'" => 'string', + ]; + + $result = $db->createCommand($sql) + ->withPhpTypecasting() + ->queryOne(); + + $this->assertSame($expected, $result); + + $result = $db->createCommand($sql) + ->withPhpTypecasting() + ->queryAll(); + + $this->assertSame([$expected], $result); + + $result = $db->createCommand('SELECT 2.5 FROM DUAL') + ->withPhpTypecasting() + ->queryScalar(); + + $this->assertSame(2.5, $result); + + $result = $db->createCommand('SELECT 2.5 FROM DUAL UNION SELECT 3.3 FROM DUAL') + ->withPhpTypecasting() + ->queryColumn(); + + $this->assertSame([2.5, 3.3], $result); + + $db->close(); + } + public function testPhpTypeCast(): void { $db = $this->getConnection(); @@ -39,26 +169,12 @@ public function testPhpTypeCast(): void $db->close(); $db = $this->getConnection(true); - - $command = $db->createCommand(); $schema = $db->getSchema(); $tableSchema = $schema->getTableSchema('type'); - $command->insert('type', [ - 'int_col' => 1, - 'char_col' => str_repeat('x', 100), - 'char_col3' => null, - 'float_col' => 1.234, - 'blob_col' => "\x10\x11\x12", - 'timestamp_col' => new Expression("TIMESTAMP '2023-07-11 14:50:23'"), - 'bool_col' => false, - 'bit_col' => 0b0110_0110, // 102 - 'json_col' => [['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], - ]); - $command->execute(); - $query = (new Query($db))->from('type')->one(); + $this->insertTypeValues($db); - $this->assertNotNull($tableSchema); + $query = (new Query($db))->from('type')->one(); $intColPhpType = $tableSchema->getColumn('int_col')?->phpTypecast($query['int_col']); $charColPhpType = $tableSchema->getColumn('char_col')?->phpTypecast($query['char_col']); diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index f54947a8..dd8014a7 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -90,6 +90,11 @@ public static function columns(): array size: 6, defaultValue: new Expression("to_timestamp('2002-01-01 00:00:00', 'yyyy-mm-dd hh24:mi:ss')"), ), + 'timestamp_local' => new StringColumn( + ColumnType::TIMESTAMP, + dbType: 'timestamp with local time zone', + size:6, + ), 'time_col' => new StringColumn( ColumnType::TIME, dbType: 'interval day to second', @@ -216,6 +221,164 @@ public static function constraints(): array return $constraints; } + public static function resultColumns(): array + { + return [ + [null, []], + [null, ['oci:decl_type' => '']], + [new IntegerColumn(dbType: 'number', name: 'int_col', notNull: true, size: 38, scale: 0), [ + 'oci:decl_type' => 'NUMBER', + 'native_type' => 'NUMBER', + 'pdo_type' => 2, + 'scale' => 0, + 'flags' => ['not_null'], + 'name' => 'int_col', + 'len' => 22, + 'precision' => 38, + ]], + [new IntegerColumn(dbType: 'number', name: 'tinyint_col', notNull: false, size: 3, scale: 0), [ + 'oci:decl_type' => 'NUMBER', + 'native_type' => 'NUMBER', + 'pdo_type' => 2, + 'scale' => 0, + 'flags' => ['nullable'], + 'name' => 'tinyint_col', + 'len' => 22, + 'precision' => 3, + ]], + [new StringColumn(ColumnType::CHAR, dbType: 'char', name: 'char_col', notNull: true, size: 100), [ + 'oci:decl_type' => 'CHAR', + 'native_type' => 'CHAR', + 'pdo_type' => 2, + 'scale' => 0, + 'flags' => ['not_null'], + 'name' => 'char_col', + 'len' => 100, + 'precision' => 0, + ]], + [new StringColumn(dbType: 'varchar2', name: 'char_col2', notNull: false, size: 100), [ + 'oci:decl_type' => 'VARCHAR2', + 'native_type' => 'VARCHAR2', + 'pdo_type' => 2, + 'scale' => 0, + 'flags' => ['nullable'], + 'name' => 'char_col2', + 'len' => 100, + 'precision' => 0, + ]], + [new DoubleColumn(dbType: 'float', name: 'float_col', notNull: true, size: 126), [ + 'oci:decl_type' => 'FLOAT', + 'native_type' => 'FLOAT', + 'pdo_type' => 2, + 'scale' => -127, + 'flags' => ['not_null'], + 'name' => 'float_col', + 'len' => 22, + 'precision' => 126, + ]], + [new BinaryColumn(dbType: 'blob', name: 'blob_col', notNull: false, size: 4000), [ + 'oci:decl_type' => 'BLOB', + 'native_type' => 'BLOB', + 'pdo_type' => 3, + 'scale' => 0, + 'flags' => ['blob', 'nullable'], + 'name' => 'blob_col', + 'len' => 4000, + 'precision' => 0, + ]], + [new DoubleColumn(ColumnType::DECIMAL, dbType: 'number', name: 'numeric_col', notNull: false, size: 5, scale: 2), [ + 'oci:decl_type' => 'NUMBER', + 'native_type' => 'NUMBER', + 'pdo_type' => 2, + 'scale' => 2, + 'flags' => ['nullable'], + 'name' => 'numeric_col', + 'len' => 22, + 'precision' => 5, + ]], + [new StringColumn(ColumnType::TIMESTAMP, dbType: 'timestamp', name: 'timestamp_col', notNull: true, size: 6), [ + 'oci:decl_type' => 'TIMESTAMP', + 'native_type' => 'TIMESTAMP', + 'pdo_type' => 2, + 'scale' => 6, + 'flags' => ['not_null'], + 'name' => 'timestamp_col', + 'len' => 11, + 'precision' => 0, + ]], + [new StringColumn(ColumnType::TIME, dbType: 'interval day to second', name: 'time_col', notNull: false, size: 0), [ + 'oci:decl_type' => 'INTERVAL DAY TO SECOND', + 'native_type' => 'INTERVAL DAY TO SECOND', + 'pdo_type' => 2, + 'scale' => 0, + 'flags' => ['nullable'], + 'name' => 'time_col', + 'len' => 11, + 'precision' => 0, + ]], + [new BinaryColumn(dbType: 'clob', name: 'json_col', notNull: false, size: 4000), [ + 'oci:decl_type' => 'CLOB', + 'native_type' => 'CLOB', + 'pdo_type' => 3, + 'scale' => 0, + 'flags' => ['blob', 'nullable'], + 'name' => 'json_col', + 'len' => 4000, + 'precision' => 0, + ]], + [new JsonColumn(dbType: 'json', name: 'json_col', notNull: false, size: 8200), [ + 'oci:decl_type' => 119, + 'native_type' => 'UNKNOWN', + 'pdo_type' => 2, + 'scale' => 0, + 'flags' => ['nullable'], + 'name' => 'json_col', + 'len' => 8200, + 'precision' => 0, + ]], + [new StringColumn(dbType: 'varchar2', name: 'NULL', notNull: false), [ + 'oci:decl_type' => 'VARCHAR2', + 'native_type' => 'VARCHAR2', + 'pdo_type' => 2, + 'scale' => 0, + 'flags' => ['nullable'], + 'name' => 'NULL', + 'len' => 0, + 'precision' => 0, + ]], + [new DoubleColumn(dbType: 'number', name: '1', notNull: false), [ + 'oci:decl_type' => 'NUMBER', + 'native_type' => 'NUMBER', + 'pdo_type' => 2, + 'scale' => -127, + 'flags' => ['nullable'], + 'name' => '1', + 'len' => 2, + 'precision' => 0, + ]], + [new StringColumn(ColumnType::CHAR, dbType: 'char', name: "'STRING'", notNull: false, size: 6), [ + 'oci:decl_type' => 'CHAR', + 'native_type' => 'CHAR', + 'pdo_type' => 2, + 'scale' => 0, + 'flags' => ['nullable'], + 'name' => "'STRING'", + 'len' => 6, + 'precision' => 0, + ]], + [new StringColumn(ColumnType::TIMESTAMP, dbType: 'timestamp with time zone', name: 'TIMESTAMP(3)', notNull: false, size: 3), [ + 'oci:decl_type' => 'TIMESTAMP WITH TIMEZONE', + 'native_type' => 'TIMESTAMP WITH TIMEZONE', + 'pdo_type' => 2, + 'scale' => 3, + 'flags' => ['nullable'], + 'name' => 'TIMESTAMP(3)', + 'len' => 13, + 'precision' => 0, + ]], + ]; + } + public static function tableSchemaWithDbSchemes(): array { return [ diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 88dc049e..25a55893 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Oracle\Tests; +use PHPUnit\Framework\Attributes\DataProviderExternal; use Yiisoft\Db\Command\CommandInterface; use Yiisoft\Db\Connection\ConnectionInterface; use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; @@ -11,7 +12,9 @@ use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Oracle\Schema; +use Yiisoft\Db\Oracle\Tests\Provider\SchemaProvider; use Yiisoft\Db\Oracle\Tests\Support\TestTrait; +use Yiisoft\Db\Schema\Column\ColumnInterface; use Yiisoft\Db\Tests\Common\CommonSchemaTest; use Yiisoft\Db\Tests\Support\DbHelper; @@ -279,4 +282,10 @@ public function testNotConnectionPDO(): void $schema->refresh(); } + + #[DataProviderExternal(SchemaProvider::class, 'resultColumns')] + public function testGetResultColumn(ColumnInterface|null $expected, array $info): void + { + parent::testGetResultColumn($expected, $info); + } } diff --git a/tests/Support/Fixture/oci.sql b/tests/Support/Fixture/oci.sql index 2d4308b8..0bd45a18 100644 --- a/tests/Support/Fixture/oci.sql +++ b/tests/Support/Fixture/oci.sql @@ -175,6 +175,7 @@ CREATE TABLE "type" ( "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_local" timestamp with local time zone, "time_col" interval day (0) to second(0) DEFAULT INTERVAL '0 10:33:21' DAY(0) TO SECOND(0), "interval_day_col" interval day (1) to second(0) DEFAULT INTERVAL '2 04:56:12' DAY(1) TO SECOND(0), "bool_col" char NOT NULL check ("bool_col" in (0,1)), diff --git a/tests/Support/Fixture/oci21.sql b/tests/Support/Fixture/oci21.sql index 9a022035..c5b604a8 100644 --- a/tests/Support/Fixture/oci21.sql +++ b/tests/Support/Fixture/oci21.sql @@ -16,6 +16,7 @@ CREATE TABLE "type" ( "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_local" timestamp with local time zone, "time_col" interval day (0) to second(0) DEFAULT INTERVAL '0 10:33:21' DAY(0) TO SECOND(0), "interval_day_col" interval day (1) to second(0) DEFAULT INTERVAL '2 04:56:12' DAY(1) TO SECOND(0), "bool_col" char NOT NULL check ("bool_col" in (0,1)), diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index 8004eb81..08a2fe31 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -4,8 +4,6 @@ namespace Yiisoft\Db\Oracle\Tests\Support; -use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; -use Yiisoft\Db\Driver\Pdo\PdoDriverInterface; use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Oracle\Connection; @@ -23,7 +21,7 @@ trait TestTrait * @throws InvalidConfigException * @throws Exception */ - protected function getConnection(bool $fixture = false): PdoConnectionInterface + protected function getConnection(bool $fixture = false): Connection { $db = new Connection($this->getDriver(), DbHelper::getSchemaCache()); @@ -34,7 +32,7 @@ protected function getConnection(bool $fixture = false): PdoConnectionInterface return $db; } - protected static function getDb(): PdoConnectionInterface + protected static function getDb(): Connection { $dsn = (new Dsn( host: self::getHost(), @@ -70,7 +68,7 @@ protected function setDsn(string $dsn): void $this->dsn = $dsn; } - protected function getDriver(): PdoDriverInterface + protected function getDriver(): Driver { return new Driver($this->getDsn(), self::getUsername(), self::getPassword()); }