packages/ztd-query-mysql/tests/Unit/Rewrite/MySqlRewriterTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite;
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\MySql\Rewrite\LoadData\MySqlLoadDataProjector;
13use ZtdQuery\Platform\MySql\Rewrite\MySqlQueryGuard;
14use ZtdQuery\Platform\MySql\Rewrite\MySqlRewriter;
15use ZtdQuery\Platform\MySql\Rewrite\Partition\MySqlPartitionSelectionRewriter;
16use ZtdQuery\Platform\MySql\Rewrite\Transformer\DeleteTransformer;
17use ZtdQuery\Platform\MySql\Rewrite\Transformer\InsertTransformer;
18use ZtdQuery\Platform\MySql\Rewrite\Transformer\MySqlTransformer;
19use ZtdQuery\Platform\MySql\Rewrite\Transformer\ReplaceTransformer;
20use ZtdQuery\Platform\MySql\Rewrite\Transformer\SelectTransformer;
21use ZtdQuery\Platform\MySql\Rewrite\Transformer\UpdateTransformer;
22use ZtdQuery\Platform\MySql\Schema\MySqlSchemaParser;
23use ZtdQuery\Platform\MySql\Schema\Partition\MySqlPartitioningParser;
24use ZtdQuery\Platform\MySql\Schema\View\MySqlViewDefinitionParser;
25use ZtdQuery\Platform\MySql\Shadow\Mutation\Table\AlterTableMutation;
26use ZtdQuery\Platform\MySql\Shadow\MySqlMutationResolver;
27use ZtdQuery\Platform\MySql\Sql\Dml\DmlWhereClauseExtractor;
28use ZtdQuery\Platform\MySql\Sql\Dml\InsertSelectSourceExtractor;
29use ZtdQuery\Platform\MySql\Sql\Dml\UpdateAssignmentExtractor;
30use ZtdQuery\Platform\MySql\Sql\Dml\UpdateSourceExtractor;
31use ZtdQuery\Platform\MySql\Sql\MySqlParser;
32use ZtdQuery\Platform\MySql\Sql\Upsert\MySqlUpsertAssignmentExtractor;
33use ZtdQuery\Rewrite\QueryKind;
34use ZtdQuery\Schema\TableDefinitionRegistry;
35use ZtdQuery\Schema\ViewDefinitionSet;
36use ZtdQuery\Shadow\Mutation\Row\DeleteMutation;
37use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
38use ZtdQuery\Shadow\Mutation\Row\ReplaceMutation;
39use ZtdQuery\Shadow\Mutation\Row\UpdateMutation;
40use ZtdQuery\Shadow\Mutation\Table\CreateTableAsSelectMutation;
41use ZtdQuery\Shadow\Mutation\Table\CreateTableLikeMutation;
42use ZtdQuery\Shadow\Mutation\Table\CreateTableMutation;
43use ZtdQuery\Shadow\Mutation\Table\DropTableMutation;
44use ZtdQuery\Shadow\Mutation\Table\TruncateMutation;
45use ZtdQuery\Shadow\ShadowStore;
46use ZtdQuery\Shadow\ShadowTableState;
47
48#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\ColumnAction::class)]
49#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\ColumnAlteration::class)]
50#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\ColumnDefinitionParser::class)]
51#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\CreateTableRenderer::class)]
52#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\OperationApplier::class)]
53#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\PrimaryKeyAlteration::class)]
54#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\StoredColumns::class)]
55#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\Alter\UnsupportedKeyword::class)]
56#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Row\RowMutationResolver::class)]
57#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Table\TableMutationResolver::class)]
58#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Dml\TargetName::class)]
59#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Alter\OptionList::class)]
60#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Dml\AssignmentExpression::class)]
61#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Cte\HeaderParser::class)]
62#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Cte\IdentifierReferences::class)]
63#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Diagnostic\DiagnosticKeywords::class)]
64#[UsesClass(\ZtdQuery\Platform\MySql\Sql\OptionalInsertIntoNormalizer::class)]
65#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Relation\ExpressionNames::class)]
66#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Relation\ReferenceReader::class)]
67#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Transaction\TokenReader::class)]
68#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Upsert\AssignmentReader::class)]
69#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Upsert\ExpressionReader::class)]
70#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Upsert\LiteralReader::class)]
71#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Upsert\StringLiteral::class)]
72#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\FullText\ExpressionEditor::class)]
73#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\LoadData\ColumnMapping::class)]
74#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\LoadData\InputFile::class)]
75#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\LoadData\InputFormat::class)]
76#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\LoadData\InsertQuery::class)]
77#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\LoadData\RecordParser::class)]
78#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\LoadData\RowProjector::class)]
79#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\SelectionReader::class)]
80#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\SourceProjection::class)]
81#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\ConflictPredicate::class)]
82#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\ExpressionBinder::class)]
83#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\MetadataColumns::class)]
84#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\QualifiedColumn::class)]
85#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Classification\CteStatementKind::class)]
86
87#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Validation\AlterTableGuard::class)]
88
89#[UsesClass(\ZtdQuery\Platform\MySql\Schema\DefinitionBuilder::class)]
90#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Key\DefinitionReader::class)]
91#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Key\TokenReader::class)]
92#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Partition\PredicateCompiler::class)]
93#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Delete\ResultSelect::class)]
94#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Delete\TargetProjection::class)]
95#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Insert\InsertTarget::class)]
96#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Insert\ReplaceStatementConverter::class)]
97#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Insert\ResultProjection::class)]
98#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Select\ExpressionAliaser::class)]
99#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Set\OrderRewriter::class)]
100#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Set\ValueNormalizer::class)]
101#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Shadow\CteRows::class)]
102#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Update\ResultSelect::class)]
103#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Update\TargetProjection::class)]
104#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\CastTypeResolver::class)]
105#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Type\RankEdits::class)]
106#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\ScalarExpression::class)]
107#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\StringCoercion::class)]
108#[CoversClass(MySqlRewriter::class)]
109#[UsesClass(\ZtdQuery\Platform\MySql\Schema\MySqlColumnTypeMapper::class)]
110#[UsesClass(\ZtdQuery\Platform\MySql\Schema\Key\MySqlForeignKeyDefinitionParser::class)]
111#[UsesClass(MySqlParser::class)]
112#[UsesClass(MySqlMutationResolver::class)]
113#[UsesClass(\ZtdQuery\Platform\MySql\Shadow\Mutation\Upsert\MySqlUpsertExpressionParser::class)]
114#[UsesClass(MySqlLoadDataProjector::class)]
115#[UsesClass(MySqlSchemaParser::class)]
116#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Relation\MySqlSelectRelationParser::class)]
117#[UsesClass(MySqlPartitioningParser::class)]
118#[UsesClass(MySqlPartitionSelectionRewriter::class)]
119#[UsesClass(MySqlUpsertAssignmentExtractor::class)]
120#[UsesClass(MySqlQueryGuard::class)]
121#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Diagnostic\MySqlReadOnlyDiagnosticStatement::class)]
122#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Transaction\MySqlTransactionStatementParser::class)]
123#[UsesClass(MySqlTransformer::class)]
124#[UsesClass(SelectTransformer::class)]
125#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\FullText\MySqlFullTextSearchRewriter::class)]
126#[UsesClass(InsertTransformer::class)]
127#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\InsertRowRenderer::class)]
128#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\InsertSelectRenderer::class)]
129#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\MySqlSelectListAliaser::class)]
130#[UsesClass(InsertSelectSourceExtractor::class)]
131#[UsesClass(UpdateTransformer::class)]
132#[UsesClass(UpdateAssignmentExtractor::class)]
133#[UsesClass(UpdateSourceExtractor::class)]
134#[UsesClass(DeleteTransformer::class)]
135#[UsesClass(DmlWhereClauseExtractor::class)]
136#[UsesClass(ReplaceTransformer::class)]
137#[UsesClass(AlterTableMutation::class)]
138#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\MySqlCastRenderer::class)]
139#[UsesClass(\ZtdQuery\Platform\MySql\Sql\MySqlIdentifierQuoter::class)]
140#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\MySqlValueRenderer::class)]
141#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Type\MySqlTypeSemantics::class)]
142#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Cte\MySqlCteShadowComposer::class)]
143#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Upsert\MySqlNativeUpsertProjector::class)]
144#[UsesClass(MySqlViewDefinitionParser::class)]
145#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\View\MySqlViewShadowRenderer::class)]
146#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\GeneratedColumn\MySqlGeneratedColumnProjector::class)]
147#[UsesClass(\ZtdQuery\Platform\MySql\Sql\MySqlLexerProfile::class)]
148#[CoversClass(\ZtdQuery\Platform\MySql\Rewrite\StatementRewriter::class)]
149#[CoversClass(\ZtdQuery\Platform\MySql\Rewrite\Context\TableContext::class)]
150#[CoversClass(\ZtdQuery\Platform\MySql\Rewrite\Validation\ReplaceColumns::class)]
151final class MySqlRewriterTest extends TestCase
152{
153    public function testPartitionSelectionUsesRegisteredPartitionMetadata(): void
154    {
155        $store = new ShadowStore();
156        $store->set('events', [
157            ['id' => 1, 'event_date' => '2023-06-01'],
158            ['id' => 2, 'event_date' => '2024-06-01'],
159        ]);
160        $registry = new TableDefinitionRegistry();
161        $definition = (new MySqlSchemaParser(new MySqlParser()))->parse(
162            'CREATE TABLE events (id INT, event_date DATE) '
163            . 'PARTITION BY RANGE (YEAR(event_date)) ('
164            . 'PARTITION p2023 VALUES LESS THAN (2024), '
165            . 'PARTITION p2024 VALUES LESS THAN (2025), '
166            . 'PARTITION pmax VALUES LESS THAN MAXVALUE)',
167        );
168        self::assertNotNull($definition);
169        $registry->register('events', $definition);
170
171        $parser = new MySqlParser();
172        $schemaParser = new MySqlSchemaParser($parser);
173        $selectTransformer = new SelectTransformer();
174        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
175        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
176        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
177        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
178        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
179        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
180        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
181
182        $sql = $rewriter
183            ->rewrite('SELECT id FROM events PARTITION (p2024)')
184            ->sql();
185
186        self::assertStringStartsWith('WITH `events` AS', $sql);
187        self::assertStringContainsString(
188            'FROM (SELECT * FROM events WHERE ((YEAR(event_date)) >= 2024 AND (YEAR(event_date)) < 2025)) AS events',
189            $sql,
190        );
191    }
192
193    public function testPartitionSelectionUsesDefinitionWithoutMaterializedRows(): void
194    {
195        $registry = new TableDefinitionRegistry();
196        $definition = (new MySqlSchemaParser(new MySqlParser()))->parse(
197            'CREATE TABLE events (id INT) PARTITION BY RANGE (id) ('
198            . 'PARTITION p0 VALUES LESS THAN (10), PARTITION pmax VALUES LESS THAN MAXVALUE)',
199        );
200        self::assertNotNull($definition);
201        $registry->register('events', $definition);
202
203        $store = new ShadowStore();
204        $parser = new MySqlParser();
205        $schemaParser = new MySqlSchemaParser($parser);
206        $selectTransformer = new SelectTransformer();
207        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
208        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
209        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
210        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
211        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
212        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
213        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
214
215        $sql = $rewriter
216            ->rewrite('SELECT id FROM events PARTITION (p0)')
217            ->sql();
218
219        self::assertStringContainsString('FROM (SELECT * FROM events WHERE ((id) IS NULL OR (id) < 10)) AS events', $sql);
220    }
221
222    public function testGeneratedExpressionIsPresentBeforeTheFirstShadowWrite(): void
223    {
224        $store = new ShadowStore();
225        $registry = new TableDefinitionRegistry();
226        $definition = (new MySqlSchemaParser(new MySqlParser()))->parse(
227            'CREATE TABLE orders (qty INT, total INT GENERATED ALWAYS AS (qty * 2) STORED)',
228        );
229        self::assertNotNull($definition);
230        $registry->register('orders', $definition);
231
232        $parser = new MySqlParser();
233        $schemaParser = new MySqlSchemaParser($parser);
234        $selectTransformer = new SelectTransformer();
235        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
236        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
237        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
238        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
239        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
240        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
241        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
242
243        $sql = $rewriter->rewrite('SELECT total FROM orders')->sql();
244
245        self::assertStringContainsString('(qty * 2) AS `total`', $sql);
246    }
247
248    public function testRegisteredViewIsKnownAndMaterialized(): void
249    {
250        $store = new ShadowStore();
251        $registry = new TableDefinitionRegistry();
252        $parser = new MySqlParser();
253        $schemaParser = new MySqlSchemaParser($parser);
254        $definition = $schemaParser->parse(<<<'SQL'
255CREATE TABLE users (
256    id INT NOT NULL AUTO_INCREMENT,
257    name VARCHAR(255) NOT NULL,
258    email VARCHAR(255) NOT NULL,
259    PRIMARY KEY (id)
260)
261SQL);
262        self::assertNotNull($definition);
263        $registry->register('users', $definition);
264        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
265        $selectTransformer = new SelectTransformer();
266        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
267        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
268        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
269        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
270        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
271        $resolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
272        $views = new ViewDefinitionSet();
273        $views->register('active_users', (new MySqlViewDefinitionParser())->fromQuery('SELECT id FROM app.users'));
274        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $resolver, $parser, $views);
275
276        $sql = $rewriter->rewrite('SELECT * FROM active_users')->sql();
277        self::assertStringStartsWith('WITH `users` AS', $sql);
278        self::assertStringContainsString('`active_users` AS (SELECT id FROM users)', $sql);
279
280        $viewOnlyStore = new ShadowStore();
281        $viewOnlyRegistry = new TableDefinitionRegistry();
282        $viewOnlyViews = new ViewDefinitionSet();
283        $viewOnlyViews->register('constant_view', (new MySqlViewDefinitionParser())->fromQuery('SELECT 1 AS id'));
284        $viewOnlyResolver = new MySqlMutationResolver($viewOnlyStore, $viewOnlyRegistry, $schemaParser, $updateTransformer, $deleteTransformer);
285        $viewOnlyRewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $viewOnlyStore, $viewOnlyRegistry, $transformer, $viewOnlyResolver, $parser, $viewOnlyViews);
286
287        $this->expectException(UnknownSchemaException::class);
288        $viewOnlyRewriter->rewrite('SELECT * FROM missing_table');
289    }
290
291    public function testDatabaseQualifiedSelectUsesShadowCte(): void
292    {
293        $store = new ShadowStore();
294        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
295        $registry = new TableDefinitionRegistry();
296        $definition = (new MySqlSchemaParser(new MySqlParser()))->parse(<<<'SQL'
297CREATE TABLE users (
298    id INT NOT NULL AUTO_INCREMENT,
299    name VARCHAR(255) NOT NULL,
300    email VARCHAR(255) NOT NULL,
301    PRIMARY KEY (id)
302)
303SQL);
304        self::assertNotNull($definition);
305        $registry->register('users', $definition);
306
307        $parser = new MySqlParser();
308        $schemaParser = new MySqlSchemaParser($parser);
309        $selectTransformer = new SelectTransformer();
310        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
311        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
312        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
313        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
314        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
315        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
316        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
317
318        $plan = $rewriter->rewrite('SELECT name FROM app.users');
319
320        self::assertStringStartsWith('WITH `users` AS', $plan->sql());
321        self::assertStringEndsWith('SELECT name FROM users', $plan->sql());
322    }
323
324    public function testExplainPassesThroughUnchanged(): void
325    {
326        $store = new ShadowStore();
327        $registry = new TableDefinitionRegistry();
328        $parser = new MySqlParser();
329        $schemaParser = new MySqlSchemaParser($parser);
330        $selectTransformer = new SelectTransformer();
331        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
332        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
333        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
334        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
335        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
336        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
337        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
338
339        $sql = 'EXPLAIN SELECT * FROM users';
340
341        $plan = $rewriter->rewrite($sql);
342
343        self::assertSame(QueryKind::READ, $plan->kind());
344        self::assertSame($sql, $plan->sql());
345    }
346
347    public function testDescribePassesThroughUnchanged(): void
348    {
349        $store = new ShadowStore();
350        $registry = new TableDefinitionRegistry();
351        $parser = new MySqlParser();
352        $schemaParser = new MySqlSchemaParser($parser);
353        $selectTransformer = new SelectTransformer();
354        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
355        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
356        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
357        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
358        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
359        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
360        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
361
362        $sql = 'DESCRIBE users';
363
364        $plan = $rewriter->rewrite($sql);
365
366        self::assertSame(QueryKind::READ, $plan->kind());
367        self::assertSame($sql, $plan->sql());
368    }
369
370    public function testShowCreateTablePassesThroughUnchanged(): void
371    {
372        $store = new ShadowStore();
373        $registry = new TableDefinitionRegistry();
374        $parser = new MySqlParser();
375        $schemaParser = new MySqlSchemaParser($parser);
376        $selectTransformer = new SelectTransformer();
377        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
378        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
379        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
380        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
381        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
382        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
383        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
384
385        $sql = 'SHOW CREATE TABLE users';
386
387        $plan = $rewriter->rewrite($sql);
388
389        self::assertSame(QueryKind::READ, $plan->kind());
390        self::assertSame($sql, $plan->sql());
391    }
392
393    public function testDerivedTableKeepsNestedPhysicalRelationInShadowScope(): void
394    {
395        $store = new ShadowStore();
396        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
397        $registry = new TableDefinitionRegistry();
398        $definition = (new MySqlSchemaParser(new MySqlParser()))->parse(<<<'SQL'
399CREATE TABLE users (
400    id INT NOT NULL AUTO_INCREMENT,
401    name VARCHAR(255) NOT NULL,
402    email VARCHAR(255) NOT NULL,
403    PRIMARY KEY (id)
404)
405SQL);
406        self::assertNotNull($definition);
407        $registry->register('users', $definition);
408
409        $parser = new MySqlParser();
410        $schemaParser = new MySqlSchemaParser($parser);
411        $selectTransformer = new SelectTransformer();
412        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
413        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
414        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
415        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
416        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
417        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
418        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
419
420        $plan = $rewriter->rewrite('SELECT * FROM (SELECT id, name FROM users) AS selected');
421
422        self::assertStringStartsWith('WITH `users` AS', $plan->sql());
423    }
424
425    public function testHashCommentDoesNotCreateAPhantomSelectSource(): void
426    {
427        $store = new ShadowStore();
428        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
429        $registry = new TableDefinitionRegistry();
430        $definition = (new MySqlSchemaParser(new MySqlParser()))->parse(<<<'SQL'
431CREATE TABLE users (
432    id INT NOT NULL AUTO_INCREMENT,
433    name VARCHAR(255) NOT NULL,
434    email VARCHAR(255) NOT NULL,
435    PRIMARY KEY (id)
436)
437SQL);
438        self::assertNotNull($definition);
439        $registry->register('users', $definition);
440
441        $parser = new MySqlParser();
442        $schemaParser = new MySqlSchemaParser($parser);
443        $selectTransformer = new SelectTransformer();
444        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
445        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
446        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
447        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
448        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
449        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
450        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
451
452        $plan = $rewriter->rewrite("# SELECT * FROM unknown_table\nSELECT * FROM users");
453
454        self::assertSame(QueryKind::READ, $plan->kind());
455        self::assertStringContainsString('FROM users', $plan->sql());
456    }
457
458    public function testCteReferencesAreMatchedCaseInsensitivelyDuringSchemaValidation(): void
459    {
460        $registry = new TableDefinitionRegistry();
461        $definition = (new MySqlSchemaParser(new MySqlParser()))->parse('CREATE TABLE known_table (id INT PRIMARY KEY)');
462        self::assertNotNull($definition);
463        $registry->register('known_table', $definition);
464
465        $store = new ShadowStore();
466        $parser = new MySqlParser();
467        $schemaParser = new MySqlSchemaParser($parser);
468        $selectTransformer = new SelectTransformer();
469        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
470        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
471        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
472        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
473        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
474        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
475        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
476
477        $plan = $rewriter->rewrite(
478            'WITH users AS (SELECT 1 AS id) SELECT * FROM Users',
479        );
480
481        self::assertSame(QueryKind::READ, $plan->kind());
482    }
483
484    public function testUnknownTableAfterDeclaredCteIsRejected(): void
485    {
486        $registry = new TableDefinitionRegistry();
487        $definition = (new MySqlSchemaParser(new MySqlParser()))->parse('CREATE TABLE known_table (id INT PRIMARY KEY)');
488        self::assertNotNull($definition);
489        $registry->register('known_table', $definition);
490
491        $this->expectException(UnknownSchemaException::class);
492        $this->expectExceptionMessage('missing_table');
493
494        $store = new ShadowStore();
495        $parser = new MySqlParser();
496        $schemaParser = new MySqlSchemaParser($parser);
497        $selectTransformer = new SelectTransformer();
498        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
499        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
500        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
501        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
502        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
503        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
504        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
505
506        $rewriter->rewrite(
507            'WITH users AS (SELECT 1 AS id) SELECT * FROM Users JOIN missing_table ON TRUE',
508        );
509    }
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531    public function testRewriteReadAddsCte(): void
532    {
533        $shadowStore = new ShadowStore();
534        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
535        $parser = new MySqlParser();
536        $schemaParser = new MySqlSchemaParser($parser);
537        $registry = new TableDefinitionRegistry();
538        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
539        self::assertNotNull($def);
540        $registry->register('users', $def);
541
542        $selectTransformer = new SelectTransformer();
543        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
544        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
545        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
546        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
547        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
548        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
549        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
550
551        $plan = $rewriter->rewrite('SELECT * FROM users');
552
553        self::assertSame(QueryKind::READ, $plan->kind());
554        self::assertStringStartsWith('WITH `users` AS', $plan->sql());
555    }
556
557    public function testRewritesSingleSetExpressionsWithoutTreatingThemAsMultipleStatements(): void
558    {
559        $store = new ShadowStore();
560        $registry = new TableDefinitionRegistry();
561        $parser = new MySqlParser();
562        $schemaParser = new MySqlSchemaParser($parser);
563        $selectTransformer = new SelectTransformer();
564        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
565        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
566        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
567        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
568        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
569        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
570        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
571
572
573        $except = $rewriter->rewrite('SELECT 1 EXCEPT SELECT 2');
574        $intersect = $rewriter->rewrite('SELECT 1 INTERSECT SELECT 2');
575        $caseExists = $rewriter->rewrite('SELECT 1 WHERE CASE WHEN EXISTS(SELECT 1) THEN TRUE ELSE FALSE END');
576
577        self::assertSame(QueryKind::READ, $except->kind());
578        self::assertSame('SELECT 1 EXCEPT SELECT 2', $except->sql());
579        self::assertSame(QueryKind::READ, $intersect->kind());
580        self::assertSame('SELECT 1 INTERSECT SELECT 2', $intersect->sql());
581        self::assertSame(QueryKind::READ, $caseExists->kind());
582    }
583
584    public function testCompositeReadRejectsSelectInto(): void
585    {
586        $store = new ShadowStore();
587        $registry = new TableDefinitionRegistry();
588        $parser = new MySqlParser();
589        $schemaParser = new MySqlSchemaParser($parser);
590        $selectTransformer = new SelectTransformer();
591        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
592        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
593        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
594        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
595        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
596        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
597        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
598
599
600        $this->expectException(UnsupportedSqlException::class);
601        $this->expectExceptionMessage('Statement type not supported');
602
603        $rewriter->rewrite('SELECT 1 INTO @result EXCEPT SELECT 2');
604    }
605
606    public function testCompositeReadAllowsTablesWithoutSchemaContext(): void
607    {
608        $store = new ShadowStore();
609        $registry = new TableDefinitionRegistry();
610        $parser = new MySqlParser();
611        $schemaParser = new MySqlSchemaParser($parser);
612        $selectTransformer = new SelectTransformer();
613        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
614        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
615        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
616        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
617        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
618        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
619        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
620
621
622        $plan = $rewriter->rewrite('SELECT * FROM missing EXCEPT SELECT * FROM other');
623
624        self::assertSame(QueryKind::READ, $plan->kind());
625        self::assertSame('SELECT * FROM missing EXCEPT SELECT * FROM other', $plan->sql());
626    }
627
628    public function testCompositeReadRejectsUnknownTableWithSchemaContext(): void
629    {
630        $parser = new MySqlParser();
631        $schemaParser = new MySqlSchemaParser($parser);
632        $registry = new TableDefinitionRegistry();
633        $definition = $schemaParser->parse('CREATE TABLE known (id INT)');
634        self::assertNotNull($definition);
635        $registry->register('known', $definition);
636        $store = new ShadowStore();
637        $parser = new MySqlParser();
638        $schemaParser = new MySqlSchemaParser($parser);
639        $selectTransformer = new SelectTransformer();
640        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
641        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
642        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
643        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
644        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
645        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
646        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
647
648
649        $this->expectException(UnknownSchemaException::class);
650        $this->expectExceptionMessage('unknown_table');
651
652        $rewriter->rewrite('SELECT * FROM known EXCEPT SELECT * FROM unknown_table');
653    }
654
655    public function testRewriteMultipleUsesLexicalStatementBoundaries(): void
656    {
657        $store = new ShadowStore();
658        $registry = new TableDefinitionRegistry();
659        $parser = new MySqlParser();
660        $schemaParser = new MySqlSchemaParser($parser);
661        $selectTransformer = new SelectTransformer();
662        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
663        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
664        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
665        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
666        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
667        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
668        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
669
670
671        $plan = $rewriter->rewriteMultiple('SELECT 1 EXCEPT SELECT 2; SELECT 3');
672
673        self::assertCount(2, $plan->plans());
674        self::assertSame('SELECT 1 EXCEPT SELECT 2', $plan->plans()[0]->sql());
675        self::assertSame('SELECT 3', $plan->plans()[1]->sql());
676    }
677
678    public function testRewriteUpdateCreatesMutation(): void
679    {
680        $shadowStore = new ShadowStore();
681        $parser = new MySqlParser();
682        $schemaParser = new MySqlSchemaParser($parser);
683        $registry = new TableDefinitionRegistry();
684        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
685        self::assertNotNull($def);
686        $registry->register('users', $def);
687
688        $selectTransformer = new SelectTransformer();
689        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
690        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
691        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
692        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
693        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
694        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
695        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
696
697        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
698
699        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
700        self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
701        self::assertSame('users', $plan->mutation()->tableName());
702        self::assertStringContainsString('`users`.`id` AS `__ztd_original_id`', $plan->sql());
703        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql(), 'UPDATE result-select must start with SELECT or WITH...SELECT');
704    }
705
706    public function testRewriteInsertCreatesMutation(): void
707    {
708        $shadowStore = new ShadowStore();
709        $parser = new MySqlParser();
710        $schemaParser = new MySqlSchemaParser($parser);
711        $registry = new TableDefinitionRegistry();
712        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
713        self::assertNotNull($def);
714        $registry->register('users', $def);
715
716        $selectTransformer = new SelectTransformer();
717        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
718        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
719        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
720        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
721        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
722        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
723        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
724
725        $plan = $rewriter->rewrite("INSERT INTO users (id, name) VALUES (1, 'Alice')");
726
727        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
728        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
729        self::assertSame('users', $plan->mutation()->tableName());
730        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql(), 'INSERT result-select must start with SELECT or WITH...SELECT');
731    }
732
733    public function testRewriteInsertUsesDefaultsFromRegistryWithoutShadowRows(): void
734    {
735        $schemaParser = new MySqlSchemaParser(new MySqlParser());
736        $definition = $schemaParser->parse("CREATE TABLE settings (id INT, label VARCHAR(20) DEFAULT 'new')");
737        self::assertNotNull($definition);
738        $registry = new TableDefinitionRegistry();
739        $registry->register('settings', $definition);
740        $store = new ShadowStore();
741        $parser = new MySqlParser();
742        $schemaParser = new MySqlSchemaParser($parser);
743        $selectTransformer = new SelectTransformer();
744        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
745        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
746        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
747        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
748        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
749        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
750        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
751
752
753        $plan = $rewriter->rewrite('INSERT INTO settings (id) VALUES (1)');
754
755        self::assertStringContainsString("'new'", $plan->sql());
756        self::assertStringContainsString('AS `label`', $plan->sql());
757    }
758
759    public function testRewriteInsertUsesIdentityStrategyFromRegistryWithoutShadowRows(): void
760    {
761        $schemaParser = new MySqlSchemaParser(new MySqlParser());
762        $definition = $schemaParser->parse('CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(20))');
763        self::assertNotNull($definition);
764        $registry = new TableDefinitionRegistry();
765        $registry->register('users', $definition);
766        $store = new ShadowStore();
767        $parser = new MySqlParser();
768        $schemaParser = new MySqlSchemaParser($parser);
769        $selectTransformer = new SelectTransformer();
770        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
771        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
772        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
773        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
774        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
775        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
776        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
777
778
779        $plan = $rewriter->rewrite("INSERT INTO users (name) VALUES ('Alice')");
780
781        self::assertStringContainsString('CAST(1 AS SIGNED) AS `id`', $plan->sql());
782    }
783
784    public function testRewriteDeleteCreatesMutation(): void
785    {
786        $shadowStore = new ShadowStore();
787        $parser = new MySqlParser();
788        $schemaParser = new MySqlSchemaParser($parser);
789        $registry = new TableDefinitionRegistry();
790        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
791        self::assertNotNull($def);
792        $registry->register('users', $def);
793
794        $selectTransformer = new SelectTransformer();
795        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
796        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
797        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
798        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
799        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
800        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
801        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
802
803        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
804
805        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
806        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
807        self::assertSame('users', $plan->mutation()->tableName());
808        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql(), 'DELETE result-select must start with SELECT or WITH...SELECT');
809    }
810
811    public function testRewriteForbiddenStatementThrowsException(): void
812    {
813        $shadowStore = new ShadowStore();
814        $parser = new MySqlParser();
815        $registry = new TableDefinitionRegistry();
816
817        $schemaParser = new MySqlSchemaParser($parser);
818        $selectTransformer = new SelectTransformer();
819        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
820        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
821        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
822        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
823        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
824        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
825        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
826
827        self::expectException(UnsupportedSqlException::class);
828        self::expectExceptionMessage('Statement type not supported');
829        $rewriter->rewrite('CREATE DATABASE test');
830    }
831
832    public function testRewriteTruncateCreatesMutation(): void
833    {
834        $shadowStore = new ShadowStore();
835        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
836        $parser = new MySqlParser();
837        $schemaParser = new MySqlSchemaParser($parser);
838        $registry = new TableDefinitionRegistry();
839        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
840        self::assertNotNull($def);
841        $registry->register('users', $def);
842
843        $selectTransformer = new SelectTransformer();
844        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
845        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
846        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
847        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
848        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
849        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
850        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
851
852        $plan = $rewriter->rewrite('TRUNCATE TABLE users');
853
854        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
855        self::assertInstanceOf(TruncateMutation::class, $plan->mutation());
856        self::assertSame('users', $plan->mutation()->tableName());
857        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql(), 'TRUNCATE result-select must start with SELECT or WITH...SELECT');
858    }
859
860    public function testTruncateMutationClearsTable(): void
861    {
862        $shadowStore = new ShadowStore();
863        $shadowStore->set('users', [
864            ['id' => 1, 'name' => 'Alice'],
865            ['id' => 2, 'name' => 'Bob'],
866        ]);
867        $parser = new MySqlParser();
868        $schemaParser = new MySqlSchemaParser($parser);
869        $registry = new TableDefinitionRegistry();
870        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
871        self::assertNotNull($def);
872        $registry->register('users', $def);
873
874        $selectTransformer = new SelectTransformer();
875        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
876        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
877        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
878        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
879        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
880        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
881        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
882
883        $plan = $rewriter->rewrite('TRUNCATE TABLE users');
884
885        $mutation = $plan->mutation();
886        self::assertInstanceOf(TruncateMutation::class, $mutation);
887        $mutation->apply($shadowStore, []);
888
889        self::assertSame([], $shadowStore->get('users'));
890    }
891
892    public function testRewriteReplaceCreatesMutation(): void
893    {
894        $shadowStore = new ShadowStore();
895        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
896        $parser = new MySqlParser();
897        $schemaParser = new MySqlSchemaParser($parser);
898        $registry = new TableDefinitionRegistry();
899        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
900        self::assertNotNull($def);
901        $registry->register('users', $def);
902
903        $selectTransformer = new SelectTransformer();
904        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
905        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
906        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
907        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
908        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
909        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
910        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
911
912        $plan = $rewriter->rewrite("REPLACE INTO users (id, name) VALUES (1, 'Bob')");
913
914        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
915        self::assertInstanceOf(ReplaceMutation::class, $plan->mutation());
916        self::assertSame('users', $plan->mutation()->tableName());
917        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql(), 'REPLACE result-select must start with SELECT or WITH...SELECT');
918    }
919
920    public function testReplaceMutationReplacesExistingRow(): void
921    {
922        $shadowStore = new ShadowStore();
923        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
924        $parser = new MySqlParser();
925        $schemaParser = new MySqlSchemaParser($parser);
926        $registry = new TableDefinitionRegistry();
927        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
928        self::assertNotNull($def);
929        $registry->register('users', $def);
930
931        $selectTransformer = new SelectTransformer();
932        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
933        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
934        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
935        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
936        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
937        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
938        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
939
940        $plan = $rewriter->rewrite("REPLACE INTO users (id, name) VALUES (1, 'Bob')");
941
942        $mutation = $plan->mutation();
943        self::assertInstanceOf(ReplaceMutation::class, $mutation);
944        $mutation->apply($shadowStore, [['id' => 1, 'name' => 'Bob']]);
945
946        $rows = $shadowStore->get('users');
947        self::assertCount(1, $rows);
948        self::assertSame('Bob', $rows[0]['name']);
949    }
950
951    public function testReplaceMutationInsertsNewRow(): void
952    {
953        $shadowStore = new ShadowStore();
954        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
955        $parser = new MySqlParser();
956        $schemaParser = new MySqlSchemaParser($parser);
957        $registry = new TableDefinitionRegistry();
958        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
959        self::assertNotNull($def);
960        $registry->register('users', $def);
961
962        $selectTransformer = new SelectTransformer();
963        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
964        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
965        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
966        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
967        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
968        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
969        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
970
971        $plan = $rewriter->rewrite("REPLACE INTO users (id, name) VALUES (2, 'Bob')");
972
973        $mutation = $plan->mutation();
974        self::assertInstanceOf(ReplaceMutation::class, $mutation);
975        $mutation->apply($shadowStore, [['id' => 2, 'name' => 'Bob']]);
976
977        $rows = $shadowStore->get('users');
978        self::assertCount(2, $rows);
979    }
980
981    public function testRewriteCreateTableCreatesMutation(): void
982    {
983        $shadowStore = new ShadowStore();
984        $parser = new MySqlParser();
985        $registry = new TableDefinitionRegistry();
986
987        $schemaParser = new MySqlSchemaParser($parser);
988        $selectTransformer = new SelectTransformer();
989        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
990        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
991        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
992        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
993        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
994        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
995        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
996
997        $plan = $rewriter->rewrite('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
998
999        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1000        self::assertInstanceOf(CreateTableMutation::class, $plan->mutation());
1001    }
1002
1003    public function testCreateTableMutationRegistersSchema(): void
1004    {
1005        $shadowStore = new ShadowStore();
1006        $parser = new MySqlParser();
1007        $registry = new TableDefinitionRegistry();
1008
1009        $schemaParser = new MySqlSchemaParser($parser);
1010        $selectTransformer = new SelectTransformer();
1011        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1012        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1013        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1014        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1015        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1016        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1017        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1018
1019        $plan = $rewriter->rewrite('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1020
1021        $mutation = $plan->mutation();
1022        self::assertInstanceOf(CreateTableMutation::class, $mutation);
1023        $mutation->apply($shadowStore, []);
1024
1025        $definition = $registry->get('users');
1026        self::assertNotNull($definition);
1027        self::assertContains('id', $definition->columns);
1028        self::assertContains('name', $definition->columns);
1029
1030        self::assertSame([], $shadowStore->get('users'));
1031    }
1032
1033    public function testCreateTableIfNotExistsSkipsExistingTable(): void
1034    {
1035        $shadowStore = new ShadowStore();
1036        $parser = new MySqlParser();
1037        $schemaParser = new MySqlSchemaParser($parser);
1038        $registry = new TableDefinitionRegistry();
1039        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY)');
1040        self::assertNotNull($def);
1041        $registry->register('users', $def);
1042
1043        $selectTransformer = new SelectTransformer();
1044        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1045        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1046        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1047        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1048        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1049        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1050        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1051
1052        $plan = $rewriter->rewrite('CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))');
1053
1054        $mutation = $plan->mutation();
1055        self::assertInstanceOf(CreateTableMutation::class, $mutation);
1056
1057        $mutation->apply($shadowStore, []);
1058
1059        $definition = $registry->get('users');
1060        self::assertNotNull($definition);
1061        self::assertContains('id', $definition->columns);
1062        self::assertNotContains('email', $definition->columns);
1063    }
1064
1065    public function testRewriteDropTableCreatesMutation(): void
1066    {
1067        $shadowStore = new ShadowStore();
1068        $parser = new MySqlParser();
1069        $schemaParser = new MySqlSchemaParser($parser);
1070        $registry = new TableDefinitionRegistry();
1071        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1072        self::assertNotNull($def);
1073        $registry->register('users', $def);
1074
1075        $selectTransformer = new SelectTransformer();
1076        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1077        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1078        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1079        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1080        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1081        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1082        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1083
1084        $plan = $rewriter->rewrite('DROP TABLE users');
1085
1086        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1087        self::assertInstanceOf(DropTableMutation::class, $plan->mutation());
1088    }
1089
1090    public function testDropTableMutationUnregistersSchema(): void
1091    {
1092        $shadowStore = new ShadowStore();
1093        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
1094        $parser = new MySqlParser();
1095        $schemaParser = new MySqlSchemaParser($parser);
1096        $registry = new TableDefinitionRegistry();
1097        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1098        self::assertNotNull($def);
1099        $registry->register('users', $def);
1100
1101        $selectTransformer = new SelectTransformer();
1102        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1103        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1104        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1105        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1106        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1107        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1108        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1109
1110        $plan = $rewriter->rewrite('DROP TABLE users');
1111
1112        $mutation = $plan->mutation();
1113        self::assertInstanceOf(DropTableMutation::class, $mutation);
1114        $mutation->apply($shadowStore, []);
1115
1116        self::assertNull($registry->get('users'));
1117
1118        self::assertSame([], $shadowStore->get('users'));
1119    }
1120
1121    public function testDropTableIfExistsSkipsNonExistentTable(): void
1122    {
1123        $shadowStore = new ShadowStore();
1124        $parser = new MySqlParser();
1125        $registry = new TableDefinitionRegistry();
1126
1127        $schemaParser = new MySqlSchemaParser($parser);
1128        $selectTransformer = new SelectTransformer();
1129        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1130        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1131        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1132        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1133        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1134        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1135        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1136
1137        $plan = $rewriter->rewrite('DROP TABLE IF EXISTS users');
1138
1139        $mutation = $plan->mutation();
1140        self::assertInstanceOf(DropTableMutation::class, $mutation);
1141
1142        $mutation->apply($shadowStore, []);
1143    }
1144
1145    public function testDropNonExistentTableThrowsUnknownSchemaException(): void
1146    {
1147        $shadowStore = new ShadowStore();
1148        $parser = new MySqlParser();
1149        $registry = new TableDefinitionRegistry();
1150
1151        $schemaParser = new MySqlSchemaParser($parser);
1152        $selectTransformer = new SelectTransformer();
1153        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1154        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1155        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1156        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1157        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1158        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1159        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1160
1161        self::expectException(UnknownSchemaException::class);
1162        self::expectExceptionMessage('Unknown table');
1163
1164        $rewriter->rewrite('DROP TABLE users');
1165    }
1166
1167    public function testRewriteAlterTableAddColumnCreatesMutation(): void
1168    {
1169        $shadowStore = new ShadowStore();
1170        $parser = new MySqlParser();
1171        $schemaParser = new MySqlSchemaParser($parser);
1172        $registry = new TableDefinitionRegistry();
1173        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1174        self::assertNotNull($def);
1175        $registry->register('users', $def);
1176
1177        $selectTransformer = new SelectTransformer();
1178        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1179        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1180        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1181        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1182        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1183        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1184        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1185
1186        $plan = $rewriter->rewrite('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
1187
1188        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1189        self::assertInstanceOf(AlterTableMutation::class, $plan->mutation());
1190    }
1191
1192    public function testAlterTableAddColumnModifiesSchema(): void
1193    {
1194        $shadowStore = new ShadowStore();
1195        $parser = new MySqlParser();
1196        $schemaParser = new MySqlSchemaParser($parser);
1197        $registry = new TableDefinitionRegistry();
1198        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1199        self::assertNotNull($def);
1200        $registry->register('users', $def);
1201
1202        $selectTransformer = new SelectTransformer();
1203        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1204        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1205        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1206        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1207        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1208        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1209        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1210
1211        $plan = $rewriter->rewrite('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
1212
1213        $mutation = $plan->mutation();
1214        self::assertInstanceOf(AlterTableMutation::class, $mutation);
1215        $mutation->apply($shadowStore, []);
1216
1217        $definition = $registry->get('users');
1218        self::assertNotNull($definition);
1219        self::assertContains('id', $definition->columns);
1220        self::assertContains('name', $definition->columns);
1221        self::assertContains('email', $definition->columns);
1222    }
1223
1224    public function testAlterTableDropColumnModifiesSchema(): void
1225    {
1226        $shadowStore = new ShadowStore();
1227        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
1228        $parser = new MySqlParser();
1229        $schemaParser = new MySqlSchemaParser($parser);
1230        $registry = new TableDefinitionRegistry();
1231        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))');
1232        self::assertNotNull($def);
1233        $registry->register('users', $def);
1234
1235        $selectTransformer = new SelectTransformer();
1236        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1237        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1238        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1239        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1240        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1241        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1242        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1243
1244        $plan = $rewriter->rewrite('ALTER TABLE users DROP COLUMN email');
1245
1246        $mutation = $plan->mutation();
1247        self::assertInstanceOf(AlterTableMutation::class, $mutation);
1248        $mutation->apply($shadowStore, []);
1249
1250        $definition = $registry->get('users');
1251        self::assertNotNull($definition);
1252        self::assertContains('id', $definition->columns);
1253        self::assertContains('name', $definition->columns);
1254        self::assertNotContains('email', $definition->columns);
1255
1256        $rows = $shadowStore->get('users');
1257        self::assertCount(1, $rows);
1258        self::assertArrayNotHasKey('email', $rows[0]);
1259    }
1260
1261    public function testAlterTableModifyColumnModifiesSchema(): void
1262    {
1263        $shadowStore = new ShadowStore();
1264        $parser = new MySqlParser();
1265        $schemaParser = new MySqlSchemaParser($parser);
1266        $registry = new TableDefinitionRegistry();
1267        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))');
1268        self::assertNotNull($def);
1269        $registry->register('users', $def);
1270
1271        $selectTransformer = new SelectTransformer();
1272        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1273        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1274        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1275        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1276        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1277        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1278        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1279
1280        $plan = $rewriter->rewrite('ALTER TABLE users MODIFY COLUMN name VARCHAR(500)');
1281
1282        $mutation = $plan->mutation();
1283        self::assertInstanceOf(AlterTableMutation::class, $mutation);
1284        $mutation->apply($shadowStore, []);
1285
1286        $definition = $registry->get('users');
1287        self::assertNotNull($definition);
1288        self::assertSame('VARCHAR(500)', $definition->columnTypes['name']);
1289    }
1290
1291    public function testAlterTableChangeColumnRenamesColumn(): void
1292    {
1293        $shadowStore = new ShadowStore();
1294        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
1295        $parser = new MySqlParser();
1296        $schemaParser = new MySqlSchemaParser($parser);
1297        $registry = new TableDefinitionRegistry();
1298        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1299        self::assertNotNull($def);
1300        $registry->register('users', $def);
1301
1302        $selectTransformer = new SelectTransformer();
1303        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1304        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1305        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1306        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1307        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1308        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1309        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1310
1311        $plan = $rewriter->rewrite('ALTER TABLE users CHANGE COLUMN name full_name VARCHAR(255)');
1312
1313        $mutation = $plan->mutation();
1314        self::assertInstanceOf(AlterTableMutation::class, $mutation);
1315        $mutation->apply($shadowStore, []);
1316
1317        $definition = $registry->get('users');
1318        self::assertNotNull($definition);
1319        self::assertContains('id', $definition->columns);
1320        self::assertContains('full_name', $definition->columns);
1321        self::assertNotContains('name', $definition->columns);
1322
1323        $rows = $shadowStore->get('users');
1324        self::assertCount(1, $rows);
1325        self::assertArrayHasKey('full_name', $rows[0]);
1326        self::assertSame('Alice', $rows[0]['full_name']);
1327        self::assertArrayNotHasKey('name', $rows[0]);
1328    }
1329
1330    public function testAlterTableRenameRenamesTable(): void
1331    {
1332        $shadowStore = new ShadowStore();
1333        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
1334        $parser = new MySqlParser();
1335        $schemaParser = new MySqlSchemaParser($parser);
1336        $registry = new TableDefinitionRegistry();
1337        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1338        self::assertNotNull($def);
1339        $registry->register('users', $def);
1340
1341        $selectTransformer = new SelectTransformer();
1342        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1343        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1344        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1345        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1346        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1347        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1348        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1349
1350        $plan = $rewriter->rewrite('ALTER TABLE users RENAME TO members');
1351
1352        $mutation = $plan->mutation();
1353        self::assertInstanceOf(AlterTableMutation::class, $mutation);
1354        $mutation->apply($shadowStore, []);
1355
1356        self::assertNull($registry->get('users'));
1357        $definition = $registry->get('members');
1358        self::assertNotNull($definition);
1359        self::assertContains('id', $definition->columns);
1360        self::assertContains('name', $definition->columns);
1361
1362        self::assertSame([], $shadowStore->get('users'));
1363        $rows = $shadowStore->get('members');
1364        self::assertCount(1, $rows);
1365        self::assertSame('Alice', $rows[0]['name']);
1366    }
1367
1368    public function testAlterNonExistentTableThrowsUnknownSchemaException(): void
1369    {
1370        $shadowStore = new ShadowStore();
1371        $parser = new MySqlParser();
1372        $registry = new TableDefinitionRegistry();
1373
1374        $schemaParser = new MySqlSchemaParser($parser);
1375        $selectTransformer = new SelectTransformer();
1376        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1377        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1378        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1379        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1380        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1381        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1382        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1383
1384        self::expectException(UnknownSchemaException::class);
1385        self::expectExceptionMessage('Unknown table');
1386
1387        $rewriter->rewrite('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
1388    }
1389
1390    public function testRewriteMultipleProcessesEachStatement(): void
1391    {
1392        $shadowStore = new ShadowStore();
1393        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
1394        $parser = new MySqlParser();
1395        $schemaParser = new MySqlSchemaParser($parser);
1396        $registry = new TableDefinitionRegistry();
1397        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1398        self::assertNotNull($def);
1399        $registry->register('users', $def);
1400
1401        $selectTransformer = new SelectTransformer();
1402        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1403        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1404        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1405        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1406        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1407        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1408        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1409
1410        $multiPlan = $rewriter->rewriteMultiple('SELECT * FROM users; INSERT INTO users (id, name) VALUES (2, \'Bob\')');
1411
1412        self::assertSame(2, $multiPlan->count());
1413        $firstPlan = $multiPlan->get(0);
1414        self::assertNotNull($firstPlan);
1415        self::assertSame(QueryKind::READ, $firstPlan->kind());
1416
1417        $secondPlan = $multiPlan->get(1);
1418        self::assertNotNull($secondPlan);
1419        self::assertSame(QueryKind::WRITE_SIMULATED, $secondPlan->kind());
1420        self::assertInstanceOf(InsertMutation::class, $secondPlan->mutation());
1421    }
1422
1423    public function testRewriteMultipleWithForbiddenStatementThrowsException(): void
1424    {
1425        $shadowStore = new ShadowStore();
1426        $parser = new MySqlParser();
1427        $registry = new TableDefinitionRegistry();
1428
1429        $schemaParser = new MySqlSchemaParser($parser);
1430        $selectTransformer = new SelectTransformer();
1431        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1432        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1433        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1434        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1435        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1436        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1437        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1438
1439        self::expectException(UnsupportedSqlException::class);
1440
1441        $rewriter->rewriteMultiple('SELECT 1; DROP DATABASE test');
1442    }
1443
1444    public function testRewriteSingleStatementStillWorks(): void
1445    {
1446        $shadowStore = new ShadowStore();
1447        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
1448        $parser = new MySqlParser();
1449        $schemaParser = new MySqlSchemaParser($parser);
1450        $registry = new TableDefinitionRegistry();
1451        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1452        self::assertNotNull($def);
1453        $registry->register('users', $def);
1454
1455        $selectTransformer = new SelectTransformer();
1456        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1457        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1458        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1459        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1460        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1461        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1462        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1463
1464        $plan = $rewriter->rewrite('SELECT * FROM users');
1465        self::assertSame(QueryKind::READ, $plan->kind());
1466    }
1467
1468    public function testRewriteMultipleStatementsThrowsException(): void
1469    {
1470        $shadowStore = new ShadowStore();
1471        $parser = new MySqlParser();
1472        $registry = new TableDefinitionRegistry();
1473
1474        $schemaParser = new MySqlSchemaParser($parser);
1475        $selectTransformer = new SelectTransformer();
1476        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1477        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1478        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1479        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1480        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1481        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1482        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1483
1484        self::expectException(UnsupportedSqlException::class);
1485        self::expectExceptionMessage('Multi-statement');
1486        $rewriter->rewrite('SELECT 1; SELECT 2');
1487    }
1488
1489    public function testRewriteCreateTableLikeCreatesMutation(): void
1490    {
1491        $shadowStore = new ShadowStore();
1492        $parser = new MySqlParser();
1493        $schemaParser = new MySqlSchemaParser($parser);
1494        $registry = new TableDefinitionRegistry();
1495        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))');
1496        self::assertNotNull($def);
1497        $registry->register('users', $def);
1498
1499        $selectTransformer = new SelectTransformer();
1500        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1501        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1502        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1503        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1504        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1505        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1506        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1507
1508        $plan = $rewriter->rewrite('CREATE TABLE members LIKE users');
1509
1510        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1511        self::assertInstanceOf(CreateTableLikeMutation::class, $plan->mutation());
1512    }
1513
1514    public function testCreateTableLikeCopiesSchema(): void
1515    {
1516        $shadowStore = new ShadowStore();
1517        $parser = new MySqlParser();
1518        $schemaParser = new MySqlSchemaParser($parser);
1519        $registry = new TableDefinitionRegistry();
1520        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))');
1521        self::assertNotNull($def);
1522        $registry->register('users', $def);
1523
1524        $selectTransformer = new SelectTransformer();
1525        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1526        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1527        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1528        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1529        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1530        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1531        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1532
1533        $plan = $rewriter->rewrite('CREATE TABLE members LIKE users');
1534
1535        $mutation = $plan->mutation();
1536        self::assertInstanceOf(CreateTableLikeMutation::class, $mutation);
1537        $mutation->apply($shadowStore, []);
1538
1539        $definition = $registry->get('members');
1540        self::assertNotNull($definition);
1541        self::assertContains('id', $definition->columns);
1542        self::assertContains('name', $definition->columns);
1543        self::assertContains('email', $definition->columns);
1544
1545        self::assertSame([], $shadowStore->get('members'));
1546    }
1547
1548    public function testCreateTableLikeWithUnknownSourceThrowsUnknownSchemaException(): void
1549    {
1550        $shadowStore = new ShadowStore();
1551        $parser = new MySqlParser();
1552        $registry = new TableDefinitionRegistry();
1553
1554        $schemaParser = new MySqlSchemaParser($parser);
1555        $selectTransformer = new SelectTransformer();
1556        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1557        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1558        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1559        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1560        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1561        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1562        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1563
1564        self::expectException(UnknownSchemaException::class);
1565        self::expectExceptionMessage('Unknown table');
1566
1567        $rewriter->rewrite('CREATE TABLE members LIKE users');
1568    }
1569
1570    public function testRewriteCreateTableAsSelectCreatesMutation(): void
1571    {
1572        $shadowStore = new ShadowStore();
1573        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
1574        $parser = new MySqlParser();
1575        $schemaParser = new MySqlSchemaParser($parser);
1576        $registry = new TableDefinitionRegistry();
1577        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1578        self::assertNotNull($def);
1579        $registry->register('users', $def);
1580
1581        $selectTransformer = new SelectTransformer();
1582        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1583        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1584        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1585        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1586        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1587        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1588        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1589
1590        $plan = $rewriter->rewrite('CREATE TABLE active_users AS SELECT id, name FROM users WHERE id > 0');
1591
1592        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1593        self::assertInstanceOf(CreateTableAsSelectMutation::class, $plan->mutation());
1594    }
1595
1596    public function testCreateTableAsSelectCreatesTableWithData(): void
1597    {
1598        $shadowStore = new ShadowStore();
1599        $shadowStore->set('users', [
1600            ['id' => 1, 'name' => 'Alice'],
1601            ['id' => 2, 'name' => 'Bob'],
1602        ]);
1603        $parser = new MySqlParser();
1604        $schemaParser = new MySqlSchemaParser($parser);
1605        $registry = new TableDefinitionRegistry();
1606        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1607        self::assertNotNull($def);
1608        $registry->register('users', $def);
1609
1610        $selectTransformer = new SelectTransformer();
1611        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1612        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1613        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1614        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1615        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1616        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1617        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1618
1619        $plan = $rewriter->rewrite('CREATE TABLE active_users AS SELECT id, name FROM users');
1620
1621        $mutation = $plan->mutation();
1622        self::assertInstanceOf(CreateTableAsSelectMutation::class, $mutation);
1623
1624        $mutation->apply($shadowStore, [['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob']]);
1625
1626        $definition = $registry->get('active_users');
1627        self::assertNotNull($definition);
1628        self::assertContains('id', $definition->columns);
1629        self::assertContains('name', $definition->columns);
1630
1631        $rows = $shadowStore->get('active_users');
1632        self::assertCount(2, $rows);
1633    }
1634
1635    public function testRewriteCreateTemporaryTableCreatesMutation(): void
1636    {
1637        $shadowStore = new ShadowStore();
1638        $parser = new MySqlParser();
1639        $registry = new TableDefinitionRegistry();
1640
1641        $schemaParser = new MySqlSchemaParser($parser);
1642        $selectTransformer = new SelectTransformer();
1643        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1644        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1645        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1646        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1647        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1648        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1649        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1650
1651        $plan = $rewriter->rewrite('CREATE TEMPORARY TABLE temp_users (id INT, name VARCHAR(255))');
1652
1653        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1654        self::assertInstanceOf(CreateTableMutation::class, $plan->mutation());
1655    }
1656
1657    public function testCreateTemporaryTableRegistersSchema(): void
1658    {
1659        $shadowStore = new ShadowStore();
1660        $parser = new MySqlParser();
1661        $registry = new TableDefinitionRegistry();
1662
1663        $schemaParser = new MySqlSchemaParser($parser);
1664        $selectTransformer = new SelectTransformer();
1665        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1666        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1667        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1668        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1669        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1670        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1671        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1672
1673        $plan = $rewriter->rewrite('CREATE TEMPORARY TABLE temp_users (id INT, name VARCHAR(255))');
1674
1675        $mutation = $plan->mutation();
1676        self::assertInstanceOf(CreateTableMutation::class, $mutation);
1677        $mutation->apply($shadowStore, []);
1678
1679        $definition = $registry->get('temp_users');
1680        self::assertNotNull($definition);
1681        self::assertContains('id', $definition->columns);
1682        self::assertContains('name', $definition->columns);
1683    }
1684
1685    public function testReplaceWithEmptyValuesThrowsException(): void
1686    {
1687        $shadowStore = new ShadowStore();
1688        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
1689        $parser = new MySqlParser();
1690        $schemaParser = new MySqlSchemaParser($parser);
1691        $registry = new TableDefinitionRegistry();
1692        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1693        self::assertNotNull($def);
1694        $registry->register('users', $def);
1695
1696        $selectTransformer = new SelectTransformer();
1697        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1698        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1699        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1700        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1701        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1702        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1703        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1704
1705        self::expectException(UnsupportedSqlException::class);
1706
1707        $rewriter->rewrite('REPLACE INTO users VALUE( )');
1708    }
1709
1710    public function testReplaceWithMismatchedColumnCountThrowsException(): void
1711    {
1712        $shadowStore = new ShadowStore();
1713        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
1714        $parser = new MySqlParser();
1715        $schemaParser = new MySqlSchemaParser($parser);
1716        $registry = new TableDefinitionRegistry();
1717        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1718        self::assertNotNull($def);
1719        $registry->register('users', $def);
1720
1721        $selectTransformer = new SelectTransformer();
1722        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1723        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1724        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1725        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1726        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1727        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1728        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1729
1730        self::expectException(UnsupportedSqlException::class);
1731
1732        $rewriter->rewrite('REPLACE INTO users (id, name) VALUES (1)');
1733    }
1734
1735    public function testRewriteSelectWithUnknownTableThrowsUnknownSchemaException(): void
1736    {
1737        $shadowStore = new ShadowStore();
1738        $shadowStore->set('known', [['id' => 1]]);
1739        $parser = new MySqlParser();
1740        $schemaParser = new MySqlSchemaParser($parser);
1741        $registry = new TableDefinitionRegistry();
1742        $def = $schemaParser->parse('CREATE TABLE known (id INT)');
1743        self::assertNotNull($def);
1744        $registry->register('known', $def);
1745
1746        $selectTransformer = new SelectTransformer();
1747        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1748        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1749        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1750        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1751        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1752        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1753        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1754
1755        self::expectException(UnknownSchemaException::class);
1756        $rewriter->rewrite('SELECT * FROM unknown_table');
1757    }
1758
1759    public function testRewriteSelectWithJoinAndUnknownTableThrowsUnknownSchemaException(): void
1760    {
1761        $shadowStore = new ShadowStore();
1762        $shadowStore->set('known', [['id' => 1]]);
1763        $parser = new MySqlParser();
1764        $schemaParser = new MySqlSchemaParser($parser);
1765        $registry = new TableDefinitionRegistry();
1766        $def = $schemaParser->parse('CREATE TABLE known (id INT)');
1767        self::assertNotNull($def);
1768        $registry->register('known', $def);
1769
1770        $selectTransformer = new SelectTransformer();
1771        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1772        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1773        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1774        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1775        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1776        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1777        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1778
1779        self::expectException(UnknownSchemaException::class);
1780        $rewriter->rewrite('SELECT * FROM known JOIN unknown_table ON known.id = unknown_table.id');
1781    }
1782
1783    public function testRewriteSelectWithNoSchemaContextDoesNotThrow(): void
1784    {
1785        $shadowStore = new ShadowStore();
1786        $parser = new MySqlParser();
1787        $schemaParser = new MySqlSchemaParser($parser);
1788        $registry = new TableDefinitionRegistry();
1789
1790        $selectTransformer = new SelectTransformer();
1791        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1792        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1793        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1794        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1795        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1796        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1797        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1798
1799        $plan = $rewriter->rewrite('SELECT * FROM anything');
1800        self::assertSame(QueryKind::READ, $plan->kind());
1801        self::assertSame('SELECT * FROM anything', $plan->sql());
1802    }
1803
1804    public function testRewriteUpdateEnsuresDmlTableInShadowStore(): void
1805    {
1806        $shadowStore = new ShadowStore();
1807        $parser = new MySqlParser();
1808        $schemaParser = new MySqlSchemaParser($parser);
1809        $registry = new TableDefinitionRegistry();
1810        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
1811        self::assertNotNull($def);
1812        $registry->register('users', $def);
1813
1814        $selectTransformer = new SelectTransformer();
1815        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1816        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1817        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1818        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1819        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1820        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1821        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1822
1823        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
1824        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1825        self::assertSame([], $shadowStore->get('users'));
1826    }
1827
1828    public function testRewriteDeleteEnsuresDmlTableInShadowStore(): void
1829    {
1830        $shadowStore = new ShadowStore();
1831        $parser = new MySqlParser();
1832        $schemaParser = new MySqlSchemaParser($parser);
1833        $registry = new TableDefinitionRegistry();
1834        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
1835        self::assertNotNull($def);
1836        $registry->register('users', $def);
1837
1838        $selectTransformer = new SelectTransformer();
1839        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1840        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1841        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1842        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1843        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1844        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1845        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1846
1847        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1848        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1849        self::assertSame([], $shadowStore->get('users'));
1850    }
1851
1852    public function testRewriteAlterTableWithUnsupportedSetDefaultThrows(): void
1853    {
1854        $shadowStore = new ShadowStore();
1855        $parser = new MySqlParser();
1856        $schemaParser = new MySqlSchemaParser($parser);
1857        $registry = new TableDefinitionRegistry();
1858        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
1859        self::assertNotNull($def);
1860        $registry->register('users', $def);
1861
1862        $selectTransformer = new SelectTransformer();
1863        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1864        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1865        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1866        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1867        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1868        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1869        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1870
1871        self::expectException(UnsupportedSqlException::class);
1872        $rewriter->rewrite("ALTER TABLE users ALTER COLUMN name SET DEFAULT 'foo'");
1873    }
1874
1875    public function testRewriteAlterTableWithDropDefaultThrows(): void
1876    {
1877        $shadowStore = new ShadowStore();
1878        $parser = new MySqlParser();
1879        $schemaParser = new MySqlSchemaParser($parser);
1880        $registry = new TableDefinitionRegistry();
1881        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
1882        self::assertNotNull($def);
1883        $registry->register('users', $def);
1884
1885        $selectTransformer = new SelectTransformer();
1886        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1887        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1888        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1889        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1890        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1891        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1892        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1893
1894        self::expectException(UnsupportedSqlException::class);
1895        $rewriter->rewrite('ALTER TABLE users ALTER COLUMN name DROP DEFAULT');
1896    }
1897
1898    public function testRewriteAlterTableWithOrderByThrows(): void
1899    {
1900        $shadowStore = new ShadowStore();
1901        $parser = new MySqlParser();
1902        $schemaParser = new MySqlSchemaParser($parser);
1903        $registry = new TableDefinitionRegistry();
1904        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
1905        self::assertNotNull($def);
1906        $registry->register('users', $def);
1907
1908        $selectTransformer = new SelectTransformer();
1909        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1910        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1911        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1912        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1913        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1914        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1915        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1916
1917        self::expectException(UnsupportedSqlException::class);
1918        $rewriter->rewrite('ALTER TABLE users ORDER BY name');
1919    }
1920
1921    public function testRewriteAlterTableAddIndexThrows(): void
1922    {
1923        $shadowStore = new ShadowStore();
1924        $parser = new MySqlParser();
1925        $schemaParser = new MySqlSchemaParser($parser);
1926        $registry = new TableDefinitionRegistry();
1927        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
1928        self::assertNotNull($def);
1929        $registry->register('users', $def);
1930
1931        $selectTransformer = new SelectTransformer();
1932        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1933        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1934        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1935        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1936        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1937        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1938        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1939
1940        self::expectException(UnsupportedSqlException::class);
1941        $rewriter->rewrite('ALTER TABLE users ADD INDEX idx_name (name)');
1942    }
1943
1944    public function testRewriteAlterTableDropIndexThrows(): void
1945    {
1946        $shadowStore = new ShadowStore();
1947        $parser = new MySqlParser();
1948        $schemaParser = new MySqlSchemaParser($parser);
1949        $registry = new TableDefinitionRegistry();
1950        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
1951        self::assertNotNull($def);
1952        $registry->register('users', $def);
1953
1954        $selectTransformer = new SelectTransformer();
1955        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1956        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1957        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1958        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1959        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1960        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1961        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1962
1963        self::expectException(UnsupportedSqlException::class);
1964        $rewriter->rewrite('ALTER TABLE users DROP INDEX idx_name');
1965    }
1966
1967    public function testRewriteReadWithRegistryButNoShadowStoreDataAddsCtesForKnownTables(): void
1968    {
1969        $shadowStore = new ShadowStore();
1970        $parser = new MySqlParser();
1971        $schemaParser = new MySqlSchemaParser($parser);
1972        $registry = new TableDefinitionRegistry();
1973        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
1974        self::assertNotNull($def);
1975        $registry->register('users', $def);
1976
1977        $selectTransformer = new SelectTransformer();
1978        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1979        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1980        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1981        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
1982        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
1983        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
1984        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1985
1986        $plan = $rewriter->rewrite('SELECT * FROM users');
1987        self::assertSame(QueryKind::READ, $plan->kind());
1988        self::assertStringContainsString('WITH', $plan->sql());
1989        self::assertStringContainsString('`users`', $plan->sql());
1990    }
1991
1992    public function testRewriteInsertWithOnDuplicateKeyUpdateCreatesUpsertMutation(): void
1993    {
1994        $shadowStore = new ShadowStore();
1995        $parser = new MySqlParser();
1996        $schemaParser = new MySqlSchemaParser($parser);
1997        $registry = new TableDefinitionRegistry();
1998        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
1999        self::assertNotNull($def);
2000        $registry->register('users', $def);
2001
2002        $selectTransformer = new SelectTransformer();
2003        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2004        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2005        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2006        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2007        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2008        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2009        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2010
2011        $plan = $rewriter->rewrite("INSERT INTO users (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE name = 'Bob'");
2012        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2013        self::assertNotNull($plan->mutation());
2014        self::assertStringContainsString('__ztd_upsert_value_0', $plan->sql());
2015    }
2016
2017    public function testRewriteUpsertUsesCandidateKeysForStoredTableContext(): void
2018    {
2019        $shadowStore = new ShadowStore();
2020        $shadowStore->ensure('users');
2021        $parser = new MySqlParser();
2022        $schemaParser = new MySqlSchemaParser($parser);
2023        $registry = new TableDefinitionRegistry();
2024        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
2025        self::assertNotNull($definition);
2026        $registry->register('users', $definition);
2027        $selectTransformer = new SelectTransformer();
2028        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2029        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2030        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2031        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2032        $transformer = new MySqlTransformer(
2033            $parser,
2034            $selectTransformer,
2035            $insertTransformer,
2036            $updateTransformer,
2037            $deleteTransformer,
2038            $replaceTransformer,
2039        );
2040        $resolver = new MySqlMutationResolver(
2041            $shadowStore,
2042            $registry,
2043            $schemaParser,
2044            $updateTransformer,
2045            $deleteTransformer,
2046        );
2047        $rewriter = new MySqlRewriter(
2048            new MySqlQueryGuard($parser),
2049            $shadowStore,
2050            $registry,
2051            $transformer,
2052            $resolver,
2053            $parser,
2054        );
2055
2056        $plan = $rewriter->rewrite(
2057            "INSERT INTO users (id, name) VALUES (1, 'Alice') ON DUPLICATE KEY UPDATE name = VALUES(name)",
2058        );
2059
2060        self::assertStringContainsString('`__ztd_incoming`.`name`', $plan->sql());
2061        self::assertStringContainsString('__ztd_upsert_value_0', $plan->sql());
2062    }
2063
2064    public function testRewriteCreateTableAsSelectTransformsCte(): void
2065    {
2066        $shadowStore = new ShadowStore();
2067        $shadowStore->set('source', [['id' => 1, 'name' => 'Alice']]);
2068        $parser = new MySqlParser();
2069        $schemaParser = new MySqlSchemaParser($parser);
2070        $registry = new TableDefinitionRegistry();
2071        $def = $schemaParser->parse('CREATE TABLE source (id INT, name VARCHAR(255))');
2072        self::assertNotNull($def);
2073        $registry->register('source', $def);
2074
2075        $selectTransformer = new SelectTransformer();
2076        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2077        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2078        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2079        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2080        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2081        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2082        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2083
2084        $plan = $rewriter->rewrite('CREATE TABLE dest AS SELECT id, name FROM source');
2085        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
2086        self::assertNotNull($plan->mutation());
2087        self::assertStringContainsString('SELECT', $plan->sql());
2088        self::assertStringNotContainsString('SELECT 1 WHERE FALSE', $plan->sql());
2089    }
2090
2091    public function testRewriteReplaceWithoutColumnsThrowsWhenNoContext(): void
2092    {
2093        $shadowStore = new ShadowStore();
2094        $parser = new MySqlParser();
2095        $schemaParser = new MySqlSchemaParser($parser);
2096        $registry = new TableDefinitionRegistry();
2097
2098        $selectTransformer = new SelectTransformer();
2099        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2100        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2101        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2102        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2103        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2104        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2105        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2106
2107        self::expectException(UnsupportedSqlException::class);
2108        self::expectExceptionMessage('Cannot determine columns');
2109        $rewriter->rewrite("REPLACE INTO users VALUES (1, 'Alice')");
2110    }
2111
2112    public function testRewriteReplaceWithColumnsDefined(): void
2113    {
2114        $shadowStore = new ShadowStore();
2115        $parser = new MySqlParser();
2116        $schemaParser = new MySqlSchemaParser($parser);
2117        $registry = new TableDefinitionRegistry();
2118        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
2119        self::assertNotNull($def);
2120        $registry->register('users', $def);
2121
2122        $selectTransformer = new SelectTransformer();
2123        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2124        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2125        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2126        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2127        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2128        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2129        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2130
2131        $plan = $rewriter->rewrite("REPLACE INTO users (id, name) VALUES (1, 'Alice')");
2132        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2133        self::assertNotNull($plan->mutation());
2134        self::assertInstanceOf(ReplaceMutation::class, $plan->mutation());
2135    }
2136
2137    public function testRewriteWithStatementSelectUsesClassify(): void
2138    {
2139        $shadowStore = new ShadowStore();
2140        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
2141        $parser = new MySqlParser();
2142        $schemaParser = new MySqlSchemaParser($parser);
2143        $registry = new TableDefinitionRegistry();
2144        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2145        self::assertNotNull($def);
2146        $registry->register('users', $def);
2147
2148        $selectTransformer = new SelectTransformer();
2149        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2150        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2151        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2152        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2153        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2154        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2155        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2156
2157        $plan = $rewriter->rewrite('WITH cte AS (SELECT 1) SELECT * FROM users');
2158        self::assertSame(QueryKind::READ, $plan->kind());
2159    }
2160
2161    public function testRewriteWithStatementInsertResolvesItsInnerMutation(): void
2162    {
2163        $shadowStore = new ShadowStore();
2164        $parser = new MySqlParser();
2165        $schemaParser = new MySqlSchemaParser($parser);
2166        $registry = new TableDefinitionRegistry();
2167        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
2168        self::assertNotNull($definition);
2169        $registry->register('users', $definition);
2170
2171        $selectTransformer = new SelectTransformer();
2172        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2173        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2174        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2175        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2176        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2177        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2178        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2179
2180        $plan = $rewriter->rewrite("WITH source AS (SELECT 1 AS id, 'Alice' AS name) INSERT INTO users SELECT * FROM source");
2181
2182        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2183        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
2184        self::assertStringStartsWith('WITH source AS', $plan->sql());
2185        self::assertSame(1, substr_count($plan->sql(), 'WITH source AS'));
2186        self::assertSame(1, substr_count($plan->sql(), 'WITH `__ztd_insert_source`'));
2187        self::assertStringContainsString('CAST(_ztd_insert_cast.`id` AS SIGNED)', $plan->sql());
2188    }
2189
2190    public function testRewriteWithStatementDeleteIgnoresHashCommentsAroundItsCteHeader(): void
2191    {
2192        $shadowStore = new ShadowStore();
2193        $parser = new MySqlParser();
2194        $schemaParser = new MySqlSchemaParser($parser);
2195        $registry = new TableDefinitionRegistry();
2196        $definition = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
2197        self::assertNotNull($definition);
2198        $registry->register('users', $definition);
2199
2200        $selectTransformer = new SelectTransformer();
2201        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2202        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2203        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2204        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2205        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2206        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2207        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2208        $sql = <<<'SQL'
2209            WITH RECURSIVE # modifier
2210            chosen AS (SELECT 1 AS id) # body
2211            DELETE # target
2212            FROM users WHERE id IN (SELECT id FROM chosen)
2213            SQL;
2214
2215        $plan = $rewriter->rewrite($sql);
2216
2217        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2218        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
2219        self::assertStringContainsString('SELECT', $plan->sql());
2220    }
2221
2222    public function testRewriteAlterTableConvertToCharsetThrows(): void
2223    {
2224        $shadowStore = new ShadowStore();
2225        $parser = new MySqlParser();
2226        $schemaParser = new MySqlSchemaParser($parser);
2227        $registry = new TableDefinitionRegistry();
2228        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2229        self::assertNotNull($def);
2230        $registry->register('users', $def);
2231
2232        $selectTransformer = new SelectTransformer();
2233        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2234        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2235        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2236        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2237        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2238        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2239        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2240
2241        self::expectException(UnsupportedSqlException::class);
2242        $rewriter->rewrite('ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4');
2243    }
2244
2245    public function testRewriteEmptySqlThrows(): void
2246    {
2247        $shadowStore = new ShadowStore();
2248        $parser = new MySqlParser();
2249        $schemaParser = new MySqlSchemaParser($parser);
2250        $registry = new TableDefinitionRegistry();
2251
2252        $selectTransformer = new SelectTransformer();
2253        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2254        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2255        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2256        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2257        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2258        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2259        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2260
2261        self::expectException(UnsupportedSqlException::class);
2262        self::expectExceptionMessage('Empty or unparseable');
2263        $rewriter->rewrite('');
2264    }
2265
2266    public function testRewriteMultipleEmptySqlThrows(): void
2267    {
2268        $shadowStore = new ShadowStore();
2269        $parser = new MySqlParser();
2270        $schemaParser = new MySqlSchemaParser($parser);
2271        $registry = new TableDefinitionRegistry();
2272
2273        $selectTransformer = new SelectTransformer();
2274        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2275        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2276        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2277        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2278        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2279        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2280        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2281
2282        self::expectException(UnsupportedSqlException::class);
2283        $rewriter->rewriteMultiple('');
2284    }
2285
2286    public function testBuildTableContextMergesShadowDataAndRegistryDefinitions(): void
2287    {
2288        $shadowStore = new ShadowStore();
2289        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
2290        $parser = new MySqlParser();
2291        $schemaParser = new MySqlSchemaParser($parser);
2292        $registry = new TableDefinitionRegistry();
2293        $usersDef = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2294        self::assertNotNull($usersDef);
2295        $registry->register('users', $usersDef);
2296        $ordersDef = $schemaParser->parse('CREATE TABLE orders (id INT, user_id INT)');
2297        self::assertNotNull($ordersDef);
2298        $registry->register('orders', $ordersDef);
2299
2300        $selectTransformer = new SelectTransformer();
2301        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2302        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2303        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2304        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2305        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2306        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2307        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2308
2309        $plan = $rewriter->rewrite('SELECT * FROM users JOIN orders ON users.id = orders.user_id');
2310        self::assertSame(QueryKind::READ, $plan->kind());
2311        self::assertStringContainsString('`users` AS', $plan->sql());
2312        self::assertStringContainsString('`orders` AS', $plan->sql());
2313    }
2314
2315    public function testRewriteInsertIgnoreCreatesInsertMutationWithIgnore(): void
2316    {
2317        $shadowStore = new ShadowStore();
2318        $parser = new MySqlParser();
2319        $schemaParser = new MySqlSchemaParser($parser);
2320        $registry = new TableDefinitionRegistry();
2321        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
2322        self::assertNotNull($def);
2323        $registry->register('users', $def);
2324
2325        $selectTransformer = new SelectTransformer();
2326        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2327        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2328        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2329        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2330        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2331        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2332        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2333
2334        $plan = $rewriter->rewrite("INSERT IGNORE INTO users (id, name) VALUES (1, 'Alice')");
2335        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2336        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
2337    }
2338
2339    public function testRewriteAlterTableAddFulltextIndexThrows(): void
2340    {
2341        $shadowStore = new ShadowStore();
2342        $parser = new MySqlParser();
2343        $schemaParser = new MySqlSchemaParser($parser);
2344        $registry = new TableDefinitionRegistry();
2345        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2346        self::assertNotNull($def);
2347        $registry->register('users', $def);
2348
2349        $selectTransformer = new SelectTransformer();
2350        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2351        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2352        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2353        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2354        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2355        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2356        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2357
2358        self::expectException(UnsupportedSqlException::class);
2359        $rewriter->rewrite('ALTER TABLE users ADD FULLTEXT INDEX ft_name (name)');
2360    }
2361
2362    public function testRewriteCreateTableThatAlreadyExistsThrows(): void
2363    {
2364        $shadowStore = new ShadowStore();
2365        $parser = new MySqlParser();
2366        $schemaParser = new MySqlSchemaParser($parser);
2367        $registry = new TableDefinitionRegistry();
2368        $def = $schemaParser->parse('CREATE TABLE users (id INT)');
2369        self::assertNotNull($def);
2370        $registry->register('users', $def);
2371
2372        $selectTransformer = new SelectTransformer();
2373        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2374        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2375        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2376        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2377        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2378        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2379        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2380
2381        self::expectException(UnsupportedSqlException::class);
2382        $rewriter->rewrite('CREATE TABLE users (id INT, name VARCHAR(255))');
2383    }
2384
2385    public function testRewriteReplaceWithColumnsSucceeds(): void
2386    {
2387        $shadowStore = new ShadowStore();
2388        $parser = new MySqlParser();
2389        $schemaParser = new MySqlSchemaParser($parser);
2390        $registry = new TableDefinitionRegistry();
2391        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2392        self::assertNotNull($def);
2393        $registry->register('users', $def);
2394
2395        $selectTransformer = new SelectTransformer();
2396        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2397        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2398        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2399        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2400        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2401        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2402        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2403
2404        $plan = $rewriter->rewrite("REPLACE INTO users (id, name) VALUES (1, 'Alice')");
2405        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2406        self::assertInstanceOf(ReplaceMutation::class, $plan->mutation());
2407    }
2408
2409    public function testRewriteSelectWithUnknownTableThrows(): void
2410    {
2411        $shadowStore = new ShadowStore();
2412        $parser = new MySqlParser();
2413        $schemaParser = new MySqlSchemaParser($parser);
2414        $registry = new TableDefinitionRegistry();
2415        $def = $schemaParser->parse('CREATE TABLE known_table (id INT)');
2416        self::assertNotNull($def);
2417        $registry->register('known_table', $def);
2418
2419        $selectTransformer = new SelectTransformer();
2420        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2421        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2422        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2423        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2424        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2425        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2426        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2427
2428        self::expectException(UnknownSchemaException::class);
2429        $rewriter->rewrite('SELECT * FROM unknown_table');
2430    }
2431
2432    public function testRewriteSelectWithJoinedUnknownTableThrows(): void
2433    {
2434        $shadowStore = new ShadowStore();
2435        $parser = new MySqlParser();
2436        $schemaParser = new MySqlSchemaParser($parser);
2437        $registry = new TableDefinitionRegistry();
2438        $def = $schemaParser->parse('CREATE TABLE users (id INT)');
2439        self::assertNotNull($def);
2440        $registry->register('users', $def);
2441
2442        $selectTransformer = new SelectTransformer();
2443        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2444        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2445        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2446        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2447        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2448        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2449        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2450
2451        self::expectException(UnknownSchemaException::class);
2452        $rewriter->rewrite('SELECT * FROM users JOIN unknown_orders ON users.id = unknown_orders.user_id');
2453    }
2454
2455    public function testRewriteSelectNoSchemaContextDoesNotThrow(): void
2456    {
2457        $shadowStore = new ShadowStore();
2458        $parser = new MySqlParser();
2459        $schemaParser = new MySqlSchemaParser($parser);
2460        $registry = new TableDefinitionRegistry();
2461
2462        $selectTransformer = new SelectTransformer();
2463        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2464        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2465        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2466        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2467        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2468        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2469        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2470
2471        $plan = $rewriter->rewrite('SELECT * FROM users');
2472        self::assertSame(QueryKind::READ, $plan->kind());
2473        self::assertSame('SELECT * FROM users', $plan->sql());
2474    }
2475
2476    public function testRewriteAlterTableSetDefaultThrows(): void
2477    {
2478        $shadowStore = new ShadowStore();
2479        $parser = new MySqlParser();
2480        $schemaParser = new MySqlSchemaParser($parser);
2481        $registry = new TableDefinitionRegistry();
2482        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255) DEFAULT NULL)');
2483        self::assertNotNull($def);
2484        $registry->register('users', $def);
2485
2486        $selectTransformer = new SelectTransformer();
2487        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2488        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2489        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2490        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2491        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2492        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2493        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2494
2495        self::expectException(UnsupportedSqlException::class);
2496        $rewriter->rewrite("ALTER TABLE users ALTER COLUMN name SET DEFAULT 'test'");
2497    }
2498
2499    public function testRewriteAlterTableOrderByThrows(): void
2500    {
2501        $shadowStore = new ShadowStore();
2502        $parser = new MySqlParser();
2503        $schemaParser = new MySqlSchemaParser($parser);
2504        $registry = new TableDefinitionRegistry();
2505        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2506        self::assertNotNull($def);
2507        $registry->register('users', $def);
2508
2509        $selectTransformer = new SelectTransformer();
2510        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2511        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2512        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2513        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2514        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2515        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2516        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2517
2518        self::expectException(UnsupportedSqlException::class);
2519        $rewriter->rewrite('ALTER TABLE users ORDER BY name');
2520    }
2521
2522    public function testRewriteAlterTableConvertToThrows(): void
2523    {
2524        $shadowStore = new ShadowStore();
2525        $parser = new MySqlParser();
2526        $schemaParser = new MySqlSchemaParser($parser);
2527        $registry = new TableDefinitionRegistry();
2528        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2529        self::assertNotNull($def);
2530        $registry->register('users', $def);
2531
2532        $selectTransformer = new SelectTransformer();
2533        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2534        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2535        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2536        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2537        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2538        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2539        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2540
2541        self::expectException(UnsupportedSqlException::class);
2542        $rewriter->rewrite('ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4');
2543    }
2544
2545    public function testRewriteBuildTableContextWithRowsButNoDefinition(): void
2546    {
2547        $shadowStore = new ShadowStore();
2548        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob', 'extra' => 'data']]);
2549        $parser = new MySqlParser();
2550        $schemaParser = new MySqlSchemaParser($parser);
2551        $registry = new TableDefinitionRegistry();
2552
2553        $selectTransformer = new SelectTransformer();
2554        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2555        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2556        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2557        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2558        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2559        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2560        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2561
2562        $plan = $rewriter->rewrite('SELECT * FROM users');
2563        self::assertSame(QueryKind::READ, $plan->kind());
2564        self::assertStringContainsString('`users` AS', $plan->sql());
2565        self::assertStringContainsString('AS `id`', $plan->sql());
2566        self::assertStringContainsString('AS `name`', $plan->sql());
2567        self::assertStringContainsString('AS `extra`', $plan->sql());
2568    }
2569
2570    public function testRewriteBuildTableContextWithDefinitionButNoRows(): void
2571    {
2572        $shadowStore = new ShadowStore();
2573        $parser = new MySqlParser();
2574        $schemaParser = new MySqlSchemaParser($parser);
2575        $registry = new TableDefinitionRegistry();
2576        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2577        self::assertNotNull($def);
2578        $registry->register('users', $def);
2579
2580        $selectTransformer = new SelectTransformer();
2581        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2582        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2583        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2584        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2585        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2586        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2587        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2588
2589        $plan = $rewriter->rewrite('SELECT * FROM users');
2590        self::assertSame(QueryKind::READ, $plan->kind());
2591        self::assertStringContainsString('`users` AS', $plan->sql());
2592        self::assertStringContainsString('WHERE 0', $plan->sql());
2593    }
2594
2595    public function testRewriteMultipleStatements(): void
2596    {
2597        $shadowStore = new ShadowStore();
2598        $parser = new MySqlParser();
2599        $schemaParser = new MySqlSchemaParser($parser);
2600        $registry = new TableDefinitionRegistry();
2601        $def = $schemaParser->parse('CREATE TABLE t (id INT)');
2602        self::assertNotNull($def);
2603        $registry->register('t', $def);
2604
2605        $selectTransformer = new SelectTransformer();
2606        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2607        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2608        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2609        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2610        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2611        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2612        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2613
2614        self::expectException(UnsupportedSqlException::class);
2615        $rewriter->rewrite('SELECT 1; SELECT 2');
2616    }
2617
2618    public function testRewriteEmptyStatementThrows(): void
2619    {
2620        $shadowStore = new ShadowStore();
2621        $parser = new MySqlParser();
2622        $schemaParser = new MySqlSchemaParser($parser);
2623        $registry = new TableDefinitionRegistry();
2624
2625        $selectTransformer = new SelectTransformer();
2626        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2627        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2628        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2629        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2630        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2631        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2632        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2633
2634        self::expectException(UnsupportedSqlException::class);
2635        $rewriter->rewrite('');
2636    }
2637
2638    public function testRewriteAlterTableRenameIndexThrows(): void
2639    {
2640        $shadowStore = new ShadowStore();
2641        $parser = new MySqlParser();
2642        $schemaParser = new MySqlSchemaParser($parser);
2643        $registry = new TableDefinitionRegistry();
2644        $def = $schemaParser->parse('CREATE TABLE t (id INT, name VARCHAR(255))');
2645        self::assertNotNull($def);
2646        $registry->register('t', $def);
2647
2648        $selectTransformer = new SelectTransformer();
2649        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2650        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2651        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2652        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2653        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2654        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2655        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2656
2657        self::expectException(UnsupportedSqlException::class);
2658        $rewriter->rewrite('ALTER TABLE t RENAME INDEX idx_old TO idx_new');
2659    }
2660
2661    public function testRewriteReplaceWithoutColumnsButWithShadowDataSucceeds(): void
2662    {
2663        $shadowStore = new ShadowStore();
2664        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
2665        $parser = new MySqlParser();
2666        $schemaParser = new MySqlSchemaParser($parser);
2667        $registry = new TableDefinitionRegistry();
2668
2669        $selectTransformer = new SelectTransformer();
2670        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2671        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2672        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2673        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2674        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2675        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2676        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2677
2678        $plan = $rewriter->rewrite("REPLACE INTO users VALUES (1, 'Bob')");
2679        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2680        self::assertInstanceOf(ReplaceMutation::class, $plan->mutation());
2681    }
2682
2683    public function testRewriteReplaceWithoutColumnsButWithDefinitionSucceeds(): void
2684    {
2685        $shadowStore = new ShadowStore();
2686        $parser = new MySqlParser();
2687        $schemaParser = new MySqlSchemaParser($parser);
2688        $registry = new TableDefinitionRegistry();
2689        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2690        self::assertNotNull($def);
2691        $registry->register('users', $def);
2692
2693        $selectTransformer = new SelectTransformer();
2694        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2695        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2696        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2697        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2698        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2699        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2700        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2701
2702        $plan = $rewriter->rewrite("REPLACE INTO users VALUES (1, 'Bob')");
2703        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2704        self::assertInstanceOf(ReplaceMutation::class, $plan->mutation());
2705    }
2706
2707    public function testRewriteAlterTableAddPartitionThrows(): void
2708    {
2709        $shadowStore = new ShadowStore();
2710        $parser = new MySqlParser();
2711        $schemaParser = new MySqlSchemaParser($parser);
2712        $registry = new TableDefinitionRegistry();
2713        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2714        self::assertNotNull($def);
2715        $registry->register('users', $def);
2716        $selectTransformer = new SelectTransformer();
2717        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2718        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2719        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2720        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2721        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2722        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2723        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2724        self::expectException(UnsupportedSqlException::class);
2725        $rewriter->rewrite('ALTER TABLE users ADD PARTITION (PARTITION p1 VALUES LESS THAN (100))');
2726    }
2727
2728    public function testRewriteAlterTableDropPartitionThrows(): void
2729    {
2730        $shadowStore = new ShadowStore();
2731        $parser = new MySqlParser();
2732        $schemaParser = new MySqlSchemaParser($parser);
2733        $registry = new TableDefinitionRegistry();
2734        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2735        self::assertNotNull($def);
2736        $registry->register('users', $def);
2737        $selectTransformer = new SelectTransformer();
2738        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2739        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2740        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2741        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2742        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2743        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2744        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2745        self::expectException(UnsupportedSqlException::class);
2746        $rewriter->rewrite('ALTER TABLE users DROP PARTITION p1');
2747    }
2748
2749    public function testRewriteAlterTableCoalescePartitionThrows(): void
2750    {
2751        $shadowStore = new ShadowStore();
2752        $parser = new MySqlParser();
2753        $schemaParser = new MySqlSchemaParser($parser);
2754        $registry = new TableDefinitionRegistry();
2755        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2756        self::assertNotNull($def);
2757        $registry->register('users', $def);
2758        $selectTransformer = new SelectTransformer();
2759        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2760        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2761        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2762        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2763        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2764        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2765        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2766        self::expectException(UnsupportedSqlException::class);
2767        $rewriter->rewrite('ALTER TABLE users COALESCE PARTITION 2');
2768    }
2769
2770    public function testRewriteAlterTableAnalyzePartitionThrows(): void
2771    {
2772        $shadowStore = new ShadowStore();
2773        $parser = new MySqlParser();
2774        $schemaParser = new MySqlSchemaParser($parser);
2775        $registry = new TableDefinitionRegistry();
2776        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2777        self::assertNotNull($def);
2778        $registry->register('users', $def);
2779        $selectTransformer = new SelectTransformer();
2780        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2781        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2782        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2783        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2784        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2785        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2786        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2787        self::expectException(UnsupportedSqlException::class);
2788        $rewriter->rewrite('ALTER TABLE users ANALYZE PARTITION p1');
2789    }
2790
2791    public function testRewriteAlterTableCheckPartitionThrows(): void
2792    {
2793        $shadowStore = new ShadowStore();
2794        $parser = new MySqlParser();
2795        $schemaParser = new MySqlSchemaParser($parser);
2796        $registry = new TableDefinitionRegistry();
2797        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2798        self::assertNotNull($def);
2799        $registry->register('users', $def);
2800        $selectTransformer = new SelectTransformer();
2801        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2802        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2803        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2804        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2805        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2806        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2807        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2808        self::expectException(UnsupportedSqlException::class);
2809        $rewriter->rewrite('ALTER TABLE users CHECK PARTITION p1');
2810    }
2811
2812    public function testRewriteAlterTableOptimizePartitionThrows(): void
2813    {
2814        $shadowStore = new ShadowStore();
2815        $parser = new MySqlParser();
2816        $schemaParser = new MySqlSchemaParser($parser);
2817        $registry = new TableDefinitionRegistry();
2818        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2819        self::assertNotNull($def);
2820        $registry->register('users', $def);
2821        $selectTransformer = new SelectTransformer();
2822        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2823        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2824        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2825        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2826        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2827        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2828        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2829        self::expectException(UnsupportedSqlException::class);
2830        $rewriter->rewrite('ALTER TABLE users OPTIMIZE PARTITION p1');
2831    }
2832
2833    public function testRewriteAlterTableRebuildPartitionThrows(): void
2834    {
2835        $shadowStore = new ShadowStore();
2836        $parser = new MySqlParser();
2837        $schemaParser = new MySqlSchemaParser($parser);
2838        $registry = new TableDefinitionRegistry();
2839        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2840        self::assertNotNull($def);
2841        $registry->register('users', $def);
2842        $selectTransformer = new SelectTransformer();
2843        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2844        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2845        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2846        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2847        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2848        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2849        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2850        self::expectException(UnsupportedSqlException::class);
2851        $rewriter->rewrite('ALTER TABLE users REBUILD PARTITION p1');
2852    }
2853
2854    public function testRewriteAlterTableRepairPartitionThrows(): void
2855    {
2856        $shadowStore = new ShadowStore();
2857        $parser = new MySqlParser();
2858        $schemaParser = new MySqlSchemaParser($parser);
2859        $registry = new TableDefinitionRegistry();
2860        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2861        self::assertNotNull($def);
2862        $registry->register('users', $def);
2863        $selectTransformer = new SelectTransformer();
2864        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2865        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2866        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2867        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2868        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2869        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2870        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2871        self::expectException(UnsupportedSqlException::class);
2872        $rewriter->rewrite('ALTER TABLE users REPAIR PARTITION p1');
2873    }
2874
2875    public function testRewriteAlterTableAddSpatialIndexThrows(): void
2876    {
2877        $shadowStore = new ShadowStore();
2878        $parser = new MySqlParser();
2879        $schemaParser = new MySqlSchemaParser($parser);
2880        $registry = new TableDefinitionRegistry();
2881        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2882        self::assertNotNull($def);
2883        $registry->register('users', $def);
2884        $selectTransformer = new SelectTransformer();
2885        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2886        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2887        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2888        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2889        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2890        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2891        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2892        self::expectException(UnsupportedSqlException::class);
2893        $rewriter->rewrite('ALTER TABLE users ADD SPATIAL INDEX sp_name (geom)');
2894    }
2895
2896    public function testRewriteAlterTableAddConstraintThrows(): void
2897    {
2898        $shadowStore = new ShadowStore();
2899        $parser = new MySqlParser();
2900        $schemaParser = new MySqlSchemaParser($parser);
2901        $registry = new TableDefinitionRegistry();
2902        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2903        self::assertNotNull($def);
2904        $registry->register('users', $def);
2905        $selectTransformer = new SelectTransformer();
2906        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2907        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2908        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2909        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2910        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2911        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2912        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2913        self::expectException(UnsupportedSqlException::class);
2914        $rewriter->rewrite('ALTER TABLE users ADD CONSTRAINT ck_name CHECK (id > 0)');
2915    }
2916
2917    public function testRewriteAlterTableDropConstraintThrows(): void
2918    {
2919        $shadowStore = new ShadowStore();
2920        $parser = new MySqlParser();
2921        $schemaParser = new MySqlSchemaParser($parser);
2922        $registry = new TableDefinitionRegistry();
2923        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2924        self::assertNotNull($def);
2925        $registry->register('users', $def);
2926        $selectTransformer = new SelectTransformer();
2927        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2928        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2929        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2930        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2931        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2932        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2933        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2934        self::expectException(UnsupportedSqlException::class);
2935        $rewriter->rewrite('ALTER TABLE users DROP CONSTRAINT ck_name');
2936    }
2937
2938    public function testRewriteAlterTableAddKeyThrows(): void
2939    {
2940        $shadowStore = new ShadowStore();
2941        $parser = new MySqlParser();
2942        $schemaParser = new MySqlSchemaParser($parser);
2943        $registry = new TableDefinitionRegistry();
2944        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2945        self::assertNotNull($def);
2946        $registry->register('users', $def);
2947        $selectTransformer = new SelectTransformer();
2948        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2949        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2950        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2951        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2952        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2953        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2954        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2955        self::expectException(UnsupportedSqlException::class);
2956        $rewriter->rewrite('ALTER TABLE users ADD KEY idx_name (name)');
2957    }
2958
2959    public function testRewriteAlterTableDropKeyThrows(): void
2960    {
2961        $shadowStore = new ShadowStore();
2962        $parser = new MySqlParser();
2963        $schemaParser = new MySqlSchemaParser($parser);
2964        $registry = new TableDefinitionRegistry();
2965        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2966        self::assertNotNull($def);
2967        $registry->register('users', $def);
2968        $selectTransformer = new SelectTransformer();
2969        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2970        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2971        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2972        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2973        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2974        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2975        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2976        self::expectException(UnsupportedSqlException::class);
2977        $rewriter->rewrite('ALTER TABLE users DROP KEY idx_name');
2978    }
2979
2980    public function testRewriteAlterTableRenameKeyThrows(): void
2981    {
2982        $shadowStore = new ShadowStore();
2983        $parser = new MySqlParser();
2984        $schemaParser = new MySqlSchemaParser($parser);
2985        $registry = new TableDefinitionRegistry();
2986        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
2987        self::assertNotNull($def);
2988        $registry->register('users', $def);
2989        $selectTransformer = new SelectTransformer();
2990        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2991        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2992        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2993        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
2994        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
2995        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
2996        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2997        self::expectException(UnsupportedSqlException::class);
2998        $rewriter->rewrite('ALTER TABLE users RENAME KEY old_idx TO new_idx');
2999    }
3000
3001    public function testRewriteAlterTableEngineThrows(): void
3002    {
3003        $shadowStore = new ShadowStore();
3004        $parser = new MySqlParser();
3005        $schemaParser = new MySqlSchemaParser($parser);
3006        $registry = new TableDefinitionRegistry();
3007        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
3008        self::assertNotNull($def);
3009        $registry->register('users', $def);
3010        $selectTransformer = new SelectTransformer();
3011        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3012        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3013        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3014        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3015        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3016        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3017        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3018        self::expectException(UnsupportedSqlException::class);
3019        $rewriter->rewrite('ALTER TABLE users ENGINE = InnoDB');
3020    }
3021
3022    public function testRewriteSelectWithUnknownTableThrowsWhenSchemaContextExists(): void
3023    {
3024        $parser = new MySqlParser();
3025        $schemaParser = new MySqlSchemaParser($parser);
3026        $shadowStore = new ShadowStore();
3027        $registry = new TableDefinitionRegistry();
3028        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
3029        self::assertNotNull($def);
3030        $registry->register('users', $def);
3031
3032        $selectTransformer = new SelectTransformer();
3033        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3034        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3035        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3036        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3037        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3038        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3039        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3040
3041        self::expectException(UnknownSchemaException::class);
3042        $rewriter->rewrite('SELECT * FROM unknown_table');
3043    }
3044
3045    public function testRewriteSelectWithUnknownJoinTableThrowsWhenSchemaContextExists(): void
3046    {
3047        $parser = new MySqlParser();
3048        $schemaParser = new MySqlSchemaParser($parser);
3049        $shadowStore = new ShadowStore();
3050        $registry = new TableDefinitionRegistry();
3051        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
3052        self::assertNotNull($def);
3053        $registry->register('users', $def);
3054
3055        $selectTransformer = new SelectTransformer();
3056        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3057        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3058        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3059        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3060        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3061        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3062        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3063
3064        self::expectException(UnknownSchemaException::class);
3065        $rewriter->rewrite('SELECT * FROM users JOIN unknown_table ON users.id = unknown_table.user_id');
3066    }
3067
3068    public function testBuildTableContextIncludesRegistryDefinitionsNotInStore(): void
3069    {
3070        $parser = new MySqlParser();
3071        $schemaParser = new MySqlSchemaParser($parser);
3072        $shadowStore = new ShadowStore();
3073        $registry = new TableDefinitionRegistry();
3074        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
3075        self::assertNotNull($def);
3076        $registry->register('users', $def);
3077
3078        $selectTransformer = new SelectTransformer();
3079        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3080        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3081        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3082        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3083        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3084        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3085        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3086
3087        $plan = $rewriter->rewrite("INSERT INTO users (id, name) VALUES (1, 'Alice')");
3088        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3089        self::assertNotNull($plan->mutation());
3090    }
3091
3092    public function testRewriteReplaceWithNoColumnsAndNoStoreOrRegistryThrows(): void
3093    {
3094        $parser = new MySqlParser();
3095        $schemaParser = new MySqlSchemaParser($parser);
3096        $shadowStore = new ShadowStore();
3097        $registry = new TableDefinitionRegistry();
3098
3099        $selectTransformer = new SelectTransformer();
3100        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3101        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3102        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3103        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3104        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3105        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3106        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3107
3108        self::expectException(UnsupportedSqlException::class);
3109        self::expectExceptionMessage('Cannot determine columns');
3110        $rewriter->rewrite("REPLACE INTO users VALUES (1, 'Bob')");
3111    }
3112
3113    public function testRewriteReplaceWithColumnsExplicitDoesNotThrow(): void
3114    {
3115        $shadowStore = new ShadowStore();
3116        $parser = new MySqlParser();
3117        $schemaParser = new MySqlSchemaParser($parser);
3118        $registry = new TableDefinitionRegistry();
3119        $def = $schemaParser->parse('CREATE TABLE users (id INT, name VARCHAR(255))');
3120        self::assertNotNull($def);
3121        $registry->register('users', $def);
3122        $selectTransformer = new SelectTransformer();
3123        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3124        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3125        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3126        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3127        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3128        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3129        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3130        $plan = $rewriter->rewrite("REPLACE INTO users (id, name) VALUES (1, 'Bob')");
3131        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3132    }
3133
3134    public function testRewriteReplaceWithStoreDataDoesNotThrow(): void
3135    {
3136        $parser = new MySqlParser();
3137        $schemaParser = new MySqlSchemaParser($parser);
3138        $shadowStore = new ShadowStore();
3139        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
3140        $registry = new TableDefinitionRegistry();
3141
3142        $selectTransformer = new SelectTransformer();
3143        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3144        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3145        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3146        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3147        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3148        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3149        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3150
3151        $plan = $rewriter->rewrite("REPLACE INTO users VALUES (1, 'Bob')");
3152        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3153    }
3154
3155    public function testBuildTableContextColumnsFromStoreRowKeys(): void
3156    {
3157        $parser = new MySqlParser();
3158        $schemaParser = new MySqlSchemaParser($parser);
3159        $shadowStore = new ShadowStore();
3160        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
3161        $registry = new TableDefinitionRegistry();
3162
3163        $selectTransformer = new SelectTransformer();
3164        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3165        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3166        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3167        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3168        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3169        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3170        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3171
3172        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
3173        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3174        $sql = $plan->sql();
3175        self::assertStringContainsString('AS `id`', $sql);
3176        self::assertStringContainsString('AS `name`', $sql);
3177    }
3178
3179    public function testRewriteDeleteEnsuresDmlTablesArePresentInStore(): void
3180    {
3181        $parser = new MySqlParser();
3182        $schemaParser = new MySqlSchemaParser($parser);
3183        $shadowStore = new ShadowStore();
3184        $registry = new TableDefinitionRegistry();
3185        $def = $schemaParser->parse('CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))');
3186        self::assertNotNull($def);
3187        $registry->register('users', $def);
3188
3189        $selectTransformer = new SelectTransformer();
3190        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3191        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3192        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3193        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3194        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3195        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3196        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3197
3198        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
3199        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3200        self::assertNotNull($plan->mutation());
3201    }
3202
3203    public function testRewriteReplaceWithoutColumnsOrContextThrows(): void
3204    {
3205        $parser = new MySqlParser();
3206        $schemaParser = new MySqlSchemaParser($parser);
3207        $shadowStore = new ShadowStore();
3208        $registry = new TableDefinitionRegistry();
3209
3210        $selectTransformer = new SelectTransformer();
3211        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3212        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3213        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3214        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3215        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3216        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3217        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3218
3219        self::expectException(UnsupportedSqlException::class);
3220        self::expectExceptionMessage('Cannot determine columns');
3221        $rewriter->rewrite("REPLACE INTO t VALUES (1, 'Alice')");
3222    }
3223
3224    public function testRewriteReplaceWithColumnsSpecifiedDoesNotThrow(): void
3225    {
3226        $parser = new MySqlParser();
3227        $schemaParser = new MySqlSchemaParser($parser);
3228        $shadowStore = new ShadowStore();
3229        $registry = new TableDefinitionRegistry();
3230
3231        $selectTransformer = new SelectTransformer();
3232        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3233        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3234        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3235        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3236        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3237        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3238        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3239
3240        $plan = $rewriter->rewrite("REPLACE INTO t (id, name) VALUES (1, 'Alice')");
3241        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3242    }
3243
3244    public function testRewriteReplaceWithStoreContextDoesNotThrow(): void
3245    {
3246        $parser = new MySqlParser();
3247        $schemaParser = new MySqlSchemaParser($parser);
3248        $shadowStore = new ShadowStore();
3249        $shadowStore->set('t', [['id' => 1, 'name' => 'old']]);
3250        $registry = new TableDefinitionRegistry();
3251
3252        $selectTransformer = new SelectTransformer();
3253        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3254        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3255        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3256        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3257        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3258        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3259        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3260
3261        $plan = $rewriter->rewrite("REPLACE INTO t VALUES (1, 'Alice')");
3262        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3263    }
3264
3265    public function testRewriteAlterTableOrderByThrowsUnsupported(): void
3266    {
3267        $parser = new MySqlParser();
3268        $schemaParser = new MySqlSchemaParser($parser);
3269        $shadowStore = new ShadowStore();
3270        $registry = new TableDefinitionRegistry();
3271        $def = $schemaParser->parse('CREATE TABLE t (id INT, val INT)');
3272        self::assertNotNull($def);
3273        $registry->register('t', $def);
3274
3275        $selectTransformer = new SelectTransformer();
3276        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3277        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3278        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3279        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3280        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3281        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3282        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3283
3284        self::expectException(UnsupportedSqlException::class);
3285        $rewriter->rewrite('ALTER TABLE t ORDER BY id');
3286    }
3287
3288    public function testRewriteAlterTableAddIndexThrowsUnsupported(): void
3289    {
3290        $parser = new MySqlParser();
3291        $schemaParser = new MySqlSchemaParser($parser);
3292        $shadowStore = new ShadowStore();
3293        $registry = new TableDefinitionRegistry();
3294        $def = $schemaParser->parse('CREATE TABLE t (id INT, val INT)');
3295        self::assertNotNull($def);
3296        $registry->register('t', $def);
3297
3298        $selectTransformer = new SelectTransformer();
3299        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3300        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3301        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3302        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3303        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3304        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3305        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3306
3307        self::expectException(UnsupportedSqlException::class);
3308        $rewriter->rewrite('ALTER TABLE t ADD INDEX idx_val (val)');
3309    }
3310
3311    public function testRewriteAlterTableDropIndexThrowsUnsupported(): void
3312    {
3313        $parser = new MySqlParser();
3314        $schemaParser = new MySqlSchemaParser($parser);
3315        $shadowStore = new ShadowStore();
3316        $registry = new TableDefinitionRegistry();
3317        $def = $schemaParser->parse('CREATE TABLE t (id INT, val INT)');
3318        self::assertNotNull($def);
3319        $registry->register('t', $def);
3320
3321        $selectTransformer = new SelectTransformer();
3322        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3323        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3324        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3325        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3326        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3327        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3328        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3329
3330        self::expectException(UnsupportedSqlException::class);
3331        $rewriter->rewrite('ALTER TABLE t DROP INDEX idx_val');
3332    }
3333
3334    public function testRewriteAlterTableRenameIndexThrowsUnsupported(): void
3335    {
3336        $parser = new MySqlParser();
3337        $schemaParser = new MySqlSchemaParser($parser);
3338        $shadowStore = new ShadowStore();
3339        $registry = new TableDefinitionRegistry();
3340        $def = $schemaParser->parse('CREATE TABLE t (id INT, val INT)');
3341        self::assertNotNull($def);
3342        $registry->register('t', $def);
3343
3344        $selectTransformer = new SelectTransformer();
3345        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3346        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3347        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3348        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3349        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3350        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3351        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3352
3353        self::expectException(UnsupportedSqlException::class);
3354        $rewriter->rewrite('ALTER TABLE t RENAME INDEX idx_old TO idx_new');
3355    }
3356
3357    public function testRewriteSelectWithJoinDetectsUnknownJoinTable(): void
3358    {
3359        $parser = new MySqlParser();
3360        $schemaParser = new MySqlSchemaParser($parser);
3361        $shadowStore = new ShadowStore();
3362        $registry = new TableDefinitionRegistry();
3363        $def = $schemaParser->parse('CREATE TABLE users (id INT)');
3364        self::assertNotNull($def);
3365        $registry->register('users', $def);
3366
3367        $selectTransformer = new SelectTransformer();
3368        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3369        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3370        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3371        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3372        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3373        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3374        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3375
3376        self::expectException(UnknownSchemaException::class);
3377        $rewriter->rewrite('SELECT * FROM users JOIN orders ON users.id = orders.user_id');
3378    }
3379
3380    public function testRewriteSelectNoSchemaContextDoesNotCheckUnknownTables(): void
3381    {
3382        $parser = new MySqlParser();
3383        $schemaParser = new MySqlSchemaParser($parser);
3384        $shadowStore = new ShadowStore();
3385        $registry = new TableDefinitionRegistry();
3386
3387        $selectTransformer = new SelectTransformer();
3388        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3389        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3390        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3391        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3392        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3393        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3394        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3395
3396        $plan = $rewriter->rewrite('SELECT * FROM nonexistent');
3397        self::assertSame(QueryKind::READ, $plan->kind());
3398        self::assertSame('SELECT * FROM nonexistent', $plan->sql());
3399    }
3400
3401    public function testRewriteBuildTableContextMergesExtraColumnKeysFromRows(): void
3402    {
3403        $parser = new MySqlParser();
3404        $schemaParser = new MySqlSchemaParser($parser);
3405        $shadowStore = new ShadowStore();
3406        $shadowStore->set('t', [
3407            ['id' => 1, 'name' => 'a'],
3408            ['id' => 2, 'name' => 'b', 'extra' => 'val'],
3409        ]);
3410        $registry = new TableDefinitionRegistry();
3411
3412        $selectTransformer = new SelectTransformer();
3413        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3414        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3415        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3416        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3417        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3418        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3419        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3420
3421        $plan = $rewriter->rewrite('SELECT * FROM t');
3422        self::assertStringContainsString('AS `extra`', $plan->sql());
3423        self::assertStringContainsString('AS `id`', $plan->sql());
3424        self::assertStringContainsString('AS `name`', $plan->sql());
3425    }
3426
3427    public function testRewriteCreateTableAsSelectTransformsSelectPart(): void
3428    {
3429        $parser = new MySqlParser();
3430        $schemaParser = new MySqlSchemaParser($parser);
3431        $shadowStore = new ShadowStore();
3432        $shadowStore->set('src', [['id' => 1, 'name' => 'Alice']]);
3433        $registry = new TableDefinitionRegistry();
3434        $def = $schemaParser->parse('CREATE TABLE src (id INT, name VARCHAR(255))');
3435        self::assertNotNull($def);
3436        $registry->register('src', $def);
3437
3438        $selectTransformer = new SelectTransformer();
3439        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3440        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3441        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3442        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3443        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3444        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3445        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3446
3447        $plan = $rewriter->rewrite('CREATE TABLE dest AS SELECT id, name FROM src');
3448        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
3449        self::assertNotNull($plan->mutation());
3450        self::assertStringContainsString('WITH `src` AS', $plan->sql());
3451    }
3452
3453    public function testRewriteTruncateReturnsFalseSelect(): void
3454    {
3455        $parser = new MySqlParser();
3456        $schemaParser = new MySqlSchemaParser($parser);
3457        $shadowStore = new ShadowStore();
3458        $registry = new TableDefinitionRegistry();
3459        $def = $schemaParser->parse('CREATE TABLE t (id INT)');
3460        self::assertNotNull($def);
3461        $registry->register('t', $def);
3462
3463        $selectTransformer = new SelectTransformer();
3464        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3465        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3466        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3467        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3468        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3469        $mutationResolver = new MySqlMutationResolver($shadowStore, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3470        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $shadowStore, $registry, $transformer, $mutationResolver, $parser);
3471
3472        $plan = $rewriter->rewrite('TRUNCATE TABLE t');
3473        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3474        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
3475        self::assertNotNull($plan->mutation());
3476        self::assertInstanceOf(TruncateMutation::class, $plan->mutation());
3477    }
3478
3479    public function testUpdateDoesNotPromoteMaterializedUnknownTable(): void
3480    {
3481        $store = new ShadowStore();
3482        $store->insert('late_table', [['id' => 1, 'name' => 'Alice']]);
3483        $registry = new TableDefinitionRegistry();
3484        $parser = new MySqlParser();
3485        $schemaParser = new MySqlSchemaParser($parser);
3486        $selectTransformer = new SelectTransformer();
3487        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3488        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3489        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3490        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3491        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3492        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3493        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3494
3495
3496        try {
3497            $rewriter->rewrite("UPDATE late_table SET name = 'Bob' WHERE id = 1");
3498            self::fail('Expected an unknown schema exception.');
3499        } catch (UnknownSchemaException) {
3500            self::assertSame(ShadowTableState::Materialized, $store->state('late_table'));
3501        }
3502    }
3503
3504    public function testSplitStatements(): void
3505    {
3506        $store = new ShadowStore();
3507        $registry = new TableDefinitionRegistry();
3508        $parser = new MySqlParser();
3509        $schemaParser = new MySqlSchemaParser($parser);
3510        $selectTransformer = new SelectTransformer();
3511        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3512        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3513        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3514        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3515        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3516        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3517        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3518
3519        self::assertSame(['SELECT 1', "SELECT ';'"], $rewriter->splitStatements("SELECT 1; SELECT ';'"));
3520    }
3521
3522    public function testTransactionStatement(): void
3523    {
3524        $store = new ShadowStore();
3525        $registry = new TableDefinitionRegistry();
3526        $parser = new MySqlParser();
3527        $schemaParser = new MySqlSchemaParser($parser);
3528        $selectTransformer = new SelectTransformer();
3529        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3530        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3531        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3532        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3533        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3534        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3535        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3536
3537        self::assertNotNull($rewriter->transactionStatement('BEGIN'));
3538        self::assertNull($rewriter->transactionStatement('SELECT 1'));
3539    }
3540
3541    public function testEmptyResultSelect(): void
3542    {
3543        $store = new ShadowStore();
3544        $registry = new TableDefinitionRegistry();
3545        $parser = new MySqlParser();
3546        $schemaParser = new MySqlSchemaParser($parser);
3547        $selectTransformer = new SelectTransformer();
3548        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3549        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3550        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3551        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3552        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3553        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3554        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3555
3556        self::assertSame('SELECT 1 WHERE FALSE', $rewriter->emptyResultSelect());
3557    }
3558
3559    public function testCommitRewriteState(): void
3560    {
3561        $store = new ShadowStore();
3562        $registry = new TableDefinitionRegistry();
3563        $definition = (new MySqlSchemaParser(new MySqlParser()))->parse('CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)');
3564        self::assertNotNull($definition);
3565        $registry->register('users', $definition);
3566        $parser = new MySqlParser();
3567        $schemaParser = new MySqlSchemaParser($parser);
3568        $selectTransformer = new SelectTransformer();
3569        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3570        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3571        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3572        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3573        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3574        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3575        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3576
3577        $first = $rewriter->rewrite("INSERT INTO users (name) VALUES ('a')");
3578        $rewriter->commitRewriteState();
3579        $second = $rewriter->rewrite("INSERT INTO users (name) VALUES ('b')");
3580        self::assertStringContainsString('CAST(1 AS SIGNED) AS `id`', $first->sql());
3581        self::assertStringContainsString('CAST(2 AS SIGNED) AS `id`', $second->sql());
3582    }
3583
3584    public function testSelectReturnsReadKind(): void
3585    {
3586        $store = new ShadowStore();
3587        $registry = new TableDefinitionRegistry();
3588        $parser = new MySqlParser();
3589        $schemaParser = new MySqlSchemaParser($parser);
3590        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3591        self::assertNotNull($definition);
3592        $registry->register('users', $definition);
3593        $selectTransformer = new SelectTransformer();
3594        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3595        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3596        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3597        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3598        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3599        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3600        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3601
3602        $plan = $rewriter->rewrite('SELECT id, name, email FROM users WHERE id = 1');
3603        self::assertSame(QueryKind::READ, $plan->kind());
3604    }
3605
3606    public function testInsertReturnsWriteSimulatedWithMutation(): void
3607    {
3608        $store = new ShadowStore();
3609        $registry = new TableDefinitionRegistry();
3610        $parser = new MySqlParser();
3611        $schemaParser = new MySqlSchemaParser($parser);
3612        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3613        self::assertNotNull($definition);
3614        $registry->register('users', $definition);
3615        $selectTransformer = new SelectTransformer();
3616        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3617        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3618        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3619        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3620        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3621        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3622        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3623
3624        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com')");
3625        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3626        self::assertNotNull($plan->mutation());
3627        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
3628        self::assertSame('users', $plan->mutation()->tableName());
3629    }
3630
3631    public function testUpdateReturnsWriteSimulatedWithMutation(): void
3632    {
3633        $store = new ShadowStore();
3634        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
3635        $registry = new TableDefinitionRegistry();
3636        $parser = new MySqlParser();
3637        $schemaParser = new MySqlSchemaParser($parser);
3638        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3639        self::assertNotNull($definition);
3640        $registry->register('users', $definition);
3641        $selectTransformer = new SelectTransformer();
3642        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3643        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3644        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3645        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3646        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3647        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3648        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3649
3650        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
3651        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3652        self::assertNotNull($plan->mutation());
3653        self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
3654        self::assertSame('users', $plan->mutation()->tableName());
3655    }
3656
3657    public function testDeleteReturnsWriteSimulatedWithMutation(): void
3658    {
3659        $store = new ShadowStore();
3660        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
3661        $registry = new TableDefinitionRegistry();
3662        $parser = new MySqlParser();
3663        $schemaParser = new MySqlSchemaParser($parser);
3664        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3665        self::assertNotNull($definition);
3666        $registry->register('users', $definition);
3667        $selectTransformer = new SelectTransformer();
3668        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3669        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3670        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3671        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3672        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3673        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3674        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3675
3676        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
3677        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3678        self::assertNotNull($plan->mutation());
3679        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
3680        self::assertSame('users', $plan->mutation()->tableName());
3681    }
3682
3683    public function testCreateTableReturnsDdlSimulated(): void
3684    {
3685        $store = new ShadowStore();
3686        $registry = new TableDefinitionRegistry();
3687        $parser = new MySqlParser();
3688        $schemaParser = new MySqlSchemaParser($parser);
3689        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3690        self::assertNotNull($definition);
3691        $registry->register('users', $definition);
3692        $selectTransformer = new SelectTransformer();
3693        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3694        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3695        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3696        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3697        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3698        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3699        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3700
3701        $plan = $rewriter->rewrite('CREATE TABLE orders (id INT PRIMARY KEY, amount DECIMAL(10,2))');
3702        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
3703    }
3704
3705    public function testDropTableReturnsDdlSimulated(): void
3706    {
3707        $store = new ShadowStore();
3708        $registry = new TableDefinitionRegistry();
3709        $parser = new MySqlParser();
3710        $schemaParser = new MySqlSchemaParser($parser);
3711        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3712        self::assertNotNull($definition);
3713        $registry->register('users', $definition);
3714        $selectTransformer = new SelectTransformer();
3715        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3716        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3717        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3718        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3719        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3720        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3721        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3722
3723        $plan = $rewriter->rewrite('DROP TABLE IF EXISTS orders');
3724        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
3725    }
3726
3727    public function testUnsupportedSqlThrowsException(): void
3728    {
3729        $store = new ShadowStore();
3730        $registry = new TableDefinitionRegistry();
3731        $parser = new MySqlParser();
3732        $schemaParser = new MySqlSchemaParser($parser);
3733        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3734        self::assertNotNull($definition);
3735        $registry->register('users', $definition);
3736        $selectTransformer = new SelectTransformer();
3737        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3738        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3739        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3740        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3741        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3742        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3743        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3744
3745        $this->expectException(UnsupportedSqlException::class);
3746        $rewriter->rewrite('CREATE DATABASE test_db');
3747    }
3748
3749    public function testEmptyInputThrowsException(): void
3750    {
3751        $store = new ShadowStore();
3752        $registry = new TableDefinitionRegistry();
3753        $parser = new MySqlParser();
3754        $schemaParser = new MySqlSchemaParser($parser);
3755        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3756        self::assertNotNull($definition);
3757        $registry->register('users', $definition);
3758        $selectTransformer = new SelectTransformer();
3759        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3760        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3761        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3762        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3763        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3764        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3765        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3766
3767        $this->expectException(UnsupportedSqlException::class);
3768        $rewriter->rewrite('');
3769    }
3770
3771    public function testRewriteIsDeterministic(): void
3772    {
3773        $store = new ShadowStore();
3774        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
3775        $registry = new TableDefinitionRegistry();
3776        $parser = new MySqlParser();
3777        $schemaParser = new MySqlSchemaParser($parser);
3778        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3779        self::assertNotNull($definition);
3780        $registry->register('users', $definition);
3781        $selectTransformer = new SelectTransformer();
3782        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3783        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3784        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3785        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3786        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3787        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3788        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3789
3790        $plan1 = $rewriter->rewrite('SELECT id, name, email FROM users WHERE id = 1');
3791        $plan2 = $rewriter->rewrite('SELECT id, name, email FROM users WHERE id = 1');
3792        self::assertSame($plan1->sql(), $plan2->sql());
3793        self::assertSame($plan1->kind(), $plan2->kind());
3794    }
3795
3796    public function testReadPlanHasNoMutation(): void
3797    {
3798        $store = new ShadowStore();
3799        $registry = new TableDefinitionRegistry();
3800        $parser = new MySqlParser();
3801        $schemaParser = new MySqlSchemaParser($parser);
3802        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3803        self::assertNotNull($definition);
3804        $registry->register('users', $definition);
3805        $selectTransformer = new SelectTransformer();
3806        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3807        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3808        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3809        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3810        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3811        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3812        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3813
3814        $plan = $rewriter->rewrite('SELECT id, name, email FROM users WHERE id = 1');
3815        self::assertSame(QueryKind::READ, $plan->kind());
3816        self::assertNull($plan->mutation());
3817    }
3818
3819    public function testWritePlanHasNonNullMutation(): void
3820    {
3821        $store = new ShadowStore();
3822        $registry = new TableDefinitionRegistry();
3823        $parser = new MySqlParser();
3824        $schemaParser = new MySqlSchemaParser($parser);
3825        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3826        self::assertNotNull($definition);
3827        $registry->register('users', $definition);
3828        $selectTransformer = new SelectTransformer();
3829        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3830        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3831        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3832        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3833        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3834        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3835        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3836
3837        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com')");
3838        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3839        self::assertNotNull($plan->mutation());
3840        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
3841    }
3842
3843    public function testRewriteOutputIsNonEmpty(): void
3844    {
3845        $store = new ShadowStore();
3846        $registry = new TableDefinitionRegistry();
3847        $parser = new MySqlParser();
3848        $schemaParser = new MySqlSchemaParser($parser);
3849        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3850        self::assertNotNull($definition);
3851        $registry->register('users', $definition);
3852        $selectTransformer = new SelectTransformer();
3853        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3854        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3855        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3856        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3857        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3858        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3859        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3860
3861        $plan = $rewriter->rewrite('SELECT id, name, email FROM users WHERE id = 1');
3862        self::assertNotEmpty($plan->sql());
3863        self::assertStringContainsString('SELECT', strtoupper($plan->sql()));
3864    }
3865
3866    public function testInsertRewriteOutputContainsSelect(): void
3867    {
3868        $store = new ShadowStore();
3869        $registry = new TableDefinitionRegistry();
3870        $parser = new MySqlParser();
3871        $schemaParser = new MySqlSchemaParser($parser);
3872        $definition = $schemaParser->parse('CREATE TABLE users (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
3873        self::assertNotNull($definition);
3874        $registry->register('users', $definition);
3875        $selectTransformer = new SelectTransformer();
3876        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
3877        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
3878        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
3879        $replaceTransformer = new ReplaceTransformer($parser, $selectTransformer);
3880        $transformer = new MySqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer, $replaceTransformer);
3881        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $updateTransformer, $deleteTransformer);
3882        $rewriter = new MySqlRewriter(new MySqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
3883
3884        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com')");
3885        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
3886        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql(), 'INSERT rewrite must produce a result-select query starting with SELECT or WITH...SELECT');
3887    }
3888}
3889