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 @@ -123,6 +123,7 @@
- Chg #1019: Split `In` condition to `In` and `NotIn` (@vjik)
- Chg #1018: Split `BetweenColumns` condition to `BetweenColumns` and `NotBetweenColumns` (@vjik)
- Chg #1017: Split `Between` condition to `Between` and `NotBetween` (@vjik)
- New #1020: Support column's collation (@Tigrov)
- Chg #1021: Move conjunction type from operator string value to `Like` condition constructor parameter (@vjik)

## 1.3.0 March 21, 2024
Expand Down
17 changes: 17 additions & 0 deletions src/QueryBuilder/AbstractColumnDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Yiisoft\Db\Constant\ColumnType;
use Yiisoft\Db\Constant\ReferentialAction;
use Yiisoft\Db\Schema\Column\CollatableColumnInterface;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function in_array;
Expand Down Expand Up @@ -57,6 +58,7 @@ public function build(ColumnInterface $column): string
. $this->buildDefault($column)
. $this->buildComment($column)
. $this->buildCheck($column)
. $this->buildCollate($column)
. $this->buildReferences($column)
. $this->buildExtra($column);
}
Expand Down Expand Up @@ -124,6 +126,21 @@ protected function buildCheck(ColumnInterface $column): string
return !empty($check) ? " CHECK ($check)" : '';
}

/**
* Builds the collation clause for the column.
*
* @return string A string containing the COLLATE keyword and the collation name.
*/
protected function buildCollate(ColumnInterface $column): string
{
if (!$column instanceof CollatableColumnInterface || empty($column->getCollation())) {
return '';
}

/** @psalm-suppress PossiblyNullOperand */
return ' COLLATE ' . $column->getCollation();
}

