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 @@ -178,6 +178,7 @@
- Chg #1103: Remove `AbstractCommand::refreshTableSchema()` method (@vjik)
- Chg #1106: Remove parameters from `PdoConnectionInterface::getActivePdo()` method (@vjik)
- Bug #1109: Fix column definition parsing in cases with brackets and escaped quotes (@vjik)
- New #1107: Add abstract enumeration column type (@vjik)

## 1.3.0 March 21, 2024

Expand Down
4 changes: 4 additions & 0 deletions src/Constant/ColumnType.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,8 @@ final class ColumnType
* Define the abstract column type as `json`.
*/
public const JSON = 'json';
/**
* Define the abstract column type as `enum`.
*/
public const ENUM = 'enum';
}
20 changes: 20 additions & 0 deletions src/QueryBuilder/AbstractColumnDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Yiisoft\Db\Constant\ReferentialAction;
use Yiisoft\Db\Schema\Column\CollatableColumnInterface;
use Yiisoft\Db\Schema\Column\ColumnInterface;
use Yiisoft\Db\Schema\Column\EnumColumn;

use function in_array;
use function strtolower;
Expand Down Expand Up @@ -122,6 +123,25 @@ protected function buildCheck(ColumnInterface $column): string
{
$check = $column->getCheck();

if (empty($check)
&& $column instanceof EnumColumn
&& $column->getDbType() === null
) {
$name = $column->getName();
if (!empty($name)) {
$quoter = $this->queryBuilder->getQuoter();
$quotedItems = implode(
',',
array_map(
$quoter->quoteValue(...),
$column->getValues(),
),
);
$quotedName = $quoter->quoteColumnName($name);
$check = "$quotedName IN ($quotedItems)";
}
}

return !empty($check) ? " CHECK ($check)" : '';
}

Expand Down
14 changes: 0 additions & 14 deletions src/Schema/Column/AbstractColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ abstract class AbstractColumn implements ColumnInterface
* @param string|null $comment The column's comment.
* @param bool $computed Whether the column is a computed column.
* @param string|null $dbType The column's database type.
* @param array|null $enumValues The list of possible values for an ENUM column.
* @param string|null $extra Any extra information that needs to be appended to the column's definition.
* @param bool $primaryKey Whether the column is a primary key.
* @param string|null $name The column's name.
Expand All @@ -83,7 +82,6 @@ public function __construct(
private ?string $comment = null,
private bool $computed = false,
private ?string $dbType = null,
private ?array $enumValues = null,
private ?string $extra = null,
private bool $primaryKey = false,
private ?string $name = null,
Expand Down Expand Up @@ -146,12 +144,6 @@ public function defaultValue(mixed $defaultValue): static
return $this;
}

public function enumValues(?array $enumValues): static
{
$this->enumValues = $enumValues;
return $this;
}

public function extra(?string $extra): static
{
$this->extra = $extra;
Expand Down Expand Up @@ -182,12 +174,6 @@ public function getDefaultValue(): mixed
return $this->defaultValue ?? null;
}

/** @psalm-mutation-free */
public function getEnumValues(): ?array
{
return $this->enumValues;
}

/** @psalm-mutation-free */
public function getExtra(): ?string
{
Expand Down
8 changes: 7 additions & 1 deletion src/Schema/Column/AbstractColumnFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ protected function getColumnClass(string $type, array $info = []): string
ColumnType::ARRAY => ArrayColumn::class,
ColumnType::STRUCTURED => StructuredColumn::class,
ColumnType::JSON => JsonColumn::class,
ColumnType::ENUM => EnumColumn::class,
default => StringColumn::class,
};
}
Expand All @@ -277,6 +278,10 @@ protected function getType(string $dbType, array $info = []): string
return ColumnType::ARRAY;
}

if (isset($info['values'])) {
return ColumnType::ENUM;
}

return static::TYPE_MAP[$dbType] ?? ColumnType::STRING;
}

Expand Down Expand Up @@ -333,7 +338,8 @@ protected function isType(string $type): bool
ColumnType::DATE,
ColumnType::ARRAY,
ColumnType::STRUCTURED,
ColumnType::JSON => true,
ColumnType::JSON,
ColumnType::ENUM => true,
default => isset($this->classMap[$type]),
};
}
Expand Down
11 changes: 11 additions & 0 deletions src/Schema/Column/ColumnBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,15 @@ public static function json(): ColumnInterface
{
return new JsonColumn(ColumnType::JSON);
}

/**
* Builds a column with the abstract type `enum`.
*
* @param string[]|null $values The list of possible values for the enum column.
* @param string|null $dbType The database type of the column. When null, the column will use a CHECK constraint.
*/
public static function enum(?array $values = null, ?string $dbType = null): EnumColumn
{
return new EnumColumn(dbType: $dbType, values: $values);
}
}
2 changes: 1 addition & 1 deletion src/Schema/Column/ColumnFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
* defaultValue?: mixed,
* defaultValueRaw?: string|null,
* dimension?: positive-int,
* enumValues?: array|null,
* extra?: string|null,
* fromResult?: bool,
* primaryKey?: bool,
Expand All @@ -39,6 +38,7 @@
* type?: ColumnType::*,
* unique?: bool,
* unsigned?: bool,
* values?: array|null,
* }
*/
interface ColumnFactoryInterface
Expand Down
19 changes: 0 additions & 19 deletions src/Schema/Column/ColumnInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,6 @@ public function dbTypecast(mixed $value): mixed;
*/
public function defaultValue(mixed $defaultValue): static;

