Skip to content

Commit 58758d4

Browse files
vjikTigrov
andauthored
Add ENUM column (#382)
Co-authored-by: Sergei Tigrov <rrr-r@ya.ru>
1 parent dfac432 commit 58758d4

File tree

5 files changed

+147
-14
lines changed

5 files changed

+147
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
- Enh #373: Adapt to `DQLQueryBuilderInterface::buildWithQueries()` signature changes in `yiisoft/db` package (@vjik)
6666
- Chg #378: Throw exception on "unsigned" column usage (@vjik)
6767
- Bug #383: Fix column definition parsing in cases with parentheses (@vjik)
68+
- New #382: Add enumeration column type support (@vjik)
6869

6970
## 1.3.0 March 21, 2024
7071

src/Column/ColumnDefinitionBuilder.php

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
namespace Yiisoft\Db\Oracle\Column;
66

7+
use LogicException;
78
use Yiisoft\Db\Constant\ColumnType;
89
use Yiisoft\Db\Constant\ReferentialAction;
910
use Yiisoft\Db\Exception\NotSupportedException;
1011
use Yiisoft\Db\QueryBuilder\AbstractColumnDefinitionBuilder;
1112
use Yiisoft\Db\Schema\Column\ColumnInterface;
13+
use Yiisoft\Db\Schema\Column\EnumColumn;
1214

1315
use function ceil;
16+
use function in_array;
1417
use function log10;
1518
use function strtoupper;
1619

@@ -61,23 +64,20 @@ protected function buildCheck(ColumnInterface $column): string
6164

6265
if (empty($check)) {
6366
$name = $column->getName();
64-
65-
if (empty($name)) {
66-
return '';
67+
if (!empty($name)) {
68+
$type = $column->getType();
69+
if (in_array($type, [ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON], true)) {
70+
return version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '<')
71+
? ' CHECK (' . $this->queryBuilder->getQuoter()->quoteSimpleColumnName($name) . ' IS JSON)'
72+
: '';
73+
}
74+
if ($type === ColumnType::BOOLEAN) {
75+
return ' CHECK (' . $this->queryBuilder->getQuoter()->quoteSimpleColumnName($name) . ' IN (0,1))';
76+
}
6777
}
68-
69-
return match ($column->getType()) {
70-
ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON
71-
=> version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '<')
72-
? ' CHECK (' . $this->queryBuilder->getQuoter()->quoteSimpleColumnName($name) . ' IS JSON)'
73-
: '',
74-
ColumnType::BOOLEAN
75-
=> ' CHECK (' . $this->queryBuilder->getQuoter()->quoteSimpleColumnName($name) . ' IN (0,1))',
76-
default => '',
77-
};
7878
}
7979

80-
return " CHECK ($check)";
80+
return parent::buildCheck($column);
8181
}
8282

8383
protected function buildOnDelete(string $onDelete): string
@@ -133,6 +133,7 @@ protected function getDbType(ColumnInterface $column): string
133133
=> version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '>=')
134134
? 'json'
135135
: 'clob',
136+
ColumnType::ENUM => 'varchar2(' . $this->calcEnumSize($column) . ' BYTE)',
136137
default => 'varchar2',
137138
},
138139
'timestamp with time zone' => 'timestamp' . ($size !== null ? "($size)" : '') . ' with time zone',
@@ -146,4 +147,23 @@ protected function getDefaultUuidExpression(): string
146147
{
147148
return 'sys_guid()';
148149
}
150+
151+
private function calcEnumSize(ColumnInterface $column): int
152+
{
153+
$size = $column->getSize();
154+
if ($size !== null) {
155+
return $size;
156+
}
157+
158+
if ($column instanceof EnumColumn) {
159+
return max(
160+
array_map(
161+
strlen(...),
162+
$column->getValues(),
163+
),
164+
);
165+
}
166+
167+
throw new LogicException('Cannot calculate enum size. Set the size explicitly or use `EnumColumn` instance.');
168+
}
149169
}

src/Schema.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ private function loadColumn(array $info): ColumnInterface
457457
'size' => $info['size'] !== null ? (int) $info['size'] : null,
458458
'table' => $info['table'],
459459
'unique' => $info['constraint_type'] === 'U',
460+
'values' => $this->tryGetEnumValuesFromCheck($info['column_name'], $info['check']),
460461
];
461462

