Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/Column/ColumnFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -159,7 +160,7 @@
$value = preg_replace("/::[^:']+$/", '$1', $defaultValue);

if (str_starts_with($value, "B'") && $value[-1] === "'") {
return $column->phpTypecast(substr($value, 2, -1));

Check warning on line 163 in src/Column/ColumnFactory.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "UnwrapSubstr": @@ @@ /** @var string $value */ $value = preg_replace("/::[^:']+\$/", '$1', $defaultValue); if (str_starts_with($value, "B'") && $value[-1] === "'") { - return $column->phpTypecast(substr($value, 2, -1)); + return $column->phpTypecast($value); } $value = parent::normalizeNotNullDefaultValue($value, $column); if ($value instanceof Expression) {

Check warning on line 163 in src/Column/ColumnFactory.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ /** @var string $value */ $value = preg_replace("/::[^:']+\$/", '$1', $defaultValue); if (str_starts_with($value, "B'") && $value[-1] === "'") { - return $column->phpTypecast(substr($value, 2, -1)); + return $column->phpTypecast(substr($value, 1, -1)); } $value = parent::normalizeNotNullDefaultValue($value, $column); if ($value instanceof Expression) {
}

$value = parent::normalizeNotNullDefaultValue($value, $column);
Expand Down
107 changes: 107 additions & 0 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<<<SQL
SELECT
ns.nspname AS schema,
COALESCE(t2.typname, t.typname) AS typname,
COALESCE(t2.typtype, t.typtype) AS typtype,
CASE WHEN COALESCE(t2.typtype, t.typtype) = 'e'::char
THEN array_to_string(
(
SELECT array_agg(enumlabel)
FROM pg_enum
WHERE enumtypid = COALESCE(t2.oid, t.oid)
)::varchar[],
',')
ELSE NULL
END AS enum_values
FROM pg_type AS t
LEFT JOIN pg_type AS t2 ON t.typcategory='A' AND t2.oid = t.typelem OR t.typbasetype > 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.
*
Expand Down
152 changes: 136 additions & 16 deletions tests/ColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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 = <<<SQL
SELECT
null AS "null",
1 AS "1",
2.5 AS "2.5",
true AS "true",
false AS "false",
'string' AS "string",
'VAL1'::my_type AS "enum",
'VAL2'::schema2.my_type2 AS "enum2",
'{1,2,3}'::int[] AS "intarray",
'{"a":1}'::jsonb AS "jsonb",
'(10,USD)'::currency_money_structured AS "composite"
SQL;

$expected = [
'null' => 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']);
Expand Down
Loading
Loading