Skip to content

Commit 3df8f6c

Browse files
committed
feat: Add charset and collation support for MySQL columns
1 parent 16e0874 commit 3df8f6c

2 files changed

Lines changed: 197 additions & 3 deletions

File tree

src/Driver/MySQL/Schema/MySQLColumn.php

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ class MySQLColumn extends AbstractColumn
4242
*/
4343
public const DATETIME_NOW = 'CURRENT_TIMESTAMP';
4444

45-
public const EXCLUDE_FROM_COMPARE = ['size', 'timezone', 'userType', 'attributes', 'first', 'after', 'unknownSize'];
45+
public const EXCLUDE_FROM_COMPARE = ['size', 'timezone', 'userType', 'attributes', 'first', 'after', 'unknownSize', 'charset', 'collation'];
4646
protected const INTEGER_TYPES = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'];
47+
protected const STRING_TYPES = ['varchar', 'char', 'text', 'tinytext', 'mediumtext', 'longtext', 'enum', 'set'];
4748

4849
protected array $mapping = [
4950
//Primary sequences
@@ -192,6 +193,18 @@ class MySQLColumn extends AbstractColumn
192193
#[ColumnAttribute]
193194
protected string $comment = '';
194195

196+
/**
197+
* Column character set.
198+
*/
199+
#[ColumnAttribute(self::STRING_TYPES)]
200+
protected string $charset = '';
201+
202+
/**
203+
* Column collation.
204+
*/
205+
#[ColumnAttribute(self::STRING_TYPES)]
206+
protected string $collation = '';
207+
195208
/**
196209
* Column name to position after.
197210
*/
@@ -217,6 +230,14 @@ public static function createInstance(string $table, array $schema, ?\DateTimeZo
217230
$column->defaultValue = $schema['Default'];
218231
$column->autoIncrement = \stripos($schema['Extra'], 'auto_increment') !== false;
219232

233+
if (!empty($schema['Collation'])) {
234+
$column->collation = $schema['Collation'];
235+
$pos = \strpos($schema['Collation'], '_');
236+
$column->charset = $pos !== false
237+
? \substr($schema['Collation'], 0, $pos)
238+
: $schema['Collation'];
239+
}
240+
220241
if (
221242
!\preg_match(
222243
'/^(?P<type>[a-z]+)(?:\((?P<options>[^)]+)\))?(?: (?P<attr>[a-z ]+))?/',
@@ -322,6 +343,23 @@ public function sqlStatement(DriverInterface $driver): string
322343
$statement = parent::sqlStatement($driver);
323344

324345
$this->defaultValue = $defaultValue;
346+
347+
// Add CHARACTER SET and COLLATE for string-type columns
348+
if ($this->charset !== '' || $this->collation !== '') {
349+
$charsetClause = '';
350+
if ($this->charset !== '') {
351+
$charsetClause .= ' CHARACTER SET ' . $this->charset;
352+
}
353+
if ($this->collation !== '') {
354+
$charsetClause .= ' COLLATE ' . $this->collation;
355+
}
356+
$statement = \preg_replace(
357+
'/ ((?:NOT )?NULL)\b/',
358+
$charsetClause . ' $1',
359+
$statement,
360+
1,
361+
);
362+
}
325363
}
326364

327365
$this->comment === '' or $statement .= " COMMENT {$driver->quote($this->comment)}";
@@ -358,9 +396,16 @@ public function compare(AbstractColumn $initial): bool
358396

359397
$result = \Closure::fromCallable([parent::class, 'compare'])->bindTo($self)($initial);
360398

361-
362399
if ($self->type === 'varchar' || $self->type === 'varbinary') {
363-
return $result && $self->size === $initial->size;
400+
$result = $result && $self->size === $initial->size;
401+
}
402+
403+
// Compare charset/collation only when explicitly set (non-empty means user specified a value)
404+
if ($result && $self->charset !== '' && $self->charset !== $initial->charset) {
405+
return false;
406+
}
407+
if ($result && $self->collation !== '' && $self->collation !== $initial->collation) {
408+
return false;
364409
}
365410

366411
return $result;
@@ -422,6 +467,16 @@ public function binary(int $size = 0): self
422467
return $this;
423468
}
424469

470+
public function getCharset(): string
471+
{
472+
return $this->charset;
473+
}
474+
475+
public function getCollation(): string
476+
{
477+
return $this->collation;
478+
}
479+
425480
public function getComment(): string
426481
{
427482
return $this->comment;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Database\Tests\Functional\Driver\MySQL\Schema;
6+
7+
use Cycle\Database\Driver\MySQL\Schema\MySQLColumn;
8+
use Cycle\Database\Tests\Functional\Driver\Common\BaseTest;
9+
10+
/**
11+
* @group driver
12+
* @group driver-mysql
13+
*/
14+
class CharsetCollationTest extends BaseTest
15+
{
16+
public const DRIVER = 'mysql';
17+
18+
public function testStringColumnWithCharsetAndCollation(): void
19+
{
20+
$schema = $this->schema('table');
21+
$this->assertFalse($schema->exists());
22+
23+
$schema->primary('id');
24+
$column = $schema->string('email', size: 255);
25+
$column->charset('ascii');
26+
$column->collation('ascii_bin');
27+
$column->nullable(false);
28+
$schema->save();
29+
30+
$schema = $this->schema('table');
31+
$this->assertTrue($schema->exists());
32+
33+
$emailColumn = $schema->column('email');
34+
\assert($emailColumn instanceof MySQLColumn);
35+
36+
$this->assertSame('ascii', $emailColumn->getCharset());
37+
$this->assertSame('ascii_bin', $emailColumn->getCollation());
38+
}
39+
40+
public function testStringColumnCharsetCollationPersistsAfterReload(): void
41+
{
42+
$schema = $this->schema('table');
43+
44+
$schema->primary('id');
45+
$column = $schema->string('name', size: 100);
46+
$column->charset('utf8mb4');
47+
$column->collation('utf8mb4_unicode_ci');
48+
$schema->save();
49+
50+
$schema = $this->schema('table');
51+
52+
$nameColumn = $schema->column('name');
53+
\assert($nameColumn instanceof MySQLColumn);
54+
55+
$this->assertSame('utf8mb4', $nameColumn->getCharset());
56+
$this->assertSame('utf8mb4_unicode_ci', $nameColumn->getCollation());
57+
$this->assertSameAsInDB($schema);
58+
}
59+
60+
public function testChangeCollation(): void
61+
{
62+
$schema = $this->schema('table');
63+
64+
$schema->primary('id');
65+
$column = $schema->string('title', size: 255);
66+
$column->charset('ascii');
67+
$column->collation('ascii_general_ci');
68+
$schema->save();
69+
70+
$schema = $this->schema('table');
71+
$titleColumn = $schema->column('title');
72+
\assert($titleColumn instanceof MySQLColumn);
73+
$this->assertSame('ascii_general_ci', $titleColumn->getCollation());
74+
75+
// Change collation
76+
$titleColumn->collation('ascii_bin');
77+
$schema->save();
78+
79+
$schema = $this->schema('table');
80+
$titleColumn = $schema->column('title');
81+
\assert($titleColumn instanceof MySQLColumn);
82+
$this->assertSame('ascii_bin', $titleColumn->getCollation());
83+
}
84+
85+
public function testTextColumnWithCharsetAndCollation(): void
86+
{
87+
$schema = $this->schema('table');
88+
89+
$schema->primary('id');
90+
$column = $schema->text('content');
91+
$column->charset('utf8mb4');
92+
$column->collation('utf8mb4_bin');
93+
$schema->save();
94+
95+
$schema = $this->schema('table');
96+
97+
$contentColumn = $schema->column('content');
98+
\assert($contentColumn instanceof MySQLColumn);
99+
100+
$this->assertSame('utf8mb4', $contentColumn->getCharset());
101+
$this->assertSame('utf8mb4_bin', $contentColumn->getCollation());
102+
}
103+
104+
public function testColumnWithoutExplicitCharsetInheritsDefault(): void
105+
{
106+
$schema = $this->schema('table');
107+
108+
$schema->primary('id');
109+
$schema->string('name', size: 100);
110+
$schema->save();
111+
112+
$schema = $this->schema('table');
113+
$this->assertSameAsInDB($schema);
114+
115+
$nameColumn = $schema->column('name');
116+
\assert($nameColumn instanceof MySQLColumn);
117+
118+
// Column should have inherited charset/collation from table/database defaults
119+
$this->assertNotEmpty($nameColumn->getCharset());
120+
$this->assertNotEmpty($nameColumn->getCollation());
121+
}
122+
123+
public function testCompareColumnsWithSameCharset(): void
124+
{
125+
$schema = $this->schema('table');
126+
127+
$schema->primary('id');
128+
$column = $schema->string('email', size: 255);
129+
$column->charset('ascii');
130+
$column->collation('ascii_bin');
131+
$column->nullable(false);
132+
$schema->save();
133+
134+
$schema = $this->schema('table');
135+
$dbColumn = $schema->column('email');
136+
137+
$this->assertTrue($column->compare($dbColumn));
138+
}
139+
}

0 commit comments

Comments
 (0)