diff --git a/CHANGELOG.md b/CHANGELOG.md index 2602c0711..d31ba3da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - New #391: Add `caseSensitive` option to like condition (@vjik) - Enh #396: Remove `getCacheKey()` and `getCacheTag()` methods from `Schema` class (@Tigrov) - Enh #403, #404: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov) +- New #397: Realize `Schema::loadResultColumn()` method (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index 5af7570f3..e74dd6691 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -29,6 +29,7 @@ * dimension?: int|string, * enum_values?: array|null, * extra?: string|null, + * fromResult?: bool, * primary_key?: bool|string, * name?: string, * not_null?: bool|string|null, diff --git a/src/Schema.php b/src/Schema.php index fb79aea19..95afb3920 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -697,6 +697,113 @@ protected function findColumns(TableSchemaInterface $table): bool return true; } + /** + * @psalm-param array{ + * "pgsql:oid": int, + * "pgsql:table_oid": int, + * table?: string, + * native_type: string, + * pdo_type: int, + * name: string, + * len: int, + * precision: int, + * } $metadata + * + * @psalm-suppress MoreSpecificImplementedParamType + */ + protected function loadResultColumn(array $metadata): ColumnInterface|null + { + if (empty($metadata['native_type'])) { + return null; + } + + $dbType = $metadata['native_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['precision'] !== -1) { + $columnInfo['size'] = match ($dbType) { + 'varchar', 'bpchar' => $metadata['precision'] - 4, + 'numeric' => (($metadata['precision'] - 4) >> 16) & 0xFFFF, + 'interval' => ($metadata['precision'] & 0xFFFF) === 0xFFFF ? 6 : $metadata['precision'] & 0xFFFF, + default => $metadata['precision'], + }; + + if ($dbType === 'numeric') { + $columnInfo['scale'] = ($metadata['precision'] - 4) & 0xFFFF; + } + } + + $isArray = $dbType[0] === '_'; + + if ($isArray) { + $dbType = substr($dbType, 1); + } + + if ($metadata['pgsql:oid'] > 16000) { + /** @var string[] $typeInfo */ + $typeInfo = $this->db->createCommand( + << 0 AND t2.oid = t.typbasetype + LEFT JOIN pg_namespace AS ns ON ns.oid = COALESCE(t2.typnamespace, t.typnamespace) + WHERE t.oid = :oid + SQL, + [':oid' => $metadata['pgsql:oid']] + )->queryOne(); + + $dbType = match ($typeInfo['schema']) { + $this->defaultSchema, 'pg_catalog' => $typeInfo['schema'] . '.' . $typeInfo['typname'], + default => $typeInfo['typname'], + }; + + if ($typeInfo['typtype'] === 'c') { + $structured = $this->resolveTableName($dbType); + + if ($this->findColumns($structured)) { + $columnInfo['columns'] = $structured->getColumns(); + } + + $columnInfo['type'] = ColumnType::STRUCTURED; + } elseif (!empty($typeInfo['enum_values'])) { + $columnInfo['enumValues'] = explode(',', str_replace(["''"], ["'"], $typeInfo['enum_values'])); + } + } + + $columnFactory = $this->db->getColumnFactory(); + $column = $columnFactory->fromDbType($dbType, $columnInfo); + + if ($isArray) { + $columnInfo['dbType'] = $dbType; + $columnInfo['column'] = $column; + + return $columnFactory->fromType(ColumnType::ARRAY, $columnInfo); + } + + return $column; + } + /** * Loads the column information into a {@see ColumnInterface} object. * diff --git a/tests/ColumnTest.php b/tests/ColumnTest.php index fe183859f..15cc25085 100644 --- a/tests/ColumnTest.php +++ b/tests/ColumnTest.php @@ -19,6 +19,7 @@ use Yiisoft\Db\Pgsql\Column\ColumnBuilder; use Yiisoft\Db\Pgsql\Column\IntegerColumn; use Yiisoft\Db\Pgsql\Column\StructuredColumn; +use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Schema\Column\ColumnInterface; @@ -38,19 +39,9 @@ final class ColumnTest extends AbstractColumnTest { use TestTrait; - /** - * @throws Exception - * @throws InvalidConfigException - * @throws Throwable - */ - public function testPhpTypeCast(): void + private function insertTypeValues(Connection $db): void { - $db = $this->getConnection(true); - - $command = $db->createCommand(); - $schema = $db->getSchema(); - $tableSchema = $schema->getTableSchema('type'); - $command->insert( + $db->createCommand()->insert( 'type', [ 'int_col' => 1, @@ -70,11 +61,140 @@ public function testPhpTypeCast(): void 'jsonb_col' => new JsonExpression(new ArrayExpression([1, 2, 3])), 'jsonarray_col' => [new ArrayExpression([[',', 'null', true, 'false', 'f']], ColumnType::JSON)], ] - ); - $command->execute(); - $query = (new Query($db))->from('type')->one(); + )->execute(); + } + + private function assertResultValues(array $result): void + { + $this->assertSame(1, $result['int_col']); + $this->assertSame(str_repeat('x', 100), $result['char_col']); + $this->assertSame(1.234, $result['float_col']); + $this->assertSame("\x10\x11\x12", stream_get_contents($result['blob_col'])); + $this->assertFalse($result['bool_col']); + $this->assertSame(0b0110_0100, $result['bit_col']); + $this->assertSame(0b1_1100_1000, $result['varbit_col']); + $this->assertSame(33.22, $result['numeric_col']); + $this->assertSame([1, -2, null, 42], $result['intarray_col']); + $this->assertSame([null, 1.2, -2.2, null, null], $result['numericarray_col']); + $this->assertSame(['', 'some text', '""', '\\\\', '[",","null",true,"false","f"]', null], $result['varchararray_col']); + $this->assertNull($result['textarray2_col']); + $this->assertSame([['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], $result['json_col']); + $this->assertSame(['1', '2', '3'], $result['jsonb_col']); + $this->assertSame([[[',', 'null', true, 'false', 'f']]], $result['jsonarray_col']); + } + + public function testQueryWithTypecasting(): void + { + $db = $this->getConnection(true); + + $this->insertTypeValues($db); + + $query = (new Query($db))->from('type')->withTypecasting(); + + $result = $query->one(); + + $this->assertResultValues($result); + + $result = $query->all(); + + $this->assertResultValues($result[0]); + + $db->close(); + } + + public function testCommandWithPhpTypecasting(): void + { + $db = $this->getConnection(true); + + $this->insertTypeValues($db); + + $command = $db->createCommand('SELECT * FROM type')->withPhpTypecasting(); + + $result = $command->queryOne(); + + $this->assertResultValues($result); + + $result = $command->queryAll(); + + $this->assertResultValues($result[0]); - $this->assertNotNull($tableSchema); + $db->close(); + } + + public function testSelectWithPhpTypecasting(): void + { + $db = $this->getConnection(true); + + $sql = << null, + 1 => 1, + '2.5' => 2.5, + 'true' => true, + 'false' => false, + 'string' => 'string', + 'enum' => 'VAL1', + 'enum2' => 'VAL2', + 'intarray' => [1, 2, 3], + 'jsonb' => ['a' => 1], + 'composite' => ['value' => 10.0, 'currency_code' => 'USD'], + ]; + + $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') + ->withPhpTypecasting() + ->queryScalar(); + + $this->assertSame(2.5, $result); + + $result = $db->createCommand('SELECT 2.5 UNION SELECT 3.3') + ->withPhpTypecasting() + ->queryColumn(); + + $this->assertSame([2.5, 3.3], $result); + + $db->close(); + } + + /** + * @throws Exception + * @throws InvalidConfigException + * @throws Throwable + */ + public function testPhpTypeCast(): void + { + $db = $this->getConnection(true); + $schema = $db->getSchema(); + $tableSchema = $schema->getTableSchema('type'); + + $this->insertTypeValues($db); + + $query = (new Query($db))->from('type')->one(); $intColPhpTypeCast = $tableSchema->getColumn('int_col')?->phpTypecast($query['int_col']); $charColPhpTypeCast = $tableSchema->getColumn('char_col')?->phpTypecast($query['char_col']); diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index f809d5daa..b538045d0 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -277,6 +277,216 @@ public static function constraintsOfView(): array return $result; } + public static function resultColumns(): array + { + return [ + [null, []], + [null, ['native_type' => '']], + [new IntegerColumn(dbType: 'int4', name: 'int_col'), [ + 'pgsql:oid' => 23, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'int4', + 'pdo_type' => 1, + 'name' => 'int_col', + 'len' => 4, + 'precision' => -1, + ]], + [new IntegerColumn(ColumnType::SMALLINT, dbType: 'int2', name: 'smallint_col'), [ + 'pgsql:oid' => 21, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'int2', + 'pdo_type' => 1, + 'name' => 'smallint_col', + 'len' => 2, + 'precision' => -1, + ]], + [new StringColumn(ColumnType::CHAR, dbType: 'bpchar', name: 'char_col', size: 100), [ + 'pgsql:oid' => 1042, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'bpchar', + 'pdo_type' => 2, + 'name' => 'char_col', + 'len' => -1, + 'precision' => 104, + ]], + [new StringColumn(dbType: 'varchar', name: 'char_col2', size: 100), [ + 'pgsql:oid' => 1043, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'varchar', + 'pdo_type' => 2, + 'name' => 'char_col2', + 'len' => -1, + 'precision' => 104, + ]], + [new StringColumn(ColumnType::TEXT, dbType: 'text', name: 'char_col3'), [ + 'pgsql:oid' => 25, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'text', + 'pdo_type' => 2, + 'name' => 'char_col3', + 'len' => -1, + 'precision' => -1, + ]], + [new DoubleColumn(dbType: 'float8', name: 'float_col'), [ + 'pgsql:oid' => 701, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'float8', + 'pdo_type' => 2, + 'name' => 'float_col', + 'len' => 8, + 'precision' => -1, + ]], + [new BinaryColumn(dbType: 'bytea', name: 'blob_col'), [ + 'pgsql:oid' => 17, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'bytea', + 'pdo_type' => 3, + 'name' => 'blob_col', + 'len' => -1, + 'precision' => -1, + ]], + [new DoubleColumn(ColumnType::DECIMAL, dbType: 'numeric', name: 'numeric_col', size: 5, scale: 2), [ + 'pgsql:oid' => 1700, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'numeric', + 'pdo_type' => 2, + 'name' => 'numeric_col', + 'len' => -1, + 'precision' => 327686, + ]], + [new StringColumn(ColumnType::TIMESTAMP, dbType: 'timestamp', name: 'time'), [ + 'pgsql:oid' => 1114, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'timestamp', + 'pdo_type' => 2, + 'name' => 'time', + 'len' => 8, + 'precision' => -1, + ]], + [new BooleanColumn(dbType: 'bool', name: 'bool_col'), [ + 'pgsql:oid' => 16, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'bool', + 'pdo_type' => 5, + 'name' => 'bool_col', + 'len' => 1, + 'precision' => -1, + ]], + [new BitColumn(dbType: 'bit', name: 'bit_col', size: 8), [ + 'pgsql:oid' => 1560, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'bit', + 'pdo_type' => 2, + 'name' => 'bit_col', + 'len' => -1, + 'precision' => 8, + ]], + [new ArrayColumn(dbType: 'int4', name: 'intarray_col', dimension: 1, column: new IntegerColumn(dbType: 'int4', name: 'intarray_col')), [ + 'pgsql:oid' => 1007, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => '_int4', + 'pdo_type' => 2, + 'name' => 'intarray_col', + 'len' => -1, + 'precision' => -1, + ]], + [new JsonColumn(dbType: 'jsonb', name: 'jsonb_col'), [ + 'pgsql:oid' => 3802, + 'pgsql:table_oid' => 40133105, + 'table' => 'type', + 'native_type' => 'jsonb', + 'pdo_type' => 2, + 'name' => 'jsonb_col', + 'len' => -1, + 'precision' => -1, + ]], + [new StringColumn(ColumnType::TEXT, dbType: 'text', name: 'null'), [ + 'pgsql:oid' => 25, + 'pgsql:table_oid' => 0, + 'native_type' => 'text', + 'pdo_type' => 2, + 'name' => 'null', + 'len' => -1, + 'precision' => -1, + ]], + [new IntegerColumn(dbType: 'int4', name: '1'), [ + 'pgsql:oid' => 23, + 'pgsql:table_oid' => 0, + 'native_type' => 'int4', + 'pdo_type' => 1, + 'name' => '1', + 'len' => 4, + 'precision' => -1, + ]], + [new DoubleColumn(ColumnType::DECIMAL, dbType: 'numeric', name: '2.5'), [ + 'pgsql:oid' => 1700, + 'pgsql:table_oid' => 0, + 'native_type' => 'numeric', + 'pdo_type' => 2, + 'name' => '2.5', + 'len' => -1, + 'precision' => -1, + ]], + [new BooleanColumn(dbType: 'bool', name: 'true'), [ + 'pgsql:oid' => 16, + 'pgsql:table_oid' => 0, + 'native_type' => 'bool', + 'pdo_type' => 5, + 'name' => 'true', + 'len' => 1, + 'precision' => -1, + ]], + [new StringColumn(ColumnType::TIMESTAMP, dbType: 'timestamptz', name: 'timestamp(3)', size: 3), [ + 'pgsql:oid' => 1184, + 'pgsql:table_oid' => 0, + 'native_type' => 'timestamptz', + 'pdo_type' => 2, + 'name' => 'timestamp(3)', + 'len' => 8, + 'precision' => 3, + ]], + [new ArrayColumn(dbType: 'int4', name: 'intarray', dimension: 1, column: new IntegerColumn(dbType: 'int4', name: 'intarray')), [ + 'pgsql:oid' => 1007, + 'pgsql:table_oid' => 0, + 'native_type' => '_int4', + 'pdo_type' => 2, + 'name' => 'intarray', + 'len' => -1, + 'precision' => -1, + ]], + [new JsonColumn(dbType: 'jsonb', name: 'jsonb'), [ + 'pgsql:oid' => 3802, + 'pgsql:table_oid' => 0, + 'native_type' => 'jsonb', + 'pdo_type' => 2, + 'name' => 'jsonb', + 'len' => -1, + 'precision' => -1, + ]], + [new StringColumn(dbType: 'interval', name: 'interval', size: 3), [ + 'pgsql:oid' => 1186, + 'pgsql:table_oid' => 0, + 'native_type' => 'interval', + 'pdo_type' => 2, + 'name' => 'interval', + 'len' => 16, + 'precision' => 2147418115, + ]], + ]; + } + public static function tableSchemaCacheWithTablePrefixes(): array { $configs = [ diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 5a464cd1f..846358730 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -5,6 +5,7 @@ namespace Yiisoft\Db\Pgsql\Tests; use JsonException; +use PHPUnit\Framework\Attributes\DataProviderExternal; use Throwable; use Yiisoft\Db\Command\CommandInterface; use Yiisoft\Db\Connection\ConnectionInterface; @@ -15,7 +16,9 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Pgsql\Schema; +use Yiisoft\Db\Pgsql\Tests\Provider\SchemaProvider; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Schema\Column\ColumnInterface; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Schema\TableSchemaInterface; use Yiisoft\Db\Tests\Common\CommonSchemaTest; @@ -615,4 +618,10 @@ public function testTableIndexes(): void $db->close(); } + + #[DataProviderExternal(SchemaProvider::class, 'resultColumns')] + public function testGetResultColumn(ColumnInterface|null $expected, array $info): void + { + parent::testGetResultColumn($expected, $info); + } } diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index bb2389f20..685d3c0e3 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -4,8 +4,6 @@ namespace Yiisoft\Db\Pgsql\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\Pgsql\Connection; @@ -22,7 +20,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()); @@ -33,7 +31,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(), @@ -83,7 +81,7 @@ public static function setUpBeforeClass(): void $db->close(); } - protected function getDriver(): PdoDriverInterface + protected function getDriver(): Driver { $driver = new Driver($this->getDsn(), self::getUsername(), self::getPassword()); $driver->charset('utf8');