packages/ztd-query-sqlite/tests/Unit/Shadow/SqliteMutationResolverTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Shadow;
6
7use Generator;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\DataProvider;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use ZtdQuery\Exception\ColumnAlreadyExistsException;
13use ZtdQuery\Exception\ColumnNotFoundException;
14use ZtdQuery\Exception\UnknownSchemaException;
15use ZtdQuery\Exception\UnsupportedSqlException;
16use ZtdQuery\Platform\SchemaParser;
17use ZtdQuery\Platform\Sqlite\Schema\SqliteSchemaParser;
18use ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\AlterTableMutation;
19use ZtdQuery\Platform\Sqlite\Shadow\SqliteMutationResolver;
20use ZtdQuery\Platform\Sqlite\Sql\SqliteIdentifierQuoter;
21use ZtdQuery\Platform\Sqlite\Sql\SqliteLexicalMasker;
22use ZtdQuery\Platform\Sqlite\Sql\SqliteParser;
23use ZtdQuery\Rewrite\QueryKind;
24use ZtdQuery\Schema\ColumnDeclaration;
25use ZtdQuery\Schema\ColumnTypeFamily;
26use ZtdQuery\Schema\Key\ForeignKeyDefinition;
27use ZtdQuery\Schema\Key\IdentityGenerationStrategy;
28use ZtdQuery\Schema\TableDefinition;
29use ZtdQuery\Schema\TableDefinitionRegistry;
30use ZtdQuery\Shadow\Mutation\Row\DeleteMutation;
31use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
32use ZtdQuery\Shadow\Mutation\Row\ReplaceMutation;
33use ZtdQuery\Shadow\Mutation\Row\UpdateMutation;
34use ZtdQuery\Shadow\Mutation\Table\CreateTableMutation;
35use ZtdQuery\Shadow\Mutation\Table\DropTableMutation;
36use ZtdQuery\Shadow\Mutation\UpsertMutation;
37use ZtdQuery\Shadow\ShadowStore;
38use ZtdQuery\Shadow\ShadowTableState;
39
40#[CoversClass(SqliteMutationResolver::class)]
41#[UsesClass(AlterTableMutation::class)]
42#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\AddColumnResolver::class)]
43#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\AlteredTableProjection::class)]
44#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\ColumnDefinitionEditor::class)]
45#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\DropColumnResolver::class)]
46#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\RenameResolver::class)]
47#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\InsertMutationResolver::class)]
48#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\MutationTableLookup::class)]
49#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\RowMutationResolver::class)]
50#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\TableMutationResolver::class)]
51#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Alter\AlterOperationParser::class)]
52#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentColumnParser::class)]
53#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentParser::class)]
54#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\ValueListParser::class)]
55#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Insert\InsertClauseParser::class)]
56#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\ExpressionSpan::class)]
57#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\IdentifierDecoder::class)]
58#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\LiteralMasker::class)]
59#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\OpaqueSqlSpan::class)]
60#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\QuotedSpan::class)]
61#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\TopLevelKeywordScanner::class)]
62#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\RelationSourceParser::class)]
63#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementClassifier::class)]
64#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementStructure::class)]
65#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\TargetTableParser::class)]
66#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Update\UpdateClauseParser::class)]
67#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ArithmeticExpressionParser::class)]
68#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ComparisonExpressionParser::class)]
69#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ExpressionCursor::class)]
70#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ExpressionTokenDecoder::class)]
71#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\LogicalExpressionParser::class)]
72#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\PrimaryExpressionParser::class)]
73#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\ColumnDefinitionParser::class)]
74#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\TableBodyParser::class)]
75#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\TableDefinitionBuilder::class)]
76#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\VirtualTableParser::class)]
77#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Key\ForeignKeyEntryParser::class)]
78#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Key\ForeignKeyTokens::class)]
79#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\SqliteColumnTypeMapper::class)]
80#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Key\SqliteForeignKeyDefinitionParser::class)]
81#[UsesClass(SqliteIdentifierQuoter::class)]
82#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile::class)]
83#[UsesClass(SqliteLexicalMasker::class)]
84#[UsesClass(SqliteParser::class)]
85#[UsesClass(SqliteSchemaParser::class)]
86#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\SqliteSelectRelationParser::class)]
87#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\SqliteUpsertExpressionParser::class)]
88final class SqliteMutationResolverTest extends TestCase
89{
90    public function testResolveInsert(): void
91    {
92        $store = new ShadowStore();
93        $registry = new TableDefinitionRegistry();
94        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
95        $mutation = $resolver->resolve("INSERT INTO users (id, name) VALUES (1, 'Alice')", QueryKind::WRITE_SIMULATED);
96
97        self::assertInstanceOf(InsertMutation::class, $mutation);
98    }
99
100    public function testResolveInsertIgnore(): void
101    {
102        $registry = new TableDefinitionRegistry();
103        $registry->register('users', new TableDefinition(
104            ['id', 'name', 'email'],
105            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
106            ['id'],
107            ['id', 'name'],
108            [],
109        ));
110        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
111        $mutation = $resolver->resolve("INSERT OR IGNORE INTO users (id, name) VALUES (1, 'Alice')", QueryKind::WRITE_SIMULATED);
112
113        self::assertInstanceOf(InsertMutation::class, $mutation);
114    }
115
116    public function testResolveReplace(): void
117    {
118        $store = new ShadowStore();
119        $registry = new TableDefinitionRegistry();
120        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
121        $mutation = $resolver->resolve("REPLACE INTO users (id, name) VALUES (1, 'Alice')", QueryKind::WRITE_SIMULATED);
122
123        self::assertInstanceOf(ReplaceMutation::class, $mutation);
124    }
125
126    public function testResolveInsertOrReplace(): void
127    {
128        $store = new ShadowStore();
129        $registry = new TableDefinitionRegistry();
130        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
131        $mutation = $resolver->resolve("INSERT OR REPLACE INTO users (id, name) VALUES (1, 'Alice')", QueryKind::WRITE_SIMULATED);
132
133        self::assertInstanceOf(ReplaceMutation::class, $mutation);
134    }
135
136    public function testResolveUpsert(): void
137    {
138        $registry = new TableDefinitionRegistry();
139        $registry->register('users', new TableDefinition(
140            ['id', 'name', 'email'],
141            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
142            ['id'],
143            ['id', 'name'],
144            [],
145        ));
146        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
147        $mutation = $resolver->resolve(
148            "INSERT INTO users (id, name) VALUES (1, 'Alice') ON CONFLICT (id) DO UPDATE SET name = upper(users.name)",
149            QueryKind::WRITE_SIMULATED
150        );
151
152        self::assertInstanceOf(UpsertMutation::class, $mutation);
153    }
154
155    public function testResolveUpdate(): void
156    {
157        $store = new ShadowStore();
158        $store->ensure('users');
159        $resolver = new SqliteMutationResolver($store, new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
160        $mutation = $resolver->resolve("UPDATE users SET name = 'Bob' WHERE id = 1", QueryKind::WRITE_SIMULATED);
161
162        self::assertInstanceOf(UpdateMutation::class, $mutation);
163    }
164
165    public function testResolveDelete(): void
166    {
167        $registry = new TableDefinitionRegistry();
168        $registry->register('users', new TableDefinition(
169            ['id', 'name', 'email'],
170            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
171            ['id'],
172            ['id', 'name'],
173            [],
174        ));
175        $store = new ShadowStore();
176        $store->ensure('users');
177        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
178        $mutation = $resolver->resolve('DELETE FROM users WHERE id = 1', QueryKind::WRITE_SIMULATED);
179
180        self::assertInstanceOf(DeleteMutation::class, $mutation);
181    }
182
183    public function testResolveDeleteUnknownTableThrows(): void
184    {
185        $store = new ShadowStore();
186        $resolver = new SqliteMutationResolver($store, new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
187
188        $this->expectException(UnknownSchemaException::class);
189        $resolver->resolve('DELETE FROM unknown_table WHERE id = 1', QueryKind::WRITE_SIMULATED);
190    }
191
192    public function testResolveCreateTable(): void
193    {
194        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
195        $mutation = $resolver->resolve('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)', QueryKind::DDL_SIMULATED);
196
197        self::assertInstanceOf(CreateTableMutation::class, $mutation);
198    }
199
200    public function testResolveCreateTableAlreadyExistsThrows(): void
201    {
202        $registry = new TableDefinitionRegistry();
203        $registry->register('users', new TableDefinition(
204            ['id', 'name', 'email'],
205            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
206            ['id'],
207            ['id', 'name'],
208            [],
209        ));
210        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
211
212        $this->expectException(UnsupportedSqlException::class);
213        $resolver->resolve('CREATE TABLE users (id INTEGER PRIMARY KEY)', QueryKind::DDL_SIMULATED);
214    }
215
216    public function testResolveCreateTableIfNotExists(): void
217    {
218        $registry = new TableDefinitionRegistry();
219        $registry->register('users', new TableDefinition(
220            ['id', 'name', 'email'],
221            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
222            ['id'],
223            ['id', 'name'],
224            [],
225        ));
226        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
227        $mutation = $resolver->resolve('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY)', QueryKind::DDL_SIMULATED);
228
229        self::assertInstanceOf(CreateTableMutation::class, $mutation);
230    }
231
232    public function testResolveDropTable(): void
233    {
234        $registry = new TableDefinitionRegistry();
235        $registry->register('users', new TableDefinition(
236            ['id', 'name', 'email'],
237            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
238            ['id'],
239            ['id', 'name'],
240            [],
241        ));
242        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
243        $mutation = $resolver->resolve('DROP TABLE users', QueryKind::DDL_SIMULATED);
244
245        self::assertInstanceOf(DropTableMutation::class, $mutation);
246    }
247
248    public function testResolveDropTableUnknownThrows(): void
249    {
250        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
251
252        $this->expectException(UnknownSchemaException::class);
253        $resolver->resolve('DROP TABLE unknown_table', QueryKind::DDL_SIMULATED);
254    }
255
256    public function testResolveDropTableIfExists(): void
257    {
258        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
259        $mutation = $resolver->resolve('DROP TABLE IF EXISTS unknown_table', QueryKind::DDL_SIMULATED);
260
261        self::assertInstanceOf(DropTableMutation::class, $mutation);
262    }
263
264    public function testResolveAlterTableAddColumn(): void
265    {
266        $registry = new TableDefinitionRegistry();
267        $registry->register('users', new TableDefinition(
268            ['id', 'name', 'email'],
269            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
270            ['id'],
271            ['id', 'name'],
272            [],
273        ));
274        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
275        $mutation = $resolver->resolve('ALTER TABLE users ADD COLUMN phone TEXT', QueryKind::DDL_SIMULATED);
276
277        self::assertInstanceOf(AlterTableMutation::class, $mutation);
278    }
279
280    public function testAlterAddGeneratedColumnPreservesExistingAndAddedExpressions(): void
281    {
282        $store = new ShadowStore();
283        $registry = new TableDefinitionRegistry();
284        $registry->register('metrics', new TableDefinition(
285            ['source', 'doubled'],
286            ['source' => 'INTEGER', 'doubled' => 'INTEGER'],
287            [],
288            [],
289            [],
290            [],
291            [],
292            [],
293            ['doubled' => '(source * 2)'],
294        ));
295        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
296        $mutation = $resolver->resolve(
297            'ALTER TABLE metrics ADD COLUMN tripled INTEGER GENERATED ALWAYS AS (source * 3) STORED',
298            QueryKind::DDL_SIMULATED,
299        );
300
301        self::assertInstanceOf(AlterTableMutation::class, $mutation);
302        $mutation->apply($store, []);
303        self::assertSame([
304            'doubled' => '(source * 2)',
305            'tripled' => '(source * 3)',
306        ], $registry->get('metrics')?->generatedExpressions);
307    }
308
309    public function testAlterColumnMutationsPreserveRenameAndRemoveForeignKeys(): void
310    {
311        $store = new ShadowStore();
312        $registry = new TableDefinitionRegistry();
313        $registry->register('children', new TableDefinition(
314            ['id', 'old_parent'],
315            ['id' => 'INTEGER', 'old_parent' => 'INTEGER'],
316            ['id'],
317            [],
318            [],
319            foreignKeys: ['fk_parent' => new ForeignKeyDefinition(['old_parent'], 'parents', ['id'])],
320        ));
321        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
322        $rename = $resolver->resolve(
323            'ALTER TABLE children RENAME COLUMN old_parent TO parent_id',
324            QueryKind::DDL_SIMULATED,
325        );
326
327        self::assertInstanceOf(AlterTableMutation::class, $rename);
328        $rename->apply($store, []);
329        $definition = $registry->get('children');
330        self::assertNotNull($definition);
331        self::assertSame(['parent_id'], $definition->foreignKeys['fk_parent']->columns);
332
333        $add = $resolver->resolve('ALTER TABLE children ADD COLUMN label TEXT', QueryKind::DDL_SIMULATED);
334        self::assertInstanceOf(AlterTableMutation::class, $add);
335        $add->apply($store, []);
336        $definition = $registry->get('children');
337        self::assertNotNull($definition);
338        self::assertArrayHasKey('fk_parent', $definition->foreignKeys);
339
340        $drop = $resolver->resolve('ALTER TABLE children DROP COLUMN parent_id', QueryKind::DDL_SIMULATED);
341        self::assertInstanceOf(AlterTableMutation::class, $drop);
342        $drop->apply($store, []);
343        $definition = $registry->get('children');
344        self::assertNotNull($definition);
345        self::assertSame([], $definition->foreignKeys);
346    }
347
348    public function testAlterAddColumnPreservesExistingAndAddedForeignKeys(): void
349    {
350        $store = new ShadowStore();
351        $registry = new TableDefinitionRegistry();
352        $registry->register('children', new TableDefinition(
353            ['parent_id'],
354            ['parent_id' => 'INTEGER'],
355            [],
356            [],
357            [],
358            foreignKeys: ['foreign_0' => new ForeignKeyDefinition(['parent_id'], 'parents', ['id'])],
359        ));
360        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
361        $mutation = $resolver->resolve(
362            'ALTER TABLE children ADD COLUMN owner_id INTEGER REFERENCES owners(id) ON DELETE CASCADE',
363            QueryKind::DDL_SIMULATED,
364        );
365
366        self::assertInstanceOf(AlterTableMutation::class, $mutation);
367        $mutation->apply($store, []);
368        $definition = $registry->get('children');
369        self::assertNotNull($definition);
370        $foreignKeys = $definition->foreignKeys;
371        self::assertCount(2, $foreignKeys);
372        self::assertSame(['foreign_0', 'foreign_0_added'], array_keys($foreignKeys));
373        self::assertSame(['parents', 'owners'], array_map(
374            static fn (ForeignKeyDefinition $foreignKey): string => $foreignKey->referencedTable,
375            array_values($foreignKeys),
376        ));
377    }
378
379    public function testResolveAlterTableDropColumn(): void
380    {
381        $registry = new TableDefinitionRegistry();
382        $registry->register('users', new TableDefinition(
383            ['id', 'name', 'email'],
384            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
385            ['id'],
386            ['id', 'name'],
387            [],
388        ));
389        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
390        $mutation = $resolver->resolve('ALTER TABLE users DROP COLUMN email', QueryKind::DDL_SIMULATED);
391
392        self::assertInstanceOf(AlterTableMutation::class, $mutation);
393    }
394
395    public function testResolveAlterTableRenameColumn(): void
396    {
397        $registry = new TableDefinitionRegistry();
398        $registry->register('users', new TableDefinition(
399            ['id', 'name', 'email'],
400            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
401            ['id'],
402            ['id', 'name'],
403            [],
404        ));
405        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
406        $mutation = $resolver->resolve('ALTER TABLE users RENAME COLUMN name TO full_name', QueryKind::DDL_SIMULATED);
407
408        self::assertInstanceOf(AlterTableMutation::class, $mutation);
409    }
410
411    public function testResolveAlterTableUnknownThrows(): void
412    {
413        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
414
415        $this->expectException(UnknownSchemaException::class);
416        $resolver->resolve('ALTER TABLE unknown_table ADD COLUMN x TEXT', QueryKind::DDL_SIMULATED);
417    }
418
419    public function testResolveSelectReturnsNull(): void
420    {
421        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
422        $mutation = $resolver->resolve('SELECT * FROM users', QueryKind::READ);
423        self::assertNull($mutation);
424    }
425
426    public function testResolveEmptyReturnsNull(): void
427    {
428        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
429        $mutation = $resolver->resolve('', QueryKind::READ);
430        self::assertNull($mutation);
431    }
432
433    public function testResolveUnsupportedReturnsNull(): void
434    {
435        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
436        $mutation = $resolver->resolve('CREATE INDEX idx ON t (col)', QueryKind::READ);
437        self::assertNull($mutation);
438    }
439
440    public function testResolveUpdateWithoutTargetThrows(): void
441    {
442        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
443        $this->expectException(UnsupportedSqlException::class);
444        $resolver->resolve('UPDATE', QueryKind::WRITE_SIMULATED);
445    }
446
447    public function testResolveDeleteWithoutTargetThrows(): void
448    {
449        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
450        $this->expectException(UnsupportedSqlException::class);
451        $resolver->resolve('DELETE', QueryKind::WRITE_SIMULATED);
452    }
453
454    public function testResolveInsertWithoutTargetThrows(): void
455    {
456        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
457        $this->expectException(UnsupportedSqlException::class);
458        $resolver->resolve('INSERT', QueryKind::WRITE_SIMULATED);
459    }
460
461    public function testResolveCreateTableWithoutTargetThrows(): void
462    {
463        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
464        $this->expectException(UnsupportedSqlException::class);
465        $resolver->resolve('CREATE TABLE', QueryKind::DDL_SIMULATED);
466    }
467
468    public function testResolveDropTableWithoutTargetThrows(): void
469    {
470        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
471        $this->expectException(UnsupportedSqlException::class);
472        $resolver->resolve('DROP TABLE', QueryKind::DDL_SIMULATED);
473    }
474
475    public function testResolveAlterTableRenameTable(): void
476    {
477        $registry = new TableDefinitionRegistry();
478        $registry->register('users', new TableDefinition(
479            ['id', 'name'],
480            ['id' => 'INTEGER', 'name' => 'TEXT'],
481            ['id'],
482            ['id'],
483            [],
484        ));
485        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
486        $mutation = $resolver->resolve('ALTER TABLE users RENAME TO people', QueryKind::DDL_SIMULATED);
487        self::assertInstanceOf(AlterTableMutation::class, $mutation);
488    }
489
490    public function testResolveAlterTableUnsupportedOperationThrows(): void
491    {
492        $registry = new TableDefinitionRegistry();
493        $registry->register('users', new TableDefinition(
494            ['id', 'name'],
495            ['id' => 'INTEGER', 'name' => 'TEXT'],
496            ['id'],
497            ['id'],
498            [],
499        ));
500        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
501        $this->expectException(UnsupportedSqlException::class);
502        $resolver->resolve('ALTER TABLE users SOMETHING WEIRD', QueryKind::DDL_SIMULATED);
503    }
504
505    public function testResolveAlterTableAddColumnWithType(): void
506    {
507        $registry = new TableDefinitionRegistry();
508        $registry->register('users', new TableDefinition(
509            ['id', 'name'],
510            ['id' => 'INTEGER', 'name' => 'TEXT'],
511            ['id'],
512            ['id'],
513            [],
514        ));
515        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
516        $mutation = $resolver->resolve('ALTER TABLE users ADD COLUMN age INTEGER', QueryKind::DDL_SIMULATED);
517        self::assertInstanceOf(AlterTableMutation::class, $mutation);
518    }
519
520    public function testResolveAlterTableAddColumnWithoutType(): void
521    {
522        $registry = new TableDefinitionRegistry();
523        $registry->register('users', new TableDefinition(
524            ['id', 'name'],
525            ['id' => 'INTEGER', 'name' => 'TEXT'],
526            ['id'],
527            ['id'],
528            [],
529        ));
530        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
531        $mutation = $resolver->resolve('ALTER TABLE users ADD COLUMN notes', QueryKind::DDL_SIMULATED);
532        self::assertInstanceOf(AlterTableMutation::class, $mutation);
533    }
534
535    public function testResolveAlterTableDropColumnUpdatesSchema(): void
536    {
537        $registry = new TableDefinitionRegistry();
538        $registry->register('users', new TableDefinition(
539            ['id', 'name', 'email'],
540            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
541            ['id'],
542            ['id', 'name'],
543            ['email_UNIQUE' => ['email']],
544        ));
545        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
546        $mutation = $resolver->resolve('ALTER TABLE users DROP COLUMN email', QueryKind::DDL_SIMULATED);
547        self::assertInstanceOf(AlterTableMutation::class, $mutation);
548    }
549
550    public function testResolveAlterTableRenameColumnUpdatesSchema(): void
551    {
552        $registry = new TableDefinitionRegistry();
553        $registry->register('users', new TableDefinition(
554            ['id', 'name'],
555            ['id' => 'INTEGER', 'name' => 'TEXT'],
556            ['id'],
557            ['id', 'name'],
558            ['name_UNIQUE' => ['name']],
559        ));
560        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
561        $mutation = $resolver->resolve('ALTER TABLE users RENAME COLUMN name TO full_name', QueryKind::DDL_SIMULATED);
562        self::assertInstanceOf(AlterTableMutation::class, $mutation);
563    }
564
565    public function testResolveAlterTableWithoutTargetThrows(): void
566    {
567        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
568        $this->expectException(UnsupportedSqlException::class);
569        $resolver->resolve('ALTER TABLE', QueryKind::DDL_SIMULATED);
570    }
571
572    public function testResolveDeleteWithExistingRows(): void
573    {
574        $store = new ShadowStore();
575        $store->ensure('users');
576        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
577        $registry = new TableDefinitionRegistry();
578        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
579        $mutation = $resolver->resolve('DELETE FROM users WHERE id = 1', QueryKind::WRITE_SIMULATED);
580        self::assertInstanceOf(DeleteMutation::class, $mutation);
581    }
582
583    public function testResolveUpdateWithPrimaryKeys(): void
584    {
585        $registry = new TableDefinitionRegistry();
586        $registry->register('users', new TableDefinition(
587            ['id', 'name'],
588            ['id' => 'INTEGER', 'name' => 'TEXT'],
589            ['id'],
590            [],
591            [],
592        ));
593        $store = new ShadowStore();
594        $store->ensure('users');
595        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
596        $mutation = $resolver->resolve("UPDATE users SET name = 'Bob' WHERE id = 1", QueryKind::WRITE_SIMULATED);
597        self::assertInstanceOf(UpdateMutation::class, $mutation);
598    }
599
600    public function testResolveOnConflictWithEmptyUpdatesReturnsInsert(): void
601    {
602        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
603        $mutation = $resolver->resolve(
604            'INSERT INTO t (id) VALUES (1) ON CONFLICT (id) DO NOTHING',
605            QueryKind::WRITE_SIMULATED
606        );
607        self::assertInstanceOf(InsertMutation::class, $mutation);
608    }
609
610    public function testResolveAlterTableAddWithoutColumnKeyword(): void
611    {
612        $registry = new TableDefinitionRegistry();
613        $registry->register('users', new TableDefinition(
614            ['id', 'name'],
615            ['id' => 'INTEGER', 'name' => 'TEXT'],
616            ['id'],
617            ['id'],
618            [],
619        ));
620        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
621        $mutation = $resolver->resolve('ALTER TABLE users ADD phone TEXT', QueryKind::DDL_SIMULATED);
622        self::assertInstanceOf(AlterTableMutation::class, $mutation);
623    }
624
625    public function testResolveAlterTableRenameWithoutColumnKeyword(): void
626    {
627        $registry = new TableDefinitionRegistry();
628        $registry->register('users', new TableDefinition(
629            ['id', 'name'],
630            ['id' => 'INTEGER', 'name' => 'TEXT'],
631            ['id'],
632            ['id'],
633            [],
634        ));
635        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
636        $mutation = $resolver->resolve('ALTER TABLE users RENAME name TO full_name', QueryKind::DDL_SIMULATED);
637        self::assertInstanceOf(AlterTableMutation::class, $mutation);
638    }
639
640    public function testResolveAlterTableAddColumnUnknownTableThrows(): void
641    {
642        $registry = new TableDefinitionRegistry();
643        $registry->register('users', new TableDefinition(
644            ['id'],
645            [],
646            [],
647            [],
648            [],
649        ));
650        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
651        $mutation = $resolver->resolve('ALTER TABLE users ADD COLUMN x', QueryKind::DDL_SIMULATED);
652        self::assertInstanceOf(AlterTableMutation::class, $mutation);
653    }
654
655    public function testResolveAlterTableDropColumnUnknownThrows(): void
656    {
657        $registry = new TableDefinitionRegistry();
658        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
659        $this->expectException(UnknownSchemaException::class);
660        $resolver->resolve('ALTER TABLE missing DROP COLUMN x', QueryKind::DDL_SIMULATED);
661    }
662
663    public function testResolveAlterTableRenameColumnUnknownThrows(): void
664    {
665        $registry = new TableDefinitionRegistry();
666        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
667        $this->expectException(UnknownSchemaException::class);
668        $resolver->resolve('ALTER TABLE missing RENAME COLUMN x TO y', QueryKind::DDL_SIMULATED);
669    }
670
671    public function testResolveCreateTableIfNotExistsLowercase(): void
672    {
673        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
674        $mutation = $resolver->resolve('create table if not exists t (id integer primary key)', QueryKind::DDL_SIMULATED);
675        self::assertInstanceOf(CreateTableMutation::class, $mutation);
676    }
677
678    public function testResolveDropTableIfExistsLowercase(): void
679    {
680        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
681        $mutation = $resolver->resolve('drop table if exists nonexistent', QueryKind::DDL_SIMULATED);
682        self::assertInstanceOf(DropTableMutation::class, $mutation);
683    }
684
685    public function testResolveAlterTableAddColumnLowercase(): void
686    {
687        $registry = new TableDefinitionRegistry();
688        $registry->register('users', new TableDefinition(
689            ['id'],
690            ['id' => 'INTEGER'],
691            ['id'],
692            [],
693            [],
694        ));
695        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
696        $mutation = $resolver->resolve('alter table users add column email text', QueryKind::DDL_SIMULATED);
697        self::assertInstanceOf(AlterTableMutation::class, $mutation);
698    }
699
700    public function testResolveAlterTableDropColumnLowercase(): void
701    {
702        $registry = new TableDefinitionRegistry();
703        $registry->register('users', new TableDefinition(
704            ['id', 'email'],
705            ['id' => 'INTEGER', 'email' => 'TEXT'],
706            ['id'],
707            [],
708            [],
709        ));
710        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
711        $mutation = $resolver->resolve('alter table users drop column email', QueryKind::DDL_SIMULATED);
712        self::assertInstanceOf(AlterTableMutation::class, $mutation);
713    }
714
715    public function testResolveAlterTableRenameToLowercase(): void
716    {
717        $registry = new TableDefinitionRegistry();
718        $registry->register('users', new TableDefinition(
719            ['id'],
720            ['id' => 'INTEGER'],
721            ['id'],
722            [],
723            [],
724        ));
725        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
726        $mutation = $resolver->resolve('alter table users rename to people', QueryKind::DDL_SIMULATED);
727        self::assertInstanceOf(AlterTableMutation::class, $mutation);
728    }
729
730    public function testResolveAlterTableRenameColumnLowercase(): void
731    {
732        $registry = new TableDefinitionRegistry();
733        $registry->register('users', new TableDefinition(
734            ['id', 'name'],
735            ['id' => 'INTEGER', 'name' => 'TEXT'],
736            ['id'],
737            [],
738            [],
739        ));
740        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
741        $mutation = $resolver->resolve('alter table users rename column name to full_name', QueryKind::DDL_SIMULATED);
742        self::assertInstanceOf(AlterTableMutation::class, $mutation);
743    }
744
745    public function testResolveDeleteFullTableWithoutWhere(): void
746    {
747        $registry = new TableDefinitionRegistry();
748        $registry->register('users', new TableDefinition(
749            ['id', 'name'],
750            ['id' => 'INTEGER', 'name' => 'TEXT'],
751            ['id'],
752            [],
753            [],
754        ));
755        $store = new ShadowStore();
756        $store->ensure('users');
757        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
758        $mutation = $resolver->resolve('DELETE FROM users', QueryKind::WRITE_SIMULATED);
759        self::assertInstanceOf(DeleteMutation::class, $mutation);
760    }
761
762    public function testResolveAlterTableAddColumnNoKeyword(): void
763    {
764        $registry = new TableDefinitionRegistry();
765        $registry->register('t', new TableDefinition(
766            ['id'],
767            ['id' => 'INTEGER'],
768            ['id'],
769            [],
770            [],
771        ));
772        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
773        $mutation = $resolver->resolve('ALTER TABLE t ADD email TEXT', QueryKind::DDL_SIMULATED);
774        self::assertInstanceOf(AlterTableMutation::class, $mutation);
775    }
776
777    public function testResolveAlterTableAddColumnWithParenthesizedType(): void
778    {
779        $registry = new TableDefinitionRegistry();
780        $registry->register('t', new TableDefinition(
781            ['id'],
782            ['id' => 'INTEGER'],
783            ['id'],
784            [],
785            [],
786        ));
787        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
788        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN price DECIMAL(10,2)', QueryKind::DDL_SIMULATED);
789        self::assertInstanceOf(AlterTableMutation::class, $mutation);
790    }
791
792    public function testResolveAlterTableAddColumnWithPrimaryKeyword(): void
793    {
794        $registry = new TableDefinitionRegistry();
795        $registry->register('t', new TableDefinition(
796            ['id'],
797            ['id' => 'INTEGER'],
798            ['id'],
799            [],
800            [],
801        ));
802        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
803        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN note PRIMARY KEY', QueryKind::DDL_SIMULATED);
804        self::assertInstanceOf(AlterTableMutation::class, $mutation);
805    }
806
807    public function testResolveAlterTableAddColumnVerifyNewColumns(): void
808    {
809        $registry = new TableDefinitionRegistry();
810        $registry->register('users', new TableDefinition(
811            ['id', 'name'],
812            ['id' => 'INTEGER', 'name' => 'TEXT'],
813            ['id'],
814            ['id'],
815            [],
816        ));
817        $store = new ShadowStore();
818        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
819        $mutation = $resolver->resolve('ALTER TABLE users ADD COLUMN phone TEXT', QueryKind::DDL_SIMULATED);
820        self::assertInstanceOf(AlterTableMutation::class, $mutation);
821        self::assertSame('users', $mutation->tableName());
822        $registry->unregister('users');
823        $mutation->apply($store, []);
824        $def = $registry->get('users');
825        self::assertNotNull($def);
826        self::assertContains('phone', $def->columns);
827        self::assertContains('id', $def->columns);
828        self::assertContains('name', $def->columns);
829        self::assertSame('TEXT', $def->columnTypes['phone']);
830    }
831
832    public function testResolveAlterTableDropColumnVerifyRemovedFromAllLists(): void
833    {
834        $registry = new TableDefinitionRegistry();
835        $registry->register('users', new TableDefinition(
836            ['id', 'name', 'email'],
837            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
838            ['id'],
839            ['id', 'name'],
840            ['email_unique' => ['email']],
841        ));
842        $store = new ShadowStore();
843        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
844        $mutation = $resolver->resolve('ALTER TABLE users DROP COLUMN email', QueryKind::DDL_SIMULATED);
845        self::assertInstanceOf(AlterTableMutation::class, $mutation);
846        self::assertSame('users', $mutation->tableName());
847        $registry->unregister('users');
848        $mutation->apply($store, []);
849        $def = $registry->get('users');
850        self::assertNotNull($def);
851        self::assertNotContains('email', $def->columns);
852        self::assertContains('id', $def->columns);
853        self::assertContains('name', $def->columns);
854        self::assertArrayNotHasKey('email', $def->columnTypes);
855        self::assertNotContains('email', $def->notNullColumns);
856    }
857
858    public function testResolveAlterTableRenameColumnVerifyAllUpdated(): void
859    {
860        $registry = new TableDefinitionRegistry();
861        $registry->register('users', new TableDefinition(
862            ['id', 'name'],
863            ['id' => 'INTEGER', 'name' => 'TEXT'],
864            ['id'],
865            ['id', 'name'],
866            ['name_unique' => ['name']],
867        ));
868        $store = new ShadowStore();
869        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
870        $mutation = $resolver->resolve('ALTER TABLE users RENAME COLUMN name TO full_name', QueryKind::DDL_SIMULATED);
871        self::assertInstanceOf(AlterTableMutation::class, $mutation);
872        self::assertSame('users', $mutation->tableName());
873        $registry->unregister('users');
874        $mutation->apply($store, []);
875        $def = $registry->get('users');
876        self::assertNotNull($def);
877        self::assertContains('full_name', $def->columns);
878        self::assertNotContains('name', $def->columns);
879        self::assertArrayHasKey('full_name', $def->columnTypes);
880        self::assertArrayNotHasKey('name', $def->columnTypes);
881        self::assertSame('TEXT', $def->columnTypes['full_name']);
882        self::assertContains('full_name', $def->notNullColumns);
883        self::assertNotContains('name', $def->notNullColumns);
884    }
885
886    public function testResolveAlterTableRenameColumnPrimaryKeysUpdated(): void
887    {
888        $registry = new TableDefinitionRegistry();
889        $registry->register('t', new TableDefinition(
890            ['id', 'code'],
891            ['id' => 'INTEGER', 'code' => 'TEXT'],
892            ['id', 'code'],
893            [],
894            [],
895        ));
896        $store = new ShadowStore();
897        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
898        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN code TO code_new', QueryKind::DDL_SIMULATED);
899        self::assertInstanceOf(AlterTableMutation::class, $mutation);
900        $registry->unregister('t');
901        $mutation->apply($store, []);
902        $def = $registry->get('t');
903        self::assertNotNull($def);
904        self::assertContains('code_new', $def->primaryKeys);
905        self::assertNotContains('code', $def->primaryKeys);
906    }
907
908    public function testResolveAlterTableRenameColumnUniqueConstraintsUpdated(): void
909    {
910        $registry = new TableDefinitionRegistry();
911        $registry->register('t', new TableDefinition(
912            ['id', 'email'],
913            ['id' => 'INTEGER', 'email' => 'TEXT'],
914            ['id'],
915            [],
916            ['email_uq' => ['email']],
917        ));
918        $store = new ShadowStore();
919        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
920        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN email TO mail', QueryKind::DDL_SIMULATED);
921        self::assertNotNull($mutation);
922        $registry->unregister('t');
923        $mutation->apply($store, []);
924        $def = $registry->get('t');
925        self::assertNotNull($def);
926        self::assertSame(['mail'], $def->uniqueConstraints['email_uq']);
927    }
928
929    public function testResolveAlterTableRenameColumnTypedColumnsUpdated(): void
930    {
931        $registry = new TableDefinitionRegistry();
932        $registry->register('t', new TableDefinition(
933            ['id', 'val'],
934            ['id' => 'INTEGER', 'val' => 'REAL'],
935            ['id'],
936            [],
937            [],
938            ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'val' => new ColumnDeclaration(ColumnTypeFamily::FLOAT, 'REAL')],
939        ));
940        $store = new ShadowStore();
941        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
942        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN val TO value', QueryKind::DDL_SIMULATED);
943        self::assertNotNull($mutation);
944        $registry->unregister('t');
945        $mutation->apply($store, []);
946        $def = $registry->get('t');
947        self::assertNotNull($def);
948        self::assertArrayHasKey('value', $def->typedColumns);
949        self::assertArrayNotHasKey('val', $def->typedColumns);
950    }
951
952    public function testResolveAlterTableDropColumnPreservesPrimaryKeys(): void
953    {
954        $registry = new TableDefinitionRegistry();
955        $registry->register('t', new TableDefinition(
956            ['id', 'name', 'email'],
957            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
958            ['id'],
959            ['id'],
960            [],
961        ));
962        $store = new ShadowStore();
963        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
964        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN email', QueryKind::DDL_SIMULATED);
965        self::assertNotNull($mutation);
966        $registry->unregister('t');
967        $mutation->apply($store, []);
968        $def = $registry->get('t');
969        self::assertNotNull($def);
970        self::assertSame(['id'], $def->primaryKeys);
971    }
972
973    public function testResolveAlterTableDropColumnRemovesPrimaryKeyIfDropped(): void
974    {
975        $registry = new TableDefinitionRegistry();
976        $registry->register('t', new TableDefinition(
977            ['id', 'code'],
978            ['id' => 'INTEGER', 'code' => 'TEXT'],
979            ['id', 'code'],
980            [],
981            [],
982        ));
983        $store = new ShadowStore();
984        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
985        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN code', QueryKind::DDL_SIMULATED);
986        self::assertNotNull($mutation);
987        $registry->unregister('t');
988        $mutation->apply($store, []);
989        $def = $registry->get('t');
990        self::assertNotNull($def);
991        self::assertSame(['id'], $def->primaryKeys);
992        self::assertNotContains('code', $def->primaryKeys);
993    }
994
995    public function testResolveAlterTableDropColumnRemovesUniqueConstraintIfEmpty(): void
996    {
997        $registry = new TableDefinitionRegistry();
998        $registry->register('t', new TableDefinition(
999            ['id', 'email'],
1000            ['id' => 'INTEGER', 'email' => 'TEXT'],
1001            ['id'],
1002            [],
1003            ['email_uq' => ['email']],
1004        ));
1005        $store = new ShadowStore();
1006        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1007        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN email', QueryKind::DDL_SIMULATED);
1008        self::assertNotNull($mutation);
1009        $registry->unregister('t');
1010        $mutation->apply($store, []);
1011        $def = $registry->get('t');
1012        self::assertNotNull($def);
1013        self::assertArrayNotHasKey('email_uq', $def->uniqueConstraints);
1014    }
1015
1016    public function testResolveAlterTableDropColumnPreservesPartialUniqueConstraint(): void
1017    {
1018        $registry = new TableDefinitionRegistry();
1019        $registry->register('t', new TableDefinition(
1020            ['id', 'name', 'email'],
1021            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
1022            ['id'],
1023            [],
1024            ['name_email_uq' => ['name', 'email']],
1025        ));
1026        $store = new ShadowStore();
1027        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1028        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN email', QueryKind::DDL_SIMULATED);
1029        self::assertNotNull($mutation);
1030        $registry->unregister('t');
1031        $mutation->apply($store, []);
1032        $def = $registry->get('t');
1033        self::assertNotNull($def);
1034        self::assertArrayHasKey('name_email_uq', $def->uniqueConstraints);
1035        self::assertSame(['name'], $def->uniqueConstraints['name_email_uq']);
1036    }
1037
1038    public function testResolveAlterTableAddColumnVerifyParenthesizedType(): void
1039    {
1040        $registry = new TableDefinitionRegistry();
1041        $registry->register('t', new TableDefinition(
1042            ['id'],
1043            ['id' => 'INTEGER'],
1044            ['id'],
1045            [],
1046            [],
1047        ));
1048        $store = new ShadowStore();
1049        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1050        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN price DECIMAL(10,2)', QueryKind::DDL_SIMULATED);
1051        self::assertNotNull($mutation);
1052        $registry->unregister('t');
1053        $mutation->apply($store, []);
1054        $def = $registry->get('t');
1055        self::assertNotNull($def);
1056        self::assertContains('price', $def->columns);
1057        self::assertSame('DECIMAL(10,2)', $def->columnTypes['price']);
1058    }
1059
1060    public function testResolveInsertTableName(): void
1061    {
1062        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
1063        $mutation = $resolver->resolve('INSERT INTO orders (id) VALUES (1)', QueryKind::WRITE_SIMULATED);
1064        self::assertInstanceOf(InsertMutation::class, $mutation);
1065        self::assertSame('orders', $mutation->tableName());
1066    }
1067
1068    public function testResolveUpdateWithoutContextThrowsUnknownSchema(): void
1069    {
1070        $store = new ShadowStore();
1071        $registry = new TableDefinitionRegistry();
1072        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1073        $this->expectException(UnknownSchemaException::class);
1074        $resolver->resolve('UPDATE t SET x = 1', QueryKind::WRITE_SIMULATED);
1075    }
1076
1077    public function testResolveDeleteTableName(): void
1078    {
1079        $store = new ShadowStore();
1080        $store->ensure('orders');
1081        $store->set('orders', [['id' => 1]]);
1082        $registry = new TableDefinitionRegistry();
1083        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1084        $mutation = $resolver->resolve('DELETE FROM orders WHERE id = 1', QueryKind::WRITE_SIMULATED);
1085        self::assertInstanceOf(DeleteMutation::class, $mutation);
1086        self::assertSame('orders', $mutation->tableName());
1087    }
1088
1089    public function testResolveDeleteLowercaseSql(): void
1090    {
1091        $registry = new TableDefinitionRegistry();
1092        $registry->register('users', new TableDefinition(
1093            ['id', 'name'],
1094            ['id' => 'INTEGER', 'name' => 'TEXT'],
1095            ['id'],
1096            [],
1097            [],
1098        ));
1099        $store = new ShadowStore();
1100        $store->ensure('users');
1101        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1102        $mutation = $resolver->resolve('delete from users where id = 1', QueryKind::WRITE_SIMULATED);
1103        self::assertInstanceOf(DeleteMutation::class, $mutation);
1104    }
1105
1106    public function testResolveUpsertTableNameAndUpdateColumns(): void
1107    {
1108        $registry = new TableDefinitionRegistry();
1109        $registry->register('users', new TableDefinition(
1110            ['id', 'name'],
1111            ['id' => 'INTEGER', 'name' => 'TEXT'],
1112            ['id'],
1113            [],
1114            [],
1115        ));
1116        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
1117        $mutation = $resolver->resolve(
1118            "INSERT INTO users (id, name) VALUES (1, 'Alice') ON CONFLICT (id) DO UPDATE SET name = excluded.name",
1119            QueryKind::WRITE_SIMULATED
1120        );
1121        self::assertInstanceOf(UpsertMutation::class, $mutation);
1122        self::assertSame('users', $mutation->tableName());
1123    }
1124
1125    public function testResolveReplaceTableName(): void
1126    {
1127        $registry = new TableDefinitionRegistry();
1128        $registry->register('users', new TableDefinition(
1129            ['id', 'name'],
1130            ['id' => 'INTEGER', 'name' => 'TEXT'],
1131            ['id'],
1132            [],
1133            [],
1134        ));
1135        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
1136        $mutation = $resolver->resolve("REPLACE INTO users (id, name) VALUES (1, 'Alice')", QueryKind::WRITE_SIMULATED);
1137        self::assertInstanceOf(ReplaceMutation::class, $mutation);
1138        self::assertSame('users', $mutation->tableName());
1139    }
1140
1141    public function testResolveCreateTableTableName(): void
1142    {
1143        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
1144        $mutation = $resolver->resolve('CREATE TABLE orders (id INTEGER PRIMARY KEY)', QueryKind::DDL_SIMULATED);
1145        self::assertInstanceOf(CreateTableMutation::class, $mutation);
1146        self::assertSame('orders', $mutation->tableName());
1147    }
1148
1149    public function testResolveDropTableTableName(): void
1150    {
1151        $registry = new TableDefinitionRegistry();
1152        $registry->register('users', new TableDefinition(
1153            ['id'],
1154            ['id' => 'INTEGER'],
1155            ['id'],
1156            [],
1157            [],
1158        ));
1159        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
1160        $mutation = $resolver->resolve('DROP TABLE users', QueryKind::DDL_SIMULATED);
1161        self::assertInstanceOf(DropTableMutation::class, $mutation);
1162        self::assertSame('users', $mutation->tableName());
1163    }
1164
1165    public function testResolveInsertIgnoreWithPrimaryKeys(): void
1166    {
1167        $registry = new TableDefinitionRegistry();
1168        $registry->register('users', new TableDefinition(
1169            ['id', 'name'],
1170            ['id' => 'INTEGER', 'name' => 'TEXT'],
1171            ['id'],
1172            [],
1173            [],
1174        ));
1175        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
1176        $mutation = $resolver->resolve("INSERT OR IGNORE INTO users (id, name) VALUES (1, 'x')", QueryKind::WRITE_SIMULATED);
1177        self::assertInstanceOf(InsertMutation::class, $mutation);
1178        self::assertSame('users', $mutation->tableName());
1179    }
1180
1181    public function testResolveAlterTableRenameToDropsOldTable(): void
1182    {
1183        $registry = new TableDefinitionRegistry();
1184        $registry->register('old_t', new TableDefinition(
1185            ['id'],
1186            ['id' => 'INTEGER'],
1187            ['id'],
1188            [],
1189            [],
1190        ));
1191        $store = new ShadowStore();
1192        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1193        $mutation = $resolver->resolve('ALTER TABLE old_t RENAME TO new_t', QueryKind::DDL_SIMULATED);
1194        self::assertInstanceOf(AlterTableMutation::class, $mutation);
1195        self::assertSame('old_t', $mutation->tableName());
1196    }
1197
1198    public function testResolveAlterTableDropColumnTypedColumnsUpdated(): void
1199    {
1200        $registry = new TableDefinitionRegistry();
1201        $registry->register('t', new TableDefinition(
1202            ['id', 'val'],
1203            ['id' => 'INTEGER', 'val' => 'TEXT'],
1204            ['id'],
1205            [],
1206            [],
1207            ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'val' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')],
1208        ));
1209        $store = new ShadowStore();
1210        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1211        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN val', QueryKind::DDL_SIMULATED);
1212        self::assertNotNull($mutation);
1213        $registry->unregister('t');
1214        $mutation->apply($store, []);
1215        $def = $registry->get('t');
1216        self::assertNotNull($def);
1217        self::assertArrayNotHasKey('val', $def->typedColumns);
1218        self::assertArrayHasKey('id', $def->typedColumns);
1219    }
1220
1221    public function testResolveAlterTableAddColumnWithTypedColumns(): void
1222    {
1223        $registry = new TableDefinitionRegistry();
1224        $registry->register('t', new TableDefinition(
1225            ['id'],
1226            ['id' => 'INTEGER'],
1227            ['id'],
1228            [],
1229            [],
1230            ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER')],
1231        ));
1232        $store = new ShadowStore();
1233        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1234        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN val TEXT', QueryKind::DDL_SIMULATED);
1235        self::assertNotNull($mutation);
1236        $registry->unregister('t');
1237        $mutation->apply($store, []);
1238        $def = $registry->get('t');
1239        self::assertNotNull($def);
1240        self::assertArrayHasKey('val', $def->typedColumns);
1241    }
1242
1243    public function testResolveAlterTableDropColumnNotNullUpdated(): void
1244    {
1245        $registry = new TableDefinitionRegistry();
1246        $registry->register('t', new TableDefinition(
1247            ['id', 'name', 'email'],
1248            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
1249            ['id'],
1250            ['id', 'name', 'email'],
1251            [],
1252        ));
1253        $store = new ShadowStore();
1254        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1255        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN name', QueryKind::DDL_SIMULATED);
1256        self::assertNotNull($mutation);
1257        $registry->unregister('t');
1258        $mutation->apply($store, []);
1259        $def = $registry->get('t');
1260        self::assertNotNull($def);
1261        self::assertNotContains('name', $def->notNullColumns);
1262        self::assertContains('id', $def->notNullColumns);
1263        self::assertContains('email', $def->notNullColumns);
1264    }
1265
1266    public function testResolveAlterTableRenameColumnNotNullUpdated(): void
1267    {
1268        $registry = new TableDefinitionRegistry();
1269        $registry->register('t', new TableDefinition(
1270            ['id', 'name'],
1271            ['id' => 'INTEGER', 'name' => 'TEXT'],
1272            ['id'],
1273            ['id', 'name'],
1274            [],
1275        ));
1276        $store = new ShadowStore();
1277        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1278        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN name TO full_name', QueryKind::DDL_SIMULATED);
1279        self::assertNotNull($mutation);
1280        $registry->unregister('t');
1281        $mutation->apply($store, []);
1282        $def = $registry->get('t');
1283        self::assertNotNull($def);
1284        self::assertContains('full_name', $def->notNullColumns);
1285        self::assertNotContains('name', $def->notNullColumns);
1286    }
1287
1288    public function testResolveReturnsNullForUnsupportedClassification(): void
1289    {
1290        $store = new ShadowStore();
1291        $registry = new TableDefinitionRegistry();
1292        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1293        $result = $resolver->resolve('CREATE INDEX idx ON t (a)', QueryKind::DDL_SIMULATED);
1294        self::assertNull($result);
1295    }
1296
1297    public function testResolveUpdateEnsuresShadowStoreWithDefinition(): void
1298    {
1299        $store = new ShadowStore();
1300        $registry = new TableDefinitionRegistry();
1301        $registry->register('users', new TableDefinition(
1302            ['id', 'name'],
1303            ['id' => 'INTEGER', 'name' => 'TEXT'],
1304            ['id'],
1305            [],
1306            [],
1307        ));
1308        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1309        $mutation = $resolver->resolve("UPDATE users SET name = 'Bob' WHERE id = 1", QueryKind::WRITE_SIMULATED);
1310        self::assertNotNull($mutation);
1311        self::assertInstanceOf(UpdateMutation::class, $mutation);
1312        self::assertSame(ShadowTableState::Initialized, $store->state('users'));
1313    }
1314
1315    public function testResolveUpdatePrimaryKeysFromDefinition(): void
1316    {
1317        $store = new ShadowStore();
1318        $registry = new TableDefinitionRegistry();
1319        $registry->register('users', new TableDefinition(
1320            ['id', 'name'],
1321            ['id' => 'INTEGER', 'name' => 'TEXT'],
1322            ['id'],
1323            [],
1324            [],
1325        ));
1326        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1327        $mutation = $resolver->resolve("UPDATE users SET name = 'Bob' WHERE id = 1", QueryKind::WRITE_SIMULATED);
1328        self::assertInstanceOf(UpdateMutation::class, $mutation);
1329    }
1330
1331    public function testResolveUpdateWithoutDefinitionHasEmptyPrimaryKeys(): void
1332    {
1333        $store = new ShadowStore();
1334        $store->ensure('users');
1335        $store->set('users', [['id' => 1, 'name' => 'x']]);
1336        $registry = new TableDefinitionRegistry();
1337        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1338        $mutation = $resolver->resolve("UPDATE users SET name = 'Bob' WHERE id = 1", QueryKind::WRITE_SIMULATED);
1339        self::assertInstanceOf(UpdateMutation::class, $mutation);
1340    }
1341
1342    public function testResolveDeleteEnsuresShadowStore(): void
1343    {
1344        $store = new ShadowStore();
1345        $registry = new TableDefinitionRegistry();
1346        $registry->register('users', new TableDefinition(
1347            ['id', 'name'],
1348            ['id' => 'INTEGER', 'name' => 'TEXT'],
1349            ['id'],
1350            [],
1351            [],
1352        ));
1353        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1354        $mutation = $resolver->resolve('DELETE FROM users WHERE id = 1', QueryKind::WRITE_SIMULATED);
1355        self::assertInstanceOf(DeleteMutation::class, $mutation);
1356        self::assertSame([], $store->get('users'));
1357    }
1358
1359    public function testResolveDeleteWithoutDefinitionAndNoRowsThrows(): void
1360    {
1361        $store = new ShadowStore();
1362        $registry = new TableDefinitionRegistry();
1363        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1364        $this->expectException(UnknownSchemaException::class);
1365        $resolver->resolve('DELETE FROM unknown_table WHERE id = 1', QueryKind::WRITE_SIMULATED);
1366    }
1367
1368    public function testResolveUpsertWithDefinitionPrimaryKeys(): void
1369    {
1370        $store = new ShadowStore();
1371        $registry = new TableDefinitionRegistry();
1372        $registry->register('users', new TableDefinition(
1373            ['id', 'name'],
1374            ['id' => 'INTEGER', 'name' => 'TEXT'],
1375            ['id'],
1376            [],
1377            [],
1378        ));
1379        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1380        $mutation = $resolver->resolve("INSERT INTO users (id, name) VALUES (1, 'a') ON CONFLICT (id) DO UPDATE SET name = excluded.name", QueryKind::WRITE_SIMULATED);
1381        self::assertInstanceOf(UpsertMutation::class, $mutation);
1382    }
1383
1384    public function testResolveInsertIgnoreWithPrimaryKeysReturnsInsertMutation(): void
1385    {
1386        $store = new ShadowStore();
1387        $registry = new TableDefinitionRegistry();
1388        $registry->register('users', new TableDefinition(
1389            ['id', 'name'],
1390            ['id' => 'INTEGER', 'name' => 'TEXT'],
1391            ['id'],
1392            [],
1393            [],
1394        ));
1395        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1396        $mutation = $resolver->resolve("INSERT OR IGNORE INTO users (id, name) VALUES (1, 'a')", QueryKind::WRITE_SIMULATED);
1397        self::assertInstanceOf(InsertMutation::class, $mutation);
1398    }
1399
1400    public function testResolveCreateTableIfNotExistsLowercaseDoesNotThrow(): void
1401    {
1402        $store = new ShadowStore();
1403        $registry = new TableDefinitionRegistry();
1404        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1405        $mutation = $resolver->resolve('CREATE TABLE if not exists t (id INTEGER)', QueryKind::DDL_SIMULATED);
1406        self::assertInstanceOf(CreateTableMutation::class, $mutation);
1407    }
1408
1409    public function testResolveCreateTableAlreadyExistsNoIfNotExistsThrows(): void
1410    {
1411        $store = new ShadowStore();
1412        $registry = new TableDefinitionRegistry();
1413        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1414        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1415        $this->expectException(UnsupportedSqlException::class);
1416        $resolver->resolve('CREATE TABLE t (id INTEGER)', QueryKind::DDL_SIMULATED);
1417    }
1418
1419    public function testResolveAlterTableOnNonExistentTableThrows(): void
1420    {
1421        $store = new ShadowStore();
1422        $registry = new TableDefinitionRegistry();
1423        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1424        $this->expectException(UnknownSchemaException::class);
1425        $resolver->resolve('ALTER TABLE nonexistent ADD COLUMN a INTEGER', QueryKind::DDL_SIMULATED);
1426    }
1427
1428    public function testResolveAlterTableUnsupportedOperationThrowsForUnknownAction(): void
1429    {
1430        $store = new ShadowStore();
1431        $registry = new TableDefinitionRegistry();
1432        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1433        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1434        $this->expectException(UnsupportedSqlException::class);
1435        $resolver->resolve('ALTER TABLE t WHATEVER', QueryKind::DDL_SIMULATED);
1436    }
1437
1438    public function testResolveAlterTableAddColumnWithoutColumnKeyword(): void
1439    {
1440        $store = new ShadowStore();
1441        $registry = new TableDefinitionRegistry();
1442        $registry->register('t', new TableDefinition(
1443            ['id'],
1444            ['id' => 'INTEGER'],
1445            ['id'],
1446            [],
1447            [],
1448        ));
1449        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1450        $mutation = $resolver->resolve('ALTER TABLE t ADD name TEXT', QueryKind::DDL_SIMULATED);
1451        self::assertNotNull($mutation);
1452        $registry->unregister('t');
1453        $mutation->apply($store, []);
1454        $def = $registry->get('t');
1455        self::assertNotNull($def);
1456        self::assertContains('name', $def->columns);
1457    }
1458
1459    public function testResolveAlterTableRenameColumnWithoutColumnKeyword(): void
1460    {
1461        $store = new ShadowStore();
1462        $registry = new TableDefinitionRegistry();
1463        $registry->register('t', new TableDefinition(
1464            ['id', 'old_name'],
1465            ['id' => 'INTEGER', 'old_name' => 'TEXT'],
1466            ['id'],
1467            [],
1468            [],
1469        ));
1470        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1471        $mutation = $resolver->resolve('ALTER TABLE t RENAME old_name TO new_name', QueryKind::DDL_SIMULATED);
1472        self::assertNotNull($mutation);
1473        $registry->unregister('t');
1474        $mutation->apply($store, []);
1475        $def = $registry->get('t');
1476        self::assertNotNull($def);
1477        self::assertContains('new_name', $def->columns);
1478        self::assertNotContains('old_name', $def->columns);
1479    }
1480
1481    public function testResolveAlterTableAddColumnAppliesNewColumnToDefinition(): void
1482    {
1483        $store = new ShadowStore();
1484        $registry = new TableDefinitionRegistry();
1485        $registry->register('t', new TableDefinition(
1486            ['id'],
1487            ['id' => 'INTEGER'],
1488            ['id'],
1489            [],
1490            [],
1491        ));
1492        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1493        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN status TEXT', QueryKind::DDL_SIMULATED);
1494        self::assertNotNull($mutation);
1495        $registry->unregister('t');
1496        $mutation->apply($store, []);
1497        $def = $registry->get('t');
1498        self::assertNotNull($def);
1499        self::assertContains('status', $def->columns);
1500    }
1501
1502    public function testResolveAlterTableAddColumnAppliesParenthesizedType(): void
1503    {
1504        $store = new ShadowStore();
1505        $registry = new TableDefinitionRegistry();
1506        $registry->register('t', new TableDefinition(
1507            ['id'],
1508            ['id' => 'INTEGER'],
1509            ['id'],
1510            [],
1511            [],
1512        ));
1513        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1514        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN price DECIMAL(10,2)', QueryKind::DDL_SIMULATED);
1515        self::assertNotNull($mutation);
1516        $registry->unregister('t');
1517        $mutation->apply($store, []);
1518        $def = $registry->get('t');
1519        self::assertNotNull($def);
1520        self::assertContains('price', $def->columns);
1521        self::assertSame('DECIMAL(10,2)', $def->columnTypes['price']);
1522    }
1523
1524    public function testResolveDeleteWithOnlyTableNameNoWhereNoTrailing(): void
1525    {
1526        $store = new ShadowStore();
1527        $registry = new TableDefinitionRegistry();
1528        $registry->register('users', new TableDefinition(
1529            ['id'],
1530            ['id' => 'INTEGER'],
1531            ['id'],
1532            [],
1533            [],
1534        ));
1535        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1536        $mutation = $resolver->resolve('DELETE FROM users', QueryKind::WRITE_SIMULATED);
1537        self::assertInstanceOf(DeleteMutation::class, $mutation);
1538    }
1539
1540    public function testResolveDropTableIfExistsOnNonExistentTableDoesNotThrow(): void
1541    {
1542        $store = new ShadowStore();
1543        $registry = new TableDefinitionRegistry();
1544        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1545        $mutation = $resolver->resolve('DROP TABLE IF EXISTS nonexistent', QueryKind::DDL_SIMULATED);
1546        self::assertInstanceOf(DropTableMutation::class, $mutation);
1547    }
1548
1549    public function testResolveDropTableNonExistentWithoutIfExistsThrows(): void
1550    {
1551        $store = new ShadowStore();
1552        $registry = new TableDefinitionRegistry();
1553        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1554        $this->expectException(UnknownSchemaException::class);
1555        $resolver->resolve('DROP TABLE nonexistent', QueryKind::DDL_SIMULATED);
1556    }
1557
1558    public function testResolveAlterDropColumnRemovesFromColumns(): void
1559    {
1560        $store = new ShadowStore();
1561        $registry = new TableDefinitionRegistry();
1562        $registry->register('t', new TableDefinition(
1563            ['id', 'name', 'email'],
1564            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
1565            ['id'],
1566            ['id', 'name'],
1567            ['unique_email' => ['email']],
1568        ));
1569        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1570        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN email', QueryKind::DDL_SIMULATED);
1571        self::assertNotNull($mutation);
1572        $registry->unregister('t');
1573        $mutation->apply($store, []);
1574        $def = $registry->get('t');
1575        self::assertNotNull($def);
1576        self::assertSame(['id', 'name'], $def->columns);
1577        self::assertArrayNotHasKey('email', $def->columnTypes);
1578        self::assertNotContains('email', $def->notNullColumns);
1579    }
1580
1581    public function testResolveAlterDropColumnRemovesFromPrimaryKeys(): void
1582    {
1583        $store = new ShadowStore();
1584        $registry = new TableDefinitionRegistry();
1585        $registry->register('t', new TableDefinition(
1586            ['id', 'name'],
1587            ['id' => 'INTEGER', 'name' => 'TEXT'],
1588            ['id', 'name'],
1589            [],
1590            [],
1591        ));
1592        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1593        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN name', QueryKind::DDL_SIMULATED);
1594        self::assertNotNull($mutation);
1595        $registry->unregister('t');
1596        $mutation->apply($store, []);
1597        $def = $registry->get('t');
1598        self::assertNotNull($def);
1599        self::assertSame(['id'], $def->primaryKeys);
1600    }
1601
1602    public function testResolveAlterDropColumnRemovesFromUniqueConstraints(): void
1603    {
1604        $store = new ShadowStore();
1605        $registry = new TableDefinitionRegistry();
1606        $registry->register('t', new TableDefinition(
1607            ['id', 'email', 'name'],
1608            ['id' => 'INTEGER', 'email' => 'TEXT', 'name' => 'TEXT'],
1609            ['id'],
1610            [],
1611            ['unique_email' => ['email'], 'unique_name' => ['name']],
1612        ));
1613        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1614        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN email', QueryKind::DDL_SIMULATED);
1615        self::assertNotNull($mutation);
1616        $registry->unregister('t');
1617        $mutation->apply($store, []);
1618        $def = $registry->get('t');
1619        self::assertNotNull($def);
1620        self::assertArrayNotHasKey('unique_email', $def->uniqueConstraints);
1621        self::assertArrayHasKey('unique_name', $def->uniqueConstraints);
1622    }
1623
1624    public function testResolveAlterRenameColumnRenamesInColumns(): void
1625    {
1626        $store = new ShadowStore();
1627        $registry = new TableDefinitionRegistry();
1628        $registry->register('t', new TableDefinition(
1629            ['id', 'old_col'],
1630            ['id' => 'INTEGER', 'old_col' => 'TEXT'],
1631            ['id'],
1632            ['old_col'],
1633            ['unique_old' => ['old_col']],
1634        ));
1635        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1636        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN old_col TO new_col', QueryKind::DDL_SIMULATED);
1637        self::assertNotNull($mutation);
1638        $registry->unregister('t');
1639        $mutation->apply($store, []);
1640        $def = $registry->get('t');
1641        self::assertNotNull($def);
1642        self::assertContains('new_col', $def->columns);
1643        self::assertNotContains('old_col', $def->columns);
1644        self::assertArrayHasKey('new_col', $def->columnTypes);
1645        self::assertArrayNotHasKey('old_col', $def->columnTypes);
1646        self::assertContains('new_col', $def->notNullColumns);
1647    }
1648
1649    public function testResolveAlterRenameColumnRenamesInPrimaryKeys(): void
1650    {
1651        $store = new ShadowStore();
1652        $registry = new TableDefinitionRegistry();
1653        $registry->register('t', new TableDefinition(
1654            ['old_pk', 'val'],
1655            ['old_pk' => 'INTEGER', 'val' => 'TEXT'],
1656            ['old_pk'],
1657            [],
1658            [],
1659        ));
1660        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1661        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN old_pk TO new_pk', QueryKind::DDL_SIMULATED);
1662        self::assertNotNull($mutation);
1663        $registry->unregister('t');
1664        $mutation->apply($store, []);
1665        $def = $registry->get('t');
1666        self::assertNotNull($def);
1667        self::assertContains('new_pk', $def->primaryKeys);
1668        self::assertNotContains('old_pk', $def->primaryKeys);
1669    }
1670
1671    public function testResolveAlterRenameColumnRenamesInUniqueConstraints(): void
1672    {
1673        $store = new ShadowStore();
1674        $registry = new TableDefinitionRegistry();
1675        $registry->register('t', new TableDefinition(
1676            ['id', 'old_name'],
1677            ['id' => 'INTEGER', 'old_name' => 'TEXT'],
1678            ['id'],
1679            [],
1680            ['uq_name' => ['old_name']],
1681        ));
1682        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1683        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN old_name TO new_name', QueryKind::DDL_SIMULATED);
1684        self::assertNotNull($mutation);
1685        $registry->unregister('t');
1686        $mutation->apply($store, []);
1687        $def = $registry->get('t');
1688        self::assertNotNull($def);
1689        self::assertContains('new_name', $def->uniqueConstraints['uq_name']);
1690        self::assertNotContains('old_name', $def->uniqueConstraints['uq_name']);
1691    }
1692
1693    public function testResolveAlterRenameTableProducesDropMutation(): void
1694    {
1695        $store = new ShadowStore();
1696        $registry = new TableDefinitionRegistry();
1697        $registry->register('old_t', new TableDefinition(
1698            ['id'],
1699            ['id' => 'INTEGER'],
1700            ['id'],
1701            [],
1702            [],
1703        ));
1704        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1705        $mutation = $resolver->resolve('ALTER TABLE old_t RENAME TO new_t', QueryKind::DDL_SIMULATED);
1706        self::assertInstanceOf(AlterTableMutation::class, $mutation);
1707    }
1708
1709    public function testResolveDeleteWithStrippedCommentsMatchesRegex(): void
1710    {
1711        $store = new ShadowStore();
1712        $registry = new TableDefinitionRegistry();
1713        $registry->register('users', new TableDefinition(
1714            ['id'],
1715            ['id' => 'INTEGER'],
1716            ['id'],
1717            [],
1718            [],
1719        ));
1720        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1721        $mutation = $resolver->resolve('DELETE FROM users', QueryKind::WRITE_SIMULATED);
1722        self::assertInstanceOf(DeleteMutation::class, $mutation);
1723    }
1724
1725    public function testResolveInsertOnConflictDoNothingIsIgnore(): void
1726    {
1727        $store = new ShadowStore();
1728        $store->set('users', [['id' => 1, 'name' => 'existing']]);
1729        $registry = new TableDefinitionRegistry();
1730        $registry->register('users', new TableDefinition(
1731            ['id', 'name'],
1732            ['id' => 'INTEGER', 'name' => 'TEXT'],
1733            ['id'],
1734            [],
1735            [],
1736        ));
1737        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1738        $mutation = $resolver->resolve("INSERT INTO users (id, name) VALUES (1, 'a') ON CONFLICT DO NOTHING", QueryKind::WRITE_SIMULATED);
1739        self::assertInstanceOf(InsertMutation::class, $mutation);
1740        $mutation->apply($store, [['id' => 1, 'name' => 'ignored']]);
1741        self::assertSame([['id' => 1, 'name' => 'existing']], $store->get('users'));
1742    }
1743
1744    public function testResolveAlterAddColumnTypeDefaultKeywordExcluded(): void
1745    {
1746        $store = new ShadowStore();
1747        $registry = new TableDefinitionRegistry();
1748        $registry->register('t', new TableDefinition(
1749            ['id'],
1750            ['id' => 'INTEGER'],
1751            ['id'],
1752            [],
1753            [],
1754        ));
1755        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1756        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN active DEFAULT 1', QueryKind::DDL_SIMULATED);
1757        self::assertNotNull($mutation);
1758        $registry->unregister('t');
1759        $mutation->apply($store, []);
1760        $def = $registry->get('t');
1761        self::assertNotNull($def);
1762        self::assertContains('active', $def->columns);
1763        self::assertArrayNotHasKey('active', $def->columnTypes);
1764    }
1765
1766    public function testResolveAlterAddColumnTypeNotKeywordExcluded(): void
1767    {
1768        $store = new ShadowStore();
1769        $registry = new TableDefinitionRegistry();
1770        $registry->register('t', new TableDefinition(
1771            ['id'],
1772            ['id' => 'INTEGER'],
1773            ['id'],
1774            [],
1775            [],
1776        ));
1777        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1778        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN active NOT NULL', QueryKind::DDL_SIMULATED);
1779        self::assertNotNull($mutation);
1780        $registry->unregister('t');
1781        $mutation->apply($store, []);
1782        $def = $registry->get('t');
1783        self::assertNotNull($def);
1784        self::assertContains('active', $def->columns);
1785        self::assertArrayNotHasKey('active', $def->columnTypes);
1786    }
1787
1788    public function testResolveAlterAddColumnWithTextType(): void
1789    {
1790        $store = new ShadowStore();
1791        $registry = new TableDefinitionRegistry();
1792        $registry->register('t', new TableDefinition(
1793            ['id'],
1794            ['id' => 'INTEGER'],
1795            ['id'],
1796            [],
1797            [],
1798        ));
1799        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1800        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN name TEXT', QueryKind::DDL_SIMULATED);
1801        self::assertNotNull($mutation);
1802        $registry->unregister('t');
1803        $mutation->apply($store, []);
1804        $def = $registry->get('t');
1805        self::assertNotNull($def);
1806        self::assertSame('TEXT', $def->columnTypes['name']);
1807    }
1808
1809    public function testResolveAlterAddColumnWithNoRest(): void
1810    {
1811        $store = new ShadowStore();
1812        $registry = new TableDefinitionRegistry();
1813        $registry->register('t', new TableDefinition(
1814            ['id'],
1815            ['id' => 'INTEGER'],
1816            ['id'],
1817            [],
1818            [],
1819        ));
1820        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1821        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN val', QueryKind::DDL_SIMULATED);
1822        self::assertNotNull($mutation);
1823        $registry->unregister('t');
1824        $mutation->apply($store, []);
1825        $def = $registry->get('t');
1826        self::assertNotNull($def);
1827        self::assertContains('val', $def->columns);
1828        self::assertArrayNotHasKey('val', $def->columnTypes);
1829    }
1830
1831    public function testResolveUpdateWithoutTargetContextThrowsUnknownSchema(): void
1832    {
1833        $store = new ShadowStore();
1834        $registry = new TableDefinitionRegistry();
1835        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1836        $this->expectException(UnknownSchemaException::class);
1837        $resolver->resolve('UPDATE users SET x = 1', QueryKind::WRITE_SIMULATED);
1838    }
1839
1840    public function testResolveInsertIsReplaceReturnsPrimaryKeys(): void
1841    {
1842        $store = new ShadowStore();
1843        $registry = new TableDefinitionRegistry();
1844        $registry->register('users', new TableDefinition(
1845            ['id', 'name'],
1846            ['id' => 'INTEGER', 'name' => 'TEXT'],
1847            ['id'],
1848            [],
1849            [],
1850        ));
1851        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1852        $mutation = $resolver->resolve("REPLACE INTO users (id, name) VALUES (1, 'a')", QueryKind::WRITE_SIMULATED);
1853        self::assertInstanceOf(ReplaceMutation::class, $mutation);
1854        self::assertSame('users', $mutation->tableName());
1855    }
1856
1857    public function testResolveCreateTableIfNotExistsExistingTableNoError(): void
1858    {
1859        $store = new ShadowStore();
1860        $registry = new TableDefinitionRegistry();
1861        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1862        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1863        $mutation = $resolver->resolve('CREATE TABLE IF NOT EXISTS t (id INTEGER)', QueryKind::DDL_SIMULATED);
1864        self::assertInstanceOf(CreateTableMutation::class, $mutation);
1865    }
1866
1867    public function testResolveDropTableExistingReturnsDropMutation(): void
1868    {
1869        $store = new ShadowStore();
1870        $registry = new TableDefinitionRegistry();
1871        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1872        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1873        $mutation = $resolver->resolve('DROP TABLE t', QueryKind::DDL_SIMULATED);
1874        self::assertInstanceOf(DropTableMutation::class, $mutation);
1875        self::assertSame('t', $mutation->tableName());
1876    }
1877
1878    public function testResolveAlterTableWithAddWithoutColumnKeywordDetected(): void
1879    {
1880        $store = new ShadowStore();
1881        $registry = new TableDefinitionRegistry();
1882        $registry->register('t', new TableDefinition(
1883            ['id'],
1884            ['id' => 'INTEGER'],
1885            ['id'],
1886            [],
1887            [],
1888        ));
1889        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1890        $mutation = $resolver->resolve('ALTER TABLE t ADD email TEXT', QueryKind::DDL_SIMULATED);
1891        self::assertNotNull($mutation);
1892        $registry->unregister('t');
1893        $mutation->apply($store, []);
1894        $def = $registry->get('t');
1895        self::assertNotNull($def);
1896        self::assertContains('email', $def->columns);
1897        self::assertSame('TEXT', $def->columnTypes['email']);
1898    }
1899
1900    public function testResolveReturnsNullForUnclassifiableStatement(): void
1901    {
1902        $store = new ShadowStore();
1903        $registry = new TableDefinitionRegistry();
1904        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1905        self::assertNull($resolver->resolve('PRAGMA table_info(users)', QueryKind::READ));
1906    }
1907
1908    public function testResolveDeleteWithCommentsMatchesRegex(): void
1909    {
1910        $store = new ShadowStore();
1911        $registry = new TableDefinitionRegistry();
1912        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1913        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1914        $mutation = $resolver->resolve('/* comment */ DELETE FROM users WHERE id = 1', QueryKind::WRITE_SIMULATED);
1915        self::assertInstanceOf(DeleteMutation::class, $mutation);
1916    }
1917
1918    public function testResolveDeleteCaseInsensitiveRegex(): void
1919    {
1920        $store = new ShadowStore();
1921        $registry = new TableDefinitionRegistry();
1922        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1923        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1924        $mutation = $resolver->resolve('delete from users where id = 1', QueryKind::WRITE_SIMULATED);
1925        self::assertInstanceOf(DeleteMutation::class, $mutation);
1926    }
1927
1928    public function testResolveUpsertWithDefinitionReturnsPrimaryKeys(): void
1929    {
1930        $store = new ShadowStore();
1931        $registry = new TableDefinitionRegistry();
1932        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1933        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1934        $mutation = $resolver->resolve("INSERT INTO t (id, name) VALUES (1, 'a') ON CONFLICT (id) DO UPDATE SET name = excluded.name", QueryKind::WRITE_SIMULATED);
1935        self::assertInstanceOf(UpsertMutation::class, $mutation);
1936    }
1937
1938    public function testResolveUpsertWithoutDefinitionReturnsEmptyPrimaryKeys(): void
1939    {
1940        $store = new ShadowStore();
1941        $registry = new TableDefinitionRegistry();
1942        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1943        $mutation = $resolver->resolve("INSERT INTO t (id, name) VALUES (1, 'a') ON CONFLICT (id) DO UPDATE SET name = excluded.name", QueryKind::WRITE_SIMULATED);
1944        self::assertInstanceOf(UpsertMutation::class, $mutation);
1945    }
1946
1947    public function testResolveInsertIgnoreWithDefinitionReturnsPrimaryKeys(): void
1948    {
1949        $store = new ShadowStore();
1950        $registry = new TableDefinitionRegistry();
1951        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1952        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1953        $mutation = $resolver->resolve("INSERT OR IGNORE INTO t (id, name) VALUES (1, 'a')", QueryKind::WRITE_SIMULATED);
1954        self::assertInstanceOf(InsertMutation::class, $mutation);
1955    }
1956
1957    public function testResolveInsertIgnoreWithoutDefinitionReturnsEmptyPrimaryKeys(): void
1958    {
1959        $store = new ShadowStore();
1960        $registry = new TableDefinitionRegistry();
1961        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1962        $mutation = $resolver->resolve("INSERT OR IGNORE INTO t (id, name) VALUES (1, 'a')", QueryKind::WRITE_SIMULATED);
1963        self::assertInstanceOf(InsertMutation::class, $mutation);
1964    }
1965
1966    public function testResolveCreateTableCaseInsensitiveIfNotExists(): void
1967    {
1968        $store = new ShadowStore();
1969        $registry = new TableDefinitionRegistry();
1970        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1971        $mutation = $resolver->resolve('create table if not exists t (id integer primary key)', QueryKind::DDL_SIMULATED);
1972        self::assertInstanceOf(CreateTableMutation::class, $mutation);
1973    }
1974
1975    public function testResolveAlterTableCaseInsensitiveAddColumn(): void
1976    {
1977        $store = new ShadowStore();
1978        $registry = new TableDefinitionRegistry();
1979        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1980        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1981        $mutation = $resolver->resolve('alter table t add column name text', QueryKind::DDL_SIMULATED);
1982        self::assertNotNull($mutation);
1983        $registry->unregister('t');
1984        $mutation->apply($store, []);
1985        $def = $registry->get('t');
1986        self::assertNotNull($def);
1987        self::assertContains('name', $def->columns);
1988    }
1989
1990    public function testResolveAlterTableCaseInsensitiveDropColumn(): void
1991    {
1992        $store = new ShadowStore();
1993        $registry = new TableDefinitionRegistry();
1994        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], ['id'], ['idx_name' => ['name']]));
1995        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
1996        $mutation = $resolver->resolve('alter table t drop column name', QueryKind::DDL_SIMULATED);
1997        self::assertNotNull($mutation);
1998        $registry->unregister('t');
1999        $mutation->apply($store, []);
2000        $def = $registry->get('t');
2001        self::assertNotNull($def);
2002        self::assertNotContains('name', $def->columns);
2003    }
2004
2005    public function testResolveAlterTableCaseInsensitiveRenameColumn(): void
2006    {
2007        $store = new ShadowStore();
2008        $registry = new TableDefinitionRegistry();
2009        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], ['name'], ['idx_name' => ['name']]));
2010        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2011        $mutation = $resolver->resolve('alter table t rename column name to full_name', QueryKind::DDL_SIMULATED);
2012        self::assertNotNull($mutation);
2013        $registry->unregister('t');
2014        $mutation->apply($store, []);
2015        $def = $registry->get('t');
2016        self::assertNotNull($def);
2017        self::assertContains('full_name', $def->columns);
2018        self::assertNotContains('name', $def->columns);
2019    }
2020
2021    public function testResolveAlterRenameColumnRenamesInTypedColumns(): void
2022    {
2023        $store = new ShadowStore();
2024        $registry = new TableDefinitionRegistry();
2025        $registry->register('t', new TableDefinition(
2026            ['id', 'name'],
2027            ['id' => 'INTEGER', 'name' => 'TEXT'],
2028            ['id'],
2029            [],
2030            [],
2031        ));
2032        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2033        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN name TO full_name', QueryKind::DDL_SIMULATED);
2034        self::assertNotNull($mutation);
2035        $registry->unregister('t');
2036        $mutation->apply($store, []);
2037        $def = $registry->get('t');
2038        self::assertNotNull($def);
2039        self::assertArrayHasKey('full_name', $def->columnTypes);
2040        self::assertArrayNotHasKey('name', $def->columnTypes);
2041    }
2042
2043    public function testResolveAlterTableCaseInsensitiveRenameTo(): void
2044    {
2045        $store = new ShadowStore();
2046        $registry = new TableDefinitionRegistry();
2047        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2048        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2049        $mutation = $resolver->resolve('alter table t rename to t2', QueryKind::DDL_SIMULATED);
2050        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2051    }
2052
2053    public function testResolveAlterRenameWithoutColumnKeyword(): void
2054    {
2055        $store = new ShadowStore();
2056        $registry = new TableDefinitionRegistry();
2057        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
2058        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2059        $mutation = $resolver->resolve('ALTER TABLE t RENAME name TO full_name', QueryKind::DDL_SIMULATED);
2060        self::assertNotNull($mutation);
2061        $registry->unregister('t');
2062        $mutation->apply($store, []);
2063        $def = $registry->get('t');
2064        self::assertNotNull($def);
2065        self::assertContains('full_name', $def->columns);
2066    }
2067
2068    public function testResolveAlterAddColumnWithTrimmedRest(): void
2069    {
2070        $store = new ShadowStore();
2071        $registry = new TableDefinitionRegistry();
2072        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2073        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2074        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN name  TEXT ', QueryKind::DDL_SIMULATED);
2075        self::assertNotNull($mutation);
2076        $registry->unregister('t');
2077        $mutation->apply($store, []);
2078        $def = $registry->get('t');
2079        self::assertNotNull($def);
2080        self::assertSame('TEXT', $def->columnTypes['name']);
2081    }
2082
2083    public function testResolveAlterAddColumnNonTypeKeywordNotStored(): void
2084    {
2085        $store = new ShadowStore();
2086        $registry = new TableDefinitionRegistry();
2087        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2088        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2089        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN name PRIMARY KEY', QueryKind::DDL_SIMULATED);
2090        self::assertNotNull($mutation);
2091        $registry->unregister('t');
2092        $mutation->apply($store, []);
2093        $def = $registry->get('t');
2094        self::assertNotNull($def);
2095        self::assertContains('name', $def->columns);
2096        self::assertArrayNotHasKey('name', $def->columnTypes);
2097    }
2098
2099    public function testResolveAlterAddColumnWithParensInType(): void
2100    {
2101        $store = new ShadowStore();
2102        $registry = new TableDefinitionRegistry();
2103        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2104        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2105        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN amount DECIMAL(10,2)', QueryKind::DDL_SIMULATED);
2106        self::assertNotNull($mutation);
2107        $registry->unregister('t');
2108        $mutation->apply($store, []);
2109        $def = $registry->get('t');
2110        self::assertNotNull($def);
2111        self::assertSame('DECIMAL(10,2)', $def->columnTypes['amount']);
2112    }
2113
2114    public function testResolveAlterDropColumnRemovesTypedColumns(): void
2115    {
2116        $store = new ShadowStore();
2117        $registry = new TableDefinitionRegistry();
2118        $registry->register('t', new TableDefinition(
2119            ['id', 'name'],
2120            ['id' => 'INTEGER', 'name' => 'TEXT'],
2121            ['id'],
2122            ['id'],
2123            [],
2124        ));
2125        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2126        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN name', QueryKind::DDL_SIMULATED);
2127        self::assertNotNull($mutation);
2128        $registry->unregister('t');
2129        $mutation->apply($store, []);
2130        $def = $registry->get('t');
2131        self::assertNotNull($def);
2132        self::assertArrayNotHasKey('name', $def->columnTypes);
2133    }
2134
2135    public function testResolveAlterDropColumnPreservesRemainingUniqueConstraints(): void
2136    {
2137        $store = new ShadowStore();
2138        $registry = new TableDefinitionRegistry();
2139        $registry->register('t', new TableDefinition(
2140            ['id', 'name', 'email'],
2141            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
2142            ['id'],
2143            [],
2144            ['idx_name' => ['name'], 'idx_email' => ['email']],
2145        ));
2146        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2147        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN name', QueryKind::DDL_SIMULATED);
2148        self::assertNotNull($mutation);
2149        $registry->unregister('t');
2150        $mutation->apply($store, []);
2151        $def = $registry->get('t');
2152        self::assertNotNull($def);
2153        self::assertArrayHasKey('idx_email', $def->uniqueConstraints);
2154        self::assertArrayNotHasKey('idx_name', $def->uniqueConstraints);
2155    }
2156
2157    public function testResolveAlterDropColumnFiltersMultiColumnConstraint(): void
2158    {
2159        $store = new ShadowStore();
2160        $registry = new TableDefinitionRegistry();
2161        $registry->register('t', new TableDefinition(
2162            ['id', 'name', 'email'],
2163            ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'],
2164            ['id'],
2165            [],
2166            ['idx_combo' => ['name', 'email']],
2167        ));
2168        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2169        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN name', QueryKind::DDL_SIMULATED);
2170        self::assertNotNull($mutation);
2171        $registry->unregister('t');
2172        $mutation->apply($store, []);
2173        $def = $registry->get('t');
2174        self::assertNotNull($def);
2175        self::assertArrayHasKey('idx_combo', $def->uniqueConstraints);
2176        self::assertSame(['email'], $def->uniqueConstraints['idx_combo']);
2177    }
2178
2179    public function testResolveAlterRenameColumnUpdatesUniqueConstraints(): void
2180    {
2181        $store = new ShadowStore();
2182        $registry = new TableDefinitionRegistry();
2183        $registry->register('t', new TableDefinition(
2184            ['id', 'name'],
2185            ['id' => 'INTEGER', 'name' => 'TEXT'],
2186            ['id'],
2187            [],
2188            ['idx_name' => ['name']],
2189        ));
2190        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2191        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN name TO full_name', QueryKind::DDL_SIMULATED);
2192        self::assertNotNull($mutation);
2193        $registry->unregister('t');
2194        $mutation->apply($store, []);
2195        $def = $registry->get('t');
2196        self::assertNotNull($def);
2197        self::assertSame(['full_name'], $def->uniqueConstraints['idx_name']);
2198    }
2199
2200    public function testResolveAlterRenameColumnUpdatesPrimaryKeys(): void
2201    {
2202        $store = new ShadowStore();
2203        $registry = new TableDefinitionRegistry();
2204        $registry->register('t', new TableDefinition(
2205            ['id', 'name'],
2206            ['id' => 'INTEGER', 'name' => 'TEXT'],
2207            ['id', 'name'],
2208            [],
2209            [],
2210        ));
2211        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2212        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN name TO full_name', QueryKind::DDL_SIMULATED);
2213        self::assertNotNull($mutation);
2214        $registry->unregister('t');
2215        $mutation->apply($store, []);
2216        $def = $registry->get('t');
2217        self::assertNotNull($def);
2218        self::assertContains('full_name', $def->primaryKeys);
2219        self::assertNotContains('name', $def->primaryKeys);
2220    }
2221
2222    public function testResolveAlterRenameColumnUpdatesNotNullColumns(): void
2223    {
2224        $store = new ShadowStore();
2225        $registry = new TableDefinitionRegistry();
2226        $registry->register('t', new TableDefinition(
2227            ['id', 'name'],
2228            ['id' => 'INTEGER', 'name' => 'TEXT'],
2229            ['id'],
2230            ['name'],
2231            [],
2232        ));
2233        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2234        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN name TO full_name', QueryKind::DDL_SIMULATED);
2235        self::assertNotNull($mutation);
2236        $registry->unregister('t');
2237        $mutation->apply($store, []);
2238        $def = $registry->get('t');
2239        self::assertNotNull($def);
2240        self::assertContains('full_name', $def->notNullColumns);
2241        self::assertNotContains('name', $def->notNullColumns);
2242    }
2243
2244    public function testResolveDropTableCaseInsensitiveIfExists(): void
2245    {
2246        $store = new ShadowStore();
2247        $registry = new TableDefinitionRegistry();
2248        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2249        $mutation = $resolver->resolve('drop table if exists nonexistent', QueryKind::DDL_SIMULATED);
2250        self::assertInstanceOf(DropTableMutation::class, $mutation);
2251    }
2252
2253    public function testResolveUpdateWithDefinitionReturnsPrimaryKeys(): void
2254    {
2255        $store = new ShadowStore();
2256        $registry = new TableDefinitionRegistry();
2257        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
2258        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2259        $mutation = $resolver->resolve("UPDATE t SET name = 'Bob' WHERE id = 1", QueryKind::WRITE_SIMULATED);
2260        self::assertInstanceOf(UpdateMutation::class, $mutation);
2261    }
2262
2263    public function testResolveUpdateWithoutDefinitionReturnsEmptyPrimaryKeys(): void
2264    {
2265        $store = new ShadowStore();
2266        $registry = new TableDefinitionRegistry();
2267        $store->ensure('t');
2268        $store->set('t', [['id' => 1, 'name' => 'Alice']]);
2269        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2270        $mutation = $resolver->resolve("UPDATE t SET name = 'Bob' WHERE id = 1", QueryKind::WRITE_SIMULATED);
2271        self::assertInstanceOf(UpdateMutation::class, $mutation);
2272    }
2273
2274    public function testResolveDeleteWithDefinitionReturnsPrimaryKeys(): void
2275    {
2276        $store = new ShadowStore();
2277        $registry = new TableDefinitionRegistry();
2278        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
2279        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2280        $mutation = $resolver->resolve('DELETE FROM t WHERE id = 1', QueryKind::WRITE_SIMULATED);
2281        self::assertInstanceOf(DeleteMutation::class, $mutation);
2282    }
2283
2284    public function testResolveDeleteWithoutDefinitionButWithShadowRows(): void
2285    {
2286        $store = new ShadowStore();
2287        $registry = new TableDefinitionRegistry();
2288        $store->ensure('t');
2289        $store->set('t', [['id' => 1]]);
2290        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2291        $mutation = $resolver->resolve('DELETE FROM t WHERE id = 1', QueryKind::WRITE_SIMULATED);
2292        self::assertInstanceOf(DeleteMutation::class, $mutation);
2293    }
2294
2295    public function testResolveOnConflictDoNothingIsNotUpsert(): void
2296    {
2297        $store = new ShadowStore();
2298        $registry = new TableDefinitionRegistry();
2299        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2300        $mutation = $resolver->resolve('INSERT INTO t (id) VALUES (1) ON CONFLICT DO NOTHING', QueryKind::WRITE_SIMULATED);
2301        self::assertInstanceOf(InsertMutation::class, $mutation);
2302    }
2303
2304    public function testResolveAlterAddColumnWithEmptyRestNoType(): void
2305    {
2306        $store = new ShadowStore();
2307        $registry = new TableDefinitionRegistry();
2308        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2309        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2310        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN name', QueryKind::DDL_SIMULATED);
2311        self::assertNotNull($mutation);
2312        $registry->unregister('t');
2313        $mutation->apply($store, []);
2314        $def = $registry->get('t');
2315        self::assertNotNull($def);
2316        self::assertContains('name', $def->columns);
2317        self::assertArrayNotHasKey('name', $def->columnTypes);
2318    }
2319
2320    public function testResolveAlterAddColumnUpperCasesTypeName(): void
2321    {
2322        $store = new ShadowStore();
2323        $registry = new TableDefinitionRegistry();
2324        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2325        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2326        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN name text', QueryKind::DDL_SIMULATED);
2327        self::assertNotNull($mutation);
2328        $registry->unregister('t');
2329        $mutation->apply($store, []);
2330        $def = $registry->get('t');
2331        self::assertNotNull($def);
2332        self::assertSame('TEXT', $def->columnTypes['name']);
2333    }
2334
2335    public function testResolveAlterAddWithoutColumnKeywordLowercase(): void
2336    {
2337        $registry = new TableDefinitionRegistry();
2338        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2339        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
2340        $mutation = $resolver->resolve('alter table t add email text', QueryKind::DDL_SIMULATED);
2341        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2342    }
2343
2344    public function testResolveCreateTableIfNotExistsExistingTableLowercase(): void
2345    {
2346        $registry = new TableDefinitionRegistry();
2347        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2348        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
2349        $mutation = $resolver->resolve('create table if not exists t (id integer primary key)', QueryKind::DDL_SIMULATED);
2350        self::assertInstanceOf(CreateTableMutation::class, $mutation);
2351    }
2352
2353    public function testResolveAlterRenameColumnLowercaseRegex(): void
2354    {
2355        $registry = new TableDefinitionRegistry();
2356        $registry->register('t', new TableDefinition(
2357            ['id', 'name'],
2358            ['id' => 'INTEGER', 'name' => 'TEXT'],
2359            ['id'],
2360            [],
2361            [],
2362        ));
2363        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
2364        $mutation = $resolver->resolve('alter table t rename column name to fullname', QueryKind::DDL_SIMULATED);
2365        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2366        $registry->unregister('t');
2367        $store = new ShadowStore();
2368        $mutation->apply($store, []);
2369        $def = $registry->get('t');
2370        self::assertNotNull($def);
2371        self::assertContains('fullname', $def->columns);
2372        self::assertNotContains('name', $def->columns);
2373    }
2374
2375    public function testResolveAlterRenameWithoutColumnKeywordLowercase(): void
2376    {
2377        $registry = new TableDefinitionRegistry();
2378        $registry->register('t', new TableDefinition(
2379            ['id', 'name'],
2380            ['id' => 'INTEGER', 'name' => 'TEXT'],
2381            ['id'],
2382            [],
2383            [],
2384        ));
2385        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
2386        $mutation = $resolver->resolve('alter table t rename name to fullname', QueryKind::DDL_SIMULATED);
2387        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2388    }
2389
2390    public function testResolveAlterDropColumnLowercaseRegex(): void
2391    {
2392        $registry = new TableDefinitionRegistry();
2393        $registry->register('t', new TableDefinition(
2394            ['id', 'name'],
2395            ['id' => 'INTEGER', 'name' => 'TEXT'],
2396            ['id'],
2397            [],
2398            [],
2399        ));
2400        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
2401        $mutation = $resolver->resolve('alter table t drop column name', QueryKind::DDL_SIMULATED);
2402        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2403        $registry->unregister('t');
2404        $store = new ShadowStore();
2405        $mutation->apply($store, []);
2406        $def = $registry->get('t');
2407        self::assertNotNull($def);
2408        self::assertNotContains('name', $def->columns);
2409    }
2410
2411    public function testResolveAlterAddColumnLowercaseTypeExtraction(): void
2412    {
2413        $registry = new TableDefinitionRegistry();
2414        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2415        $store = new ShadowStore();
2416        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2417        $mutation = $resolver->resolve('alter table t add column age integer', QueryKind::DDL_SIMULATED);
2418        self::assertNotNull($mutation);
2419        $registry->unregister('t');
2420        $mutation->apply($store, []);
2421        $def = $registry->get('t');
2422        self::assertNotNull($def);
2423        self::assertContains('age', $def->columns);
2424        self::assertSame('INTEGER', $def->columnTypes['age']);
2425    }
2426
2427    public function testResolveAlterRenameToLowercaseRegex(): void
2428    {
2429        $registry = new TableDefinitionRegistry();
2430        $registry->register('t', new TableDefinition(
2431            ['id'],
2432            ['id' => 'INTEGER'],
2433            ['id'],
2434            [],
2435            [],
2436        ));
2437        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
2438        $mutation = $resolver->resolve('alter table t rename to t2', QueryKind::DDL_SIMULATED);
2439        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2440    }
2441
2442    public function testResolveDropTableIfExistsLowercaseRegex(): void
2443    {
2444        $registry = new TableDefinitionRegistry();
2445        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
2446        $mutation = $resolver->resolve('drop table if exists nonexistent', QueryKind::DDL_SIMULATED);
2447        self::assertInstanceOf(DropTableMutation::class, $mutation);
2448    }
2449
2450    public function testResolveDeleteFullTableLowercaseMatchesRegex(): void
2451    {
2452        $registry = new TableDefinitionRegistry();
2453        $registry->register('t', new TableDefinition(
2454            ['id'],
2455            ['id' => 'INTEGER'],
2456            ['id'],
2457            [],
2458            [],
2459        ));
2460        $store = new ShadowStore();
2461        $store->ensure('t');
2462        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2463        $mutation = $resolver->resolve('delete from t', QueryKind::WRITE_SIMULATED);
2464        self::assertInstanceOf(DeleteMutation::class, $mutation);
2465    }
2466
2467    public function testResolveAlterAddQuotedColumnLowercaseAdd(): void
2468    {
2469        $registry = new TableDefinitionRegistry();
2470        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2471        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
2472        $mutation = $resolver->resolve('alter table t add "email" text', QueryKind::DDL_SIMULATED);
2473        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2474    }
2475
2476    public function testResolveCreateTableIfNotExistsWithoutExistingTable(): void
2477    {
2478        $resolver = new SqliteMutationResolver(new ShadowStore(), new TableDefinitionRegistry(), new SqliteSchemaParser(), new SqliteParser());
2479        $mutation = $resolver->resolve('CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY)', QueryKind::DDL_SIMULATED);
2480        self::assertInstanceOf(CreateTableMutation::class, $mutation);
2481        self::assertSame('t', $mutation->tableName());
2482    }
2483
2484    public function testResolveCreateTableWithoutIfNotExistsAlreadyExistsThrows(): void
2485    {
2486        $registry = new TableDefinitionRegistry();
2487        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2488        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, new SqliteSchemaParser(), new SqliteParser());
2489        $this->expectException(UnsupportedSqlException::class);
2490        $resolver->resolve('CREATE TABLE t (id INTEGER PRIMARY KEY)', QueryKind::DDL_SIMULATED);
2491    }
2492
2493    public function testResolveAlterAddColumnMultiWordTypeLowercase(): void
2494    {
2495        $registry = new TableDefinitionRegistry();
2496        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2497        $store = new ShadowStore();
2498        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2499        $mutation = $resolver->resolve('alter table t add column descr varying character(100)', QueryKind::DDL_SIMULATED);
2500        self::assertNotNull($mutation);
2501        $registry->unregister('t');
2502        $mutation->apply($store, []);
2503        $def = $registry->get('t');
2504        self::assertNotNull($def);
2505        self::assertContains('descr', $def->columns);
2506        self::assertSame('VARYING CHARACTER(100)', $def->columnTypes['descr']);
2507    }
2508
2509    public function testResolveAlterAddColumnIfNotExistsDoesNotThrow(): void
2510    {
2511        $registry = new TableDefinitionRegistry();
2512        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2513        $store = new ShadowStore();
2514        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2515        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN email TEXT', QueryKind::DDL_SIMULATED);
2516        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2517        $mutation->apply($store, []);
2518        self::assertTrue($registry->has('t'));
2519    }
2520
2521    public function testResolveAlterDropColumnIfNotExistsDoesNotThrow(): void
2522    {
2523        $registry = new TableDefinitionRegistry();
2524        $registry->register('t', new TableDefinition(
2525            ['id', 'name'],
2526            ['id' => 'INTEGER', 'name' => 'TEXT'],
2527            ['id'],
2528            [],
2529            [],
2530        ));
2531        $store = new ShadowStore();
2532        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2533        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN name', QueryKind::DDL_SIMULATED);
2534        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2535        $mutation->apply($store, []);
2536        self::assertTrue($registry->has('t'));
2537    }
2538
2539    public function testResolveAlterRenameTableIfExistsDoesNotThrow(): void
2540    {
2541        $registry = new TableDefinitionRegistry();
2542        $registry->register('t', new TableDefinition(
2543            ['id'],
2544            ['id' => 'INTEGER'],
2545            ['id'],
2546            [],
2547            [],
2548        ));
2549        $store = new ShadowStore();
2550        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2551        $mutation = $resolver->resolve('ALTER TABLE t RENAME TO t2', QueryKind::DDL_SIMULATED);
2552        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2553        $mutation->apply($store, []);
2554        self::assertFalse($registry->has('t'));
2555    }
2556
2557    public function testResolveAlterRenameColumnIfNotExistsDoesNotThrow(): void
2558    {
2559        $registry = new TableDefinitionRegistry();
2560        $registry->register('t', new TableDefinition(
2561            ['id', 'name'],
2562            ['id' => 'INTEGER', 'name' => 'TEXT'],
2563            ['id'],
2564            [],
2565            [],
2566        ));
2567        $store = new ShadowStore();
2568        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2569        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN name TO fullname', QueryKind::DDL_SIMULATED);
2570        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2571        $mutation->apply($store, []);
2572        self::assertTrue($registry->has('t'));
2573    }
2574
2575    public function testResolveAlterAddColumnMultilineType(): void
2576    {
2577        $registry = new TableDefinitionRegistry();
2578        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2579        $store = new ShadowStore();
2580        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2581        $mutation = $resolver->resolve("ALTER TABLE t ADD COLUMN email\nTEXT", QueryKind::DDL_SIMULATED);
2582        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2583        $registry->unregister('t');
2584        $mutation->apply($store, []);
2585        $def = $registry->get('t');
2586        self::assertNotNull($def);
2587        self::assertContains('email', $def->columns);
2588        self::assertSame('TEXT', $def->columnTypes['email']);
2589    }
2590
2591    public function testResolveAlterDropColumnFiltersByColumn(): void
2592    {
2593        $registry = new TableDefinitionRegistry();
2594        $registry->register('t', new TableDefinition(
2595            ['id', 'name', 'age'],
2596            ['id' => 'INTEGER', 'name' => 'TEXT', 'age' => 'INTEGER'],
2597            ['id'],
2598            ['id', 'name'],
2599            [],
2600        ));
2601        $store = new ShadowStore();
2602        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2603        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN name', QueryKind::DDL_SIMULATED);
2604        self::assertNotNull($mutation);
2605        $registry->unregister('t');
2606        $mutation->apply($store, []);
2607        $def = $registry->get('t');
2608        self::assertNotNull($def);
2609        self::assertSame(['id', 'age'], $def->columns);
2610        self::assertSame(['id'], $def->notNullColumns);
2611    }
2612
2613    public function testResolveUnknownUpdateDoesNotCreateShadowStoreEntry(): void
2614    {
2615        $store = new ShadowStore();
2616        $registry = new TableDefinitionRegistry();
2617        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2618        try {
2619            $resolver->resolve('UPDATE t SET x = 1', QueryKind::WRITE_SIMULATED);
2620            self::fail('Expected an unknown schema exception.');
2621        } catch (UnknownSchemaException) {
2622            self::assertSame([], $store->getAll());
2623        }
2624    }
2625
2626    public function testResolveDeleteEnsuresShadowStoreEntryCreated(): void
2627    {
2628        $store = new ShadowStore();
2629        $registry = new TableDefinitionRegistry();
2630        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2631        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2632        self::assertSame([], $store->getAll());
2633        $resolver->resolve('DELETE FROM t WHERE id = 1', QueryKind::WRITE_SIMULATED);
2634        self::assertArrayHasKey('t', $store->getAll());
2635    }
2636
2637    public function testResolveDeleteWithLeadingWhitespaceLowercase(): void
2638    {
2639        $store = new ShadowStore();
2640        $registry = new TableDefinitionRegistry();
2641        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2642        $store->ensure('users');
2643        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2644        $mutation = $resolver->resolve('  delete from users where id = 1  ', QueryKind::WRITE_SIMULATED);
2645        self::assertInstanceOf(DeleteMutation::class, $mutation);
2646    }
2647
2648    public function testResolveAlterAddColumnDoesNotThrowWhenApplied(): void
2649    {
2650        $registry = new TableDefinitionRegistry();
2651        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2652        $store = new ShadowStore();
2653        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2654        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN val TEXT', QueryKind::DDL_SIMULATED);
2655        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2656        $mutation->apply($store, []);
2657        self::assertTrue($registry->has('t'));
2658    }
2659
2660    public function testResolveAlterDropColumnDoesNotThrowWhenApplied(): void
2661    {
2662        $registry = new TableDefinitionRegistry();
2663        $registry->register('t', new TableDefinition(['id', 'val'], ['id' => 'INTEGER', 'val' => 'TEXT'], ['id'], [], []));
2664        $store = new ShadowStore();
2665        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2666        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN val', QueryKind::DDL_SIMULATED);
2667        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2668        $mutation->apply($store, []);
2669        self::assertTrue($registry->has('t'));
2670    }
2671
2672    public function testResolveAlterRenameTableAppliesAsDrop(): void
2673    {
2674        $registry = new TableDefinitionRegistry();
2675        $registry->register('old_t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2676        $store = new ShadowStore();
2677        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2678        $mutation = $resolver->resolve('ALTER TABLE old_t RENAME TO new_t', QueryKind::DDL_SIMULATED);
2679        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2680        $mutation->apply($store, []);
2681        self::assertFalse($registry->has('old_t'));
2682    }
2683
2684    public function testResolveAlterRenameColumnDoesNotThrowWhenApplied(): void
2685    {
2686        $registry = new TableDefinitionRegistry();
2687        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
2688        $store = new ShadowStore();
2689        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2690        $mutation = $resolver->resolve('ALTER TABLE t RENAME COLUMN name TO full_name', QueryKind::DDL_SIMULATED);
2691        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2692        $mutation->apply($store, []);
2693        self::assertTrue($registry->has('t'));
2694    }
2695
2696    public function testResolveAlterRenameToExtractsCorrectNewName(): void
2697    {
2698        $registry = new TableDefinitionRegistry();
2699        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2700        $store = new ShadowStore();
2701        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2702        $mutation = $resolver->resolve('ALTER TABLE t RENAME TO new_table_name', QueryKind::DDL_SIMULATED);
2703        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2704        self::assertSame('t', $mutation->tableName());
2705    }
2706
2707    public function testResolveAlterAddColumnEmptyParensDoesNotAppendParens(): void
2708    {
2709        $registry = new TableDefinitionRegistry();
2710        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2711        $store = new ShadowStore();
2712        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2713        $mutation = $resolver->resolve('ALTER TABLE t ADD COLUMN val INT()', QueryKind::DDL_SIMULATED);
2714        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2715        $registry->unregister('t');
2716        $mutation->apply($store, []);
2717        $def = $registry->get('t');
2718        self::assertNotNull($def);
2719        self::assertSame('INT', $def->columnTypes['val']);
2720    }
2721
2722    public function testResolveAlterDropColumnReindexesNotNull(): void
2723    {
2724        $registry = new TableDefinitionRegistry();
2725        $registry->register('t', new TableDefinition(
2726            ['id', 'name', 'age'],
2727            ['id' => 'INTEGER', 'name' => 'TEXT', 'age' => 'INTEGER'],
2728            ['id'],
2729            ['id', 'name', 'age'],
2730            [],
2731        ));
2732        $store = new ShadowStore();
2733        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2734        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN name', QueryKind::DDL_SIMULATED);
2735        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2736        $registry->unregister('t');
2737        $mutation->apply($store, []);
2738        $def = $registry->get('t');
2739        self::assertNotNull($def);
2740        self::assertSame([0, 1], array_keys($def->notNullColumns));
2741        self::assertSame([0], array_keys($def->primaryKeys));
2742    }
2743
2744    public function testResolveAlterRenameToDoesNotThrowWhenTableAlreadyGone(): void
2745    {
2746        $registry = new TableDefinitionRegistry();
2747        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2748        $store = new ShadowStore();
2749        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2750        $mutation = $resolver->resolve('ALTER TABLE t RENAME TO new_t', QueryKind::DDL_SIMULATED);
2751        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2752        $registry->unregister('t');
2753        $mutation->apply($store, []);
2754        self::assertFalse($registry->has('t'));
2755    }
2756
2757    public function testResolveAlterDropColumnReindexesPrimaryKeys(): void
2758    {
2759        $registry = new TableDefinitionRegistry();
2760        $registry->register('t', new TableDefinition(
2761            ['a', 'b', 'c'],
2762            ['a' => 'INTEGER', 'b' => 'INTEGER', 'c' => 'INTEGER'],
2763            ['a', 'b', 'c'],
2764            [],
2765            [],
2766        ));
2767        $store = new ShadowStore();
2768        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
2769        $mutation = $resolver->resolve('ALTER TABLE t DROP COLUMN b', QueryKind::DDL_SIMULATED);
2770        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2771        $registry->unregister('t');
2772        $mutation->apply($store, []);
2773        $def = $registry->get('t');
2774        self::assertNotNull($def);
2775        self::assertSame([0, 1], array_keys($def->primaryKeys));
2776        self::assertSame(['a', 'c'], $def->primaryKeys);
2777    }
2778
2779    public function testUpdateDoesNotTreatRowsFromUnknownInsertAsSchema(): void
2780    {
2781        $store = new ShadowStore();
2782        $store->insert('late_table', [['id' => 1, 'name' => 'Alice']]);
2783        $resolver = new SqliteMutationResolver(
2784            $store,
2785            new TableDefinitionRegistry(),
2786            new SqliteSchemaParser(),
2787            new SqliteParser(),
2788        );
2789
2790        $this->expectException(UnknownSchemaException::class);
2791        $resolver->resolve("UPDATE late_table SET name = 'Bob' WHERE id = 1", QueryKind::WRITE_SIMULATED);
2792    }
2793
2794    #[DataProvider('providerRemovedTableMutations')]
2795    public function testRemovedTableRejectsEveryFurtherMutation(string $sql): void
2796    {
2797        $registry = new TableDefinitionRegistry();
2798        $registry->register('records', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2799        $registry->markRemoved('records');
2800        $resolver = new SqliteMutationResolver(
2801            new ShadowStore(),
2802            $registry,
2803            new SqliteSchemaParser(),
2804            new SqliteParser(),
2805        );
2806
2807        $this->expectException(UnsupportedSqlException::class);
2808        $this->expectExceptionMessage('Table was removed from the virtual schema');
2809        $resolver->resolve($sql, QueryKind::WRITE_SIMULATED);
2810    }
2811
2812    /**
2813     * @return Generator<string, array{string}>
2814     */
2815    public static function providerRemovedTableMutations(): Generator
2816    {
2817        yield 'update' => ['UPDATE records SET id = 2'];
2818        yield 'delete' => ['DELETE FROM records'];
2819        yield 'insert' => ['INSERT INTO records VALUES (2)'];
2820        yield 'alter' => ['ALTER TABLE records ADD COLUMN name TEXT'];
2821    }
2822
2823    public function testAlterAddPreservesCompleteDefinitionAndBuildsMigrationProjection(): void
2824    {
2825        $parser = new SqliteSchemaParser();
2826        $existing = $parser->parse(
2827            "CREATE TABLE records (id INTEGER PRIMARY KEY AUTOINCREMENT, legacy TEXT NOT NULL UNIQUE DEFAULT 'old')",
2828        );
2829        self::assertNotNull($existing);
2830        $expected = $parser->parse(
2831            "CREATE TABLE expected (id INTEGER PRIMARY KEY AUTOINCREMENT, legacy TEXT NOT NULL UNIQUE DEFAULT 'old', age INTEGER NOT NULL DEFAULT 7)",
2832        );
2833        self::assertNotNull($expected);
2834        $registry = new TableDefinitionRegistry();
2835        $registry->register('records', $existing);
2836        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, $parser, new SqliteParser());
2837
2838        $mutation = $resolver->resolve(
2839            'ALTER TABLE records ADD COLUMN age INTEGER NOT NULL DEFAULT 7;',
2840            QueryKind::DDL_SIMULATED,
2841        );
2842
2843        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2844        self::assertSame('SELECT "id", "legacy", 7 AS "age" FROM "records"', $mutation->resultSelect());
2845        $mutation->apply(new ShadowStore(), [['id' => 1, 'legacy' => 'old', 'age' => 7]]);
2846        $actual = $registry->get('records');
2847        self::assertNotNull($actual);
2848        self::assertSame($expected->columns, $actual->columns);
2849        self::assertSame($expected->columnTypes, $actual->columnTypes);
2850        self::assertSame($expected->primaryKeys, $actual->primaryKeys);
2851        self::assertSame($expected->notNullColumns, $actual->notNullColumns);
2852        self::assertSame($expected->uniqueConstraints, $actual->uniqueConstraints);
2853        self::assertEquals($expected->typedColumns, $actual->typedColumns);
2854        self::assertSame($expected->columnDefaults, $actual->columnDefaults);
2855        self::assertSame($expected->identityStrategies, $actual->identityStrategies);
2856    }
2857
2858    public function testAlterDropRemovesEveryColumnMetadataEntryAndBuildsMigrationProjection(): void
2859    {
2860        $integer = new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER');
2861        $text = new ColumnDeclaration(ColumnTypeFamily::STRING, 'TEXT');
2862        $definition = new TableDefinition(
2863            ['id', 'legacy', 'kept', 'tail'],
2864            ['id' => 'INTEGER', 'legacy' => 'TEXT', 'kept' => 'TEXT', 'tail' => 'TEXT'],
2865            ['id', 'legacy'],
2866            ['id', 'legacy', 'kept', 'tail'],
2867            [
2868                'id_legacy' => ['id', 'legacy'],
2869                'kept_tail' => ['kept', 'tail'],
2870                'legacy_only' => ['legacy'],
2871            ],
2872            ['id' => $integer, 'legacy' => $text, 'kept' => $text, 'tail' => $text],
2873            ['id' => '1', 'legacy' => "'old'", 'kept' => "'kept'", 'tail' => "'tail'"],
2874            [
2875                'id' => IdentityGenerationStrategy::MaxValue,
2876                'legacy' => IdentityGenerationStrategy::MaxValue,
2877                'kept' => IdentityGenerationStrategy::MaxValue,
2878                'tail' => IdentityGenerationStrategy::MaxValue,
2879            ],
2880        );
2881        $registry = new TableDefinitionRegistry();
2882        $registry->register('records', $definition);
2883        $resolver = new SqliteMutationResolver(
2884            new ShadowStore(),
2885            $registry,
2886            new SqliteSchemaParser(),
2887            new SqliteParser(),
2888        );
2889
2890        $mutation = $resolver->resolve('ALTER TABLE records DROP COLUMN legacy;', QueryKind::DDL_SIMULATED);
2891
2892        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2893        self::assertSame('SELECT "id", "kept", "tail" FROM "records"', $mutation->resultSelect());
2894        $mutation->apply(new ShadowStore(), []);
2895        $actual = $registry->get('records');
2896        self::assertNotNull($actual);
2897        self::assertSame(['id', 'kept', 'tail'], $actual->columns);
2898        self::assertSame(['id' => 'INTEGER', 'kept' => 'TEXT', 'tail' => 'TEXT'], $actual->columnTypes);
2899        self::assertSame(['id'], $actual->primaryKeys);
2900        self::assertSame(['id', 'kept', 'tail'], $actual->notNullColumns);
2901        self::assertSame(['id_legacy' => ['id'], 'kept_tail' => ['kept', 'tail']], $actual->uniqueConstraints);
2902        self::assertSame(['id' => $integer, 'kept' => $text, 'tail' => $text], $actual->typedColumns);
2903        self::assertSame(['id' => '1', 'kept' => "'kept'", 'tail' => "'tail'"], $actual->columnDefaults);
2904        self::assertSame([
2905            'id' => IdentityGenerationStrategy::MaxValue,
2906            'kept' => IdentityGenerationStrategy::MaxValue,
2907            'tail' => IdentityGenerationStrategy::MaxValue,
2908        ], $actual->identityStrategies);
2909    }
2910
2911    public function testAlterRenameColumnMovesEveryMetadataEntryAndBuildsMigrationProjection(): void
2912    {
2913        $text = new ColumnDeclaration(ColumnTypeFamily::STRING, 'TEXT');
2914        $definition = new TableDefinition(
2915            ['id', 'legacy'],
2916            ['id' => 'INTEGER', 'legacy' => 'TEXT'],
2917            ['legacy'],
2918            ['legacy'],
2919            ['legacy_unique' => ['legacy']],
2920            ['legacy' => $text],
2921            ['legacy' => "'old'"],
2922            ['legacy' => IdentityGenerationStrategy::MaxValue],
2923        );
2924        $registry = new TableDefinitionRegistry();
2925        $registry->register('records', $definition);
2926        $resolver = new SqliteMutationResolver(
2927            new ShadowStore(),
2928            $registry,
2929            new SqliteSchemaParser(),
2930            new SqliteParser(),
2931        );
2932
2933        $mutation = $resolver->resolve(
2934            'ALTER TABLE records RENAME COLUMN legacy TO label;',
2935            QueryKind::DDL_SIMULATED,
2936        );
2937
2938        self::assertInstanceOf(AlterTableMutation::class, $mutation);
2939        self::assertSame('SELECT "id", "legacy" AS "label" FROM "records"', $mutation->resultSelect());
2940        $mutation->apply(new ShadowStore(), []);
2941        $actual = $registry->get('records');
2942        self::assertNotNull($actual);
2943        self::assertSame(['id', 'label'], $actual->columns);
2944        self::assertSame(['id' => 'INTEGER', 'label' => 'TEXT'], $actual->columnTypes);
2945        self::assertSame(['label'], $actual->primaryKeys);
2946        self::assertSame(['label'], $actual->notNullColumns);
2947        self::assertSame(['legacy_unique' => ['label']], $actual->uniqueConstraints);
2948        self::assertSame(['label' => $text], $actual->typedColumns);
2949        self::assertSame(['label' => "'old'"], $actual->columnDefaults);
2950        self::assertSame(['label' => IdentityGenerationStrategy::MaxValue], $actual->identityStrategies);
2951    }
2952
2953    public function testAlterRenameColumnRejectsMissingSource(): void
2954    {
2955        $registry = new TableDefinitionRegistry();
2956        $registry->register('records', new TableDefinition(
2957            ['id', 'label'],
2958            ['id' => 'INTEGER', 'label' => 'TEXT'],
2959            ['id'],
2960            [],
2961            [],
2962        ));
2963        $resolver = new SqliteMutationResolver(
2964            new ShadowStore(),
2965            $registry,
2966            new SqliteSchemaParser(),
2967            new SqliteParser(),
2968        );
2969
2970        $this->expectException(ColumnNotFoundException::class);
2971        $resolver->resolve('ALTER TABLE records RENAME COLUMN missing TO title', QueryKind::DDL_SIMULATED);
2972    }
2973
2974    public function testAlterRenameColumnRejectsExistingTarget(): void
2975    {
2976        $registry = new TableDefinitionRegistry();
2977        $registry->register('records', new TableDefinition(
2978            ['id', 'label'],
2979            ['id' => 'INTEGER', 'label' => 'TEXT'],
2980            ['id'],
2981            [],
2982            [],
2983        ));
2984        $resolver = new SqliteMutationResolver(
2985            new ShadowStore(),
2986            $registry,
2987            new SqliteSchemaParser(),
2988            new SqliteParser(),
2989        );
2990
2991        $this->expectException(ColumnAlreadyExistsException::class);
2992        $resolver->resolve('ALTER TABLE records RENAME COLUMN label TO id', QueryKind::DDL_SIMULATED);
2993    }
2994
2995    public function testAlterRenameColumnSupportsCaseOnlyTarget(): void
2996    {
2997        $registry = new TableDefinitionRegistry();
2998        $registry->register('records', new TableDefinition(
2999            ['id', 'label'],
3000            ['id' => 'INTEGER', 'label' => 'TEXT'],
3001            ['id'],
3002            [],
3003            [],
3004        ));
3005        $resolver = new SqliteMutationResolver(
3006            new ShadowStore(),
3007            $registry,
3008            new SqliteSchemaParser(),
3009            new SqliteParser(),
3010        );
3011
3012        $caseOnly = $resolver->resolve(
3013            'ALTER TABLE records RENAME COLUMN label TO LABEL',
3014            QueryKind::DDL_SIMULATED,
3015        );
3016        self::assertInstanceOf(AlterTableMutation::class, $caseOnly);
3017        self::assertSame('SELECT "id", "label" AS "LABEL" FROM "records"', $caseOnly->resultSelect());
3018    }
3019
3020    #[DataProvider('providerMalformedAlterIdentifierClauses')]
3021    public function testAlterIdentifierClausesRejectExtraOrMissingTokens(string $sql): void
3022    {
3023        $registry = new TableDefinitionRegistry();
3024        $registry->register('records', new TableDefinition(
3025            ['id', 'label'],
3026            ['id' => 'INTEGER', 'label' => 'TEXT'],
3027            ['id'],
3028            [],
3029            [],
3030        ));
3031        $resolver = new SqliteMutationResolver(
3032            new ShadowStore(),
3033            $registry,
3034            new SqliteSchemaParser(),
3035            new SqliteParser(),
3036        );
3037
3038        $this->expectException(UnsupportedSqlException::class);
3039        $resolver->resolve($sql, QueryKind::DDL_SIMULATED);
3040    }
3041
3042    /**
3043     * @return Generator<string, array{string}>
3044     */
3045    public static function providerMalformedAlterIdentifierClauses(): Generator
3046    {
3047        yield 'missing operation' => ['ALTER TABLE records'];
3048        yield 'missing add clause' => ['ALTER TABLE records ADD'];
3049        yield 'missing add column definition' => ['ALTER TABLE records ADD COLUMN'];
3050        yield 'missing drop clause' => ['ALTER TABLE records DROP'];
3051        yield 'drop missing column keyword' => ['ALTER TABLE records DROP label'];
3052        yield 'drop missing keyword with extra token' => ['ALTER TABLE records DROP label extra'];
3053        yield 'unknown operation' => ['ALTER TABLE records BROKEN label TO title'];
3054        yield 'rename missing clause' => ['ALTER TABLE records RENAME'];
3055        yield 'drop extra token' => ['ALTER TABLE records DROP COLUMN label extra'];
3056        yield 'rename missing to' => ['ALTER TABLE records RENAME COLUMN label title'];
3057        yield 'rename invalid separator' => ['ALTER TABLE records RENAME COLUMN label nope title'];
3058        yield 'rename invalid source' => ['ALTER TABLE records RENAME (label) TO title'];
3059        yield 'rename mismatched bracket source' => ['ALTER TABLE records RENAME ( label ] TO title'];
3060        yield 'rename missing target' => ['ALTER TABLE records RENAME COLUMN label TO'];
3061        yield 'rename extra token' => ['ALTER TABLE records RENAME COLUMN label TO title extra'];
3062        yield 'rename table extra token' => ['ALTER TABLE records RENAME TO target extra'];
3063    }
3064
3065    public function testAlterAcceptsBracketQuotedColumnIdentifiers(): void
3066    {
3067        $registry = new TableDefinitionRegistry();
3068        $registry->register('records', new TableDefinition(
3069            ['id', 'label'],
3070            ['id' => 'INTEGER', 'label' => 'TEXT'],
3071            ['id'],
3072            [],
3073            [],
3074        ));
3075        $resolver = new SqliteMutationResolver(
3076            new ShadowStore(),
3077            $registry,
3078            new SqliteSchemaParser(),
3079            new SqliteParser(),
3080        );
3081
3082        $mutation = $resolver->resolve(
3083            'ALTER TABLE records RENAME COLUMN [label] TO [title]',
3084            QueryKind::DDL_SIMULATED,
3085        );
3086
3087        self::assertInstanceOf(AlterTableMutation::class, $mutation);
3088        self::assertSame('SELECT "id", "label" AS "title" FROM "records"', $mutation->resultSelect());
3089    }
3090
3091    public function testAlterRenameTableBuildsMigrationProjectionAndMovesDefinition(): void
3092    {
3093        $definition = new TableDefinition(
3094            ['id', 'label'],
3095            ['id' => 'INTEGER', 'label' => 'TEXT'],
3096            ['id'],
3097            [],
3098            [],
3099        );
3100        $registry = new TableDefinitionRegistry();
3101        $registry->register('records', $definition);
3102        $store = new ShadowStore();
3103        $resolver = new SqliteMutationResolver($store, $registry, new SqliteSchemaParser(), new SqliteParser());
3104
3105        $mutation = $resolver->resolve('ALTER TABLE records RENAME TO archived;', QueryKind::DDL_SIMULATED);
3106
3107        self::assertInstanceOf(AlterTableMutation::class, $mutation);
3108        self::assertSame('SELECT "id", "label" FROM "records"', $mutation->resultSelect());
3109        $mutation->apply($store, [['id' => 1, 'label' => 'kept']]);
3110        self::assertSame($definition, $registry->get('archived'));
3111        self::assertTrue($registry->isRemoved('records'));
3112        self::assertSame([['id' => 1, 'label' => 'kept']], $store->get('archived'));
3113    }
3114
3115    public function testAlterAddRejectsMissingParsedColumn(): void
3116    {
3117        $registry = new TableDefinitionRegistry();
3118        $registry->register('records', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
3119        $nullParser = static::createStub(SchemaParser::class);
3120        $nullParser->method('parse')->willReturn(null);
3121        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, $nullParser, new SqliteParser());
3122
3123        $this->expectException(UnsupportedSqlException::class);
3124        $resolver->resolve('ALTER TABLE records ADD COLUMN value TEXT', QueryKind::DDL_SIMULATED);
3125    }
3126
3127    public function testAlterAddRejectsAmbiguousParsedColumn(): void
3128    {
3129        $registry = new TableDefinitionRegistry();
3130        $registry->register('records', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
3131        $emptyParser = static::createStub(SchemaParser::class);
3132        $emptyParser->method('parse')->willReturn(new TableDefinition([], [], [], [], []));
3133        $resolver = new SqliteMutationResolver(new ShadowStore(), $registry, $emptyParser, new SqliteParser());
3134
3135        $this->expectException(UnsupportedSqlException::class);
3136        $resolver->resolve('ALTER TABLE records ADD COLUMN value TEXT', QueryKind::DDL_SIMULATED);
3137    }
3138}
3139