/**
* The list of possible values for the `ENUM` column.
*
* ```php
* $columns = [
* 'status' => ColumnBuilder::string(16)->enumValues(['active', 'inactive']),
* ];
* ```
*/
public function enumValues(?array $enumValues): static;

/**
* Extra SQL to append to the generated SQL for a column.
*
Expand Down Expand Up @@ -166,14 +155,6 @@ public function getDbType(): ?string;
*/
public function getDefaultValue(): mixed;

/**
* @return array|null The enum values of the column.
*
* @see enumValues()
* @psalm-mutation-free
*/
public function getEnumValues(): ?array;

/**
* @return string|null The extra SQL for the column.
*
Expand Down
43 changes: 43 additions & 0 deletions src/Schema/Column/EnumColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Schema\Column;

use LogicException;
use Yiisoft\Db\Constant\ColumnType;

final class EnumColumn extends StringColumn
{
protected const DEFAULT_TYPE = ColumnType::ENUM;

/**
* @var string[]|null $values The list of possible values for an ENUM column.
* @psalm-var non-empty-list<string>|null
*/
protected ?array $values = null;

/**
* @param string[] $values The list of possible values for the `ENUM` column.
* @psalm-param non-empty-list<string> $values
*/
public function values(array $values): static
{
$this->values = $values;
return $this;
}

/**
* @return string[] The enum values of the column.
* @psalm-return non-empty-list<string>
*
* @see values()
*/
public function getValues(): array
{
if ($this->values === null) {
throw new LogicException('Enum values have not been set.');
}
return $this->values;
}
}
6 changes: 3 additions & 3 deletions src/Syntax/ColumnDefinitionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ class ColumnDefinitionParser
* comment?: string,
* defaultValueRaw?: string,
* dimension?: positive-int,
* enumValues?: list<string>,
* extra?: string,
* notNull?: bool,
* scale?: int,
* size?: int,
* type: lowercase-string,
* unique?: bool,
* unsigned?: bool,
* values?: list<string>,
* }
*/
public function parse(string $definition): array
Expand Down Expand Up @@ -80,7 +80,7 @@ public function parse(string $definition): array
}

/**
* @psalm-return array{enumValues: list<string>}
* @psalm-return array{values: list<string>}
*/
protected function enumInfo(string $values): array
{
Expand All @@ -91,7 +91,7 @@ protected function enumInfo(string $values): array
$matches[1],
);

return ['enumValues' => $values];
return ['values' => $values];
}

/**
Expand Down
109 changes: 109 additions & 0 deletions tests/Common/CommonEnumColumnTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Tests\Common;

use PHPUnit\Framework\Attributes\TestWith;
use Yiisoft\Db\Schema\Column\EnumColumn;
use Yiisoft\Db\Schema\TableSchemaInterface;
use Yiisoft\Db\Tests\Support\IntegrationTestCase;

abstract class CommonEnumColumnTest extends IntegrationTestCase
{
protected function setUp(): void
{
$this->executeStatements(...$this->dropDatabaseObjectsStatements());
$this->executeStatements(...$this->createDatabaseObjectsStatements());
}

protected function tearDown(): void
{
$this->executeStatements(...$this->dropDatabaseObjectsStatements());
}

public function testSchema(): void
{
$db = $this->getSharedConnection();

$column = $db->getTableSchema('tbl_enum')->getColumn('status');

$this->assertInstanceOf(EnumColumn::class, $column, 'Column class is "' . $column::class . '"');
$this->assertEqualsCanonicalizing(
['active', 'unactive', 'pending'],
$column->getValues(),
);
}

public function testInsertAndQuery(): void
{
$db = $this->getSharedConnection();

$db->createCommand()->insertBatch('tbl_enum', [
['id' => 1, 'status' => 'active'],
['id' => 2, 'status' => 'pending'],
['id' => 3, 'status' => 'pending'],
['id' => 4, 'status' => 'unactive'],
])->execute();

$rows = $db->select('status')->from('tbl_enum')->orderBy('id ASC')->column();

$this->assertSame(
['active', 'pending', 'pending', 'unactive'],
$rows,
);
}

#[TestWith([['active', 'inactive', 'pending']])]
#[TestWith([['[one]', 'the [two]', 'the [three] to']])]
#[TestWith([['(one)', 'the (two)', 'the (three) to']])]
#[TestWith([["hello''world''", "the '[feature']"]])]
public function testCreateTable(array $items): void
{
$this->dropTable('test_enum_table');

$db = $this->getSharedConnection();
$columnBuilder = $db->getColumnBuilderClass();

$db->createCommand()
->createTable(
'test_enum_table',
[
'id' => $columnBuilder::integer(),
'status' => $columnBuilder::enum($items),
],
)
->execute();

$tableSchema = $db->getTableSchema('test_enum_table');
$this->assertInstanceOf(TableSchemaInterface::class, $tableSchema);

$columns = $tableSchema->getColumns();
$this->assertEqualsCanonicalizing(['id', 'status'], array_keys($columns));

$column = $columns['status'];
$this->assertInstanceOf(EnumColumn::class, $column, $column::class);
$this->assertEqualsCanonicalizing($items, $column->getValues());

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

/**
* SQL statements for create all database objects needed for the test.
*
* Table "tbl_enum" with columns:
* - column "id" of type integer;
* - enum column "status" that accepts values 'active', 'unactive', 'pending'.
*
* @return string[]
*/
abstract protected function createDatabaseObjectsStatements(): array;

/**
* SQL statements for remove all database objects created for the test.
* Take account that object can be absent.
*
* @return string[]
*/
abstract protected function dropDatabaseObjectsStatements(): array;
}
Loading