packages/ztd-query-mysql/tests/Unit/Shadow/MySqlMutationResolverTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Shadow;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Exception\UnknownSchemaException;
11use ZtdQuery\Platform\MySql\Rewrite\Transformer\DeleteTransformer;
12use ZtdQuery\Platform\MySql\Rewrite\Transformer\SelectTransformer;
13use ZtdQuery\Platform\MySql\Rewrite\Transformer\UpdateTransformer;
14use ZtdQuery\Platform\MySql\Schema\MySqlSchemaParser;
15use ZtdQuery\Platform\MySql\Shadow\Mutation\Table\AlterTableMutation;
16use ZtdQuery\Platform\MySql\Shadow\MySqlMutationResolver;
17use ZtdQuery\Platform\MySql\Sql\Dml\DmlWhereClauseExtractor;
18use ZtdQuery\Platform\MySql\Sql\Dml\UpdateSourceExtractor;
19use ZtdQuery\Platform\MySql\Sql\MySqlIdentifierQuoter;
20use ZtdQuery\Platform\MySql\Sql\MySqlParser;
21use ZtdQuery\Platform\MySql\Sql\Upsert\MySqlUpsertAssignmentExtractor;
22use ZtdQuery\Platform\MySql\Sql\Value\MySqlCastRenderer;
23use ZtdQuery\Rewrite\QueryKind;
24use ZtdQuery\Schema\TableDefinition;
25use ZtdQuery\Schema\TableDefinitionRegistry;
26use ZtdQuery\Shadow\Mutation\Row\DeleteMutation;
27use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
28use ZtdQuery\Shadow\Mutation\Row\MultiDeleteMutation;
29use ZtdQuery\Shadow\Mutation\Row\MultiUpdateMutation;
30use ZtdQuery\Shadow\Mutation\Row\ReplaceMutation;
31use ZtdQuery\Shadow\Mutation\Row\UpdateMutation;
32use ZtdQuery\Shadow\Mutation\Table\CreateTableAsSelectMutation;
33use ZtdQuery\Shadow\Mutation\Table\CreateTableLikeMutation;
34use ZtdQuery\Shadow\Mutation\Table\CreateTableMutation;
35use ZtdQuery\Shadow\Mutation\Table\DropTableMutation;
36use ZtdQuery\Shadow\Mutation\Table\TruncateMutation;
37use ZtdQuery\Shadow\Mutation\UpsertMutation;
38use ZtdQuery\Shadow\ShadowStore;
39use ZtdQuery\Shadow\ShadowTableState;
40
41#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\ColumnAction::class)]
42#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\ColumnAlteration::class)]
43#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\ColumnDefinitionParser::class)]
44#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\CreateTableRenderer::class)]
45#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\OperationApplier::class)]
46#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\PrimaryKeyAlteration::class)]
47#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\StoredColumns::class)]
48#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\UnsupportedKeyword::class)]
49
50
51
52#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\MySqlPartitionSelectionRewriter::class)]
53#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Partition\MySqlPartitioningParser::class)]
54#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Relation\MySqlSelectRelationParser::class)]
55#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Alter\OptionList::class)]
56#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Dml\AssignmentExpression::class)]
57#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Cte\HeaderParser::class)]
58#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Cte\IdentifierReferences::class)]
59#[UsesClass(\ZtdQuery\Platform\MySql\Sql\OptionalInsertIntoNormalizer::class)]
60#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Relation\ExpressionNames::class)]
61#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Relation\ReferenceReader::class)]
62#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Upsert\AssignmentReader::class)]
63#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Upsert\ExpressionReader::class)]
64#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Upsert\LiteralReader::class)]
65#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Upsert\StringLiteral::class)]
66#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\FullText\ExpressionEditor::class)]
67#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\SelectionReader::class)]
68#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\SourceProjection::class)]
69#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\ConflictPredicate::class)]
70#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\ExpressionBinder::class)]
71#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\MetadataColumns::class)]
72#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\QualifiedColumn::class)]
73#[UsesClass(\ZtdQuery\Platform\MySql\Schema\DefinitionBuilder::class)]
74#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Key\DefinitionReader::class)]
75#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Key\TokenReader::class)]
76#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Partition\PredicateCompiler::class)]
77#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Delete\ResultSelect::class)]
78#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Delete\TargetProjection::class)]
79#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Set\OrderRewriter::class)]
80#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Set\ValueNormalizer::class)]
81#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Shadow\CteRows::class)]
82#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Update\ResultSelect::class)]
83#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Update\TargetProjection::class)]
84#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\CastTypeResolver::class)]
85#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Type\RankEdits::class)]
86#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\ScalarExpression::class)]
87#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\StringCoercion::class)]
88#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Dml\UpdateAssignmentExtractor::class)]
89#[CoversClass(MySqlMutationResolver::class)]
90#[UsesClass(\ZtdQuery\Platform\MySql\Schema\MySqlColumnTypeMapper::class)]
91#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Key\MySqlForeignKeyDefinitionParser::class)]
92#[UsesClass(MySqlParser::class)]
93#[UsesClass(MySqlSchemaParser::class)]
94#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Upsert\MySqlUpsertExpressionParser::class)]
95#[UsesClass(MySqlUpsertAssignmentExtractor::class)]
96#[UsesClass(SelectTransformer::class)]
97#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\FullText\MySqlFullTextSearchRewriter::class)]
98#[UsesClass(UpdateTransformer::class)]
99#[UsesClass(DeleteTransformer::class)]
100#[UsesClass(DmlWhereClauseExtractor::class)]
101#[UsesClass(UpdateSourceExtractor::class)]
102#[UsesClass(MySqlCastRenderer::class)]
103#[UsesClass(MySqlIdentifierQuoter::class)]
104#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\MySqlValueRenderer::class)]
105#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Type\MySqlTypeSemantics::class)]
106#[UsesClass(AlterTableMutation::class)]
107#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Cte\MySqlCteShadowComposer::class)]
108#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\MySqlNativeUpsertProjector::class)]
109#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\GeneratedColumn\MySqlGeneratedColumnProjector::class)]
110#[UsesClass(\ZtdQuery\Platform\MySql\Sql\MySqlLexerProfile::class)]
111#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Row\RowMutationResolver::class)]
112#[CoversClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\TableMutationResolver::class)]
113#[CoversClass(\ZtdQuery\Platform\MySql\Sql\Dml\TargetName::class)]
114final class MySqlMutationResolverTest extends TestCase
115{
116    public function testResolveInsertReturnsInsertMutation(): void
117    {
118        $parser = new MySqlParser();
119        $schemaParser = new MySqlSchemaParser($parser);
120        $shadowStore = new ShadowStore();
121        $registry = new TableDefinitionRegistry();
122
123        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
124        $registry->register('users', $definition);
125        $shadowStore->set('users', [['id' => '1', 'name' => 'Alice']]);
126
127        $selectTransformer = new SelectTransformer();
128        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
129        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
130        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
131
132        $sql = "INSERT INTO users (id, name) VALUES (2, 'Bob')";
133        $statements = $parser->parse($sql);
134        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
135
136        self::assertInstanceOf(InsertMutation::class, $mutation);
137    }
138
139    public function testResolveTruncateReturnsTruncateMutation(): void
140    {
141        $parser = new MySqlParser();
142        $schemaParser = new MySqlSchemaParser($parser);
143        $shadowStore = new ShadowStore();
144        $registry = new TableDefinitionRegistry();
145
146        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
147        $registry->register('users', $definition);
148
149        $selectTransformer = new SelectTransformer();
150        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
151        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
152        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
153
154        $sql = 'TRUNCATE TABLE users';
155        $statements = $parser->parse($sql);
156        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
157
158        self::assertInstanceOf(TruncateMutation::class, $mutation);
159    }
160
161    public function testResolveReturnsNullForUnhandledStatement(): void
162    {
163        $parser = new MySqlParser();
164        $schemaParser = new MySqlSchemaParser($parser);
165        $shadowStore = new ShadowStore();
166        $registry = new TableDefinitionRegistry();
167
168        $selectTransformer = new SelectTransformer();
169        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
170        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
171        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
172
173        $sql = 'SELECT 1';
174        $statements = $parser->parse($sql);
175        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::READ);
176
177        self::assertNull($mutation);
178    }
179
180    public function testResolveInsertIgnoreReturnsInsertMutationWithIgnore(): void
181    {
182        $parser = new MySqlParser();
183        $schemaParser = new MySqlSchemaParser($parser);
184        $shadowStore = new ShadowStore();
185        $registry = new TableDefinitionRegistry();
186
187        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
188        $registry->register('users', $definition);
189
190        $selectTransformer = new SelectTransformer();
191        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
192        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
193        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
194
195        $sql = "INSERT IGNORE INTO users (id, name) VALUES (1, 'Alice')";
196        $statements = $parser->parse($sql);
197        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
198
199        self::assertInstanceOf(InsertMutation::class, $mutation);
200        self::assertSame('users', $mutation->tableName());
201    }
202
203    public function testResolveInsertOnDuplicateKeyReturnsUpsertMutation(): void
204    {
205        $parser = new MySqlParser();
206        $schemaParser = new MySqlSchemaParser($parser);
207        $shadowStore = new ShadowStore();
208        $registry = new TableDefinitionRegistry();
209
210        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
211        $registry->register('users', $definition);
212
213        $selectTransformer = new SelectTransformer();
214        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
215        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
216        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
217
218        $sql = "INSERT INTO users (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE name = CONCAT(name, ' updated')";
219        $statements = $parser->parse($sql);
220        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
221
222        self::assertInstanceOf(UpsertMutation::class, $mutation);
223        self::assertSame('users', $mutation->tableName());
224    }
225
226    public function testResolveRowAliasUpsertAppliesIncomingColumns(): void
227    {
228        $parser = new MySqlParser();
229        $schemaParser = new MySqlSchemaParser($parser);
230        $shadowStore = new ShadowStore();
231        $shadowStore->set('users', [['id' => 1, 'name' => 'old', 'score' => 10]]);
232        $registry = new TableDefinitionRegistry();
233        $registry->register('users', new TableDefinition(
234            ['id', 'name', 'score'],
235            ['id' => 'INT', 'name' => 'VARCHAR(255)', 'score' => 'INT'],
236            ['id'],
237            ['id'],
238            [],
239        ));
240        $selectTransformer = new SelectTransformer();
241        $resolver = new MySqlMutationResolver(
242            $shadowStore,
243            $registry,
244            $schemaParser,
245            new UpdateTransformer($parser, $selectTransformer),
246            new DeleteTransformer($parser, $selectTransformer),
247        );
248        $sql = "INSERT INTO users VALUES (1, 'new', 20) AS incoming ON DUPLICATE KEY UPDATE name = incoming.name, score = incoming.score";
249        $statements = $parser->parse($sql);
250
251        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
252        self::assertInstanceOf(UpsertMutation::class, $mutation);
253        $mutation->apply($shadowStore, [['id' => 1, 'name' => 'new', 'score' => 20]]);
254
255        self::assertSame([['id' => 1, 'name' => 'new', 'score' => 20]], $shadowStore->get('users'));
256    }
257
258    public function testResolveInsertOnDuplicateKeyWithoutRegisteredSchema(): void
259    {
260        $parser = new MySqlParser();
261        $schemaParser = new MySqlSchemaParser($parser);
262        $shadowStore = new ShadowStore();
263        $registry = new TableDefinitionRegistry();
264        $selectTransformer = new SelectTransformer();
265        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
266        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
267        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
268        $sql = "INSERT INTO users (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE name = VALUES(name)";
269        $statements = $parser->parse($sql);
270
271        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
272
273        self::assertInstanceOf(UpsertMutation::class, $mutation);
274    }
275
276    public function testResolveUpdateReturnsUpdateMutation(): void
277    {
278        $parser = new MySqlParser();
279        $schemaParser = new MySqlSchemaParser($parser);
280        $shadowStore = new ShadowStore();
281        $registry = new TableDefinitionRegistry();
282
283        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
284        $registry->register('users', $definition);
285        $shadowStore->ensure('users');
286
287        $selectTransformer = new SelectTransformer();
288        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
289        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
290        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
291
292        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
293        $statements = $parser->parse($sql);
294        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
295
296        self::assertInstanceOf(UpdateMutation::class, $mutation);
297        self::assertSame('users', $mutation->tableName());
298    }
299
300    public function testResolveDeleteReturnsDeleteMutation(): void
301    {
302        $parser = new MySqlParser();
303        $schemaParser = new MySqlSchemaParser($parser);
304        $shadowStore = new ShadowStore();
305        $registry = new TableDefinitionRegistry();
306
307        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
308        $registry->register('users', $definition);
309        $shadowStore->ensure('users');
310
311        $selectTransformer = new SelectTransformer();
312        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
313        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
314        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
315
316        $sql = 'DELETE FROM users WHERE id = 1';
317        $statements = $parser->parse($sql);
318        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
319
320        self::assertInstanceOf(DeleteMutation::class, $mutation);
321        self::assertSame('users', $mutation->tableName());
322    }
323
324    public function testResolveReplaceReturnsReplaceMutation(): void
325    {
326        $parser = new MySqlParser();
327        $schemaParser = new MySqlSchemaParser($parser);
328        $shadowStore = new ShadowStore();
329        $registry = new TableDefinitionRegistry();
330
331        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
332        $registry->register('users', $definition);
333
334        $selectTransformer = new SelectTransformer();
335        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
336        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
337        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
338
339        $sql = "REPLACE INTO users (id, name) VALUES (1, 'Alice')";
340        $statements = $parser->parse($sql);
341        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
342
343        self::assertInstanceOf(ReplaceMutation::class, $mutation);
344        self::assertSame('users', $mutation->tableName());
345    }
346
347    public function testResolveReplaceWithoutRegisteredSchema(): void
348    {
349        $parser = new MySqlParser();
350        $schemaParser = new MySqlSchemaParser($parser);
351        $shadowStore = new ShadowStore();
352        $registry = new TableDefinitionRegistry();
353        $selectTransformer = new SelectTransformer();
354        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
355        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
356        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
357        $sql = "REPLACE INTO users (id, name) VALUES (1, 'Alice')";
358        $statements = $parser->parse($sql);
359
360        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
361
362        self::assertInstanceOf(ReplaceMutation::class, $mutation);
363    }
364
365    public function testResolveUpdateMultiTableReturnsMultiUpdateMutation(): void
366    {
367        $parser = new MySqlParser();
368        $schemaParser = new MySqlSchemaParser($parser);
369        $shadowStore = new ShadowStore();
370        $registry = new TableDefinitionRegistry();
371
372        $definition1 = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
373        $registry->register('users', $definition1);
374        $definition2 = new TableDefinition(['id', 'user_id', 'status'], ['id' => 'INT', 'user_id' => 'INT', 'status' => 'VARCHAR(50)'], ['id'], ['id'], []);
375        $registry->register('orders', $definition2);
376
377        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
378        $shadowStore->set('orders', [['id' => 1, 'user_id' => 1, 'status' => 'pending']]);
379
380        $selectTransformer = new SelectTransformer();
381        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
382        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
383        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
384
385        $sql = "UPDATE users u, orders o SET u.name = 'Updated', o.status = 'done' WHERE u.id = o.user_id";
386        $statements = $parser->parse($sql);
387        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
388
389        self::assertInstanceOf(MultiUpdateMutation::class, $mutation);
390        $mutation->apply($shadowStore, [[
391            '__ztd_multi_0_value_0' => 1,
392            '__ztd_multi_0_value_1' => 'Updated',
393            '__ztd_multi_0_identity_0' => 1,
394            '__ztd_multi_1_value_0' => 1,
395            '__ztd_multi_1_value_1' => 1,
396            '__ztd_multi_1_value_2' => 'done',
397            '__ztd_multi_1_identity_0' => 1,
398        ]]);
399
400        self::assertSame([['id' => 1, 'name' => 'Updated']], $shadowStore->get('users'));
401        self::assertSame(
402            [['id' => 1, 'user_id' => 1, 'status' => 'done']],
403            $shadowStore->get('orders'),
404        );
405    }
406
407    public function testResolveDeleteMultiTableReturnsMultiDeleteMutation(): void
408    {
409        $parser = new MySqlParser();
410        $schemaParser = new MySqlSchemaParser($parser);
411        $shadowStore = new ShadowStore();
412        $registry = new TableDefinitionRegistry();
413
414        $definition1 = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
415        $registry->register('users', $definition1);
416        $definition2 = new TableDefinition(['id', 'user_id'], ['id' => 'INT', 'user_id' => 'INT'], ['id'], ['id'], []);
417        $registry->register('orders', $definition2);
418
419        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
420        $shadowStore->set('orders', [['id' => 9, 'user_id' => 1]]);
421
422        $selectTransformer = new SelectTransformer();
423        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
424        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
425        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
426
427        $sql = 'DELETE u, o FROM users u JOIN orders o ON u.id = o.user_id WHERE u.id = 1';
428        $statements = $parser->parse($sql);
429        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
430
431        self::assertInstanceOf(MultiDeleteMutation::class, $mutation);
432        $mutation->apply($shadowStore, [[
433            '__ztd_multi_0_value_0' => 1,
434            '__ztd_multi_1_value_0' => 9,
435        ]]);
436
437        self::assertSame([], $shadowStore->get('users'));
438        self::assertSame([], $shadowStore->get('orders'));
439    }
440
441    public function testResolveInsertWithOnDuplicateKeyReturnsUpsertMutation(): void
442    {
443        $parser = new MySqlParser();
444        $schemaParser = new MySqlSchemaParser($parser);
445        $shadowStore = new ShadowStore();
446        $registry = new TableDefinitionRegistry();
447
448        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
449        $registry->register('users', $definition);
450
451        $selectTransformer = new SelectTransformer();
452        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
453        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
454        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
455
456        $sql = "INSERT INTO users (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE name = VALUES(name)";
457        $statements = $parser->parse($sql);
458        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
459
460        self::assertInstanceOf(UpsertMutation::class, $mutation);
461        self::assertSame('users', $mutation->tableName());
462    }
463
464    public function testResolveSelectReturnsNull(): void
465    {
466        $parser = new MySqlParser();
467        $schemaParser = new MySqlSchemaParser($parser);
468        $shadowStore = new ShadowStore();
469        $registry = new TableDefinitionRegistry();
470
471        $selectTransformer = new SelectTransformer();
472        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
473        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
474        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
475
476        $sql = 'SELECT * FROM users';
477        $statements = $parser->parse($sql);
478        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::READ);
479
480        self::assertNull($mutation);
481    }
482
483    public function testResolveUpdateWithShadowDataButNoDefinition(): void
484    {
485        $parser = new MySqlParser();
486        $schemaParser = new MySqlSchemaParser($parser);
487        $shadowStore = new ShadowStore();
488        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
489        $registry = new TableDefinitionRegistry();
490
491        $selectTransformer = new SelectTransformer();
492        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
493        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
494        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
495
496        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
497        $statements = $parser->parse($sql);
498        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
499
500        self::assertInstanceOf(UpdateMutation::class, $mutation);
501        self::assertSame('users', $mutation->tableName());
502    }
503
504    public function testResolveDeleteWithShadowDataButNoDefinition(): void
505    {
506        $parser = new MySqlParser();
507        $schemaParser = new MySqlSchemaParser($parser);
508        $shadowStore = new ShadowStore();
509        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
510        $registry = new TableDefinitionRegistry();
511
512        $selectTransformer = new SelectTransformer();
513        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
514        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
515        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
516
517        $sql = 'DELETE FROM users WHERE id = 1';
518        $statements = $parser->parse($sql);
519        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
520
521        self::assertInstanceOf(DeleteMutation::class, $mutation);
522        self::assertSame('users', $mutation->tableName());
523    }
524
525    public function testResolveInsertWithQualifiedOnDuplicateKey(): void
526    {
527        $parser = new MySqlParser();
528        $schemaParser = new MySqlSchemaParser($parser);
529        $shadowStore = new ShadowStore();
530        $registry = new TableDefinitionRegistry();
531
532        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
533        $registry->register('users', $definition);
534
535        $selectTransformer = new SelectTransformer();
536        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
537        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
538        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
539
540        $sql = "INSERT INTO users (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE users.name = VALUES(name)";
541        $statements = $parser->parse($sql);
542        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
543
544        self::assertInstanceOf(UpsertMutation::class, $mutation);
545    }
546
547    public function testResolveCreateTableReturnsCreateTableMutation(): void
548    {
549        $parser = new MySqlParser();
550        $schemaParser = new MySqlSchemaParser($parser);
551        $shadowStore = new ShadowStore();
552        $registry = new TableDefinitionRegistry();
553
554        $selectTransformer = new SelectTransformer();
555        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
556        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
557        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
558
559        $sql = 'CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))';
560        $statements = $parser->parse($sql);
561        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
562
563        self::assertInstanceOf(CreateTableMutation::class, $mutation);
564        self::assertSame('t', $mutation->tableName());
565    }
566
567    public function testResolveDropTableReturnsDropTableMutation(): void
568    {
569        $parser = new MySqlParser();
570        $schemaParser = new MySqlSchemaParser($parser);
571        $shadowStore = new ShadowStore();
572        $registry = new TableDefinitionRegistry();
573
574        $definition = new TableDefinition(['id'], ['id' => 'INT'], ['id'], ['id'], []);
575        $registry->register('users', $definition);
576
577        $selectTransformer = new SelectTransformer();
578        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
579        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
580        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
581
582        $sql = 'DROP TABLE users';
583        $statements = $parser->parse($sql);
584        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
585
586        self::assertInstanceOf(DropTableMutation::class, $mutation);
587        self::assertSame('users', $mutation->tableName());
588    }
589
590    public function testResolveAlterTableReturnsAlterTableMutation(): void
591    {
592        $parser = new MySqlParser();
593        $schemaParser = new MySqlSchemaParser($parser);
594        $shadowStore = new ShadowStore();
595        $registry = new TableDefinitionRegistry();
596
597        $definition = new TableDefinition(['id'], ['id' => 'INT'], ['id'], ['id'], []);
598        $registry->register('t', $definition);
599
600        $selectTransformer = new SelectTransformer();
601        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
602        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
603        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
604
605        $sql = 'ALTER TABLE t ADD COLUMN name VARCHAR(255)';
606        $statements = $parser->parse($sql);
607        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
608
609        self::assertInstanceOf(AlterTableMutation::class, $mutation);
610        self::assertSame('t', $mutation->tableName());
611    }
612
613    public function testResolveCreateTableLikeReturnsCreateTableLikeMutation(): void
614    {
615        $parser = new MySqlParser();
616        $schemaParser = new MySqlSchemaParser($parser);
617        $shadowStore = new ShadowStore();
618        $registry = new TableDefinitionRegistry();
619
620        $definition = new TableDefinition(['id'], ['id' => 'INT'], ['id'], ['id'], []);
621        $registry->register('source', $definition);
622
623        $selectTransformer = new SelectTransformer();
624        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
625        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
626        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
627
628        $sql = 'CREATE TABLE dest LIKE source';
629        $statements = $parser->parse($sql);
630        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
631
632        self::assertInstanceOf(CreateTableLikeMutation::class, $mutation);
633        self::assertSame('dest', $mutation->tableName());
634    }
635
636    public function testResolveCreateTableAsSelectReturnsCreateTableAsSelectMutation(): void
637    {
638        $parser = new MySqlParser();
639        $schemaParser = new MySqlSchemaParser($parser);
640        $shadowStore = new ShadowStore();
641        $registry = new TableDefinitionRegistry();
642
643        $selectTransformer = new SelectTransformer();
644        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
645        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
646        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
647
648        $sql = 'CREATE TABLE dest AS SELECT id, name FROM source';
649        $statements = $parser->parse($sql);
650        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
651
652        self::assertInstanceOf(CreateTableAsSelectMutation::class, $mutation);
653        self::assertSame('dest', $mutation->tableName());
654    }
655
656    public function testUpdateEnsuresTableInShadowStore(): void
657    {
658        $parser = new MySqlParser();
659        $schemaParser = new MySqlSchemaParser($parser);
660        $shadowStore = new ShadowStore();
661        $registry = new TableDefinitionRegistry();
662        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
663        $registry->register('users', $definition);
664
665        $selectTransformer = new SelectTransformer();
666        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
667        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
668        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
669
670        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
671        $statements = $parser->parse($sql);
672        $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
673
674        self::assertSame([], $shadowStore->get('users'));
675    }
676
677    public function testUpdateUsesColumnsFromStoreWhenAvailable(): void
678    {
679        $parser = new MySqlParser();
680        $schemaParser = new MySqlSchemaParser($parser);
681        $shadowStore = new ShadowStore();
682        $shadowStore->set('users', [['id' => '1', 'name' => 'Alice']]);
683        $registry = new TableDefinitionRegistry();
684
685        $selectTransformer = new SelectTransformer();
686        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
687        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
688        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
689
690        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
691        $statements = $parser->parse($sql);
692        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
693
694        self::assertInstanceOf(UpdateMutation::class, $mutation);
695    }
696
697    public function testMultiTableUpdateEnsuresTablesInStore(): void
698    {
699        $parser = new MySqlParser();
700        $schemaParser = new MySqlSchemaParser($parser);
701        $shadowStore = new ShadowStore();
702        $registry = new TableDefinitionRegistry();
703        $userDef = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
704        $orderDef = new TableDefinition(['id', 'user_id', 'status'], ['id' => 'INT', 'user_id' => 'INT', 'status' => 'VARCHAR(50)'], ['id'], ['id'], []);
705        $registry->register('users', $userDef);
706        $registry->register('orders', $orderDef);
707
708        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
709        $shadowStore->set('orders', [['id' => 1, 'user_id' => 1, 'status' => 'pending']]);
710
711        $selectTransformer = new SelectTransformer();
712        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
713        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
714        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
715
716        $sql = "UPDATE users u, orders o SET u.name = 'X', o.status = 'done' WHERE u.id = o.user_id";
717        $statements = $parser->parse($sql);
718        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
719
720        self::assertInstanceOf(MultiUpdateMutation::class, $mutation);
721    }
722
723    public function testDeleteEnsuresTableInShadowStore(): void
724    {
725        $parser = new MySqlParser();
726        $schemaParser = new MySqlSchemaParser($parser);
727        $shadowStore = new ShadowStore();
728        $registry = new TableDefinitionRegistry();
729        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
730        $registry->register('users', $definition);
731
732        $selectTransformer = new SelectTransformer();
733        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
734        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
735        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
736
737        $sql = 'DELETE FROM users WHERE id = 1';
738        $statements = $parser->parse($sql);
739        $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
740
741        self::assertSame([], $shadowStore->get('users'));
742    }
743
744    public function testDeleteUsesColumnsFromShadowStore(): void
745    {
746        $parser = new MySqlParser();
747        $schemaParser = new MySqlSchemaParser($parser);
748        $shadowStore = new ShadowStore();
749        $shadowStore->set('users', [['id' => '1', 'name' => 'Alice']]);
750        $registry = new TableDefinitionRegistry();
751
752        $selectTransformer = new SelectTransformer();
753        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
754        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
755        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
756
757        $sql = 'DELETE FROM users WHERE id = 1';
758        $statements = $parser->parse($sql);
759        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
760
761        self::assertInstanceOf(DeleteMutation::class, $mutation);
762    }
763
764    public function testMultiDeleteEnsuresTablesInStore(): void
765    {
766        $parser = new MySqlParser();
767        $schemaParser = new MySqlSchemaParser($parser);
768        $shadowStore = new ShadowStore();
769        $registry = new TableDefinitionRegistry();
770        $userDef = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
771        $orderDef = new TableDefinition(['id', 'user_id', 'status'], ['id' => 'INT'], ['id'], ['id'], []);
772        $registry->register('users', $userDef);
773        $registry->register('orders', $orderDef);
774
775        $selectTransformer = new SelectTransformer();
776        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
777        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
778        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
779
780        $sql = "DELETE u, o FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'old'";
781        $statements = $parser->parse($sql);
782        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
783
784        self::assertInstanceOf(MultiDeleteMutation::class, $mutation);
785        self::assertSame([], $shadowStore->get('users'));
786        self::assertSame([], $shadowStore->get('orders'));
787        self::assertSame(ShadowTableState::Initialized, $shadowStore->state('users'));
788        self::assertSame(ShadowTableState::Initialized, $shadowStore->state('orders'));
789    }
790
791    public function testInsertOnDuplicateKeyUpdateReturnsUpsert(): void
792    {
793        $parser = new MySqlParser();
794        $schemaParser = new MySqlSchemaParser($parser);
795        $shadowStore = new ShadowStore();
796        $registry = new TableDefinitionRegistry();
797        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
798        $registry->register('users', $definition);
799
800        $selectTransformer = new SelectTransformer();
801        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
802        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
803        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
804
805        $sql = "INSERT INTO users (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE name = 'Bob'";
806        $statements = $parser->parse($sql);
807        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
808
809        self::assertInstanceOf(UpsertMutation::class, $mutation);
810        self::assertSame('users', $mutation->tableName());
811    }
812
813    public function testInsertOnDuplicateKeyUpdateWithQualifiedColumn(): void
814    {
815        $parser = new MySqlParser();
816        $schemaParser = new MySqlSchemaParser($parser);
817        $shadowStore = new ShadowStore();
818        $registry = new TableDefinitionRegistry();
819        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
820        $registry->register('users', $definition);
821
822        $selectTransformer = new SelectTransformer();
823        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
824        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
825        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
826
827        $sql = "INSERT INTO users (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE `users`.`name` = 'Bob'";
828        $statements = $parser->parse($sql);
829        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
830
831        self::assertInstanceOf(UpsertMutation::class, $mutation);
832    }
833
834    public function testInsertIgnoreReturnsInsertMutation(): void
835    {
836        $parser = new MySqlParser();
837        $schemaParser = new MySqlSchemaParser($parser);
838        $shadowStore = new ShadowStore();
839        $registry = new TableDefinitionRegistry();
840        $definition = new TableDefinition(['id', 'name'], ['id' => 'INT', 'name' => 'VARCHAR(255)'], ['id'], ['id'], []);
841        $registry->register('users', $definition);
842
843        $selectTransformer = new SelectTransformer();
844        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
845        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
846        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
847
848        $sql = "INSERT IGNORE INTO users (id, name) VALUES (1, 'Alice')";
849        $statements = $parser->parse($sql);
850        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
851
852        self::assertInstanceOf(InsertMutation::class, $mutation);
853    }
854
855    public function testCreateTableIfNotExistsWhenExists(): void
856    {
857        $parser = new MySqlParser();
858        $schemaParser = new MySqlSchemaParser($parser);
859        $shadowStore = new ShadowStore();
860        $registry = new TableDefinitionRegistry();
861        $definition = new TableDefinition(['id'], ['id' => 'INT'], ['id'], ['id'], []);
862        $registry->register('users', $definition);
863
864        $selectTransformer = new SelectTransformer();
865        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
866        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
867        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
868
869        $sql = 'CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY)';
870        $statements = $parser->parse($sql);
871        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
872
873        self::assertInstanceOf(CreateTableMutation::class, $mutation);
874    }
875
876    public function testCreateTableWithoutIfNotExistsThrowsWhenExists(): void
877    {
878        $parser = new MySqlParser();
879        $schemaParser = new MySqlSchemaParser($parser);
880        $shadowStore = new ShadowStore();
881        $registry = new TableDefinitionRegistry();
882        $definition = new TableDefinition(['id'], ['id' => 'INT'], ['id'], ['id'], []);
883        $registry->register('users', $definition);
884
885        $selectTransformer = new SelectTransformer();
886        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
887        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
888        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
889
890        $sql = 'CREATE TABLE users (id INT PRIMARY KEY)';
891        $statements = $parser->parse($sql);
892
893        $this->expectException(\ZtdQuery\Exception\UnsupportedSqlException::class);
894        $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
895    }
896
897    public function testDropTableIfExistsWhenNotExists(): void
898    {
899        $parser = new MySqlParser();
900        $schemaParser = new MySqlSchemaParser($parser);
901        $shadowStore = new ShadowStore();
902        $registry = new TableDefinitionRegistry();
903
904        $selectTransformer = new SelectTransformer();
905        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
906        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
907        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
908
909        $sql = 'DROP TABLE IF EXISTS users';
910        $statements = $parser->parse($sql);
911        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
912
913        self::assertInstanceOf(DropTableMutation::class, $mutation);
914    }
915
916    public function testDropTableWithoutIfExistsThrowsWhenNotExists(): void
917    {
918        $parser = new MySqlParser();
919        $schemaParser = new MySqlSchemaParser($parser);
920        $shadowStore = new ShadowStore();
921        $registry = new TableDefinitionRegistry();
922
923        $selectTransformer = new SelectTransformer();
924        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
925        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
926        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
927
928        $sql = 'DROP TABLE users';
929        $statements = $parser->parse($sql);
930
931        $this->expectException(UnknownSchemaException::class);
932        $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
933    }
934
935    public function testCreateTableLikeThrowsForUnknownSource(): void
936    {
937        $parser = new MySqlParser();
938        $schemaParser = new MySqlSchemaParser($parser);
939        $shadowStore = new ShadowStore();
940        $registry = new TableDefinitionRegistry();
941
942        $selectTransformer = new SelectTransformer();
943        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
944        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
945        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
946
947        $sql = 'CREATE TABLE t LIKE unknown_table';
948        $statements = $parser->parse($sql);
949
950        $this->expectException(UnknownSchemaException::class);
951        $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
952    }
953
954    public function testResolveUpdateUsesColumnsFromShadowStoreFirst(): void
955    {
956        $parser = new MySqlParser();
957        $schemaParser = new MySqlSchemaParser($parser);
958        $shadowStore = new ShadowStore();
959        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'a@b.c']]);
960        $registry = new TableDefinitionRegistry();
961
962        $selectTransformer = new SelectTransformer();
963        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
964        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
965        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
966
967        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
968        $statements = $parser->parse($sql);
969        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
970
971        self::assertInstanceOf(UpdateMutation::class, $mutation);
972        self::assertSame('users', $mutation->tableName());
973    }
974
975    public function testResolveUpdateUsesColumnsFromRegistryWhenStoreEmpty(): void
976    {
977        $parser = new MySqlParser();
978        $schemaParser = new MySqlSchemaParser($parser);
979        $shadowStore = new ShadowStore();
980        $registry = new TableDefinitionRegistry();
981        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
982        self::assertNotNull($def);
983        $registry->register('users', $def);
984
985        $selectTransformer = new SelectTransformer();
986        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
987        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
988        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
989
990        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
991        $statements = $parser->parse($sql);
992        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
993
994        self::assertInstanceOf(UpdateMutation::class, $mutation);
995        self::assertSame('users', $mutation->tableName());
996        self::assertSame([], $shadowStore->get('users'));
997        self::assertSame(ShadowTableState::Initialized, $shadowStore->state('users'));
998    }
999
1000    public function testResolveUpdateThrowsWhenNoSchemaAvailable(): void
1001    {
1002        $parser = new MySqlParser();
1003        $schemaParser = new MySqlSchemaParser($parser);
1004        $shadowStore = new ShadowStore();
1005        $registry = new TableDefinitionRegistry();
1006
1007        $selectTransformer = new SelectTransformer();
1008        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1009        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1010        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1011
1012        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
1013        $statements = $parser->parse($sql);
1014
1015        $this->expectException(UnknownSchemaException::class);
1016        $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1017    }
1018
1019    public function testResolveDeleteUsesColumnsFromShadowStoreFirst(): void
1020    {
1021        $parser = new MySqlParser();
1022        $schemaParser = new MySqlSchemaParser($parser);
1023        $shadowStore = new ShadowStore();
1024        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
1025        $registry = new TableDefinitionRegistry();
1026
1027        $selectTransformer = new SelectTransformer();
1028        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1029        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1030        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1031
1032        $sql = 'DELETE FROM users WHERE id = 1';
1033        $statements = $parser->parse($sql);
1034        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1035
1036        self::assertInstanceOf(DeleteMutation::class, $mutation);
1037        self::assertSame('users', $mutation->tableName());
1038    }
1039
1040    public function testResolveDeleteUsesColumnsFromRegistryWhenStoreEmpty(): void
1041    {
1042        $parser = new MySqlParser();
1043        $schemaParser = new MySqlSchemaParser($parser);
1044        $shadowStore = new ShadowStore();
1045        $registry = new TableDefinitionRegistry();
1046        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1047        self::assertNotNull($def);
1048        $registry->register('users', $def);
1049
1050        $selectTransformer = new SelectTransformer();
1051        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1052        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1053        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1054
1055        $sql = 'DELETE FROM users WHERE id = 1';
1056        $statements = $parser->parse($sql);
1057        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1058
1059        self::assertInstanceOf(DeleteMutation::class, $mutation);
1060        self::assertSame('users', $mutation->tableName());
1061    }
1062
1063    public function testResolveInsertOnDuplicateKeyExtractsUpdateColumns(): void
1064    {
1065        $parser = new MySqlParser();
1066        $schemaParser = new MySqlSchemaParser($parser);
1067        $shadowStore = new ShadowStore();
1068        $registry = new TableDefinitionRegistry();
1069        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))');
1070        self::assertNotNull($def);
1071        $registry->register('users', $def);
1072
1073        $selectTransformer = new SelectTransformer();
1074        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1075        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1076        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1077
1078        $sql = "INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.c') ON DUPLICATE KEY UPDATE name = VALUES(name), email = VALUES(email)";
1079        $statements = $parser->parse($sql);
1080        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1081
1082        self::assertInstanceOf(UpsertMutation::class, $mutation);
1083        self::assertSame('users', $mutation->tableName());
1084    }
1085
1086    public function testResolveInsertIgnoreIncludesPrimaryKeys(): void
1087    {
1088        $parser = new MySqlParser();
1089        $schemaParser = new MySqlSchemaParser($parser);
1090        $shadowStore = new ShadowStore();
1091        $registry = new TableDefinitionRegistry();
1092        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1093        self::assertNotNull($def);
1094        $registry->register('users', $def);
1095
1096        $selectTransformer = new SelectTransformer();
1097        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1098        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1099        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1100
1101        $sql = "INSERT IGNORE INTO users (id, name) VALUES (1, 'Alice')";
1102        $statements = $parser->parse($sql);
1103        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1104
1105        self::assertInstanceOf(InsertMutation::class, $mutation);
1106        self::assertSame('users', $mutation->tableName());
1107    }
1108
1109    public function testResolveCreateTableAsSelectExtractsColumnNames(): void
1110    {
1111        $parser = new MySqlParser();
1112        $schemaParser = new MySqlSchemaParser($parser);
1113        $shadowStore = new ShadowStore();
1114        $registry = new TableDefinitionRegistry();
1115
1116        $selectTransformer = new SelectTransformer();
1117        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1118        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1119        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1120
1121        $sql = 'CREATE TABLE archive AS SELECT id, name AS full_name FROM users';
1122        $statements = $parser->parse($sql);
1123        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1124
1125        self::assertInstanceOf(CreateTableAsSelectMutation::class, $mutation);
1126        self::assertSame('archive', $mutation->tableName());
1127    }
1128
1129    public function testResolveCreateTableAsSelectExtractsAliasedAndExprColumns(): void
1130    {
1131        $parser = new MySqlParser();
1132        $schemaParser = new MySqlSchemaParser($parser);
1133        $shadowStore = new ShadowStore();
1134        $registry = new TableDefinitionRegistry();
1135
1136        $selectTransformer = new SelectTransformer();
1137        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1138        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1139        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1140
1141        $sql = 'CREATE TABLE stats AS SELECT COUNT(*) AS cnt, department FROM employees GROUP BY department';
1142        $statements = $parser->parse($sql);
1143        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1144
1145        self::assertInstanceOf(CreateTableAsSelectMutation::class, $mutation);
1146        self::assertSame('stats', $mutation->tableName());
1147    }
1148
1149    public function testResolveReplaceReturnsPrimaryKeysFromRegistry(): void
1150    {
1151        $parser = new MySqlParser();
1152        $schemaParser = new MySqlSchemaParser($parser);
1153        $shadowStore = new ShadowStore();
1154        $registry = new TableDefinitionRegistry();
1155        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1156        self::assertNotNull($def);
1157        $registry->register('users', $def);
1158
1159        $selectTransformer = new SelectTransformer();
1160        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1161        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1162        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1163
1164        $sql = "REPLACE INTO users (id, name) VALUES (1, 'Alice')";
1165        $statements = $parser->parse($sql);
1166        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1167
1168        self::assertInstanceOf(ReplaceMutation::class, $mutation);
1169        self::assertSame('users', $mutation->tableName());
1170    }
1171
1172    public function testResolveDropTableIfExistsDoesNotThrow(): void
1173    {
1174        $parser = new MySqlParser();
1175        $schemaParser = new MySqlSchemaParser($parser);
1176        $shadowStore = new ShadowStore();
1177        $registry = new TableDefinitionRegistry();
1178
1179        $selectTransformer = new SelectTransformer();
1180        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1181        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1182        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1183
1184        $sql = 'DROP TABLE IF EXISTS nonexistent';
1185        $statements = $parser->parse($sql);
1186        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1187
1188        self::assertInstanceOf(DropTableMutation::class, $mutation);
1189        self::assertSame('nonexistent', $mutation->tableName());
1190    }
1191
1192    public function testResolveAlterTableThrowsForUnknownTable(): void
1193    {
1194        $parser = new MySqlParser();
1195        $schemaParser = new MySqlSchemaParser($parser);
1196        $shadowStore = new ShadowStore();
1197        $registry = new TableDefinitionRegistry();
1198
1199        $selectTransformer = new SelectTransformer();
1200        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1201        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1202        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1203
1204        $sql = 'ALTER TABLE unknown_table ADD COLUMN x INT';
1205        $statements = $parser->parse($sql);
1206
1207        $this->expectException(UnknownSchemaException::class);
1208        $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1209    }
1210
1211    public function testResolveMultiUpdateThrowsForUnknownTable(): void
1212    {
1213        $parser = new MySqlParser();
1214        $schemaParser = new MySqlSchemaParser($parser);
1215        $shadowStore = new ShadowStore();
1216        $registry = new TableDefinitionRegistry();
1217        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1218        self::assertNotNull($def);
1219        $registry->register('users', $def);
1220
1221        $selectTransformer = new SelectTransformer();
1222        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1223        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1224        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1225
1226        $sql = "UPDATE users, orders SET users.name = 'x' WHERE users.id = orders.user_id";
1227        $statements = $parser->parse($sql);
1228
1229        $this->expectException(UnknownSchemaException::class);
1230        $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1231    }
1232
1233    public function testResolveCreateTableIfNotExistsSkipsWhenExists(): void
1234    {
1235        $parser = new MySqlParser();
1236        $schemaParser = new MySqlSchemaParser($parser);
1237        $shadowStore = new ShadowStore();
1238        $registry = new TableDefinitionRegistry();
1239        $def = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY)');
1240        self::assertNotNull($def);
1241        $registry->register('t', $def);
1242
1243        $selectTransformer = new SelectTransformer();
1244        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1245        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1246        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1247
1248        $sql = 'CREATE TABLE IF NOT EXISTS t (id INT PRIMARY KEY)';
1249        $statements = $parser->parse($sql);
1250        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1251
1252        self::assertInstanceOf(CreateTableMutation::class, $mutation);
1253        self::assertSame('t', $mutation->tableName());
1254    }
1255
1256    public function testResolveDeleteThrowsWhenNoSchemaContextExists(): void
1257    {
1258        $parser = new MySqlParser();
1259        $schemaParser = new MySqlSchemaParser($parser);
1260        $shadowStore = new ShadowStore();
1261        $registry = new TableDefinitionRegistry();
1262
1263        $selectTransformer = new SelectTransformer();
1264        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1265        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1266        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1267
1268        $sql = 'DELETE FROM users WHERE id = 1';
1269        $statements = $parser->parse($sql);
1270
1271        $this->expectException(UnknownSchemaException::class);
1272        $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1273    }
1274
1275    public function testResolveInsertOnDuplicateKeyWithDottedColumn(): void
1276    {
1277        $parser = new MySqlParser();
1278        $schemaParser = new MySqlSchemaParser($parser);
1279        $shadowStore = new ShadowStore();
1280        $registry = new TableDefinitionRegistry();
1281        $def = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
1282        self::assertNotNull($def);
1283        $registry->register('t', $def);
1284
1285        $selectTransformer = new SelectTransformer();
1286        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1287        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1288        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1289
1290        $sql = "INSERT INTO t (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE t.name = VALUES(name)";
1291        $statements = $parser->parse($sql);
1292        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1293
1294        self::assertInstanceOf(UpsertMutation::class, $mutation);
1295        self::assertSame('t', $mutation->tableName());
1296    }
1297
1298    public function testResolveMultiDeleteEnsuresAllTablesInStore(): void
1299    {
1300        $parser = new MySqlParser();
1301        $schemaParser = new MySqlSchemaParser($parser);
1302        $shadowStore = new ShadowStore();
1303        $registry = new TableDefinitionRegistry();
1304        $def1 = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
1305        self::assertNotNull($def1);
1306        $registry->register('users', $def1);
1307        $def2 = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)');
1308        self::assertNotNull($def2);
1309        $registry->register('orders', $def2);
1310
1311        $selectTransformer = new SelectTransformer();
1312        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1313        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1314        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1315
1316        $sql = 'DELETE users, orders FROM users INNER JOIN orders ON users.id = orders.user_id WHERE users.id = 1';
1317        $statements = $parser->parse($sql);
1318        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1319
1320        self::assertInstanceOf(MultiDeleteMutation::class, $mutation);
1321        self::assertSame([], $shadowStore->get('users'));
1322        self::assertSame([], $shadowStore->get('orders'));
1323    }
1324
1325    public function testResolveMultiDeleteThrowsForUnknownTable(): void
1326    {
1327        $parser = new MySqlParser();
1328        $schemaParser = new MySqlSchemaParser($parser);
1329        $shadowStore = new ShadowStore();
1330        $registry = new TableDefinitionRegistry();
1331        $def1 = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
1332        self::assertNotNull($def1);
1333        $registry->register('users', $def1);
1334
1335        $selectTransformer = new SelectTransformer();
1336        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1337        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1338        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1339
1340        $sql = 'DELETE users, unknown_tbl FROM users INNER JOIN unknown_tbl ON users.id = unknown_tbl.user_id';
1341        $statements = $parser->parse($sql);
1342
1343        $this->expectException(UnknownSchemaException::class);
1344        $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1345    }
1346
1347    public function testResolveInsertIgnoreReturnsPrimaryKeysFromDefinition(): void
1348    {
1349        $parser = new MySqlParser();
1350        $schemaParser = new MySqlSchemaParser($parser);
1351        $shadowStore = new ShadowStore();
1352        $registry = new TableDefinitionRegistry();
1353        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1354        self::assertNotNull($def);
1355        $registry->register('users', $def);
1356
1357        $selectTransformer = new SelectTransformer();
1358        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1359        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1360        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1361
1362        $sql = "INSERT IGNORE INTO users (id, name) VALUES (1, 'Alice')";
1363        $statements = $parser->parse($sql);
1364        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1365
1366        self::assertInstanceOf(InsertMutation::class, $mutation);
1367        self::assertSame('users', $mutation->tableName());
1368    }
1369
1370    public function testResolveInsertIgnoreWithoutDefinitionReturnEmptyPks(): void
1371    {
1372        $parser = new MySqlParser();
1373        $schemaParser = new MySqlSchemaParser($parser);
1374        $shadowStore = new ShadowStore();
1375        $shadowStore->set('users', [['id' => 1, 'name' => 'A']]);
1376        $registry = new TableDefinitionRegistry();
1377
1378        $selectTransformer = new SelectTransformer();
1379        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1380        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1381        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1382
1383        $sql = "INSERT IGNORE INTO users (id, name) VALUES (1, 'Alice')";
1384        $statements = $parser->parse($sql);
1385        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1386
1387        self::assertInstanceOf(InsertMutation::class, $mutation);
1388        self::assertSame('users', $mutation->tableName());
1389    }
1390
1391    public function testResolveUpsertReturnsPrimaryKeysFromDefinition(): void
1392    {
1393        $parser = new MySqlParser();
1394        $schemaParser = new MySqlSchemaParser($parser);
1395        $shadowStore = new ShadowStore();
1396        $registry = new TableDefinitionRegistry();
1397        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))');
1398        self::assertNotNull($def);
1399        $registry->register('users', $def);
1400
1401        $selectTransformer = new SelectTransformer();
1402        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1403        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1404        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1405
1406        $sql = "INSERT INTO users (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE name = 'Bob'";
1407        $statements = $parser->parse($sql);
1408        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1409
1410        self::assertInstanceOf(UpsertMutation::class, $mutation);
1411        self::assertSame('users', $mutation->tableName());
1412    }
1413
1414    public function testResolveMultiUpdateReturnsPrimaryKeysFromDefinition(): void
1415    {
1416        $parser = new MySqlParser();
1417        $schemaParser = new MySqlSchemaParser($parser);
1418        $shadowStore = new ShadowStore();
1419        $registry = new TableDefinitionRegistry();
1420        $def1 = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1421        self::assertNotNull($def1);
1422        $registry->register('users', $def1);
1423        $def2 = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)');
1424        self::assertNotNull($def2);
1425        $registry->register('orders', $def2);
1426
1427        $selectTransformer = new SelectTransformer();
1428        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1429        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1430        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1431
1432        $sql = "UPDATE users, orders SET users.name = 'Bob' WHERE users.id = orders.user_id";
1433        $statements = $parser->parse($sql);
1434        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1435
1436        self::assertInstanceOf(MultiUpdateMutation::class, $mutation);
1437        self::assertSame(ShadowTableState::Initialized, $shadowStore->state('users'));
1438        self::assertSame(ShadowTableState::Initialized, $shadowStore->state('orders'));
1439    }
1440
1441    public function testResolveMultiDeleteReturnsPrimaryKeysFromDefinition(): void
1442    {
1443        $parser = new MySqlParser();
1444        $schemaParser = new MySqlSchemaParser($parser);
1445        $shadowStore = new ShadowStore();
1446        $registry = new TableDefinitionRegistry();
1447        $def1 = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
1448        self::assertNotNull($def1);
1449        $registry->register('users', $def1);
1450        $def2 = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, user_id INT)');
1451        self::assertNotNull($def2);
1452        $registry->register('orders', $def2);
1453
1454        $selectTransformer = new SelectTransformer();
1455        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1456        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1457        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1458
1459        $sql = 'DELETE users, orders FROM users INNER JOIN orders ON users.id = orders.user_id WHERE users.id = 1';
1460        $statements = $parser->parse($sql);
1461        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1462
1463        self::assertInstanceOf(MultiDeleteMutation::class, $mutation);
1464    }
1465
1466    public function testResolveDeleteEnsuresTargetTableInStore(): void
1467    {
1468        $parser = new MySqlParser();
1469        $schemaParser = new MySqlSchemaParser($parser);
1470        $shadowStore = new ShadowStore();
1471        $registry = new TableDefinitionRegistry();
1472        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1473        self::assertNotNull($def);
1474        $registry->register('users', $def);
1475
1476        $selectTransformer = new SelectTransformer();
1477        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1478        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1479        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1480
1481        $sql = 'DELETE FROM users WHERE id = 1';
1482        $statements = $parser->parse($sql);
1483        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1484
1485        self::assertInstanceOf(DeleteMutation::class, $mutation);
1486        self::assertSame('users', $mutation->tableName());
1487    }
1488
1489    public function testResolveInsertOnDuplicateKeyWithBacktickedColumn(): void
1490    {
1491        $parser = new MySqlParser();
1492        $schemaParser = new MySqlSchemaParser($parser);
1493        $shadowStore = new ShadowStore();
1494        $registry = new TableDefinitionRegistry();
1495        $def = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
1496        self::assertNotNull($def);
1497        $registry->register('t', $def);
1498
1499        $selectTransformer = new SelectTransformer();
1500        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1501        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1502        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1503
1504        $sql = "INSERT INTO t (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE `name` = 'Bob'";
1505        $statements = $parser->parse($sql);
1506        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1507
1508        self::assertInstanceOf(UpsertMutation::class, $mutation);
1509        self::assertSame('t', $mutation->tableName());
1510    }
1511
1512    public function testResolveCreateTableAsSelectWithColumnNames(): void
1513    {
1514        $parser = new MySqlParser();
1515        $schemaParser = new MySqlSchemaParser($parser);
1516        $shadowStore = new ShadowStore();
1517        $registry = new TableDefinitionRegistry();
1518        $def = $schemaParser->parse('CREATE TABLE src (id INT PRIMARY KEY, name VARCHAR(255))');
1519        self::assertNotNull($def);
1520        $registry->register('src', $def);
1521
1522        $selectTransformer = new SelectTransformer();
1523        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1524        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1525        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1526
1527        $sql = 'CREATE TABLE dest AS SELECT id, name FROM src';
1528        $statements = $parser->parse($sql);
1529        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1530
1531        self::assertInstanceOf(CreateTableAsSelectMutation::class, $mutation);
1532    }
1533
1534    public function testResolveCreateTableAsSelectExtractsColumnsFromAliasedExprs(): void
1535    {
1536        $parser = new MySqlParser();
1537        $schemaParser = new MySqlSchemaParser($parser);
1538        $shadowStore = new ShadowStore();
1539        $registry = new TableDefinitionRegistry();
1540
1541        $selectTransformer = new SelectTransformer();
1542        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1543        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1544        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1545
1546        $sql = 'CREATE TABLE dest AS SELECT id AS uid, name AS label FROM src';
1547        $statements = $parser->parse($sql);
1548        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1549
1550        self::assertInstanceOf(CreateTableAsSelectMutation::class, $mutation);
1551        $mutation->apply($shadowStore, [['uid' => 1, 'label' => 'Alice']]);
1552        $def = $registry->get('dest');
1553        self::assertNotNull($def);
1554        self::assertContains('uid', $def->columns);
1555        self::assertContains('label', $def->columns);
1556    }
1557
1558    public function testResolveCreateTableAsSelectExtractsPlainColumnNames(): void
1559    {
1560        $parser = new MySqlParser();
1561        $schemaParser = new MySqlSchemaParser($parser);
1562        $shadowStore = new ShadowStore();
1563        $registry = new TableDefinitionRegistry();
1564
1565        $selectTransformer = new SelectTransformer();
1566        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1567        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1568        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1569
1570        $sql = 'CREATE TABLE dest AS SELECT id, name FROM src';
1571        $statements = $parser->parse($sql);
1572        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1573
1574        self::assertInstanceOf(CreateTableAsSelectMutation::class, $mutation);
1575        $mutation->apply($shadowStore, [['id' => 1, 'name' => 'Bob']]);
1576        $def = $registry->get('dest');
1577        self::assertNotNull($def);
1578        self::assertContains('id', $def->columns);
1579        self::assertContains('name', $def->columns);
1580    }
1581
1582    public function testResolveCreateTableAsSelectHandlesExpressionWithoutAlias(): void
1583    {
1584        $parser = new MySqlParser();
1585        $schemaParser = new MySqlSchemaParser($parser);
1586        $shadowStore = new ShadowStore();
1587        $registry = new TableDefinitionRegistry();
1588
1589        $selectTransformer = new SelectTransformer();
1590        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1591        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1592        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1593
1594        $sql = 'CREATE TABLE dest AS SELECT COUNT(*) AS cnt FROM src';
1595        $statements = $parser->parse($sql);
1596        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1597
1598        self::assertInstanceOf(CreateTableAsSelectMutation::class, $mutation);
1599        $mutation->apply($shadowStore, [['cnt' => 5]]);
1600        $def = $registry->get('dest');
1601        self::assertNotNull($def);
1602        self::assertContains('cnt', $def->columns);
1603    }
1604
1605    public function testResolveInsertIgnoreProducesInsertMutation(): void
1606    {
1607        $parser = new MySqlParser();
1608        $schemaParser = new MySqlSchemaParser($parser);
1609        $shadowStore = new ShadowStore();
1610        $registry = new TableDefinitionRegistry();
1611        $def = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
1612        self::assertNotNull($def);
1613        $registry->register('t', $def);
1614
1615        $selectTransformer = new SelectTransformer();
1616        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1617        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1618        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1619
1620        $sql = "INSERT IGNORE INTO t (id, name) VALUES (1, 'Alice')";
1621        $statements = $parser->parse($sql);
1622        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1623
1624        self::assertInstanceOf(InsertMutation::class, $mutation);
1625        self::assertSame('t', $mutation->tableName());
1626    }
1627
1628    public function testResolveUpdateMultiTableReturnsPrimaryKeysFromDefinition(): void
1629    {
1630        $parser = new MySqlParser();
1631        $schemaParser = new MySqlSchemaParser($parser);
1632        $shadowStore = new ShadowStore();
1633        $registry = new TableDefinitionRegistry();
1634        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1635        self::assertNotNull($def);
1636        $registry->register('users', $def);
1637
1638        $selectTransformer = new SelectTransformer();
1639        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1640        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1641        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1642
1643        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
1644        $statements = $parser->parse($sql);
1645        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1646
1647        self::assertInstanceOf(UpdateMutation::class, $mutation);
1648        self::assertSame('users', $mutation->tableName());
1649    }
1650
1651    public function testResolveDeleteWithSingleTargetReturnsPrimaryKeys(): void
1652    {
1653        $parser = new MySqlParser();
1654        $schemaParser = new MySqlSchemaParser($parser);
1655        $shadowStore = new ShadowStore();
1656        $registry = new TableDefinitionRegistry();
1657        $def = $schemaParser->parse('CREATE TABLE orders (id INT PRIMARY KEY, total DECIMAL(10,2))');
1658        self::assertNotNull($def);
1659        $registry->register('orders', $def);
1660
1661        $selectTransformer = new SelectTransformer();
1662        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1663        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1664        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1665
1666        $sql = 'DELETE FROM orders WHERE id = 1';
1667        $statements = $parser->parse($sql);
1668        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1669
1670        self::assertInstanceOf(DeleteMutation::class, $mutation);
1671        self::assertSame('orders', $mutation->tableName());
1672    }
1673
1674    public function testResolveInsertOnDuplicateKeyExtractsQualifiedColumnName(): void
1675    {
1676        $parser = new MySqlParser();
1677        $schemaParser = new MySqlSchemaParser($parser);
1678        $shadowStore = new ShadowStore();
1679        $registry = new TableDefinitionRegistry();
1680        $def = $schemaParser->parse('CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))');
1681        self::assertNotNull($def);
1682        $registry->register('t', $def);
1683
1684        $selectTransformer = new SelectTransformer();
1685        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1686        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1687        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1688
1689        $sql = "INSERT INTO t (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE t.name = 'Bob'";
1690        $statements = $parser->parse($sql);
1691        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1692
1693        self::assertInstanceOf(UpsertMutation::class, $mutation);
1694        self::assertSame('t', $mutation->tableName());
1695    }
1696
1697    public function testResolveDropTableWithIfExistsDoesNotThrowForMissingTable(): void
1698    {
1699        $parser = new MySqlParser();
1700        $schemaParser = new MySqlSchemaParser($parser);
1701        $shadowStore = new ShadowStore();
1702        $registry = new TableDefinitionRegistry();
1703
1704        $selectTransformer = new SelectTransformer();
1705        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1706        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1707        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1708
1709        $sql = 'DROP TABLE IF EXISTS nonexistent';
1710        $statements = $parser->parse($sql);
1711        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1712
1713        self::assertInstanceOf(DropTableMutation::class, $mutation);
1714    }
1715
1716    public function testResolveDropTableWithoutIfExistsThrowsForMissingTable(): void
1717    {
1718        $parser = new MySqlParser();
1719        $schemaParser = new MySqlSchemaParser($parser);
1720        $shadowStore = new ShadowStore();
1721        $registry = new TableDefinitionRegistry();
1722
1723        $selectTransformer = new SelectTransformer();
1724        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1725        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1726        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1727
1728        $sql = 'DROP TABLE nonexistent';
1729        $statements = $parser->parse($sql);
1730
1731        $this->expectException(UnknownSchemaException::class);
1732        $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1733    }
1734
1735    public function testResolveCreateTableIfNotExistsDoesNotThrowForExistingTable(): void
1736    {
1737        $parser = new MySqlParser();
1738        $schemaParser = new MySqlSchemaParser($parser);
1739        $shadowStore = new ShadowStore();
1740        $registry = new TableDefinitionRegistry();
1741        $def = $schemaParser->parse('CREATE TABLE t (id INT)');
1742        self::assertNotNull($def);
1743        $registry->register('t', $def);
1744
1745        $selectTransformer = new SelectTransformer();
1746        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1747        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1748        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1749
1750        $sql = 'CREATE TABLE IF NOT EXISTS t (id INT)';
1751        $statements = $parser->parse($sql);
1752        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1753
1754        self::assertInstanceOf(CreateTableMutation::class, $mutation);
1755    }
1756
1757    public function testResolveCreateTableLike(): void
1758    {
1759        $parser = new MySqlParser();
1760        $schemaParser = new MySqlSchemaParser($parser);
1761        $shadowStore = new ShadowStore();
1762        $registry = new TableDefinitionRegistry();
1763        $def = $schemaParser->parse('CREATE TABLE src (id INT, name VARCHAR(255))');
1764        self::assertNotNull($def);
1765        $registry->register('src', $def);
1766
1767        $selectTransformer = new SelectTransformer();
1768        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1769        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1770        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1771
1772        $sql = 'CREATE TABLE dest LIKE src';
1773        $statements = $parser->parse($sql);
1774        $mutation = $resolver->resolve($sql, $statements[0], QueryKind::DDL_SIMULATED);
1775
1776        self::assertInstanceOf(CreateTableLikeMutation::class, $mutation);
1777    }
1778
1779    public function testUpdateDoesNotTreatRowsFromUnknownInsertAsSchema(): void
1780    {
1781        $parser = new MySqlParser();
1782        $schemaParser = new MySqlSchemaParser($parser);
1783        $shadowStore = new ShadowStore();
1784        $shadowStore->insert('late_table', [['id' => 1, 'name' => 'Alice']]);
1785        $registry = new TableDefinitionRegistry();
1786        $selectTransformer = new SelectTransformer();
1787        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1788        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1789        $resolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1790        $sql = "UPDATE late_table SET name = 'Bob' WHERE id = 1";
1791        $statements = $parser->parse($sql);
1792
1793        $this->expectException(UnknownSchemaException::class);
1794        $resolver->resolve($sql, $statements[0], QueryKind::WRITE_SIMULATED);
1795    }
1796}
1797