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