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