462463
if ($dbType === 'timestamp with local time zone') {
@@ -544,4 +545,30 @@ private function loadTableConstraints(string $tableName, string $returnType): ar
544545

545546
return $result[$returnType];
546547
}
548+
549+
/**
550+
* @psalm-return list<string>|null
551+
*/
552+
private function tryGetEnumValuesFromCheck(string $columnName, ?string $check): ?array
553+
{
554+
if ($check === null) {
555+
return null;
556+
}
557+
558+
$quotedColumnName = preg_quote($columnName, '~');
559+
if (!preg_match(
560+
"~^\s*(?:\"$quotedColumnName\"|$quotedColumnName)\s+IN\s*\(\s*(('(?:''|[^'])*')(?:,\s*(?2))*)\s*\)\s*$~i",
561+
$check,
562+
$block,
563+
)) {
564+
return null;
565+
}
566+
567+
preg_match_all("~'((?:''|[^'])*)'~", $block[1], $matches);
568+
569+
return array_map(
570+
static fn($v) => str_replace("''", "'", $v),
571+
$matches[1],
572+
);
573+
}
547574
}

tests/Column/EnumColumnTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Oracle\Tests\Column;
6+
7+
use PHPUnit\Framework\Attributes\TestWith;
8+
use Yiisoft\Db\Oracle\Tests\Support\IntegrationTestTrait;
9+
use Yiisoft\Db\Schema\Column\EnumColumn;
10+
use Yiisoft\Db\Tests\Common\CommonEnumColumnTest;
11+
12+
final class EnumColumnTest extends CommonEnumColumnTest
13+
{
14+
use IntegrationTestTrait;
15+
16+
#[TestWith(['INTEGER CHECK ("status" IN (1, 2, 3))'])]
17+
#[TestWith(["VARCHAR2(10) CHECK (\"status\" != 'abc')"])]
18+
#[TestWith(["VARCHAR2(10) CHECK (\"status\" NOT IN ('a', 'b', 'c'))"])]
19+
#[TestWith(["VARCHAR2(10) CHECK (\"status\" IN ('a', 'b', 'c') OR \"status\" = 'x')"])]
20+
#[TestWith(["VARCHAR2(10) CHECK (\"status\" IN ('a', 'b', 'c') OR \"status\" IN ('x', 'y', 'z'))"])]
21+
public function testNonEnumCheck(string $columnDefinition): void
22+
{
23+
$this->dropTable('test_enum_table');
24+
$this->executeStatements(
25+
<<<SQL
26+
CREATE TABLE "test_enum_table" (
27+
"id" INTEGER,
28+
"status" $columnDefinition
29+
)
30+
SQL,
31+
);
32+
33+
$db = $this->getSharedConnection();
34+
$column = $db->getTableSchema('test_enum_table')->getColumn('status');
35+
36+
$this->assertNotInstanceOf(EnumColumn::class, $column);
37+
38+
$this->dropTable('test_enum_table');
39+
}
40+
41+
protected function createDatabaseObjectsStatements(): array
42+
{
43+
return [
44+
<<<SQL
45+
CREATE TABLE "tbl_enum" (
46+
"id" NUMBER,
47+
"status" NVARCHAR2(8) CHECK ("status" IN ('pending', 'unactive', 'active'))
48+
)
49+
SQL,
50+
];
51+
}
52+
53+
protected function dropDatabaseObjectsStatements(): array
54+
{
55+
return [
56+
<<<SQL
57+
BEGIN
58+
EXECUTE IMMEDIATE 'DROP TABLE "tbl_enum"';
59+
EXCEPTION
60+
WHEN OTHERS THEN
61+
IF SQLCODE != -942 THEN
62+
RAISE;
63+
END IF;
64+
END;
65+
SQL,
66+
];
67+
}
68+
}

tests/Support/IntegrationTestTrait.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ protected function parseDump(string $content): array
4141
);
4242
}
4343

44+
protected function dropTable(string $table): void
45+
{
46+
$db = TestConnection::getShared();
47+
$table = $db->getQuoter()->quoteTableName($table);
48+
$sql = <<<SQL
49+
BEGIN
50+
EXECUTE IMMEDIATE 'DROP TABLE $table';
51+
EXCEPTION
52+
WHEN OTHERS THEN
53+
IF SQLCODE != -942 THEN
54+
RAISE;
55+
END IF;
56+
END;
57+
SQL;
58+
$db->createCommand($sql)->execute();
59+
}
60+
4461
protected function dropView(ConnectionInterface $db, string $view): void
4562
{
4663
$view = $db->getQuoter()->quoteTableName($view);

0 commit comments

Comments
 (0)