Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
- New #307: Add range and multirange columns support (@vjik, @Gerych1984)
- Enh #464: Load column's check expressions for table schema (@Tigrov)
- Bug #467: Fix column definition parsing in cases with parentheses (@vjik)
- New #465: Add enumeration column type support (@vjik)

## 1.3.0 March 21, 2024

Expand Down
1 change: 1 addition & 0 deletions src/Column/ColumnDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ protected function getDbType(ColumnInterface $column): string
ColumnType::DATE => 'date',
ColumnType::STRUCTURED => 'jsonb',
ColumnType::JSON => 'jsonb',
ColumnType::ENUM => 'varchar',
default => 'varchar',
},
'timestamp without time zone' => 'timestamp',
Expand Down
43 changes: 35 additions & 8 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
* column_default: string|null,
* is_autoinc: bool|string,
* sequence_name: string|null,
* enum_values: string|null,
* values: string|null,
* size: int|string|null,
* scale: int|string|null,
* contype: string|null,
Expand Down Expand Up @@ -292,7 +292,7 @@ protected function findColumns(TableSchemaInterface $table): bool
)::varchar[],
',')
ELSE NULL
END AS enum_values,
END AS values,
COALESCE(
information_schema._pg_char_max_length(
COALESCE(td.oid, tb.oid, a.atttypid),
Expand Down Expand Up @@ -467,7 +467,7 @@ protected function loadResultColumn(array $metadata): ?ColumnInterface
)::varchar[],
',')
ELSE NULL
END AS enum_values
END AS 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)
Expand All @@ -486,8 +486,8 @@ protected function loadResultColumn(array $metadata): ?ColumnInterface
}

$columnInfo['type'] = ColumnType::STRUCTURED;
} elseif (!empty($typeInfo['enum_values'])) {
$columnInfo['enumValues'] = explode(',', str_replace(["''"], ["'"], $typeInfo['enum_values']));
} elseif (!empty($typeInfo['values'])) {
$columnInfo['values'] = explode(',', str_replace(["''"], ["'"], $typeInfo['values']));
}
}

Expand Down Expand Up @@ -525,9 +525,9 @@ private function loadColumn(array $info): ColumnInterface
'collation' => $collation,
'comment' => $info['column_comment'],
'dbType' => $dbType,
'enumValues' => $info['enum_values'] !== null
? explode(',', str_replace(["''"], ["'"], $info['enum_values']))
: null,
'values' => $info['values'] !== null
? explode(',', str_replace(["''"], ["'"], $info['values']))
: $this->tryGetEnumValuesFromCheck($info['check']),
'name' => $info['column_name'],
'notNull' => !$info['is_nullable'],
'primaryKey' => $info['contype'] === 'p',
Expand Down Expand Up @@ -679,4 +679,31 @@ private function loadTableConstraints(string $tableName, string $returnType): ar

return $result[$returnType];
}

/**
* @psalm-return list<string>|null
*/
private function tryGetEnumValuesFromCheck(?string $check): ?array
{
if ($check === null) {
return null;
}

preg_match_all(
"~ANY\s*\(\(ARRAY\[('(?:''|[^'])*'[^,\]]*)(?:,\s*(?1))*~",
$check,
$block,
);

if (empty($block[0][0])) {
return null;
}

preg_match_all("~'((?:''|[^'])*)'~", $block[0][0], $matches);

return array_map(
static fn($v) => str_replace("''", "'", $v),
$matches[1] ?? [],
);
}
}
62 changes: 62 additions & 0 deletions tests/Column/EnumColumnTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Tests\Column;

use PHPUnit\Framework\Attributes\TestWith;
use Yiisoft\Db\Pgsql\Tests\Support\IntegrationTestTrait;
use Yiisoft\Db\Schema\Column\EnumColumn;
use Yiisoft\Db\Tests\Common\CommonEnumColumnTest;

final class EnumColumnTest extends CommonEnumColumnTest
{
use IntegrationTestTrait;

#[TestWith(['INTEGER CHECK (status IN (1, 2, 3))'])]
#[TestWith(["TEXT CHECK (status != 'abc')"])]
#[TestWith(["TEXT CHECK (status IN ('a', 'b') OR status = 'x')"])]
#[TestWith(["TEXT CHECK (status IN ('a', 'b') OR status IN ('x', 'y'))"])]
public function testNonEnumCheck(string $columnDefinition): void
{
$this->dropTable('test_enum_table');
$this->executeStatements(
<<<SQL
CREATE TABLE test_enum_table (
id INTEGER,
status $columnDefinition
)
SQL,
);

$db = $this->getSharedConnection();
$column = $db->getTableSchema('test_enum_table')->getColumn('status');

$this->assertNotInstanceOf(EnumColumn::class, $column);

$this->dropTable('test_enum_table');
}

protected function createDatabaseObjectsStatements(): array
{
return [
<<<SQL
CREATE TYPE enum_status AS ENUM ('active', 'unactive', 'pending')
SQL,
<<<SQL
CREATE TABLE tbl_enum (
id INTEGER,
status enum_status
)
SQL,
];
}

protected function dropDatabaseObjectsStatements(): array
{
return [
'DROP TABLE IF EXISTS tbl_enum',
'DROP TYPE IF EXISTS enum_status',
];
}
}
Loading