packages/ztd-query-mysql/tests/Unit/Shadow/Mutation/Table/AlterTableMutationTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Shadow\Mutation\Table;
6
7use PhpMyAdmin\SqlParser\Parser;
8use PhpMyAdmin\SqlParser\Statements\AlterStatement;
9use PHPUnit\Framework\Attributes\CoversClass;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use ZtdQuery\Exception\ColumnAlreadyExistsException;
13use ZtdQuery\Exception\ColumnNotFoundException;
14use ZtdQuery\Exception\SchemaNotFoundException;
15use ZtdQuery\Exception\UnsupportedSqlException;
16use ZtdQuery\Platform\MySql\Schema\MySqlSchemaParser;
17use ZtdQuery\Platform\MySql\Schema\Partition\MySqlPartitioningParser;
18use ZtdQuery\Platform\MySql\Shadow\Mutation\Table\AlterTableMutation;
19use ZtdQuery\Platform\MySql\Sql\MySqlParser;
20use ZtdQuery\Schema\ColumnDeclaration;
21use ZtdQuery\Schema\ColumnTypeFamily;
22use ZtdQuery\Schema\TableDefinition;
23use ZtdQuery\Schema\TableDefinitionRegistry;
24use ZtdQuery\Shadow\ShadowStore;
25
26#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Alter\OptionList::class)]
27#[UsesClass(\ZtdQuery\Platform\MySql\Sql\OptionalInsertIntoNormalizer::class)]
28#[UsesClass(\ZtdQuery\Platform\MySql\Schema\DefinitionBuilder::class)]
29#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Key\DefinitionReader::class)]
30#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Key\TokenReader::class)]
31#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Partition\PredicateCompiler::class)]
32#[CoversClass(AlterTableMutation::class)]
33#[UsesClass(\ZtdQuery\Platform\MySql\Schema\MySqlColumnTypeMapper::class)]
34#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Key\MySqlForeignKeyDefinitionParser::class)]
35#[UsesClass(MySqlParser::class)]
36#[UsesClass(MySqlSchemaParser::class)]
37#[UsesClass(MySqlPartitioningParser::class)]
38#[UsesClass(\ZtdQuery\Platform\MySql\Sql\MySqlLexerProfile::class)]
39#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\ColumnAction::class)]
40#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\ColumnAlteration::class)]
41#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\ColumnDefinitionParser::class)]
42#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\CreateTableRenderer::class)]
43#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\OperationApplier::class)]
44#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\PrimaryKeyAlteration::class)]
45#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\StoredColumns::class)]
46#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\UnsupportedKeyword::class)]
47final class AlterTableMutationTest extends TestCase
48{
49    public function testApplyAddColumnAddsNewColumn(): void
50    {
51        $schemaParser = new MySqlSchemaParser(new MySqlParser());
52        $registry = new TableDefinitionRegistry();
53        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
54        self::assertNotNull($definition);
55        $registry->register('users', $definition);
56        $store = new ShadowStore();
57
58        $parser = new Parser('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
59        $alterStmt = $parser->statements[0];
60        self::assertInstanceOf(AlterStatement::class, $alterStmt);
61        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
62        $mutation->apply($store, []);
63
64        $newDef = $registry->get('users');
65        self::assertNotNull($newDef);
66        self::assertContains('email', $newDef->columns);
67    }
68
69    public function testApplyAddColumnPreservesForeignKeyMetadata(): void
70    {
71        $schemaParser = new MySqlSchemaParser(new MySqlParser());
72        $registry = new TableDefinitionRegistry();
73        $definition = $schemaParser->parse(
74            'CREATE TABLE children (id INT PRIMARY KEY, parent_id INT, CONSTRAINT fk_parent '
75            . 'FOREIGN KEY (parent_id) REFERENCES parents (id) ON DELETE CASCADE ON UPDATE CASCADE)'
76        );
77        self::assertNotNull($definition);
78        $registry->register('children', $definition);
79        $store = new ShadowStore();
80
81        $parser = new Parser('ALTER TABLE children ADD COLUMN label VARCHAR(255)');
82        $alterStmt = $parser->statements[0];
83        self::assertInstanceOf(AlterStatement::class, $alterStmt);
84        (new AlterTableMutation('children', $alterStmt, $registry, $schemaParser))->apply($store, []);
85
86        $foreignKey = $registry->get('children')?->foreignKeys['fk_parent'] ?? null;
87        self::assertNotNull($foreignKey);
88        self::assertSame(['parent_id'], $foreignKey->columns);
89        self::assertSame(['id'], $foreignKey->referencedColumns);
90        self::assertSame('CASCADE', $foreignKey->onDelete->value);
91        self::assertSame('CASCADE', $foreignKey->onUpdate->value);
92    }
93
94    public function testApplyAddColumnPreservesPartitionMetadata(): void
95    {
96        $schemaParser = new MySqlSchemaParser(new MySqlParser());
97        $registry = new TableDefinitionRegistry();
98        $definition = $schemaParser->parse('CREATE TABLE events (id INT, event_date DATE) '
99            . 'PARTITION BY RANGE (YEAR(event_date)) ('
100            . 'PARTITION p2024 VALUES LESS THAN (2025), PARTITION pmax VALUES LESS THAN MAXVALUE)');
101        self::assertNotNull($definition);
102        $registry->register('events', $definition);
103
104        $parser = new Parser('ALTER TABLE events ADD COLUMN label VARCHAR(255)');
105        $alterStmt = $parser->statements[0] ?? null;
106        self::assertInstanceOf(AlterStatement::class, $alterStmt);
107        (new AlterTableMutation('events', $alterStmt, $registry, $schemaParser))->apply(new ShadowStore(), []);
108
109        $newDefinition = $registry->get('events');
110        self::assertNotNull($newDefinition);
111        self::assertContains('label', $newDefinition->columns);
112        self::assertSame(
113            ['(YEAR(event_date)) >= 2025'],
114            $newDefinition->partitioning?->predicatesFor(['pmax']),
115        );
116    }
117
118    public function testApplyAddColumnPreservesQuotedForeignKeyIdentifiers(): void
119    {
120        $schemaParser = new MySqlSchemaParser(new MySqlParser());
121        $registry = new TableDefinitionRegistry();
122        $definition = $schemaParser->parse(
123            'CREATE TABLE children (`child id` INT PRIMARY KEY, `parent id` INT, CONSTRAINT `fk parent` '
124            . 'FOREIGN KEY (`parent id`) REFERENCES `parent table` (`parent key`) '
125            . 'ON DELETE CASCADE ON UPDATE CASCADE)'
126        );
127        self::assertNotNull($definition);
128        $registry->register('children', $definition);
129
130        $parser = new Parser('ALTER TABLE children ADD COLUMN label VARCHAR(255)');
131        $alterStmt = $parser->statements[0];
132        self::assertInstanceOf(AlterStatement::class, $alterStmt);
133        (new AlterTableMutation('children', $alterStmt, $registry, $schemaParser))->apply(new ShadowStore(), []);
134
135        $foreignKey = $registry->get('children')?->foreignKeys['fk parent'] ?? null;
136        self::assertNotNull($foreignKey);
137        self::assertSame(['parent id'], $foreignKey->columns);
138        self::assertSame('parent table', $foreignKey->referencedTable);
139        self::assertSame(['parent key'], $foreignKey->referencedColumns);
140    }
141
142    public function testApplyAddColumnWithoutColumnKeyword(): void
143    {
144        $schemaParser = new MySqlSchemaParser(new MySqlParser());
145        $registry = new TableDefinitionRegistry();
146        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
147        self::assertNotNull($definition);
148        $registry->register('users', $definition);
149        $store = new ShadowStore();
150
151        $parser = new Parser('ALTER TABLE users ADD name VARCHAR(255)');
152        $alterStmt = $parser->statements[0];
153        self::assertInstanceOf(AlterStatement::class, $alterStmt);
154        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
155        $mutation->apply($store, []);
156
157        $newDef = $registry->get('users');
158        self::assertNotNull($newDef);
159        self::assertContains('name', $newDef->columns);
160    }
161
162    public function testApplyAddColumnThrowsExceptionWhenColumnExists(): void
163    {
164        $schemaParser = new MySqlSchemaParser(new MySqlParser());
165        $registry = new TableDefinitionRegistry();
166        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
167        self::assertNotNull($definition);
168        $registry->register('users', $definition);
169        $store = new ShadowStore();
170
171        $parser = new Parser('ALTER TABLE users ADD COLUMN name VARCHAR(100)');
172        $alterStmt = $parser->statements[0];
173        self::assertInstanceOf(AlterStatement::class, $alterStmt);
174        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
175
176        $this->expectException(ColumnAlreadyExistsException::class);
177        $this->expectExceptionMessage("Column 'name' already exists in table 'users'.");
178        $mutation->apply($store, []);
179    }
180
181    public function testApplyDropColumnRemovesColumn(): void
182    {
183        $schemaParser = new MySqlSchemaParser(new MySqlParser());
184        $registry = new TableDefinitionRegistry();
185        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
186        self::assertNotNull($definition);
187        $registry->register('users', $definition);
188        $store = new ShadowStore();
189        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
190
191        $parser = new Parser('ALTER TABLE users DROP COLUMN name');
192        $alterStmt = $parser->statements[0];
193        self::assertInstanceOf(AlterStatement::class, $alterStmt);
194        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
195        $mutation->apply($store, []);
196
197        $newDef = $registry->get('users');
198        self::assertNotNull($newDef);
199        self::assertNotContains('name', $newDef->columns);
200        self::assertArrayNotHasKey('name', $store->get('users')[0]);
201    }
202
203    public function testApplyDropColumnThrowsExceptionWhenColumnNotFound(): void
204    {
205        $schemaParser = new MySqlSchemaParser(new MySqlParser());
206        $registry = new TableDefinitionRegistry();
207        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
208        self::assertNotNull($definition);
209        $registry->register('users', $definition);
210        $store = new ShadowStore();
211
212        $parser = new Parser('ALTER TABLE users DROP COLUMN email');
213        $alterStmt = $parser->statements[0];
214        self::assertInstanceOf(AlterStatement::class, $alterStmt);
215        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
216
217        $this->expectException(ColumnNotFoundException::class);
218        $this->expectExceptionMessage("Column 'email' does not exist in table 'users'.");
219        $mutation->apply($store, []);
220    }
221
222    public function testApplyModifyColumnModifiesColumnDefinition(): void
223    {
224        $schemaParser = new MySqlSchemaParser(new MySqlParser());
225        $registry = new TableDefinitionRegistry();
226        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
227        self::assertNotNull($definition);
228        $registry->register('users', $definition);
229        $store = new ShadowStore();
230
231        $parser = new Parser('ALTER TABLE users MODIFY COLUMN name VARCHAR(255) NOT NULL');
232        $alterStmt = $parser->statements[0];
233        self::assertInstanceOf(AlterStatement::class, $alterStmt);
234        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
235        $mutation->apply($store, []);
236
237        $newDef = $registry->get('users');
238        self::assertNotNull($newDef);
239        self::assertContains('name', $newDef->columns);
240    }
241
242    public function testApplyModifyColumnThrowsExceptionWhenColumnNotFound(): void
243    {
244        $schemaParser = new MySqlSchemaParser(new MySqlParser());
245        $registry = new TableDefinitionRegistry();
246        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
247        self::assertNotNull($definition);
248        $registry->register('users', $definition);
249        $store = new ShadowStore();
250
251        $parser = new Parser('ALTER TABLE users MODIFY COLUMN email VARCHAR(255)');
252        $alterStmt = $parser->statements[0];
253        self::assertInstanceOf(AlterStatement::class, $alterStmt);
254        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
255
256        $this->expectException(ColumnNotFoundException::class);
257        $mutation->apply($store, []);
258    }
259
260    public function testApplyChangeColumnRenamesAndModifiesColumn(): void
261    {
262        $schemaParser = new MySqlSchemaParser(new MySqlParser());
263        $registry = new TableDefinitionRegistry();
264        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
265        self::assertNotNull($definition);
266        $registry->register('users', $definition);
267        $store = new ShadowStore();
268        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
269
270        $parser = new Parser('ALTER TABLE users CHANGE COLUMN name full_name VARCHAR(255)');
271        $alterStmt = $parser->statements[0];
272        self::assertInstanceOf(AlterStatement::class, $alterStmt);
273        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
274        $mutation->apply($store, []);
275
276        $newDef = $registry->get('users');
277        self::assertNotNull($newDef);
278        self::assertContains('full_name', $newDef->columns);
279        self::assertArrayHasKey('full_name', $store->get('users')[0]);
280        self::assertArrayNotHasKey('name', $store->get('users')[0]);
281    }
282
283    public function testApplyChangeColumnThrowsExceptionWhenColumnNotFound(): void
284    {
285        $schemaParser = new MySqlSchemaParser(new MySqlParser());
286        $registry = new TableDefinitionRegistry();
287        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
288        self::assertNotNull($definition);
289        $registry->register('users', $definition);
290        $store = new ShadowStore();
291
292        $parser = new Parser('ALTER TABLE users CHANGE COLUMN email new_email VARCHAR(255)');
293        $alterStmt = $parser->statements[0];
294        self::assertInstanceOf(AlterStatement::class, $alterStmt);
295        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
296
297        $this->expectException(ColumnNotFoundException::class);
298        $mutation->apply($store, []);
299    }
300
301    public function testApplyThrowsExceptionWhenTableNotFound(): void
302    {
303        $schemaParser = new MySqlSchemaParser(new MySqlParser());
304        $registry = new TableDefinitionRegistry();
305        $store = new ShadowStore();
306
307        $parser = new Parser('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
308        $alterStmt = $parser->statements[0];
309        self::assertInstanceOf(AlterStatement::class, $alterStmt);
310        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
311
312        $this->expectException(SchemaNotFoundException::class);
313        $this->expectExceptionMessage("Table 'users' does not exist.");
314        $mutation->apply($store, []);
315    }
316
317    public function testTableNameReturnsTableName(): void
318    {
319        $registry = new TableDefinitionRegistry();
320        $schemaParser = new MySqlSchemaParser(new MySqlParser());
321        $parser = new Parser('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
322        $alterStmt = $parser->statements[0];
323        self::assertInstanceOf(AlterStatement::class, $alterStmt);
324        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
325
326        self::assertSame('users', $mutation->tableName());
327    }
328
329    public function testApplyRenameColumnRenamesExistingColumn(): void
330    {
331        $schemaParser = new MySqlSchemaParser(new MySqlParser());
332        $registry = new TableDefinitionRegistry();
333        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
334        self::assertNotNull($definition);
335        $registry->register('users', $definition);
336        $store = new ShadowStore();
337        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
338
339        $parser = new Parser('ALTER TABLE users RENAME COLUMN name TO full_name');
340        $alterStmt = $parser->statements[0];
341        self::assertInstanceOf(AlterStatement::class, $alterStmt);
342        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
343        $mutation->apply($store, []);
344
345        self::assertArrayHasKey('full_name', $store->get('users')[0]);
346        self::assertSame('Alice', $store->get('users')[0]['full_name']);
347    }
348
349    public function testApplyRenameColumnThrowsExceptionWhenColumnNotFound(): void
350    {
351        $schemaParser = new MySqlSchemaParser(new MySqlParser());
352        $registry = new TableDefinitionRegistry();
353        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
354        self::assertNotNull($definition);
355        $registry->register('users', $definition);
356        $store = new ShadowStore();
357
358        $parser = new Parser('ALTER TABLE users RENAME COLUMN email TO new_email');
359        $alterStmt = $parser->statements[0];
360        self::assertInstanceOf(AlterStatement::class, $alterStmt);
361        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
362
363        $this->expectException(ColumnNotFoundException::class);
364        $mutation->apply($store, []);
365    }
366
367    public function testApplyDropPrimaryKeyRemovesPrimaryKey(): void
368    {
369        $schemaParser = new MySqlSchemaParser(new MySqlParser());
370        $registry = new TableDefinitionRegistry();
371        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
372        self::assertNotNull($definition);
373        $registry->register('users', $definition);
374        $store = new ShadowStore();
375
376        $parser = new Parser('ALTER TABLE users DROP PRIMARY KEY');
377        $alterStmt = $parser->statements[0];
378        self::assertInstanceOf(AlterStatement::class, $alterStmt);
379        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
380        $mutation->apply($store, []);
381
382        $newDef = $registry->get('users');
383        self::assertNotNull($newDef);
384        self::assertSame([], $newDef->primaryKeys);
385    }
386
387    public function testBuildCreateTableSqlPreservesNotNull(): void
388    {
389        $schemaParser = new MySqlSchemaParser(new MySqlParser());
390        $registry = new TableDefinitionRegistry();
391        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100) NOT NULL)');
392        self::assertNotNull($definition);
393        $registry->register('users', $definition);
394        $store = new ShadowStore();
395
396        $parser = new Parser('ALTER TABLE users ADD COLUMN email TEXT');
397        $alterStmt = $parser->statements[0];
398        self::assertInstanceOf(AlterStatement::class, $alterStmt);
399        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
400        $mutation->apply($store, []);
401
402        $def = $registry->get('users');
403        self::assertNotNull($def);
404        self::assertContains('name', $def->notNullColumns);
405        self::assertContains('email', $def->columns);
406    }
407
408    public function testBuildCreateTableSqlSinglePrimaryKey(): void
409    {
410        $schemaParser = new MySqlSchemaParser(new MySqlParser());
411        $registry = new TableDefinitionRegistry();
412        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
413        self::assertNotNull($definition);
414        $registry->register('users', $definition);
415        $store = new ShadowStore();
416
417        $parser = new Parser('ALTER TABLE users ADD COLUMN email TEXT');
418        $alterStmt = $parser->statements[0];
419        self::assertInstanceOf(AlterStatement::class, $alterStmt);
420        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
421        $mutation->apply($store, []);
422
423        $def = $registry->get('users');
424        self::assertNotNull($def);
425        self::assertSame(['id'], $def->primaryKeys);
426    }
427
428    public function testBuildCreateTableSqlCompositePrimaryKey(): void
429    {
430        $schemaParser = new MySqlSchemaParser(new MySqlParser());
431        $registry = new TableDefinitionRegistry();
432        $definition = $schemaParser->parse('CREATE TABLE order_items (order_id INT NOT NULL, item_id INT NOT NULL, qty INT, PRIMARY KEY (order_id, item_id))');
433        self::assertNotNull($definition);
434        $registry->register('order_items', $definition);
435        $store = new ShadowStore();
436
437        $parser = new Parser('ALTER TABLE order_items ADD COLUMN note TEXT');
438        $alterStmt = $parser->statements[0];
439        self::assertInstanceOf(AlterStatement::class, $alterStmt);
440        $mutation = new AlterTableMutation('order_items', $alterStmt, $registry, $schemaParser);
441        $mutation->apply($store, []);
442
443        $def = $registry->get('order_items');
444        self::assertNotNull($def);
445        self::assertContains('order_id', $def->primaryKeys);
446        self::assertContains('item_id', $def->primaryKeys);
447        self::assertCount(2, $def->primaryKeys);
448    }
449
450    public function testBuildCreateTableSqlPreservesUniqueConstraints(): void
451    {
452        $schemaParser = new MySqlSchemaParser(new MySqlParser());
453        $registry = new TableDefinitionRegistry();
454        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, email VARCHAR(255), UNIQUE KEY uk_email (email))');
455        self::assertNotNull($definition);
456        $registry->register('users', $definition);
457        $store = new ShadowStore();
458
459        $parser = new Parser('ALTER TABLE users ADD COLUMN phone VARCHAR(20)');
460        $alterStmt = $parser->statements[0];
461        self::assertInstanceOf(AlterStatement::class, $alterStmt);
462        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
463        $mutation->apply($store, []);
464
465        $def = $registry->get('users');
466        self::assertNotNull($def);
467        self::assertNotEmpty($def->uniqueConstraints);
468    }
469
470    public function testDropColumnWithoutColumnKeyword(): void
471    {
472        $schemaParser = new MySqlSchemaParser(new MySqlParser());
473        $registry = new TableDefinitionRegistry();
474        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100), email TEXT)');
475        self::assertNotNull($definition);
476        $registry->register('users', $definition);
477        $store = new ShadowStore();
478        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'a@b.com']]);
479
480        $parser = new Parser('ALTER TABLE users DROP name');
481        $alterStmt = $parser->statements[0];
482        self::assertInstanceOf(AlterStatement::class, $alterStmt);
483        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
484        $mutation->apply($store, []);
485
486        $def = $registry->get('users');
487        self::assertNotNull($def);
488        self::assertNotContains('name', $def->columns);
489        self::assertArrayNotHasKey('name', $store->get('users')[0]);
490    }
491
492    public function testRenameTableMovesDataAndRegistry(): void
493    {
494        $schemaParser = new MySqlSchemaParser(new MySqlParser());
495        $registry = new TableDefinitionRegistry();
496        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
497        self::assertNotNull($definition);
498        $registry->register('users', $definition);
499        $store = new ShadowStore();
500        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
501
502        $parser = new Parser('ALTER TABLE users RENAME TO members');
503        $alterStmt = $parser->statements[0];
504        self::assertInstanceOf(AlterStatement::class, $alterStmt);
505        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
506        $mutation->apply($store, []);
507
508        self::assertSame([['id' => 1, 'name' => 'Alice']], $store->get('members'));
509        self::assertSame([], $store->get('users'));
510        self::assertNotNull($registry->get('members'));
511        self::assertNull($registry->get('users'));
512        self::assertSame('members', $mutation->tableName());
513    }
514
515    public function testAddPrimaryKey(): void
516    {
517        $schemaParser = new MySqlSchemaParser(new MySqlParser());
518        $registry = new TableDefinitionRegistry();
519        $definition = $schemaParser->parse('CREATE TABLE data (id INT NOT NULL, name VARCHAR(100))');
520        self::assertNotNull($definition);
521        $registry->register('data', $definition);
522        $store = new ShadowStore();
523
524        $parser = new Parser('ALTER TABLE data ADD PRIMARY KEY (`id`)');
525        $alterStmt = $parser->statements[0];
526        self::assertInstanceOf(AlterStatement::class, $alterStmt);
527        $mutation = new AlterTableMutation('data', $alterStmt, $registry, $schemaParser);
528        $mutation->apply($store, []);
529
530        $def = $registry->get('data');
531        self::assertNotNull($def);
532        self::assertContains('id', $def->columns);
533    }
534
535    public function testAddForeignKeySkipped(): void
536    {
537        $schemaParser = new MySqlSchemaParser(new MySqlParser());
538        $registry = new TableDefinitionRegistry();
539        $definition = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)');
540        self::assertNotNull($definition);
541        $registry->register('orders', $definition);
542        $store = new ShadowStore();
543
544        $parser = new Parser('ALTER TABLE orders ADD FOREIGN KEY (user_id) REFERENCES users(id)');
545        $alterStmt = $parser->statements[0];
546        self::assertInstanceOf(AlterStatement::class, $alterStmt);
547        $mutation = new AlterTableMutation('orders', $alterStmt, $registry, $schemaParser);
548        $mutation->apply($store, []);
549
550        self::assertNotNull($registry->get('orders'));
551    }
552
553    public function testDropForeignKeySkipped(): void
554    {
555        $schemaParser = new MySqlSchemaParser(new MySqlParser());
556        $registry = new TableDefinitionRegistry();
557        $definition = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)');
558        self::assertNotNull($definition);
559        $registry->register('orders', $definition);
560        $store = new ShadowStore();
561
562        $parser = new Parser('ALTER TABLE orders DROP FOREIGN KEY fk_user');
563        $alterStmt = $parser->statements[0];
564        self::assertInstanceOf(AlterStatement::class, $alterStmt);
565        $mutation = new AlterTableMutation('orders', $alterStmt, $registry, $schemaParser);
566        $mutation->apply($store, []);
567
568        self::assertNotNull($registry->get('orders'));
569    }
570
571    public function testUnsupportedOperationThrows(): void
572    {
573        $schemaParser = new MySqlSchemaParser(new MySqlParser());
574        $registry = new TableDefinitionRegistry();
575        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
576        self::assertNotNull($definition);
577        $registry->register('users', $definition);
578        $store = new ShadowStore();
579
580        $parser = new Parser('ALTER TABLE users LOCK = EXCLUSIVE');
581        $alterStmt = $parser->statements[0];
582        self::assertInstanceOf(AlterStatement::class, $alterStmt);
583        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
584
585        $this->expectException(UnsupportedSqlException::class);
586        $mutation->apply($store, []);
587    }
588
589    public function testBuildCreateTableSqlUsesTypedColumnNativeType(): void
590    {
591        $schemaParser = new MySqlSchemaParser(new MySqlParser());
592        $registry = new TableDefinitionRegistry();
593        $registry->register('t', new TableDefinition(
594            ['id', 'price'],
595            ['id' => 'INT', 'price' => 'DECIMAL(10,2)'],
596            ['id'],
597            ['id'],
598            [],
599            [
600                'id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INT'),
601                'price' => new ColumnDeclaration(ColumnTypeFamily::DECIMAL, 'DECIMAL(10,2)'),
602            ],
603        ));
604        $store = new ShadowStore();
605
606        $parser = new Parser('ALTER TABLE t ADD COLUMN note TEXT');
607        $alterStmt = $parser->statements[0];
608        self::assertInstanceOf(AlterStatement::class, $alterStmt);
609        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
610        $mutation->apply($store, []);
611
612        $def = $registry->get('t');
613        self::assertNotNull($def);
614        self::assertContains('note', $def->columns);
615        self::assertContains('id', $def->columns);
616        self::assertContains('price', $def->columns);
617    }
618
619    public function testMultipleAlterOperations(): void
620    {
621        $schemaParser = new MySqlSchemaParser(new MySqlParser());
622        $registry = new TableDefinitionRegistry();
623        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100), email TEXT)');
624        self::assertNotNull($definition);
625        $registry->register('users', $definition);
626        $store = new ShadowStore();
627
628        $parser = new Parser('ALTER TABLE users DROP COLUMN email, ADD COLUMN phone VARCHAR(20)');
629        $alterStmt = $parser->statements[0];
630        self::assertInstanceOf(AlterStatement::class, $alterStmt);
631        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
632        $mutation->apply($store, []);
633
634        $def = $registry->get('users');
635        self::assertNotNull($def);
636        self::assertNotContains('email', $def->columns);
637        self::assertContains('phone', $def->columns);
638    }
639
640    public function testDropColumnWithEmptyStore(): void
641    {
642        $schemaParser = new MySqlSchemaParser(new MySqlParser());
643        $registry = new TableDefinitionRegistry();
644        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
645        self::assertNotNull($definition);
646        $registry->register('users', $definition);
647        $store = new ShadowStore();
648
649        $parser = new Parser('ALTER TABLE users DROP COLUMN name');
650        $alterStmt = $parser->statements[0];
651        self::assertInstanceOf(AlterStatement::class, $alterStmt);
652        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
653        $mutation->apply($store, []);
654
655        $def = $registry->get('users');
656        self::assertNotNull($def);
657        self::assertNotContains('name', $def->columns);
658    }
659
660    public function testAlterDropDefaultUnsupported(): void
661    {
662        $schemaParser = new MySqlSchemaParser(new MySqlParser());
663        $registry = new TableDefinitionRegistry();
664        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100) DEFAULT "test")');
665        self::assertNotNull($definition);
666        $registry->register('users', $definition);
667        $store = new ShadowStore();
668
669        $parser = new Parser('ALTER TABLE users ALTER COLUMN name DROP DEFAULT');
670        $alterStmt = $parser->statements[0];
671        self::assertInstanceOf(AlterStatement::class, $alterStmt);
672        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
673
674        $this->expectException(UnsupportedSqlException::class);
675        $mutation->apply($store, []);
676    }
677
678    public function testModifyWithoutColumnKeyword(): void
679    {
680        $schemaParser = new MySqlSchemaParser(new MySqlParser());
681        $registry = new TableDefinitionRegistry();
682        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
683        self::assertNotNull($definition);
684        $registry->register('users', $definition);
685        $store = new ShadowStore();
686
687        $parser = new Parser('ALTER TABLE users MODIFY name VARCHAR(255)');
688        $alterStmt = $parser->statements[0];
689        self::assertInstanceOf(AlterStatement::class, $alterStmt);
690        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
691        $mutation->apply($store, []);
692
693        $def = $registry->get('users');
694        self::assertNotNull($def);
695        self::assertContains('name', $def->columns);
696    }
697
698    public function testChangeWithoutColumnKeyword(): void
699    {
700        $schemaParser = new MySqlSchemaParser(new MySqlParser());
701        $registry = new TableDefinitionRegistry();
702        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
703        self::assertNotNull($definition);
704        $registry->register('users', $definition);
705        $store = new ShadowStore();
706        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
707
708        $parser = new Parser('ALTER TABLE users CHANGE name username VARCHAR(255)');
709        $alterStmt = $parser->statements[0];
710        self::assertInstanceOf(AlterStatement::class, $alterStmt);
711        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
712        $mutation->apply($store, []);
713
714        $def = $registry->get('users');
715        self::assertNotNull($def);
716        self::assertContains('username', $def->columns);
717        self::assertNotContains('name', $def->columns);
718        self::assertSame('Alice', $store->get('users')[0]['username']);
719    }
720
721    public function testChangeColumnSameNameModifiesType(): void
722    {
723        $schemaParser = new MySqlSchemaParser(new MySqlParser());
724        $registry = new TableDefinitionRegistry();
725        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
726        self::assertNotNull($definition);
727        $registry->register('users', $definition);
728        $store = new ShadowStore();
729        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
730
731        $parser = new Parser('ALTER TABLE users CHANGE COLUMN name name VARCHAR(255) NOT NULL');
732        $alterStmt = $parser->statements[0];
733        self::assertInstanceOf(AlterStatement::class, $alterStmt);
734        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
735        $mutation->apply($store, []);
736
737        $def = $registry->get('users');
738        self::assertNotNull($def);
739        self::assertContains('name', $def->columns);
740        self::assertContains('name', $def->notNullColumns);
741        self::assertSame('Alice', $store->get('users')[0]['name']);
742    }
743
744    public function testRenameColumnUpdatesStoreData(): void
745    {
746        $schemaParser = new MySqlSchemaParser(new MySqlParser());
747        $registry = new TableDefinitionRegistry();
748        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
749        self::assertNotNull($definition);
750        $registry->register('users', $definition);
751        $store = new ShadowStore();
752        $store->set('users', [
753            ['id' => 1, 'name' => 'Alice'],
754            ['id' => 2, 'name' => 'Bob'],
755        ]);
756
757        $parser = new Parser('ALTER TABLE users RENAME COLUMN name TO full_name');
758        $alterStmt = $parser->statements[0];
759        self::assertInstanceOf(AlterStatement::class, $alterStmt);
760        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
761        $mutation->apply($store, []);
762
763        $rows = $store->get('users');
764        self::assertCount(2, $rows);
765        self::assertSame('Alice', $rows[0]['full_name']);
766        self::assertSame('Bob', $rows[1]['full_name']);
767        self::assertArrayNotHasKey('name', $rows[0]);
768        self::assertArrayNotHasKey('name', $rows[1]);
769    }
770
771    public function testBuildCreateTableSqlCorrectTypedColumnNativeType(): void
772    {
773        $schemaParser = new MySqlSchemaParser(new MySqlParser());
774        $registry = new TableDefinitionRegistry();
775        $registry->register('t', new TableDefinition(
776            ['id', 'val'],
777            ['id' => 'INT', 'val' => 'TEXT'],
778            ['id'],
779            ['id'],
780            [],
781            [
782                'id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INT'),
783                'val' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT'),
784            ],
785        ));
786        $store = new ShadowStore();
787
788        $parser = new Parser('ALTER TABLE t ADD COLUMN extra INT');
789        $alterStmt = $parser->statements[0];
790        self::assertInstanceOf(AlterStatement::class, $alterStmt);
791        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
792        $mutation->apply($store, []);
793
794        $def = $registry->get('t');
795        self::assertNotNull($def);
796        self::assertContains('extra', $def->columns);
797        self::assertContains('id', $def->columns);
798        self::assertContains('val', $def->columns);
799    }
800
801    public function testBuildCreateTableSqlFallsBackToColumnTypesWhenNoTypedColumn(): void
802    {
803        $schemaParser = new MySqlSchemaParser(new MySqlParser());
804        $registry = new TableDefinitionRegistry();
805        $registry->register('t', new TableDefinition(
806            ['id', 'val'],
807            ['id' => 'INT', 'val' => 'TEXT'],
808            ['id'],
809            ['id'],
810            [],
811            ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INT')],
812        ));
813        $store = new ShadowStore();
814
815        $parser = new Parser('ALTER TABLE t ADD COLUMN extra INT');
816        $alterStmt = $parser->statements[0];
817        self::assertInstanceOf(AlterStatement::class, $alterStmt);
818        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
819        $mutation->apply($store, []);
820
821        $def = $registry->get('t');
822        self::assertNotNull($def);
823        self::assertContains('val', $def->columns);
824    }
825
826    public function testBuildCreateTableSqlFallsBackToTextWhenNoTypes(): void
827    {
828        $schemaParser = new MySqlSchemaParser(new MySqlParser());
829        $registry = new TableDefinitionRegistry();
830        $registry->register('t', new TableDefinition(
831            ['id', 'val'],
832            [],
833            [],
834            [],
835            [],
836            [],
837        ));
838        $store = new ShadowStore();
839
840        $parser = new Parser('ALTER TABLE t ADD COLUMN extra INT');
841        $alterStmt = $parser->statements[0];
842        self::assertInstanceOf(AlterStatement::class, $alterStmt);
843        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
844        $mutation->apply($store, []);
845
846        $def = $registry->get('t');
847        self::assertNotNull($def);
848        self::assertContains('extra', $def->columns);
849    }
850
851    public function testApplyUnregistersOldTableBeforeRegistering(): void
852    {
853        $schemaParser = new MySqlSchemaParser(new MySqlParser());
854        $registry = new TableDefinitionRegistry();
855        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
856        self::assertNotNull($definition);
857        $registry->register('users', $definition);
858        $store = new ShadowStore();
859
860        $parser = new Parser('ALTER TABLE users ADD COLUMN email TEXT');
861        $alterStmt = $parser->statements[0];
862        self::assertInstanceOf(AlterStatement::class, $alterStmt);
863        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
864        $mutation->apply($store, []);
865
866        $def = $registry->get('users');
867        self::assertNotNull($def);
868        self::assertContains('email', $def->columns);
869        self::assertContains('id', $def->columns);
870        self::assertContains('name', $def->columns);
871    }
872
873    public function testBuildCreateTableSqlCompositePrimaryKeyProducesBacktickedColumns(): void
874    {
875        $schemaParser = new MySqlSchemaParser(new MySqlParser());
876        $registry = new TableDefinitionRegistry();
877        $definition = $schemaParser->parse('CREATE TABLE t (a INT NOT NULL, b INT NOT NULL, c TEXT, PRIMARY KEY (a, b))');
878        self::assertNotNull($definition);
879        $registry->register('t', $definition);
880        $store = new ShadowStore();
881
882        $parser = new Parser('ALTER TABLE t ADD COLUMN d TEXT');
883        $alterStmt = $parser->statements[0];
884        self::assertInstanceOf(AlterStatement::class, $alterStmt);
885        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
886        $mutation->apply($store, []);
887
888        $def = $registry->get('t');
889        self::assertNotNull($def);
890        self::assertCount(2, $def->primaryKeys);
891        self::assertContains('a', $def->primaryKeys);
892        self::assertContains('b', $def->primaryKeys);
893    }
894
895    public function testBuildCreateTableSqlUniqueConstraintsProducesBacktickedColumns(): void
896    {
897        $schemaParser = new MySqlSchemaParser(new MySqlParser());
898        $registry = new TableDefinitionRegistry();
899        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, email VARCHAR(255), UNIQUE KEY uk_email (email))');
900        self::assertNotNull($definition);
901        $registry->register('t', $definition);
902        $store = new ShadowStore();
903
904        $parser = new Parser('ALTER TABLE t ADD COLUMN phone VARCHAR(20)');
905        $alterStmt = $parser->statements[0];
906        self::assertInstanceOf(AlterStatement::class, $alterStmt);
907        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
908        $mutation->apply($store, []);
909
910        $def = $registry->get('t');
911        self::assertNotNull($def);
912        self::assertNotEmpty($def->uniqueConstraints);
913        $matches = array_values(array_filter($def->uniqueConstraints, static fn (array $cols): bool => in_array('email', $cols, true)));
914        self::assertNotEmpty($matches);
915        self::assertSame(['email'], $matches[0]);
916    }
917
918    public function testDropColumnRemovesFromMultipleStoreRows(): void
919    {
920        $schemaParser = new MySqlSchemaParser(new MySqlParser());
921        $registry = new TableDefinitionRegistry();
922        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, a TEXT, b TEXT)');
923        self::assertNotNull($definition);
924        $registry->register('t', $definition);
925        $store = new ShadowStore();
926        $store->set('t', [
927            ['id' => 1, 'a' => 'x', 'b' => 'y'],
928            ['id' => 2, 'a' => 'p', 'b' => 'q'],
929        ]);
930
931        $parser = new Parser('ALTER TABLE t DROP COLUMN a');
932        $alterStmt = $parser->statements[0];
933        self::assertInstanceOf(AlterStatement::class, $alterStmt);
934        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
935        $mutation->apply($store, []);
936
937        $rows = $store->get('t');
938        self::assertCount(2, $rows);
939        self::assertArrayNotHasKey('a', $rows[0]);
940        self::assertArrayNotHasKey('a', $rows[1]);
941        self::assertArrayHasKey('b', $rows[0]);
942        self::assertArrayHasKey('b', $rows[1]);
943    }
944
945    public function testAlterSetDefaultUnsupported(): void
946    {
947        $schemaParser = new MySqlSchemaParser(new MySqlParser());
948        $registry = new TableDefinitionRegistry();
949        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(100))');
950        self::assertNotNull($definition);
951        $registry->register('t', $definition);
952        $store = new ShadowStore();
953
954        $parser = new Parser("ALTER TABLE t ALTER COLUMN name SET DEFAULT 'test'");
955        $alterStmt = $parser->statements[0];
956        self::assertInstanceOf(AlterStatement::class, $alterStmt);
957        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
958
959        $this->expectException(UnsupportedSqlException::class);
960        $mutation->apply($store, []);
961    }
962
963    public function testRenameTablePreservesSchemaDefinition(): void
964    {
965        $schemaParser = new MySqlSchemaParser(new MySqlParser());
966        $registry = new TableDefinitionRegistry();
967        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(100) NOT NULL)');
968        self::assertNotNull($definition);
969        $registry->register('t', $definition);
970        $store = new ShadowStore();
971        $store->set('t', [['id' => 1, 'name' => 'Alice']]);
972
973        $parser = new Parser('ALTER TABLE t RENAME TO t2');
974        $alterStmt = $parser->statements[0];
975        self::assertInstanceOf(AlterStatement::class, $alterStmt);
976        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
977        $mutation->apply($store, []);
978
979        self::assertNull($registry->get('t'));
980        $def2 = $registry->get('t2');
981        self::assertNotNull($def2);
982        self::assertSame(['id', 'name'], $def2->columns);
983        self::assertContains('name', $def2->notNullColumns);
984        self::assertSame([['id' => 1, 'name' => 'Alice']], $store->get('t2'));
985        self::assertSame([], $store->get('t'));
986    }
987
988    public function testModifyColumnChangesType(): void
989    {
990        $schemaParser = new MySqlSchemaParser(new MySqlParser());
991        $registry = new TableDefinitionRegistry();
992        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(100))');
993        self::assertNotNull($definition);
994        $registry->register('t', $definition);
995        $store = new ShadowStore();
996
997        $parser = new Parser('ALTER TABLE t MODIFY COLUMN name TEXT NOT NULL');
998        $alterStmt = $parser->statements[0];
999        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1000        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1001        $mutation->apply($store, []);
1002
1003        $def = $registry->get('t');
1004        self::assertNotNull($def);
1005        self::assertContains('name', $def->notNullColumns);
1006    }
1007
1008    public function testAddPrimaryKeyPopulatesPrimaryKeys(): void
1009    {
1010        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1011        $registry = new TableDefinitionRegistry();
1012        $definition = $schemaParser->parse('CREATE TABLE data (id INT NOT NULL, name VARCHAR(100))');
1013        self::assertNotNull($definition);
1014        $registry->register('data', $definition);
1015        $store = new ShadowStore();
1016
1017        $parser = new Parser('ALTER TABLE data ADD PRIMARY KEY (`id`)');
1018        $alterStmt = $parser->statements[0];
1019        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1020        $mutation = new AlterTableMutation('data', $alterStmt, $registry, $schemaParser);
1021        $mutation->apply($store, []);
1022
1023        $def = $registry->get('data');
1024        self::assertNotNull($def);
1025        self::assertContains('id', $def->primaryKeys);
1026        self::assertCount(1, $def->primaryKeys);
1027    }
1028
1029    public function testAddCompositePrimaryKey(): void
1030    {
1031        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1032        $registry = new TableDefinitionRegistry();
1033        $definition = $schemaParser->parse('CREATE TABLE data (a INT NOT NULL, b INT NOT NULL, c TEXT)');
1034        self::assertNotNull($definition);
1035        $registry->register('data', $definition);
1036        $store = new ShadowStore();
1037
1038        $parser = new Parser('ALTER TABLE data ADD PRIMARY KEY (`a`, `b`)');
1039        $alterStmt = $parser->statements[0];
1040        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1041        $mutation = new AlterTableMutation('data', $alterStmt, $registry, $schemaParser);
1042        $mutation->apply($store, []);
1043
1044        $def = $registry->get('data');
1045        self::assertNotNull($def);
1046        self::assertContains('a', $def->primaryKeys);
1047        self::assertContains('b', $def->primaryKeys);
1048        self::assertCount(2, $def->primaryKeys);
1049    }
1050
1051    public function testDropPrimaryKeyRemovesCompositePrimaryKey(): void
1052    {
1053        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1054        $registry = new TableDefinitionRegistry();
1055        $definition = $schemaParser->parse('CREATE TABLE t (a INT NOT NULL, b INT NOT NULL, c TEXT, PRIMARY KEY (a, b))');
1056        self::assertNotNull($definition);
1057        $registry->register('t', $definition);
1058        $store = new ShadowStore();
1059
1060        $parser = new Parser('ALTER TABLE t DROP PRIMARY KEY');
1061        $alterStmt = $parser->statements[0];
1062        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1063        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1064        $mutation->apply($store, []);
1065
1066        $def = $registry->get('t');
1067        self::assertNotNull($def);
1068        self::assertSame([], $def->primaryKeys);
1069    }
1070
1071    public function testDropColumnUpdatesFieldsArrayValues(): void
1072    {
1073        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1074        $registry = new TableDefinitionRegistry();
1075        $definition = $schemaParser->parse('CREATE TABLE t (a INT PRIMARY KEY, b TEXT, c TEXT)');
1076        self::assertNotNull($definition);
1077        $registry->register('t', $definition);
1078        $store = new ShadowStore();
1079
1080        $parser = new Parser('ALTER TABLE t DROP COLUMN b');
1081        $alterStmt = $parser->statements[0];
1082        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1083        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1084        $mutation->apply($store, []);
1085
1086        $def = $registry->get('t');
1087        self::assertNotNull($def);
1088        self::assertSame(['a', 'c'], $def->columns);
1089    }
1090
1091    public function testRenameTableUnregistersOldName(): void
1092    {
1093        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1094        $registry = new TableDefinitionRegistry();
1095        $definition = $schemaParser->parse('CREATE TABLE old_t (id INT PRIMARY KEY)');
1096        self::assertNotNull($definition);
1097        $registry->register('old_t', $definition);
1098        $store = new ShadowStore();
1099
1100        $parser = new Parser('ALTER TABLE old_t RENAME TO new_t');
1101        $alterStmt = $parser->statements[0];
1102        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1103        $mutation = new AlterTableMutation('old_t', $alterStmt, $registry, $schemaParser);
1104        $mutation->apply($store, []);
1105
1106        self::assertNull($registry->get('old_t'));
1107        self::assertNotNull($registry->get('new_t'));
1108        self::assertSame(['id'], $registry->get('new_t')->columns);
1109    }
1110
1111    public function testModifyColumnUpdatesColumnTypeInSchema(): void
1112    {
1113        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1114        $registry = new TableDefinitionRegistry();
1115        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, val VARCHAR(100))');
1116        self::assertNotNull($definition);
1117        $registry->register('t', $definition);
1118        $store = new ShadowStore();
1119
1120        $parser = new Parser('ALTER TABLE t MODIFY COLUMN val TEXT');
1121        $alterStmt = $parser->statements[0];
1122        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1123        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1124        $mutation->apply($store, []);
1125
1126        $def = $registry->get('t');
1127        self::assertNotNull($def);
1128        self::assertContains('val', $def->columns);
1129        self::assertSame(['id', 'val'], $def->columns);
1130    }
1131
1132    public function testAddColumnBacktickNameNormalized(): void
1133    {
1134        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1135        $registry = new TableDefinitionRegistry();
1136        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
1137        self::assertNotNull($definition);
1138        $registry->register('t', $definition);
1139        $store = new ShadowStore();
1140
1141        $parser = new Parser('ALTER TABLE t ADD COLUMN `email` VARCHAR(255)');
1142        $alterStmt = $parser->statements[0];
1143        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1144        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1145        $mutation->apply($store, []);
1146
1147        $def = $registry->get('t');
1148        self::assertNotNull($def);
1149        self::assertContains('email', $def->columns);
1150    }
1151
1152    public function testApplyUnregistersOldDefinitionBeforeReregistering(): void
1153    {
1154        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1155        $registry = new TableDefinitionRegistry();
1156        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1157        self::assertNotNull($definition);
1158        $registry->register('users', $definition);
1159        $store = new ShadowStore();
1160
1161        $parser = new Parser('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
1162        $alterStmt = $parser->statements[0];
1163        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1164        $mutation = new AlterTableMutation('users', $alterStmt, $registry, $schemaParser);
1165        $mutation->apply($store, []);
1166
1167        $def = $registry->get('users');
1168        self::assertNotNull($def);
1169        self::assertContains('email', $def->columns);
1170        self::assertCount(3, $def->columns);
1171    }
1172
1173    public function testBuildCreateTableSqlCompositePrimaryKeyWrapsBackticks(): void
1174    {
1175        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1176        $registry = new TableDefinitionRegistry();
1177        $definition = $schemaParser->parse('CREATE TABLE items (order_id INT NOT NULL, product_id INT NOT NULL, qty INT, PRIMARY KEY (order_id, product_id))');
1178        self::assertNotNull($definition);
1179        $registry->register('items', $definition);
1180        $store = new ShadowStore();
1181
1182        $parser = new Parser('ALTER TABLE items ADD COLUMN note TEXT');
1183        $alterStmt = $parser->statements[0];
1184        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1185        $mutation = new AlterTableMutation('items', $alterStmt, $registry, $schemaParser);
1186        $mutation->apply($store, []);
1187
1188        $def = $registry->get('items');
1189        self::assertNotNull($def);
1190        self::assertContains('note', $def->columns);
1191        self::assertSame(['order_id', 'product_id'], $def->primaryKeys);
1192    }
1193
1194    public function testBuildCreateTableSqlUniqueConstraintWrapsBackticks(): void
1195    {
1196        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1197        $registry = new TableDefinitionRegistry();
1198        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, email VARCHAR(255), UNIQUE KEY uk_email (email))');
1199        self::assertNotNull($definition);
1200        $registry->register('t', $definition);
1201        $store = new ShadowStore();
1202
1203        $parser = new Parser('ALTER TABLE t ADD COLUMN name VARCHAR(255)');
1204        $alterStmt = $parser->statements[0];
1205        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1206        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1207        $mutation->apply($store, []);
1208
1209        $def = $registry->get('t');
1210        self::assertNotNull($def);
1211        self::assertContains('name', $def->columns);
1212        self::assertNotEmpty($def->uniqueConstraints);
1213        self::assertArrayHasKey('uk_email', $def->uniqueConstraints);
1214        self::assertSame(['email'], $def->uniqueConstraints['uk_email']);
1215    }
1216
1217    public function testApplyAddForeignKeyDoesNotThrow(): void
1218    {
1219        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1220        $registry = new TableDefinitionRegistry();
1221        $definition = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)');
1222        self::assertNotNull($definition);
1223        $registry->register('orders', $definition);
1224        $store = new ShadowStore();
1225
1226        $parser = new Parser('ALTER TABLE orders ADD FOREIGN KEY (user_id) REFERENCES users(id)');
1227        $alterStmt = $parser->statements[0];
1228        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1229        $mutation = new AlterTableMutation('orders', $alterStmt, $registry, $schemaParser);
1230        $mutation->apply($store, []);
1231
1232        $def = $registry->get('orders');
1233        self::assertNotNull($def);
1234        self::assertSame(['id', 'user_id'], $def->columns);
1235    }
1236
1237    public function testApplyDropForeignKeyDoesNotThrow(): void
1238    {
1239        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1240        $registry = new TableDefinitionRegistry();
1241        $definition = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)');
1242        self::assertNotNull($definition);
1243        $registry->register('orders', $definition);
1244        $store = new ShadowStore();
1245
1246        $parser = new Parser('ALTER TABLE orders DROP FOREIGN KEY fk_user');
1247        $alterStmt = $parser->statements[0];
1248        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1249        $mutation = new AlterTableMutation('orders', $alterStmt, $registry, $schemaParser);
1250        $mutation->apply($store, []);
1251
1252        $def = $registry->get('orders');
1253        self::assertNotNull($def);
1254        self::assertSame(['id', 'user_id'], $def->columns);
1255    }
1256
1257    public function testApplyRenameColumnUpdatesDefinitionAndStore(): void
1258    {
1259        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1260        $registry = new TableDefinitionRegistry();
1261        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, old_name VARCHAR(255))');
1262        self::assertNotNull($definition);
1263        $registry->register('t', $definition);
1264        $store = new ShadowStore();
1265        $store->set('t', [['id' => 1, 'old_name' => 'Alice']]);
1266
1267        $parser = new Parser('ALTER TABLE t RENAME COLUMN old_name TO new_name');
1268        $alterStmt = $parser->statements[0];
1269        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1270        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1271        $mutation->apply($store, []);
1272
1273        $def = $registry->get('t');
1274        self::assertNotNull($def);
1275        self::assertContains('new_name', $def->columns);
1276        self::assertNotContains('old_name', $def->columns);
1277
1278        $rows = $store->get('t');
1279        self::assertNotEmpty($rows);
1280        self::assertArrayHasKey('new_name', $rows[0]);
1281        self::assertArrayNotHasKey('old_name', $rows[0]);
1282        self::assertSame('Alice', $rows[0]['new_name']);
1283    }
1284
1285    public function testApplyAlterSetDefaultIsHandledViaRewriter(): void
1286    {
1287        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1288        $registry = new TableDefinitionRegistry();
1289        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
1290        self::assertNotNull($definition);
1291        $registry->register('t', $definition);
1292        $store = new ShadowStore();
1293
1294        $parser = new Parser("ALTER TABLE t ALTER COLUMN name SET DEFAULT 'test'");
1295        $alterStmt = $parser->statements[0];
1296        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1297
1298        $altered = $alterStmt->altered ?? [];
1299        self::assertNotEmpty($altered);
1300    }
1301
1302    public function testModifyColumnWithMultipleFieldsOnlyModifiesTarget(): void
1303    {
1304        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1305        $registry = new TableDefinitionRegistry();
1306        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, a VARCHAR(100), b VARCHAR(100))');
1307        self::assertNotNull($definition);
1308        $registry->register('t', $definition);
1309        $store = new ShadowStore();
1310
1311        $parser = new Parser('ALTER TABLE t MODIFY COLUMN a TEXT');
1312        $alterStmt = $parser->statements[0];
1313        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1314        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1315        $mutation->apply($store, []);
1316
1317        $def = $registry->get('t');
1318        self::assertNotNull($def);
1319        self::assertContains('a', $def->columns);
1320        self::assertContains('b', $def->columns);
1321        self::assertSame('TEXT', $def->columnTypes['a']);
1322    }
1323
1324    public function testChangeColumnRenamesAndUpdatesType(): void
1325    {
1326        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1327        $registry = new TableDefinitionRegistry();
1328        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, old_col VARCHAR(100))');
1329        self::assertNotNull($definition);
1330        $registry->register('t', $definition);
1331        $store = new ShadowStore();
1332        $store->set('t', [['id' => 1, 'old_col' => 'val']]);
1333
1334        $parser = new Parser('ALTER TABLE t CHANGE COLUMN old_col new_col TEXT');
1335        $alterStmt = $parser->statements[0];
1336        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1337        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1338        $mutation->apply($store, []);
1339
1340        $def = $registry->get('t');
1341        self::assertNotNull($def);
1342        self::assertContains('new_col', $def->columns);
1343        self::assertNotContains('old_col', $def->columns);
1344
1345        $rows = $store->get('t');
1346        self::assertNotEmpty($rows);
1347        self::assertArrayHasKey('new_col', $rows[0]);
1348        self::assertArrayNotHasKey('old_col', $rows[0]);
1349    }
1350
1351    public function testDropPrimaryKeyRemovesPkFromDefinition(): void
1352    {
1353        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1354        $registry = new TableDefinitionRegistry();
1355        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
1356        self::assertNotNull($definition);
1357        $registry->register('t', $definition);
1358        $store = new ShadowStore();
1359
1360        $parser = new Parser('ALTER TABLE t DROP PRIMARY KEY');
1361        $alterStmt = $parser->statements[0];
1362        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1363        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1364        $mutation->apply($store, []);
1365
1366        $def = $registry->get('t');
1367        self::assertNotNull($def);
1368        self::assertSame([], $def->primaryKeys);
1369    }
1370
1371    public function testAddPrimaryKeyDoesNotRemoveExistingColumns(): void
1372    {
1373        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1374        $registry = new TableDefinitionRegistry();
1375        $definition = $schemaParser->parse('CREATE TABLE t (a INT, b INT, c VARCHAR(255))');
1376        self::assertNotNull($definition);
1377        $registry->register('t', $definition);
1378        $store = new ShadowStore();
1379
1380        $parser = new Parser('ALTER TABLE t ADD PRIMARY KEY (a, b)');
1381        $alterStmt = $parser->statements[0];
1382        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1383        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1384        $mutation->apply($store, []);
1385
1386        $def = $registry->get('t');
1387        self::assertNotNull($def);
1388        self::assertContains('a', $def->columns);
1389        self::assertContains('b', $def->columns);
1390        self::assertContains('c', $def->columns);
1391    }
1392
1393    public function testRenameTableUpdatesRegistryAndStore(): void
1394    {
1395        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1396        $registry = new TableDefinitionRegistry();
1397        $definition = $schemaParser->parse('CREATE TABLE old_table (id INT PRIMARY KEY)');
1398        self::assertNotNull($definition);
1399        $registry->register('old_table', $definition);
1400        $store = new ShadowStore();
1401        $store->set('old_table', [['id' => 1]]);
1402
1403        $parser = new Parser('ALTER TABLE old_table RENAME TO new_table');
1404        $alterStmt = $parser->statements[0];
1405        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1406        $mutation = new AlterTableMutation('old_table', $alterStmt, $registry, $schemaParser);
1407        $mutation->apply($store, []);
1408
1409        self::assertTrue($registry->has('new_table'));
1410        self::assertSame([], $store->get('old_table'));
1411        self::assertNotEmpty($store->get('new_table'));
1412        self::assertSame('new_table', $mutation->tableName());
1413    }
1414
1415    public function testDropColumnNotFoundThrows(): void
1416    {
1417        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1418        $registry = new TableDefinitionRegistry();
1419        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
1420        self::assertNotNull($definition);
1421        $registry->register('t', $definition);
1422        $store = new ShadowStore();
1423
1424        $parser = new Parser('ALTER TABLE t DROP COLUMN nonexistent');
1425        $alterStmt = $parser->statements[0];
1426        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1427        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1428
1429        $this->expectException(ColumnNotFoundException::class);
1430        $mutation->apply($store, []);
1431    }
1432
1433    public function testModifyColumnNotFoundThrows(): void
1434    {
1435        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1436        $registry = new TableDefinitionRegistry();
1437        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
1438        self::assertNotNull($definition);
1439        $registry->register('t', $definition);
1440        $store = new ShadowStore();
1441
1442        $parser = new Parser('ALTER TABLE t MODIFY COLUMN nonexistent TEXT');
1443        $alterStmt = $parser->statements[0];
1444        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1445        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1446
1447        $this->expectException(ColumnNotFoundException::class);
1448        $mutation->apply($store, []);
1449    }
1450
1451    public function testChangeColumnNotFoundThrows(): void
1452    {
1453        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1454        $registry = new TableDefinitionRegistry();
1455        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
1456        self::assertNotNull($definition);
1457        $registry->register('t', $definition);
1458        $store = new ShadowStore();
1459
1460        $parser = new Parser('ALTER TABLE t CHANGE COLUMN nonexistent new_col TEXT');
1461        $alterStmt = $parser->statements[0];
1462        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1463        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1464
1465        $this->expectException(ColumnNotFoundException::class);
1466        $mutation->apply($store, []);
1467    }
1468
1469    public function testRenameColumnNotFoundThrows(): void
1470    {
1471        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1472        $registry = new TableDefinitionRegistry();
1473        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
1474        self::assertNotNull($definition);
1475        $registry->register('t', $definition);
1476        $store = new ShadowStore();
1477
1478        $parser = new Parser('ALTER TABLE t RENAME COLUMN nonexistent TO new_col');
1479        $alterStmt = $parser->statements[0];
1480        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1481        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1482
1483        $this->expectException(ColumnNotFoundException::class);
1484        $mutation->apply($store, []);
1485    }
1486
1487    public function testDropColumnRemovesFromStoreData(): void
1488    {
1489        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1490        $registry = new TableDefinitionRegistry();
1491        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, a VARCHAR(100), b VARCHAR(100))');
1492        self::assertNotNull($definition);
1493        $registry->register('t', $definition);
1494        $store = new ShadowStore();
1495        $store->set('t', [['id' => 1, 'a' => 'x', 'b' => 'y'], ['id' => 2, 'a' => 'p', 'b' => 'q']]);
1496
1497        $parser = new Parser('ALTER TABLE t DROP COLUMN a');
1498        $alterStmt = $parser->statements[0];
1499        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1500        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1501        $mutation->apply($store, []);
1502
1503        $rows = $store->get('t');
1504        self::assertCount(2, $rows);
1505        self::assertArrayNotHasKey('a', $rows[0]);
1506        self::assertArrayHasKey('b', $rows[0]);
1507        self::assertArrayHasKey('id', $rows[0]);
1508    }
1509
1510    public function testBuildCreateTableSqlColumnWithNoTypedColumnFallsBackToColumnTypes(): void
1511    {
1512        $registry = new TableDefinitionRegistry();
1513        $definition = new TableDefinition(
1514            ['id', 'name'],
1515            ['id' => 'INT', 'name' => 'VARCHAR(255)'],
1516            ['id'],
1517            ['id'],
1518            [],
1519            [],
1520        );
1521        $registry->register('t', $definition);
1522        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1523        $store = new ShadowStore();
1524
1525        $parser = new Parser('ALTER TABLE t ADD COLUMN email TEXT');
1526        $alterStmt = $parser->statements[0];
1527        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1528        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1529        $mutation->apply($store, []);
1530
1531        $def = $registry->get('t');
1532        self::assertNotNull($def);
1533        self::assertContains('email', $def->columns);
1534        self::assertContains('id', $def->columns);
1535        self::assertContains('name', $def->columns);
1536    }
1537
1538    public function testApplyUnregistersOldDefinitionAndRegistersNew(): void
1539    {
1540        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1541        $registry = new TableDefinitionRegistry();
1542        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
1543        self::assertNotNull($definition);
1544        $registry->register('t', $definition);
1545        $store = new ShadowStore();
1546
1547        $originalDef = $registry->get('t');
1548        self::assertNotNull($originalDef);
1549        self::assertCount(2, $originalDef->columns);
1550
1551        $parser = new Parser('ALTER TABLE t ADD COLUMN email VARCHAR(100)');
1552        $alterStmt = $parser->statements[0];
1553        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1554        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1555        $mutation->apply($store, []);
1556
1557        $newDef = $registry->get('t');
1558        self::assertNotNull($newDef);
1559        self::assertCount(3, $newDef->columns);
1560        self::assertSame(['id', 'name', 'email'], $newDef->columns);
1561    }
1562
1563    public function testBuildCreateTableSqlCompositePkBacktickWrappingExact(): void
1564    {
1565        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1566        $registry = new TableDefinitionRegistry();
1567        $definition = $schemaParser->parse('CREATE TABLE items (a INT NOT NULL, b INT NOT NULL, c TEXT, PRIMARY KEY (a, b))');
1568        self::assertNotNull($definition);
1569        $registry->register('items', $definition);
1570        $store = new ShadowStore();
1571
1572        $parser = new Parser('ALTER TABLE items ADD COLUMN d INT');
1573        $alterStmt = $parser->statements[0];
1574        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1575        $mutation = new AlterTableMutation('items', $alterStmt, $registry, $schemaParser);
1576        $mutation->apply($store, []);
1577
1578        $def = $registry->get('items');
1579        self::assertNotNull($def);
1580        self::assertSame(['a', 'b'], $def->primaryKeys);
1581        self::assertContains('d', $def->columns);
1582    }
1583
1584    public function testBuildCreateTableSqlUniqueConstraintClosingParenthesisExact(): void
1585    {
1586        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1587        $registry = new TableDefinitionRegistry();
1588        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, email VARCHAR(255), slug VARCHAR(255), UNIQUE KEY uk_email (email), UNIQUE KEY uk_slug (slug))');
1589        self::assertNotNull($definition);
1590        $registry->register('t', $definition);
1591        $store = new ShadowStore();
1592
1593        $parser = new Parser('ALTER TABLE t ADD COLUMN name TEXT');
1594        $alterStmt = $parser->statements[0];
1595        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1596        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1597        $mutation->apply($store, []);
1598
1599        $def = $registry->get('t');
1600        self::assertNotNull($def);
1601        self::assertArrayHasKey('uk_email', $def->uniqueConstraints);
1602        self::assertSame(['email'], $def->uniqueConstraints['uk_email']);
1603        self::assertArrayHasKey('uk_slug', $def->uniqueConstraints);
1604        self::assertSame(['slug'], $def->uniqueConstraints['uk_slug']);
1605    }
1606
1607    public function testApplyAddForeignKeyLeavesSchemaUnchanged(): void
1608    {
1609        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1610        $registry = new TableDefinitionRegistry();
1611        $definition = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT NOT NULL)');
1612        self::assertNotNull($definition);
1613        $registry->register('orders', $definition);
1614        $store = new ShadowStore();
1615
1616        $parser = new Parser('ALTER TABLE orders ADD FOREIGN KEY (user_id) REFERENCES users(id)');
1617        $alterStmt = $parser->statements[0];
1618        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1619        $mutation = new AlterTableMutation('orders', $alterStmt, $registry, $schemaParser);
1620        $mutation->apply($store, []);
1621
1622        $def = $registry->get('orders');
1623        self::assertNotNull($def);
1624        self::assertSame(['id', 'user_id'], $def->columns);
1625        self::assertSame(['id'], $def->primaryKeys);
1626    }
1627
1628    public function testApplyDropForeignKeyLeavesSchemaUnchanged(): void
1629    {
1630        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1631        $registry = new TableDefinitionRegistry();
1632        $definition = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT NOT NULL)');
1633        self::assertNotNull($definition);
1634        $registry->register('orders', $definition);
1635        $store = new ShadowStore();
1636
1637        $parser = new Parser('ALTER TABLE orders DROP FOREIGN KEY fk_user');
1638        $alterStmt = $parser->statements[0];
1639        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1640        $mutation = new AlterTableMutation('orders', $alterStmt, $registry, $schemaParser);
1641        $mutation->apply($store, []);
1642
1643        $def = $registry->get('orders');
1644        self::assertNotNull($def);
1645        self::assertSame(['id', 'user_id'], $def->columns);
1646        self::assertSame(['id'], $def->primaryKeys);
1647    }
1648
1649    public function testApplyRenameTableTransfersStoreDataToNewName(): void
1650    {
1651        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1652        $registry = new TableDefinitionRegistry();
1653        $definition = $schemaParser->parse('CREATE TABLE src (id INT PRIMARY KEY, val TEXT)');
1654        self::assertNotNull($definition);
1655        $registry->register('src', $definition);
1656        $store = new ShadowStore();
1657        $store->set('src', [['id' => 1, 'val' => 'x'], ['id' => 2, 'val' => 'y']]);
1658
1659        $parser = new Parser('ALTER TABLE src RENAME TO dst');
1660        $alterStmt = $parser->statements[0];
1661        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1662        $mutation = new AlterTableMutation('src', $alterStmt, $registry, $schemaParser);
1663        $mutation->apply($store, []);
1664
1665        self::assertSame([], $store->get('src'));
1666        self::assertSame([['id' => 1, 'val' => 'x'], ['id' => 2, 'val' => 'y']], $store->get('dst'));
1667        self::assertTrue($registry->has('dst'));
1668        self::assertFalse($registry->has('src'));
1669        self::assertSame('dst', $mutation->tableName());
1670    }
1671
1672    public function testApplyRenameColumnUpdatesStoreData(): void
1673    {
1674        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1675        $registry = new TableDefinitionRegistry();
1676        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, old_col VARCHAR(255))');
1677        self::assertNotNull($definition);
1678        $registry->register('t', $definition);
1679        $store = new ShadowStore();
1680        $store->set('t', [['id' => 1, 'old_col' => 'a'], ['id' => 2, 'old_col' => 'b']]);
1681
1682        $parser = new Parser('ALTER TABLE t RENAME COLUMN old_col TO new_col');
1683        $alterStmt = $parser->statements[0];
1684        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1685        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1686        $mutation->apply($store, []);
1687
1688        $rows = $store->get('t');
1689        self::assertCount(2, $rows);
1690        self::assertArrayHasKey('new_col', $rows[0]);
1691        self::assertArrayNotHasKey('old_col', $rows[0]);
1692        self::assertSame('a', $rows[0]['new_col']);
1693        self::assertArrayHasKey('new_col', $rows[1]);
1694        self::assertSame('b', $rows[1]['new_col']);
1695    }
1696
1697    public function testApplyDropColumnRemovesColumnFromStoreRows(): void
1698    {
1699        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1700        $registry = new TableDefinitionRegistry();
1701        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255), age INT)');
1702        self::assertNotNull($definition);
1703        $registry->register('t', $definition);
1704        $store = new ShadowStore();
1705        $store->set('t', [['id' => 1, 'name' => 'Alice', 'age' => 30], ['id' => 2, 'name' => 'Bob', 'age' => 25]]);
1706
1707        $parser = new Parser('ALTER TABLE t DROP COLUMN age');
1708        $alterStmt = $parser->statements[0];
1709        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1710        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1711        $mutation->apply($store, []);
1712
1713        $rows = $store->get('t');
1714        self::assertCount(2, $rows);
1715        self::assertArrayNotHasKey('age', $rows[0]);
1716        self::assertSame(['id' => 1, 'name' => 'Alice'], $rows[0]);
1717        self::assertSame(['id' => 2, 'name' => 'Bob'], $rows[1]);
1718    }
1719
1720    public function testApplyDropColumnWithoutColumnKeywordRemovesColumn(): void
1721    {
1722        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1723        $registry = new TableDefinitionRegistry();
1724        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255), extra TEXT)');
1725        self::assertNotNull($definition);
1726        $registry->register('t', $definition);
1727        $store = new ShadowStore();
1728
1729        $parser = new Parser('ALTER TABLE t DROP extra');
1730        $alterStmt = $parser->statements[0];
1731        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1732        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1733        $mutation->apply($store, []);
1734
1735        $def = $registry->get('t');
1736        self::assertNotNull($def);
1737        self::assertSame(['id', 'name'], $def->columns);
1738        self::assertNotContains('extra', $def->columns);
1739    }
1740
1741    public function testApplyChangeColumnUpdatesStoreColumnName(): void
1742    {
1743        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1744        $registry = new TableDefinitionRegistry();
1745        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, old_name VARCHAR(100))');
1746        self::assertNotNull($definition);
1747        $registry->register('t', $definition);
1748        $store = new ShadowStore();
1749        $store->set('t', [['id' => 1, 'old_name' => 'val']]);
1750
1751        $parser = new Parser('ALTER TABLE t CHANGE old_name new_name TEXT');
1752        $alterStmt = $parser->statements[0];
1753        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1754        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1755        $mutation->apply($store, []);
1756
1757        $rows = $store->get('t');
1758        self::assertArrayHasKey('new_name', $rows[0]);
1759        self::assertArrayNotHasKey('old_name', $rows[0]);
1760        self::assertSame('val', $rows[0]['new_name']);
1761
1762        $def = $registry->get('t');
1763        self::assertNotNull($def);
1764        self::assertContains('new_name', $def->columns);
1765    }
1766
1767    public function testApplyAddPrimaryKeyPreservesAllColumns(): void
1768    {
1769        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1770        $registry = new TableDefinitionRegistry();
1771        $definition = $schemaParser->parse('CREATE TABLE t (id INT, name VARCHAR(255), email TEXT)');
1772        self::assertNotNull($definition);
1773        $registry->register('t', $definition);
1774        $store = new ShadowStore();
1775
1776        $parser = new Parser('ALTER TABLE t ADD PRIMARY KEY (id)');
1777        $alterStmt = $parser->statements[0];
1778        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1779        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1780        $mutation->apply($store, []);
1781
1782        $def = $registry->get('t');
1783        self::assertNotNull($def);
1784        self::assertSame(['id', 'name', 'email'], $def->columns);
1785    }
1786
1787    public function testApplyDropPrimaryKeyRemovesPkConstraint(): void
1788    {
1789        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1790        $registry = new TableDefinitionRegistry();
1791        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
1792        self::assertNotNull($definition);
1793        $registry->register('t', $definition);
1794        $store = new ShadowStore();
1795
1796        $parser = new Parser('ALTER TABLE t DROP PRIMARY KEY');
1797        $alterStmt = $parser->statements[0];
1798        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1799        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1800        $mutation->apply($store, []);
1801
1802        $def = $registry->get('t');
1803        self::assertNotNull($def);
1804        self::assertSame([], $def->primaryKeys);
1805        self::assertContains('id', $def->columns);
1806        self::assertContains('name', $def->columns);
1807    }
1808
1809    public function testApplyModifyColumnPreservesOtherColumns(): void
1810    {
1811        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1812        $registry = new TableDefinitionRegistry();
1813        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, a VARCHAR(50), b VARCHAR(50), c VARCHAR(50))');
1814        self::assertNotNull($definition);
1815        $registry->register('t', $definition);
1816        $store = new ShadowStore();
1817
1818        $parser = new Parser('ALTER TABLE t MODIFY b TEXT');
1819        $alterStmt = $parser->statements[0];
1820        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1821        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1822        $mutation->apply($store, []);
1823
1824        $def = $registry->get('t');
1825        self::assertNotNull($def);
1826        self::assertSame(['id', 'a', 'b', 'c'], $def->columns);
1827        self::assertSame('TEXT', $def->columnTypes['b']);
1828        self::assertSame('VARCHAR(50)', $def->columnTypes['a'] ?? '');
1829    }
1830
1831    public function testApplyNotNullColumnPreservesNotNullInRoundTrip(): void
1832    {
1833        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1834        $registry = new TableDefinitionRegistry();
1835        $definition = $schemaParser->parse('CREATE TABLE t (id INT NOT NULL PRIMARY KEY, name VARCHAR(255) NOT NULL)');
1836        self::assertNotNull($definition);
1837        $registry->register('t', $definition);
1838        $store = new ShadowStore();
1839
1840        $parser = new Parser('ALTER TABLE t ADD COLUMN email VARCHAR(100)');
1841        $alterStmt = $parser->statements[0];
1842        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1843        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1844        $mutation->apply($store, []);
1845
1846        $def = $registry->get('t');
1847        self::assertNotNull($def);
1848        self::assertContains('id', $def->notNullColumns);
1849        self::assertContains('name', $def->notNullColumns);
1850    }
1851
1852    public function testApplySchemaNotFoundThrows(): void
1853    {
1854        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1855        $registry = new TableDefinitionRegistry();
1856        $store = new ShadowStore();
1857
1858        $parser = new Parser('ALTER TABLE nonexistent ADD COLUMN x INT');
1859        $alterStmt = $parser->statements[0];
1860        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1861        $mutation = new AlterTableMutation('nonexistent', $alterStmt, $registry, $schemaParser);
1862
1863        $this->expectException(SchemaNotFoundException::class);
1864        $mutation->apply($store, []);
1865    }
1866
1867    public function testApplyChangeColumnNotFoundThrows(): void
1868    {
1869        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1870        $registry = new TableDefinitionRegistry();
1871        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
1872        self::assertNotNull($definition);
1873        $registry->register('t', $definition);
1874        $store = new ShadowStore();
1875
1876        $parser = new Parser('ALTER TABLE t CHANGE nonexistent new_col TEXT');
1877        $alterStmt = $parser->statements[0];
1878        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1879        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1880
1881        $this->expectException(ColumnNotFoundException::class);
1882        $mutation->apply($store, []);
1883    }
1884
1885    public function testApplyRenameColumnNotFoundThrowsException(): void
1886    {
1887        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1888        $registry = new TableDefinitionRegistry();
1889        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
1890        self::assertNotNull($definition);
1891        $registry->register('t', $definition);
1892        $store = new ShadowStore();
1893
1894        $parser = new Parser('ALTER TABLE t RENAME COLUMN nonexistent TO new_col');
1895        $alterStmt = $parser->statements[0];
1896        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1897        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1898
1899        $this->expectException(ColumnNotFoundException::class);
1900        $mutation->apply($store, []);
1901    }
1902
1903    public function testBuildCreateTableSqlFallsBackToTextWhenNoColumnTypes(): void
1904    {
1905        $registry = new TableDefinitionRegistry();
1906        $definition = new TableDefinition(
1907            ['id', 'val'],
1908            [],
1909            [],
1910            [],
1911            [],
1912            [],
1913        );
1914        $registry->register('t', $definition);
1915        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1916        $store = new ShadowStore();
1917
1918        $parser = new Parser('ALTER TABLE t ADD COLUMN extra INT');
1919        $alterStmt = $parser->statements[0];
1920        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1921        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1922        $mutation->apply($store, []);
1923
1924        $def = $registry->get('t');
1925        self::assertNotNull($def);
1926        self::assertContains('extra', $def->columns);
1927        self::assertContains('id', $def->columns);
1928        self::assertContains('val', $def->columns);
1929    }
1930
1931    public function testApplyDropColumnEmptyStoreDoesNotModifyStore(): void
1932    {
1933        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1934        $registry = new TableDefinitionRegistry();
1935        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255), extra TEXT)');
1936        self::assertNotNull($definition);
1937        $registry->register('t', $definition);
1938        $store = new ShadowStore();
1939
1940        $parser = new Parser('ALTER TABLE t DROP COLUMN extra');
1941        $alterStmt = $parser->statements[0];
1942        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1943        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1944        $mutation->apply($store, []);
1945
1946        self::assertSame([], $store->get('t'));
1947        $def = $registry->get('t');
1948        self::assertNotNull($def);
1949        self::assertNotContains('extra', $def->columns);
1950    }
1951
1952    public function testApplyRenameTableEmptyStoreTransfersEmpty(): void
1953    {
1954        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1955        $registry = new TableDefinitionRegistry();
1956        $definition = $schemaParser->parse('CREATE TABLE src (id INT PRIMARY KEY)');
1957        self::assertNotNull($definition);
1958        $registry->register('src', $definition);
1959        $store = new ShadowStore();
1960
1961        $parser = new Parser('ALTER TABLE src RENAME TO dst');
1962        $alterStmt = $parser->statements[0];
1963        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1964        $mutation = new AlterTableMutation('src', $alterStmt, $registry, $schemaParser);
1965        $mutation->apply($store, []);
1966
1967        self::assertSame([], $store->get('src'));
1968        self::assertSame([], $store->get('dst'));
1969        self::assertTrue($registry->has('dst'));
1970    }
1971
1972    public function testApplyChangeColumnSameNameKeepsStoreUnchanged(): void
1973    {
1974        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1975        $registry = new TableDefinitionRegistry();
1976        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, col VARCHAR(100))');
1977        self::assertNotNull($definition);
1978        $registry->register('t', $definition);
1979        $store = new ShadowStore();
1980        $store->set('t', [['id' => 1, 'col' => 'v']]);
1981
1982        $parser = new Parser('ALTER TABLE t CHANGE col col TEXT');
1983        $alterStmt = $parser->statements[0];
1984        self::assertInstanceOf(AlterStatement::class, $alterStmt);
1985        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
1986        $mutation->apply($store, []);
1987
1988        $rows = $store->get('t');
1989        self::assertSame([['id' => 1, 'col' => 'v']], $rows);
1990
1991        $def = $registry->get('t');
1992        self::assertNotNull($def);
1993        self::assertContains('col', $def->columns);
1994    }
1995
1996    public function testApplyRenameColumnEmptyStoreDoesNotModifyStore(): void
1997    {
1998        $schemaParser = new MySqlSchemaParser(new MySqlParser());
1999        $registry = new TableDefinitionRegistry();
2000        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, old_col VARCHAR(255))');
2001        self::assertNotNull($definition);
2002        $registry->register('t', $definition);
2003        $store = new ShadowStore();
2004
2005        $parser = new Parser('ALTER TABLE t RENAME COLUMN old_col TO new_col');
2006        $alterStmt = $parser->statements[0];
2007        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2008        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2009        $mutation->apply($store, []);
2010
2011        self::assertSame([], $store->get('t'));
2012        $def = $registry->get('t');
2013        self::assertNotNull($def);
2014        self::assertContains('new_col', $def->columns);
2015    }
2016
2017    public function testApplyUnsupportedAlterThrowsException(): void
2018    {
2019        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2020        $registry = new TableDefinitionRegistry();
2021        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
2022        self::assertNotNull($definition);
2023        $registry->register('t', $definition);
2024        $store = new ShadowStore();
2025
2026        $parser = new Parser('ALTER TABLE t ADD INDEX idx_id (id)');
2027        $alterStmt = $parser->statements[0];
2028        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2029        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2030
2031        $this->expectException(UnsupportedSqlException::class);
2032        $mutation->apply($store, []);
2033    }
2034
2035    public function testApplyAlterDropDefaultIsHandled(): void
2036    {
2037        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2038        $registry = new TableDefinitionRegistry();
2039        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
2040        self::assertNotNull($definition);
2041        $registry->register('t', $definition);
2042        $store = new ShadowStore();
2043
2044        $parser = new Parser('ALTER TABLE t ALTER COLUMN name DROP DEFAULT');
2045        $alterStmt = $parser->statements[0];
2046        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2047
2048        $altered = $alterStmt->altered ?? [];
2049        self::assertNotEmpty($altered);
2050    }
2051
2052    public function testApplyAddKeyThrowsUnsupported(): void
2053    {
2054        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2055        $registry = new TableDefinitionRegistry();
2056        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
2057        self::assertNotNull($definition);
2058        $registry->register('t', $definition);
2059        $store = new ShadowStore();
2060
2061        $parser = new Parser('ALTER TABLE t ADD KEY idx_name (name)');
2062        $alterStmt = $parser->statements[0];
2063        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2064        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2065
2066        $this->expectException(UnsupportedSqlException::class);
2067        $mutation->apply($store, []);
2068    }
2069
2070    public function testApplyRenameIndexThrowsUnsupported(): void
2071    {
2072        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2073        $registry = new TableDefinitionRegistry();
2074        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
2075        self::assertNotNull($definition);
2076        $registry->register('t', $definition);
2077        $store = new ShadowStore();
2078
2079        $parser = new Parser('ALTER TABLE t RENAME INDEX old_idx TO new_idx');
2080        $alterStmt = $parser->statements[0];
2081        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2082        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2083
2084        $this->expectException(UnsupportedSqlException::class);
2085        $mutation->apply($store, []);
2086    }
2087
2088    public function testApplyRenameKeyThrowsUnsupported(): void
2089    {
2090        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2091        $registry = new TableDefinitionRegistry();
2092        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
2093        self::assertNotNull($definition);
2094        $registry->register('t', $definition);
2095        $store = new ShadowStore();
2096
2097        $parser = new Parser('ALTER TABLE t RENAME KEY old_key TO new_key');
2098        $alterStmt = $parser->statements[0];
2099        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2100        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2101
2102        $this->expectException(UnsupportedSqlException::class);
2103        $mutation->apply($store, []);
2104    }
2105
2106    public function testBuildCreateTableSqlCompositePkContainsBacktickWrappedColumns(): void
2107    {
2108        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2109        $registry = new TableDefinitionRegistry();
2110        $definition = $schemaParser->parse('CREATE TABLE t (a INT NOT NULL, b INT NOT NULL, PRIMARY KEY (a, b))');
2111        self::assertNotNull($definition);
2112        $registry->register('t', $definition);
2113        $store = new ShadowStore();
2114
2115        $parser = new Parser('ALTER TABLE t ADD COLUMN c TEXT');
2116        $alterStmt = $parser->statements[0];
2117        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2118        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2119        $mutation->apply($store, []);
2120
2121        $def = $registry->get('t');
2122        self::assertNotNull($def);
2123        self::assertSame(['a', 'b'], $def->primaryKeys);
2124        self::assertContains('c', $def->columns);
2125    }
2126
2127    public function testBuildCreateTableSqlUniqueConstraintContainsClosingParenthesis(): void
2128    {
2129        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2130        $registry = new TableDefinitionRegistry();
2131        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, email VARCHAR(255), UNIQUE KEY uk_email (email))');
2132        self::assertNotNull($definition);
2133        $registry->register('t', $definition);
2134        $store = new ShadowStore();
2135
2136        $parser = new Parser('ALTER TABLE t ADD COLUMN phone VARCHAR(20)');
2137        $alterStmt = $parser->statements[0];
2138        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2139        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2140        $mutation->apply($store, []);
2141
2142        $def = $registry->get('t');
2143        self::assertNotNull($def);
2144        self::assertArrayHasKey('uk_email', $def->uniqueConstraints);
2145        self::assertSame(['email'], $def->uniqueConstraints['uk_email']);
2146    }
2147
2148    public function testBuildCreateTableSqlColumnTypeFallbackToColumnTypes(): void
2149    {
2150        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2151        $registry = new TableDefinitionRegistry();
2152        $registry->register('t', new TableDefinition(
2153            ['id', 'val'],
2154            ['id' => 'INT', 'val' => 'VARCHAR(100)'],
2155            ['id'],
2156            [],
2157            [],
2158            [],
2159        ));
2160        $store = new ShadowStore();
2161
2162        $parser = new Parser('ALTER TABLE t ADD COLUMN extra INT');
2163        $alterStmt = $parser->statements[0];
2164        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2165        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2166        $mutation->apply($store, []);
2167
2168        $def = $registry->get('t');
2169        self::assertNotNull($def);
2170        self::assertContains('extra', $def->columns);
2171        self::assertSame(['id'], $def->primaryKeys);
2172    }
2173
2174    public function testRenameColumnEmptyStoreDoesNotModifyStore(): void
2175    {
2176        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2177        $registry = new TableDefinitionRegistry();
2178        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, old_col VARCHAR(255))');
2179        self::assertNotNull($definition);
2180        $registry->register('t', $definition);
2181        $store = new ShadowStore();
2182
2183        $parser = new Parser('ALTER TABLE t RENAME COLUMN old_col TO new_col');
2184        $alterStmt = $parser->statements[0];
2185        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2186        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2187        $mutation->apply($store, []);
2188
2189        $def = $registry->get('t');
2190        self::assertNotNull($def);
2191        self::assertContains('new_col', $def->columns);
2192        self::assertNotContains('old_col', $def->columns);
2193        self::assertSame([], $store->get('t'));
2194    }
2195
2196    public function testChangeColumnUpdatesStoreColumnKey(): void
2197    {
2198        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2199        $registry = new TableDefinitionRegistry();
2200        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, old_name VARCHAR(255))');
2201        self::assertNotNull($definition);
2202        $registry->register('t', $definition);
2203        $store = new ShadowStore();
2204        $store->set('t', [['id' => 1, 'old_name' => 'val'], ['id' => 2, 'old_name' => 'val2']]);
2205
2206        $parser = new Parser('ALTER TABLE t CHANGE COLUMN old_name new_name TEXT');
2207        $alterStmt = $parser->statements[0];
2208        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2209        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2210        $mutation->apply($store, []);
2211
2212        $rows = $store->get('t');
2213        self::assertCount(2, $rows);
2214        self::assertArrayHasKey('new_name', $rows[0]);
2215        self::assertArrayNotHasKey('old_name', $rows[0]);
2216        self::assertArrayHasKey('new_name', $rows[1]);
2217        self::assertArrayNotHasKey('old_name', $rows[1]);
2218    }
2219
2220    public function testRenameTableEmptyStoreDoesNotModifyStore(): void
2221    {
2222        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2223        $registry = new TableDefinitionRegistry();
2224        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
2225        self::assertNotNull($definition);
2226        $registry->register('t', $definition);
2227        $store = new ShadowStore();
2228
2229        $parser = new Parser('ALTER TABLE t RENAME TO t2');
2230        $alterStmt = $parser->statements[0];
2231        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2232        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2233        $mutation->apply($store, []);
2234
2235        self::assertNotNull($registry->get('t2'));
2236        self::assertNull($registry->get('t'));
2237        self::assertSame([], $store->get('t'));
2238        self::assertSame([], $store->get('t2'));
2239    }
2240
2241    public function testDropColumnRemovesFromStoreRows(): void
2242    {
2243        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2244        $registry = new TableDefinitionRegistry();
2245        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, a TEXT, b TEXT)');
2246        self::assertNotNull($definition);
2247        $registry->register('t', $definition);
2248        $store = new ShadowStore();
2249        $store->set('t', [['id' => 1, 'a' => 'x', 'b' => 'y']]);
2250
2251        $parser = new Parser('ALTER TABLE t DROP COLUMN a');
2252        $alterStmt = $parser->statements[0];
2253        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2254        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2255        $mutation->apply($store, []);
2256
2257        $rows = $store->get('t');
2258        self::assertCount(1, $rows);
2259        self::assertArrayNotHasKey('a', $rows[0]);
2260        self::assertSame('y', $rows[0]['b']);
2261    }
2262
2263    public function testAddColumnWithBacktickedNameIsNormalized(): void
2264    {
2265        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2266        $registry = new TableDefinitionRegistry();
2267        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
2268        self::assertNotNull($definition);
2269        $registry->register('t', $definition);
2270        $store = new ShadowStore();
2271
2272        $parser = new Parser('ALTER TABLE t ADD COLUMN `new_col` VARCHAR(100)');
2273        $alterStmt = $parser->statements[0];
2274        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2275        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2276        $mutation->apply($store, []);
2277
2278        $def = $registry->get('t');
2279        self::assertNotNull($def);
2280        self::assertContains('new_col', $def->columns);
2281        self::assertNotContains('`new_col`', $def->columns);
2282    }
2283
2284    public function testDropColumnWithoutColumnKeywordRemovesColumn(): void
2285    {
2286        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2287        $registry = new TableDefinitionRegistry();
2288        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, a TEXT, b TEXT)');
2289        self::assertNotNull($definition);
2290        $registry->register('t', $definition);
2291        $store = new ShadowStore();
2292
2293        $parser = new Parser('ALTER TABLE t DROP a');
2294        $alterStmt = $parser->statements[0];
2295        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2296        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2297        $mutation->apply($store, []);
2298
2299        $def = $registry->get('t');
2300        self::assertNotNull($def);
2301        self::assertNotContains('a', $def->columns);
2302        self::assertContains('b', $def->columns);
2303    }
2304
2305    public function testRenameColumnUpdatesStoreColumnName(): void
2306    {
2307        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2308        $registry = new TableDefinitionRegistry();
2309        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, a VARCHAR(255))');
2310        self::assertNotNull($definition);
2311        $registry->register('t', $definition);
2312        $store = new ShadowStore();
2313        $store->set('t', [['id' => 1, 'a' => 'val1'], ['id' => 2, 'a' => 'val2']]);
2314
2315        $parser = new Parser('ALTER TABLE t RENAME COLUMN a TO b');
2316        $alterStmt = $parser->statements[0];
2317        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2318        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2319        $mutation->apply($store, []);
2320
2321        $rows = $store->get('t');
2322        self::assertCount(2, $rows);
2323        self::assertArrayHasKey('b', $rows[0]);
2324        self::assertArrayNotHasKey('a', $rows[0]);
2325        self::assertSame('val1', $rows[0]['b']);
2326        self::assertArrayHasKey('b', $rows[1]);
2327        self::assertSame('val2', $rows[1]['b']);
2328    }
2329
2330    public function testApplyAddIndexThrowsUnsupported(): void
2331    {
2332        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2333        $registry = new TableDefinitionRegistry();
2334        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
2335        self::assertNotNull($definition);
2336        $registry->register('t', $definition);
2337        $store = new ShadowStore();
2338
2339        $parser = new Parser('ALTER TABLE t ADD INDEX idx_name (name)');
2340        $alterStmt = $parser->statements[0];
2341        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2342        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2343
2344        $this->expectException(UnsupportedSqlException::class);
2345        $mutation->apply($store, []);
2346    }
2347
2348    public function testApplyDropIndexThrowsUnsupported(): void
2349    {
2350        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2351        $registry = new TableDefinitionRegistry();
2352        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
2353        self::assertNotNull($definition);
2354        $registry->register('t', $definition);
2355        $store = new ShadowStore();
2356
2357        $parser = new Parser('ALTER TABLE t DROP INDEX idx_name');
2358        $alterStmt = $parser->statements[0];
2359        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2360        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2361
2362        $this->expectException(UnsupportedSqlException::class);
2363        $mutation->apply($store, []);
2364    }
2365
2366    public function testApplyDropKeyThrowsUnsupported(): void
2367    {
2368        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2369        $registry = new TableDefinitionRegistry();
2370        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
2371        self::assertNotNull($definition);
2372        $registry->register('t', $definition);
2373        $store = new ShadowStore();
2374
2375        $parser = new Parser('ALTER TABLE t DROP KEY idx_name');
2376        $alterStmt = $parser->statements[0];
2377        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2378        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2379
2380        $this->expectException(UnsupportedSqlException::class);
2381        $mutation->apply($store, []);
2382    }
2383
2384    public function testRenameTableTransfersStoreData(): void
2385    {
2386        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2387        $registry = new TableDefinitionRegistry();
2388        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
2389        self::assertNotNull($definition);
2390        $registry->register('t', $definition);
2391        $store = new ShadowStore();
2392        $store->set('t', [['id' => 1], ['id' => 2]]);
2393
2394        $parser = new Parser('ALTER TABLE t RENAME TO t_new');
2395        $alterStmt = $parser->statements[0];
2396        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2397        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2398        $mutation->apply($store, []);
2399
2400        self::assertSame([['id' => 1], ['id' => 2]], $store->get('t_new'));
2401        self::assertSame([], $store->get('t'));
2402        self::assertNull($registry->get('t'));
2403        self::assertNotNull($registry->get('t_new'));
2404    }
2405
2406    public function testDropColumnUpdatesFieldsArrayIndices(): void
2407    {
2408        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2409        $registry = new TableDefinitionRegistry();
2410        $definition = $schemaParser->parse('CREATE TABLE t (a INT PRIMARY KEY, b TEXT, c TEXT, d TEXT)');
2411        self::assertNotNull($definition);
2412        $registry->register('t', $definition);
2413        $store = new ShadowStore();
2414
2415        $parser = new Parser('ALTER TABLE t DROP COLUMN b');
2416        $alterStmt = $parser->statements[0];
2417        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2418        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2419        $mutation->apply($store, []);
2420
2421        $def = $registry->get('t');
2422        self::assertNotNull($def);
2423        self::assertSame(['a', 'c', 'd'], $def->columns);
2424        self::assertNotContains('b', $def->columns);
2425    }
2426
2427    public function testModifyColumnPreservesOtherColumnTypes(): void
2428    {
2429        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2430        $registry = new TableDefinitionRegistry();
2431        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, a INT, b VARCHAR(100))');
2432        self::assertNotNull($definition);
2433        $registry->register('t', $definition);
2434        $store = new ShadowStore();
2435
2436        $parser = new Parser('ALTER TABLE t MODIFY COLUMN a BIGINT');
2437        $alterStmt = $parser->statements[0];
2438        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2439        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2440        $mutation->apply($store, []);
2441
2442        $def = $registry->get('t');
2443        self::assertNotNull($def);
2444        self::assertSame(['id', 'a', 'b'], $def->columns);
2445        self::assertContains('b', $def->columns);
2446    }
2447
2448    public function testApplySchemaNotFoundThrowsException(): void
2449    {
2450        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2451        $registry = new TableDefinitionRegistry();
2452        $store = new ShadowStore();
2453
2454        $parser = new Parser('ALTER TABLE nonexistent ADD COLUMN x INT');
2455        $alterStmt = $parser->statements[0];
2456        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2457        $mutation = new AlterTableMutation('nonexistent', $alterStmt, $registry, $schemaParser);
2458
2459        $this->expectException(SchemaNotFoundException::class);
2460        $mutation->apply($store, []);
2461    }
2462
2463    public function testApplyRenameTableRegistersNewNameInRegistry(): void
2464    {
2465        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2466        $registry = new TableDefinitionRegistry();
2467        $definition = $schemaParser->parse('CREATE TABLE alpha (id INT PRIMARY KEY, name TEXT)');
2468        self::assertNotNull($definition);
2469        $registry->register('alpha', $definition);
2470        $store = new ShadowStore();
2471        $store->set('alpha', [['id' => 1, 'name' => 'test']]);
2472
2473        $parser = new Parser('ALTER TABLE alpha RENAME TO beta');
2474        $alterStmt = $parser->statements[0];
2475        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2476        $mutation = new AlterTableMutation('alpha', $alterStmt, $registry, $schemaParser);
2477        $mutation->apply($store, []);
2478
2479        $betaDef = $registry->get('beta');
2480        self::assertNotNull($betaDef);
2481        self::assertContains('id', $betaDef->columns);
2482        self::assertContains('name', $betaDef->columns);
2483        self::assertSame('beta', $mutation->tableName());
2484    }
2485
2486    public function testApplyAddForeignKeyDoesNotAffectColumns(): void
2487    {
2488        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2489        $registry = new TableDefinitionRegistry();
2490        $definition = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)');
2491        self::assertNotNull($definition);
2492        $registry->register('orders', $definition);
2493        $store = new ShadowStore();
2494
2495        $parser = new Parser('ALTER TABLE orders ADD FOREIGN KEY (user_id) REFERENCES users(id)');
2496        $alterStmt = $parser->statements[0];
2497        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2498        $mutation = new AlterTableMutation('orders', $alterStmt, $registry, $schemaParser);
2499        $mutation->apply($store, []);
2500
2501        $def = $registry->get('orders');
2502        self::assertNotNull($def);
2503        self::assertSame(['id', 'user_id'], $def->columns);
2504    }
2505
2506    public function testApplyDropForeignKeyDoesNotAffectColumns(): void
2507    {
2508        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2509        $registry = new TableDefinitionRegistry();
2510        $definition = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)');
2511        self::assertNotNull($definition);
2512        $registry->register('orders', $definition);
2513        $store = new ShadowStore();
2514
2515        $parser = new Parser('ALTER TABLE orders DROP FOREIGN KEY fk_user');
2516        $alterStmt = $parser->statements[0];
2517        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2518        $mutation = new AlterTableMutation('orders', $alterStmt, $registry, $schemaParser);
2519        $mutation->apply($store, []);
2520
2521        $def = $registry->get('orders');
2522        self::assertNotNull($def);
2523        self::assertSame(['id', 'user_id'], $def->columns);
2524    }
2525
2526    public function testBuildCreateTableSqlWithCompositePrimaryKeyBackticks(): void
2527    {
2528        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2529        $registry = new TableDefinitionRegistry();
2530        $definition = $schemaParser->parse('CREATE TABLE t (a INT NOT NULL, b INT NOT NULL, c TEXT, PRIMARY KEY (a, b))');
2531        self::assertNotNull($definition);
2532        $registry->register('t', $definition);
2533        $store = new ShadowStore();
2534
2535        $parser = new Parser('ALTER TABLE t ADD COLUMN d VARCHAR(255)');
2536        $alterStmt = $parser->statements[0];
2537        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2538        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2539        $mutation->apply($store, []);
2540
2541        $def = $registry->get('t');
2542        self::assertNotNull($def);
2543        self::assertSame(['a', 'b'], $def->primaryKeys);
2544        self::assertContains('d', $def->columns);
2545    }
2546
2547    public function testBuildCreateTableSqlPreservesUniqueConstraintsAfterAddColumn(): void
2548    {
2549        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2550        $registry = new TableDefinitionRegistry();
2551        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, email VARCHAR(255), UNIQUE KEY uk_email (email))');
2552        self::assertNotNull($definition);
2553        $registry->register('t', $definition);
2554        $store = new ShadowStore();
2555
2556        $parser = new Parser('ALTER TABLE t ADD COLUMN phone VARCHAR(20)');
2557        $alterStmt = $parser->statements[0];
2558        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2559        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2560        $mutation->apply($store, []);
2561
2562        $def = $registry->get('t');
2563        self::assertNotNull($def);
2564        self::assertArrayHasKey('uk_email', $def->uniqueConstraints);
2565        self::assertSame(['email'], $def->uniqueConstraints['uk_email']);
2566        self::assertContains('phone', $def->columns);
2567    }
2568
2569    public function testApplyRenameColumnUpdatesFieldNameInSchema(): void
2570    {
2571        $schemaParser = new MySqlSchemaParser(new MySqlParser());
2572        $registry = new TableDefinitionRegistry();
2573        $definition = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, old_name VARCHAR(255))');
2574        self::assertNotNull($definition);
2575        $registry->register('t', $definition);
2576        $store = new ShadowStore();
2577
2578        $parser = new Parser('ALTER TABLE t RENAME COLUMN old_name TO new_name');
2579        $alterStmt = $parser->statements[0];
2580        self::assertInstanceOf(AlterStatement::class, $alterStmt);
2581        $mutation = new AlterTableMutation('t', $alterStmt, $registry, $schemaParser);
2582        $mutation->apply($store, []);
2583
2584        $def = $registry->get('t');
2585        self::assertNotNull($def);
2586        self::assertContains('new_name', $def->columns);
2587        self::assertNotContains('old_name', $def->columns);
2588    }
2589}
2590