/**
* Builds the comment clause for the column. Default is empty string.
*
Expand Down
25 changes: 25 additions & 0 deletions src/Schema/Column/CollatableColumnInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Schema\Column;

/**
* Represents a column with collation.
*
* Provides methods to set and get a collation value for the column.
*/
interface CollatableColumnInterface
{
/**
* Sets the collation for the column.
*/
public function collation(string|null $collation): static;

/**
* Returns the collation of the column.
*
* @psalm-mutation-free
*/
public function getCollation(): string|null;
}
1 change: 1 addition & 0 deletions src/Schema/Column/ColumnFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* @psalm-type ColumnInfo = array{
* autoIncrement?: bool,
* check?: string|null,
* collation?: string|null,
* column?: ColumnInterface|null,
* columns?: array<string, ColumnInterface>,
* comment?: string|null,
Expand Down
19 changes: 18 additions & 1 deletion src/Schema/Column/StringColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@
/**
* Represents the metadata for a string column.
*/
class StringColumn extends AbstractColumn
class StringColumn extends AbstractColumn implements CollatableColumnInterface
{
protected const DEFAULT_TYPE = ColumnType::STRING;

/**
* @var string|null The column collation.
*/
protected string|null $collation = null;

public function collation(string|null $collation): static
{
$this->collation = $collation;
return $this;
}

public function dbTypecast(mixed $value): mixed
{
return match (gettype($value)) {
Expand All @@ -39,6 +50,12 @@ public function dbTypecast(mixed $value): mixed
};
}

/** @psalm-mutation-free */
public function getCollation(): string|null
{
return $this->collation;
}

/** @psalm-mutation-free */
public function getPhpType(): string
{
Expand Down
102 changes: 62 additions & 40 deletions src/Syntax/ColumnDefinitionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,27 @@
use function explode;
use function preg_match;
use function preg_match_all;
use function preg_replace;
use function str_replace;
use function strlen;
use function strtolower;
use function substr;
use function substr_count;
use function trim;

/**
* Parses column definition string. For example, `string(255)` or `int unsigned`.
*
* @psalm-type ExtraInfo = array{
* check?: string,
* collation?: string,
* comment?: string,
* defaultValueRaw?: string,
* extra?: string,
* notNull?: bool,
* unique?: bool,
* unsigned?: bool
* }
*/
class ColumnDefinitionParser
{
Expand All @@ -27,6 +40,7 @@ class ColumnDefinitionParser
*
* @psalm-return array{
* check?: string,
* collation?: string,
* comment?: string,
* defaultValueRaw?: string,
* dimension?: positive-int,
Expand Down Expand Up @@ -76,15 +90,7 @@ protected function enumInfo(string $values): array
}

/**
* @psalm-return array{
* check?: string,
* comment?: string,
* defaultValueRaw?: string,
* extra?: string,
* notNull?: bool,
* unique?: bool,
* unsigned?: bool
* }
* @psalm-return ExtraInfo
*/
protected function extraInfo(string $extra): array
{
Expand All @@ -96,52 +102,68 @@ protected function extraInfo(string $extra): array
$bracketsPattern = '(\(((?>[^()]+)|(?-2))*\))';
$defaultPattern = "/\\s*\\bDEFAULT\\s+('(?:[^']|'')*'|\"(?:[^\"]|\"\")*\"|[^(\\s]*$bracketsPattern?\\S*)/i";

if (preg_match($defaultPattern, $extra, $matches) === 1) {
$info['defaultValueRaw'] = $matches[1];
$extra = str_replace($matches[0], '', $extra);
$extra = $this->parseStringValue($extra, $defaultPattern, 'defaultValueRaw', $info);
$extra = $this->parseStringValue($extra, "/\\s*\\bCOMMENT\\s+'((?:[^']|'')*)'/i", 'comment', $info);
$extra = $this->parseStringValue($extra, "/\\s*\\bCHECK\\s+$bracketsPattern/i", 'check', $info);
$extra = $this->parseStringValue($extra, '/\s*\bCOLLATE\s+(\S+)/i', 'collation', $info);
$extra = $this->parseBoolValue($extra, '/\s*\bUNSIGNED\b/i', 'unsigned', $info);
$extra = $this->parseBoolValue($extra, '/\s*\bUNIQUE\b/i', 'unique', $info);
$extra = $this->parseBoolValue($extra, '/\s*\bNOT\s+NULL\b/i', 'notNull', $info);

if (empty($info['notNull'])) {
$extra = $this->parseBoolValue($extra, '/\s*\bNULL\b/i', 'notNull', $info);

if (!empty($info['notNull'])) {
$info['notNull'] = false;
}
}

if (preg_match("/\\s*\\bCOMMENT\\s+'((?:[^']|'')*)'/i", $extra, $matches) === 1) {
$info['comment'] = str_replace("''", "'", $matches[1]);
$extra = str_replace($matches[0], '', $extra);
/** @psalm-var ExtraInfo $info */
if (!empty($info['comment'])) {
$info['comment'] = str_replace("''", "'", $info['comment']);
}

if (preg_match("/\\s*\\bCHECK\\s+$bracketsPattern/i", $extra, $matches) === 1) {
$info['check'] = substr($matches[1], 1, -1);
$extra = str_replace($matches[0], '', $extra);
if (!empty($info['check'])) {
$info['check'] = substr($info['check'], 1, -1);
}

/** @var string $extra */
$extra = preg_replace('/\s*\bUNSIGNED\b/i', '', $extra, 1, $count);
if ($count > 0) {
$info['unsigned'] = true;
if (!empty($extra)) {
$info['extra'] = $extra;
}

/** @var string $extra */
$extra = preg_replace('/\s*\bUNIQUE\b/i', '', $extra, 1, $count);
if ($count > 0) {
$info['unique'] = true;
return $info;
}

/**
* @psalm-param non-empty-string $pattern
*/
protected function parseStringValue(string $extra, string $pattern, string $name, array &$info): string
{
if (!empty($extra) && preg_match($pattern, $extra, $matches) === 1) {
$info[$name] = $matches[1];
return trim(str_replace($matches[0], '', $extra));
}

/** @var string $extra */
$extra = preg_replace('/\s*\bNOT\s+NULL\b/i', '', $extra, 1, $count);
if ($count > 0) {
$info['notNull'] = true;
} else {
/** @var string $extra */
$extra = preg_replace('/\s*\bNULL\b/i', '', $extra, 1, $count);
if ($count > 0) {
$info['notNull'] = false;
}
return $extra;
}

/**
* @psalm-param non-empty-string $pattern
*/
protected function parseBoolValue(string $extra, string $pattern, string $name, array &$info): string
{
if (empty($extra)) {
return '';
}

$extra = trim($extra);
/** @psalm-suppress PossiblyNullArgument */
$extra = trim(preg_replace($pattern, '', $extra, 1, $count));

if (!empty($extra)) {
$info['extra'] = $extra;
if ($count > 0) {
$info[$name] = true;
}

return $info;
return $extra;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions tests/AbstractColumnDefinitionParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

namespace Yiisoft\Db\Tests;

use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\TestCase;
use Yiisoft\Db\Syntax\ColumnDefinitionParser;
use Yiisoft\Db\Tests\Provider\ColumnDefinitionParserProvider;
use Yiisoft\Db\Tests\Support\TestTrait;

/**
Expand All @@ -20,9 +22,7 @@ protected function createColumnDefinitionParser(): ColumnDefinitionParser
return new ColumnDefinitionParser();
}

/**
* @dataProvider \Yiisoft\Db\Tests\Provider\ColumnDefinitionParserProvider::parse
*/
#[DataProviderExternal(ColumnDefinitionParserProvider::class, 'parse')]
public function testParse(string $definition, array $expected): void
{
$parser = $this->createColumnDefinitionParser();
Expand Down
12 changes: 12 additions & 0 deletions tests/Db/Schema/Column/ColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
use Yiisoft\Db\Constraint\ForeignKey;
use Yiisoft\Db\Expression\ArrayExpression;
use Yiisoft\Db\Schema\Column\ArrayColumn;
use Yiisoft\Db\Schema\Column\CollatableColumnInterface;
use Yiisoft\Db\Schema\Column\ColumnBuilder;
use Yiisoft\Db\Schema\Column\ColumnInterface;
use Yiisoft\Db\Schema\Column\IntegerColumn;
use Yiisoft\Db\Schema\Column\StringColumn;
use Yiisoft\Db\Schema\Column\StructuredColumn;
use Yiisoft\Db\Tests\AbstractColumnTest;
use Yiisoft\Db\Tests\Support\Stub\Column;
Expand Down Expand Up @@ -366,4 +368,14 @@ public function testStructuredColumnGetColumns(): void
$this->assertSame($structuredCol, $structuredCol->columns($columns));
$this->assertSame($columns, $structuredCol->getColumns());
}

public function testStringColumnCollation(): void
{
$stringCol = new StringColumn();

$this->assertInstanceOf(CollatableColumnInterface::class, $stringCol);
$this->assertNull($stringCol->getCollation());
$this->assertSame($stringCol, $stringCol->collation('utf8mb4'));
$this->assertSame('utf8mb4', $stringCol->getCollation());
}
}
3 changes: 2 additions & 1 deletion tests/Provider/ColumnDefinitionParserProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public static function parse(): array
['int(10) NOT NULL', ['type' => 'int', 'size' => 10, 'notNull' => true]],
['text NOT NULL', ['type' => 'text', 'notNull' => true]],
['text NULL', ['type' => 'text', 'notNull' => false]],
['text COLLATE utf8mb4', ['type' => 'text', 'extra' => 'COLLATE utf8mb4']],
['text COLLATE utf8mb4', ['type' => 'text', 'collation' => 'utf8mb4']],
["text COMPRESSION 'LZ4'", ['type' => 'text', 'extra' => "COMPRESSION 'LZ4'"]],
['text DEFAULT NULL', ['type' => 'text', 'defaultValueRaw' => 'NULL']],
["text DEFAULT 'value'", ['type' => 'text', 'defaultValueRaw' => "'value'"]],
['varchar(36) DEFAULT uuid()', ['type' => 'varchar', 'size' => 36, 'defaultValueRaw' => 'uuid()']],
Expand Down
6 changes: 6 additions & 0 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,12 @@ public static function buildColumnDefinition(): array
],
"check('')" => ['integer', ColumnBuilder::integer()->check('')],
'check(null)' => ['integer', ColumnBuilder::integer()->check(null)],
"collation('collation_name')" => [
'varchar(255) COLLATE collation_name',
ColumnBuilder::string()->collation('collation_name'),
],
"collation('')" => ['varchar(255)', ColumnBuilder::string()->collation('')],
'collation(null)' => ['varchar(255)', ColumnBuilder::string()->collation(null)],
"comment('comment')" => ['varchar(255)', ColumnBuilder::string()->comment('comment')],
"comment('')" => ['varchar(255)', ColumnBuilder::string()->comment('')],
'comment(null)' => ['varchar(255)', ColumnBuilder::string()->comment(null)],
Expand Down
Loading