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