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