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