packages/ztd-query-sqlite/tests/Unit/Rewrite/SqliteRewriterTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite;
6
7use PDO;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\UsesClass;
10use ZtdQuery\Exception\UnknownSchemaException;
11use ZtdQuery\Exception\UnsupportedSqlException;
12use ZtdQuery\Platform\Sqlite\Rewrite\Returning\SqliteReturningProjectionParser;
13use ZtdQuery\Platform\Sqlite\Rewrite\SqliteQueryGuard;
14use ZtdQuery\Platform\Sqlite\Rewrite\SqliteRewriter;
15use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\DeleteTransformer;
16use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\InsertTransformer;
17use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\SelectTransformer;
18use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\SqliteTransformer;
19use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\UpdateTransformer;
20use ZtdQuery\Platform\Sqlite\Schema\SqliteSchemaParser;
21use ZtdQuery\Platform\Sqlite\Schema\View\SqliteViewDefinitionParser;
22use ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\AlterTableMutation;
23use ZtdQuery\Platform\Sqlite\Shadow\SqliteMutationResolver;
24use ZtdQuery\Platform\Sqlite\Sql\SqliteIdentifierQuoter;
25use ZtdQuery\Platform\Sqlite\Sql\SqliteLexicalMasker;
26use ZtdQuery\Platform\Sqlite\Sql\SqliteParser;
27use ZtdQuery\Platform\Sqlite\Sql\Value\SqliteCastRenderer;
28use ZtdQuery\Rewrite\QueryKind;
29use ZtdQuery\Schema\TableDefinition;
30use ZtdQuery\Schema\TableDefinitionRegistry;
31use ZtdQuery\Schema\ViewDefinitionSet;
32use ZtdQuery\Shadow\Mutation\Row\DeleteMutation;
33use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
34use ZtdQuery\Shadow\Mutation\Row\ReplaceMutation;
35use ZtdQuery\Shadow\Mutation\Row\UpdateMutation;
36use ZtdQuery\Shadow\Mutation\Table\CreateTableMutation;
37use ZtdQuery\Shadow\Mutation\Table\DropTableMutation;
38use ZtdQuery\Shadow\Mutation\UpsertMutation;
39use ZtdQuery\Shadow\ShadowStore;
40use ZtdQuery\Shadow\ShadowTableState;
41
42#[CoversClass(SqliteRewriter::class)]
43#[UsesClass(AlterTableMutation::class)]
44#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\AddColumnResolver::class)]
45#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\AlteredTableProjection::class)]
46#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\ColumnDefinitionEditor::class)]
47#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\DropColumnResolver::class)]
48#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\RenameResolver::class)]
49#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\InsertMutationResolver::class)]
50#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\MutationTableLookup::class)]
51#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\RowMutationResolver::class)]
52#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\TableMutationResolver::class)]
53#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Alter\AlterOperationParser::class)]
54#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Attach\AttachTokens::class)]
55#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentColumnParser::class)]
56#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentParser::class)]
57#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\ValueListParser::class)]
58#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Insert\InsertClauseParser::class)]
59#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\ExpressionSpan::class)]
60#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\IdentifierDecoder::class)]
61#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\LiteralMasker::class)]
62#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\OpaqueSqlSpan::class)]
63#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\QuotedSpan::class)]
64#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\TopLevelKeywordScanner::class)]
65#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\RelationSourceParser::class)]
66#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Returning\ReturningItemParser::class)]
67#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementClassifier::class)]
68#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementStructure::class)]
69#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\TargetTableParser::class)]
70#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Transaction\TransactionTokens::class)]
71#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Update\UpdateClauseParser::class)]
72#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ArithmeticExpressionParser::class)]
73#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ComparisonExpressionParser::class)]
74#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ExpressionCursor::class)]
75#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ExpressionTokenDecoder::class)]
76#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\LogicalExpressionParser::class)]
77#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\PrimaryExpressionParser::class)]
78#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Context\ReferencedTableLookup::class)]
79#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Context\TableContextBuilder::class)]
80#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CteDependencies::class)]
81#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CteHeaderParser::class)]
82#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CtePrefixMerger::class)]
83#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CteReferences::class)]
84#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\FullText\FullTextColumns::class)]
85#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\FullText\MatchExpressionRewriter::class)]
86#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Index\IndexHintTokens::class)]
87#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\CastTypeMapper::class)]
88#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\ValueExpressionRenderer::class)]
89#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\ValueLiteralRenderer::class)]
90#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\SqlEdits::class)]
91#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\StatementRewriter::class)]
92#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Upsert\UpsertConflictPredicate::class)]
93#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Upsert\UpsertExpressionBinder::class)]
94#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\ColumnDefinitionParser::class)]
95#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\TableBodyParser::class)]
96#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\TableDefinitionBuilder::class)]
97#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\VirtualTableParser::class)]
98#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Key\ForeignKeyEntryParser::class)]
99#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Key\ForeignKeyTokens::class)]
100#[UsesClass(SqliteCastRenderer::class)]
101#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\SqliteColumnTypeMapper::class)]
102#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\SqliteCteShadowComposer::class)]
103#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Key\SqliteForeignKeyDefinitionParser::class)]
104#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\FullText\SqliteFullTextSearchRewriter::class)]
105#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\GeneratedColumn\SqliteGeneratedColumnProjector::class)]
106#[UsesClass(SqliteIdentifierQuoter::class)]
107#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Attach\SqliteInMemoryAttachStatement::class)]
108#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Index\SqliteIndexHintStripper::class)]
109#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile::class)]
110#[UsesClass(SqliteLexicalMasker::class)]
111#[UsesClass(SqliteMutationResolver::class)]
112#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Upsert\SqliteNativeUpsertProjector::class)]
113#[UsesClass(SqliteParser::class)]
114#[UsesClass(SqliteQueryGuard::class)]
115#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Diagnostic\SqliteReadOnlyDiagnosticStatement::class)]
116#[UsesClass(SqliteReturningProjectionParser::class)]
117#[UsesClass(SqliteSchemaParser::class)]
118#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\SqliteSelectRelationParser::class)]
119#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Transaction\SqliteTransactionStatementParser::class)]
120#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\SqliteUpsertExpressionParser::class)]
121#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\SqliteValueRenderer::class)]
122#[UsesClass(SqliteViewDefinitionParser::class)]
123#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\View\SqliteViewShadowRenderer::class)]
124#[UsesClass(DeleteTransformer::class)]
125#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Transformer\InsertRowRenderer::class)]
126#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Transformer\InsertSelectRenderer::class)]
127#[UsesClass(InsertTransformer::class)]
128#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Transformer\Insert\InsertProjectionBuilder::class)]
129#[UsesClass(SelectTransformer::class)]
130#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Transformer\Select\ShadowCteRenderer::class)]
131#[UsesClass(SqliteTransformer::class)]
132#[UsesClass(UpdateTransformer::class)]
133final class SqliteRewriterTest extends \PHPUnit\Framework\TestCase
134{
135    public function testSelectReturnsReadKind(): void
136    {
137        $registry = new TableDefinitionRegistry();
138        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
139        $store = new ShadowStore();
140        $parser = new SqliteParser();
141        $guard = new SqliteQueryGuard($parser);
142        $schemaParser = new SqliteSchemaParser();
143        $selectTransformer = new SelectTransformer();
144        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
145        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
146        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
147        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
148        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
149        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
150        $plan = $rewriter->rewrite('SELECT * FROM users');
151        self::assertSame(QueryKind::READ, $plan->kind());
152        self::assertNull($plan->mutation());
153        self::assertStringContainsString('SELECT', strtoupper($plan->sql()));
154    }
155
156    public function testInsertReturnsWriteSimulatedWithMutation(): void
157    {
158        $registry = new TableDefinitionRegistry();
159        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
160        $store = new ShadowStore();
161        $parser = new SqliteParser();
162        $guard = new SqliteQueryGuard($parser);
163        $schemaParser = new SqliteSchemaParser();
164        $selectTransformer = new SelectTransformer();
165        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
166        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
167        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
168        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
169        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
170        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
171        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
172        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
173        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
174        self::assertSame('users', $plan->mutation()->tableName());
175        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql());
176    }
177
178    public function testUpdateReturnsWriteSimulatedWithMutation(): void
179    {
180        $registry = new TableDefinitionRegistry();
181        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
182        $store = new ShadowStore();
183        $store->ensure('users');
184        $parser = new SqliteParser();
185        $guard = new SqliteQueryGuard($parser);
186        $schemaParser = new SqliteSchemaParser();
187        $selectTransformer = new SelectTransformer();
188        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
189        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
190        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
191        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
192        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
193        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
194        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
195        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
196        self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
197        self::assertSame('users', $plan->mutation()->tableName());
198        self::assertStringContainsString('"users"."id" AS "__ztd_original_id"', $plan->sql());
199        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql());
200    }
201
202    public function testDeleteReturnsWriteSimulatedWithMutation(): void
203    {
204        $registry = new TableDefinitionRegistry();
205        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
206        $store = new ShadowStore();
207        $store->ensure('users');
208        $parser = new SqliteParser();
209        $guard = new SqliteQueryGuard($parser);
210        $schemaParser = new SqliteSchemaParser();
211        $selectTransformer = new SelectTransformer();
212        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
213        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
214        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
215        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
216        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
217        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
218        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
219        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
220        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
221        self::assertSame('users', $plan->mutation()->tableName());
222        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql());
223    }
224
225    public function testCreateTableReturnsDdlSimulated(): void
226    {
227        $store = new ShadowStore();
228        $registry = new TableDefinitionRegistry();
229        $parser = new SqliteParser();
230        $guard = new SqliteQueryGuard($parser);
231        $schemaParser = new SqliteSchemaParser();
232        $selectTransformer = new SelectTransformer();
233        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
234        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
235        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
236        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
237        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
238        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
239        $plan = $rewriter->rewrite('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)');
240        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
241        self::assertInstanceOf(CreateTableMutation::class, $plan->mutation());
242    }
243
244    public function testDropTableReturnsDdlSimulated(): void
245    {
246        $registry = new TableDefinitionRegistry();
247        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
248        $store = new ShadowStore();
249        $parser = new SqliteParser();
250        $guard = new SqliteQueryGuard($parser);
251        $schemaParser = new SqliteSchemaParser();
252        $selectTransformer = new SelectTransformer();
253        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
254        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
255        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
256        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
257        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
258        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
259        $plan = $rewriter->rewrite('DROP TABLE users');
260        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
261        self::assertInstanceOf(DropTableMutation::class, $plan->mutation());
262    }
263
264    public function testUnsupportedSqlThrowsException(): void
265    {
266        $store = new ShadowStore();
267        $registry = new TableDefinitionRegistry();
268        $parser = new SqliteParser();
269        $guard = new SqliteQueryGuard($parser);
270        $schemaParser = new SqliteSchemaParser();
271        $selectTransformer = new SelectTransformer();
272        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
273        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
274        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
275        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
276        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
277        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
278        $this->expectException(UnsupportedSqlException::class);
279        $rewriter->rewrite('CREATE INDEX idx ON users (name)');
280    }
281
282    public function testEmptyInputThrowsException(): void
283    {
284        $store = new ShadowStore();
285        $registry = new TableDefinitionRegistry();
286        $parser = new SqliteParser();
287        $guard = new SqliteQueryGuard($parser);
288        $schemaParser = new SqliteSchemaParser();
289        $selectTransformer = new SelectTransformer();
290        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
291        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
292        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
293        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
294        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
295        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
296        $this->expectException(UnsupportedSqlException::class);
297        $rewriter->rewrite('');
298    }
299
300    public function testRewriteIsDeterministic(): void
301    {
302        $registry = new TableDefinitionRegistry();
303        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
304        $store = new ShadowStore();
305        $parser = new SqliteParser();
306        $guard = new SqliteQueryGuard($parser);
307        $schemaParser = new SqliteSchemaParser();
308        $selectTransformer = new SelectTransformer();
309        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
310        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
311        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
312        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
313        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
314        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
315        $plan1 = $rewriter->rewrite('SELECT * FROM users');
316        $plan2 = $rewriter->rewrite('SELECT * FROM users');
317        self::assertSame($plan1->sql(), $plan2->sql());
318        self::assertSame($plan1->kind(), $plan2->kind());
319    }
320
321    public function testReadPlanHasNoMutation(): void
322    {
323        $registry = new TableDefinitionRegistry();
324        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
325        $store = new ShadowStore();
326        $parser = new SqliteParser();
327        $guard = new SqliteQueryGuard($parser);
328        $schemaParser = new SqliteSchemaParser();
329        $selectTransformer = new SelectTransformer();
330        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
331        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
332        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
333        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
334        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
335        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
336        $plan = $rewriter->rewrite('SELECT * FROM users');
337        self::assertSame(QueryKind::READ, $plan->kind());
338        self::assertNull($plan->mutation());
339    }
340
341    public function testWritePlanHasNonNullMutation(): void
342    {
343        $registry = new TableDefinitionRegistry();
344        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
345        $store = new ShadowStore();
346        $parser = new SqliteParser();
347        $guard = new SqliteQueryGuard($parser);
348        $schemaParser = new SqliteSchemaParser();
349        $selectTransformer = new SelectTransformer();
350        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
351        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
352        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
353        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
354        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
355        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
356        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
357        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
358        self::assertNotNull($plan->mutation());
359        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
360    }
361
362    public function testRewriteOutputIsNonEmpty(): void
363    {
364        $rewriterStore = new ShadowStore();
365        $rewriterRegistry = new TableDefinitionRegistry();
366        $rewriterParser = new SqliteSchemaParser();
367        $rewriterDefinition = $rewriterParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL)');
368        self::assertNotNull($rewriterDefinition);
369        $rewriterRegistry->register('users', $rewriterDefinition);
370        $rewriter2Store = $rewriterStore;
371        $rewriter2Registry = $rewriterRegistry;
372        $rewriter2Parser = new SqliteParser();
373        $rewriter2SchemaParser = new SqliteSchemaParser();
374        $rewriter2SelectTransformer = new SelectTransformer();
375        $rewriter2InsertTransformer = new InsertTransformer($rewriter2Parser, $rewriter2SelectTransformer);
376        $rewriter2UpdateTransformer = new UpdateTransformer($rewriter2Parser, $rewriter2SelectTransformer);
377        $rewriter2DeleteTransformer = new DeleteTransformer($rewriter2Parser, $rewriter2SelectTransformer);
378        $rewriter2Transformer = new SqliteTransformer($rewriter2Parser, $rewriter2SelectTransformer, $rewriter2InsertTransformer, $rewriter2UpdateTransformer, $rewriter2DeleteTransformer);
379        $rewriter2MutationResolver = new SqliteMutationResolver($rewriter2Store, $rewriter2Registry, $rewriter2SchemaParser, $rewriter2Parser);
380        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriter2Parser), $rewriter2Store, $rewriter2Registry, $rewriter2Transformer, $rewriter2MutationResolver, $rewriter2Parser);
381        $plan = $rewriter->rewrite('SELECT id, name, email FROM users WHERE id = 1');
382        self::assertNotEmpty($plan->sql());
383        self::assertStringContainsString('SELECT', strtoupper($plan->sql()));
384    }
385
386    public function testInsertRewriteOutputContainsSelect(): void
387    {
388        $rewriterStore = new ShadowStore();
389        $rewriterRegistry = new TableDefinitionRegistry();
390        $rewriterParser = new SqliteSchemaParser();
391        $rewriterDefinition = $rewriterParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL)');
392        self::assertNotNull($rewriterDefinition);
393        $rewriterRegistry->register('users', $rewriterDefinition);
394        $rewriter2Store = $rewriterStore;
395        $rewriter2Registry = $rewriterRegistry;
396        $rewriter2Parser = new SqliteParser();
397        $rewriter2SchemaParser = new SqliteSchemaParser();
398        $rewriter2SelectTransformer = new SelectTransformer();
399        $rewriter2InsertTransformer = new InsertTransformer($rewriter2Parser, $rewriter2SelectTransformer);
400        $rewriter2UpdateTransformer = new UpdateTransformer($rewriter2Parser, $rewriter2SelectTransformer);
401        $rewriter2DeleteTransformer = new DeleteTransformer($rewriter2Parser, $rewriter2SelectTransformer);
402        $rewriter2Transformer = new SqliteTransformer($rewriter2Parser, $rewriter2SelectTransformer, $rewriter2InsertTransformer, $rewriter2UpdateTransformer, $rewriter2DeleteTransformer);
403        $rewriter2MutationResolver = new SqliteMutationResolver($rewriter2Store, $rewriter2Registry, $rewriter2SchemaParser, $rewriter2Parser);
404        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriter2Parser), $rewriter2Store, $rewriter2Registry, $rewriter2Transformer, $rewriter2MutationResolver, $rewriter2Parser);
405        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com')");
406        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
407        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql(), 'INSERT rewrite must produce a result-select query starting with SELECT or WITH...SELECT');
408    }
409
410    public function testGeneratedExpressionIsPresentBeforeTheFirstShadowWrite(): void
411    {
412        $store = new ShadowStore();
413        $registry = new TableDefinitionRegistry();
414        $definition = (new SqliteSchemaParser())->parse('CREATE TABLE orders (qty INTEGER, total INTEGER GENERATED ALWAYS AS (qty * 2) STORED)');
415        self::assertNotNull($definition);
416        $registry->register('orders', $definition);
417        $rewriterStore = $store;
418        $rewriterRegistry = $registry;
419        $rewriterParser = new SqliteParser();
420        $rewriterSchemaParser = new SqliteSchemaParser();
421        $rewriterSelectTransformer = new SelectTransformer();
422        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
423        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
424        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
425        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
426        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
427        $sql = (new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser))->rewrite('SELECT total FROM orders')->sql();
428        self::assertStringContainsString('(qty * 2) AS "total"', $sql);
429    }
430
431    public function testRegisteredViewIsKnownAndMaterialized(): void
432    {
433        $store = new ShadowStore();
434        $registry = new TableDefinitionRegistry();
435        $parser = new SqliteParser();
436        $schemaParser = new SqliteSchemaParser();
437        $definition = $schemaParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL)');
438        self::assertNotNull($definition);
439        $registry->register('users', $definition);
440        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
441        $selectTransformer = new SelectTransformer();
442        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
443        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
444        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
445        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
446        $resolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
447        $views = new ViewDefinitionSet();
448        $views->register('active_users', (new SqliteViewDefinitionParser())->fromQuery('SELECT id FROM main.users'));
449        $rewriter = new SqliteRewriter(new SqliteQueryGuard($parser), $store, $registry, $transformer, $resolver, $parser, $views);
450        $sql = $rewriter->rewrite('SELECT * FROM active_users')->sql();
451        self::assertStringStartsWith('WITH "users" AS', $sql);
452        self::assertStringContainsString('"active_users" AS (SELECT id FROM users)', $sql);
453        $viewOnlyStore = new ShadowStore();
454        $viewOnlyRegistry = new TableDefinitionRegistry();
455        $viewOnlyViews = new ViewDefinitionSet();
456        $viewOnlyViews->register('constant_view', (new SqliteViewDefinitionParser())->fromQuery('SELECT 1 AS id'));
457        $viewOnlyResolver = new SqliteMutationResolver($viewOnlyStore, $viewOnlyRegistry, $schemaParser, $parser);
458        $viewOnlyRewriter = new SqliteRewriter(new SqliteQueryGuard($parser), $viewOnlyStore, $viewOnlyRegistry, $transformer, $viewOnlyResolver, $parser, $viewOnlyViews);
459        $this->expectException(UnknownSchemaException::class);
460        $viewOnlyRewriter->rewrite('SELECT * FROM missing_table');
461    }
462
463    public function testCteReferencesAreMatchedCaseInsensitivelyDuringSchemaValidation(): void
464    {
465        $registry = new TableDefinitionRegistry();
466        $definition = (new SqliteSchemaParser())->parse('CREATE TABLE known_table (id INTEGER PRIMARY KEY)');
467        self::assertNotNull($definition);
468        $registry->register('known_table', $definition);
469        $rewriterStore = new ShadowStore();
470        $rewriterRegistry = $registry;
471        $rewriterParser = new SqliteParser();
472        $rewriterSchemaParser = new SqliteSchemaParser();
473        $rewriterSelectTransformer = new SelectTransformer();
474        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
475        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
476        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
477        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
478        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
479        $plan = (new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser))->rewrite('WITH users AS (SELECT 1 AS id) SELECT * FROM Users');
480        self::assertSame(QueryKind::READ, $plan->kind());
481    }
482
483    public function testUnknownTableAfterDeclaredCteIsRejected(): void
484    {
485        $registry = new TableDefinitionRegistry();
486        $definition = (new SqliteSchemaParser())->parse('CREATE TABLE known_table (id INTEGER PRIMARY KEY)');
487        self::assertNotNull($definition);
488        $registry->register('known_table', $definition);
489        $this->expectException(UnknownSchemaException::class);
490        $this->expectExceptionMessage('missing_table');
491        $rewriterStore = new ShadowStore();
492        $rewriterRegistry = $registry;
493        $rewriterParser = new SqliteParser();
494        $rewriterSchemaParser = new SqliteSchemaParser();
495        $rewriterSelectTransformer = new SelectTransformer();
496        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
497        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
498        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
499        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
500        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
501        (new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser))->rewrite('WITH users AS (SELECT 1 AS id) SELECT * FROM Users JOIN missing_table ON TRUE');
502    }
503
504    public function testInMemoryAttachPassesThroughUnchanged(): void
505    {
506        $rewriterStore = new ShadowStore();
507        $rewriterRegistry = new TableDefinitionRegistry();
508        $rewriterParser = new SqliteParser();
509        $rewriterSchemaParser = new SqliteSchemaParser();
510        $rewriterSelectTransformer = new SelectTransformer();
511        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
512        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
513        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
514        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
515        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
516        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
517        $sql = "ATTACH DATABASE ':memory:' AS db2";
518        $plan = $rewriter->rewrite($sql);
519        self::assertSame(QueryKind::READ, $plan->kind());
520        self::assertSame($sql, $plan->sql());
521    }
522
523    public function testPersistentAttachRemainsUnsupported(): void
524    {
525        $rewriterStore = new ShadowStore();
526        $rewriterRegistry = new TableDefinitionRegistry();
527        $rewriterParser = new SqliteParser();
528        $rewriterSchemaParser = new SqliteSchemaParser();
529        $rewriterSelectTransformer = new SelectTransformer();
530        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
531        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
532        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
533        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
534        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
535        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
536        $this->expectException(UnsupportedSqlException::class);
537        $rewriter->rewrite("ATTACH 'test.sqlite' AS db2");
538    }
539
540    public function testSchemaQualifiedSelectUsesShadowCte(): void
541    {
542        $store = new ShadowStore();
543        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
544        $registry = new TableDefinitionRegistry();
545        $definition = (new SqliteSchemaParser())->parse('CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL)');
546        self::assertNotNull($definition);
547        $registry->register('users', $definition);
548        $rewriterStore = $store;
549        $rewriterRegistry = $registry;
550        $rewriterParser = new SqliteParser();
551        $rewriterSchemaParser = new SqliteSchemaParser();
552        $rewriterSelectTransformer = new SelectTransformer();
553        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
554        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
555        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
556        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
557        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
558        $plan = (new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser))->rewrite('SELECT name FROM main.users');
559        self::assertStringStartsWith('WITH "users" AS', $plan->sql());
560        self::assertStringEndsWith('SELECT name FROM users', $plan->sql());
561    }
562
563    public function testReadOnlyDiagnosticsPassThroughUnchanged(): void
564    {
565        $rewriterStore = new ShadowStore();
566        $rewriterRegistry = new TableDefinitionRegistry();
567        $rewriterParser = new SqliteParser();
568        $rewriterSchemaParser = new SqliteSchemaParser();
569        $rewriterSelectTransformer = new SelectTransformer();
570        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
571        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
572        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
573        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
574        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
575        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
576        $sql = 'EXPLAIN QUERY PLAN SELECT * FROM users';
577        $plan = $rewriter->rewrite($sql);
578        self::assertSame(QueryKind::READ, $plan->kind());
579        self::assertSame($sql, $plan->sql());
580    }
581
582    public function testDeleteFromWithoutWhereReturnsWriteSimulated(): void
583    {
584        $registry = new TableDefinitionRegistry();
585        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
586        $store = new ShadowStore();
587        $store->ensure('users');
588        $parser = new SqliteParser();
589        $guard = new SqliteQueryGuard($parser);
590        $schemaParser = new SqliteSchemaParser();
591        $selectTransformer = new SelectTransformer();
592        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
593        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
594        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
595        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
596        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
597        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
598        $plan = $rewriter->rewrite('DELETE FROM users');
599        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
600        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
601    }
602
603    public function testReplaceReturnsWriteSimulatedWithMutation(): void
604    {
605        $registry = new TableDefinitionRegistry();
606        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
607        $store = new ShadowStore();
608        $parser = new SqliteParser();
609        $guard = new SqliteQueryGuard($parser);
610        $schemaParser = new SqliteSchemaParser();
611        $selectTransformer = new SelectTransformer();
612        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
613        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
614        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
615        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
616        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
617        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
618        $plan = $rewriter->rewrite("REPLACE INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
619        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
620        self::assertInstanceOf(ReplaceMutation::class, $plan->mutation());
621    }
622
623    public function testInsertOrReplaceReturnsWriteSimulatedWithReplaceMutation(): void
624    {
625        $registry = new TableDefinitionRegistry();
626        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
627        $store = new ShadowStore();
628        $parser = new SqliteParser();
629        $guard = new SqliteQueryGuard($parser);
630        $schemaParser = new SqliteSchemaParser();
631        $selectTransformer = new SelectTransformer();
632        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
633        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
634        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
635        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
636        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
637        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
638        $plan = $rewriter->rewrite("INSERT OR REPLACE INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
639        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
640        self::assertInstanceOf(ReplaceMutation::class, $plan->mutation());
641    }
642
643    public function testInsertOnConflictReturnsUpsertMutation(): void
644    {
645        $registry = new TableDefinitionRegistry();
646        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
647        $store = new ShadowStore();
648        $parser = new SqliteParser();
649        $guard = new SqliteQueryGuard($parser);
650        $schemaParser = new SqliteSchemaParser();
651        $selectTransformer = new SelectTransformer();
652        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
653        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
654        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
655        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
656        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
657        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
658        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com') ON CONFLICT (id) DO UPDATE SET name = excluded.name");
659        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
660        self::assertInstanceOf(UpsertMutation::class, $plan->mutation());
661        self::assertStringContainsString('__ztd_upsert_value_0', $plan->sql());
662        self::assertStringNotContainsString('excluded.', $plan->sql());
663    }
664
665    public function testMultiStatementThrowsException(): void
666    {
667        $store = new ShadowStore();
668        $registry = new TableDefinitionRegistry();
669        $parser = new SqliteParser();
670        $guard = new SqliteQueryGuard($parser);
671        $schemaParser = new SqliteSchemaParser();
672        $selectTransformer = new SelectTransformer();
673        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
674        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
675        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
676        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
677        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
678        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
679        $this->expectException(UnsupportedSqlException::class);
680        $rewriter->rewrite('SELECT 1; SELECT 2');
681    }
682
683    public function testInsertUsesDefaultsFromRegistryWithoutShadowRows(): void
684    {
685        $definition = (new SqliteSchemaParser())->parse("CREATE TABLE settings (id INTEGER, label TEXT DEFAULT 'new')");
686        self::assertNotNull($definition);
687        $registry = new TableDefinitionRegistry();
688        $registry->register('settings', $definition);
689        $rewriterStore = new ShadowStore();
690        $rewriterRegistry = $registry;
691        $rewriterParser = new SqliteParser();
692        $rewriterSchemaParser = new SqliteSchemaParser();
693        $rewriterSelectTransformer = new SelectTransformer();
694        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
695        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
696        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
697        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
698        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
699        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
700        $plan = $rewriter->rewrite('INSERT INTO settings (id) VALUES (1)');
701        self::assertStringContainsString("'new'", $plan->sql());
702        self::assertStringContainsString('AS "label"', $plan->sql());
703    }
704
705    public function testInsertUsesIdentityStrategyFromRegistryWithoutShadowRows(): void
706    {
707        $definition = (new SqliteSchemaParser())->parse('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
708        self::assertNotNull($definition);
709        $registry = new TableDefinitionRegistry();
710        $registry->register('users', $definition);
711        $rewriterStore = new ShadowStore();
712        $rewriterRegistry = $registry;
713        $rewriterParser = new SqliteParser();
714        $rewriterSchemaParser = new SqliteSchemaParser();
715        $rewriterSelectTransformer = new SelectTransformer();
716        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
717        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
718        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
719        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
720        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
721        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
722        $plan = $rewriter->rewrite("INSERT INTO users (name) VALUES ('Alice')");
723        self::assertStringContainsString('CAST(1 AS INTEGER) AS "id"', $plan->sql());
724    }
725
726    public function testRewriteMultiple(): void
727    {
728        $registry = new TableDefinitionRegistry();
729        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
730        $store = new ShadowStore();
731        $parser = new SqliteParser();
732        $guard = new SqliteQueryGuard($parser);
733        $schemaParser = new SqliteSchemaParser();
734        $selectTransformer = new SelectTransformer();
735        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
736        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
737        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
738        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
739        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
740        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
741        $multiPlan = $rewriter->rewriteMultiple("SELECT * FROM users; INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
742        self::assertSame(2, $multiPlan->count());
743        self::assertSame(QueryKind::READ, $multiPlan->get(0)?->kind());
744        self::assertSame(QueryKind::WRITE_SIMULATED, $multiPlan->get(1)?->kind());
745    }
746
747    public function testSelectWithShadowDataIncludesCte(): void
748    {
749        $registry = new TableDefinitionRegistry();
750        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
751        $store = new ShadowStore();
752        $store->ensure('users');
753        $store->insert('users', [['id' => 1, 'name' => 'Alice', 'email' => 'a@b.com']]);
754        $parser = new SqliteParser();
755        $guard = new SqliteQueryGuard($parser);
756        $schemaParser = new SqliteSchemaParser();
757        $selectTransformer = new SelectTransformer();
758        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
759        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
760        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
761        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
762        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
763        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
764        $plan = $rewriter->rewrite('SELECT * FROM users');
765        self::assertStringStartsWith('WITH', $plan->sql());
766        self::assertStringContainsString('"users"', $plan->sql());
767        self::assertStringContainsString('SELECT', strtoupper($plan->sql()));
768    }
769
770    public function testSelectUnknownTableWithSchemaContextThrows(): void
771    {
772        $registry = new TableDefinitionRegistry();
773        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
774        $store = new ShadowStore();
775        $parser = new SqliteParser();
776        $guard = new SqliteQueryGuard($parser);
777        $schemaParser = new SqliteSchemaParser();
778        $selectTransformer = new SelectTransformer();
779        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
780        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
781        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
782        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
783        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
784        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
785        $this->expectException(UnknownSchemaException::class);
786        $rewriter->rewrite('SELECT * FROM nonexistent');
787    }
788
789    public function testSelectKnownTableNoSchemaContextDoesNotThrow(): void
790    {
791        $store = new ShadowStore();
792        $registry = new TableDefinitionRegistry();
793        $parser = new SqliteParser();
794        $guard = new SqliteQueryGuard($parser);
795        $schemaParser = new SqliteSchemaParser();
796        $selectTransformer = new SelectTransformer();
797        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
798        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
799        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
800        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
801        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
802        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
803        $plan = $rewriter->rewrite('SELECT * FROM whatever');
804        self::assertSame(QueryKind::READ, $plan->kind());
805    }
806
807    public function testSelectWithShadowStoreOnlyHasSchemaContext(): void
808    {
809        $store = new ShadowStore();
810        $store->ensure('users');
811        $store->insert('users', [['id' => 1, 'name' => 'Alice']]);
812        $registry = new TableDefinitionRegistry();
813        $parser = new SqliteParser();
814        $guard = new SqliteQueryGuard($parser);
815        $schemaParser = new SqliteSchemaParser();
816        $selectTransformer = new SelectTransformer();
817        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
818        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
819        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
820        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
821        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
822        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
823        $plan = $rewriter->rewrite('SELECT * FROM users');
824        self::assertSame(QueryKind::READ, $plan->kind());
825        self::assertStringContainsString('WITH', $plan->sql());
826    }
827
828    public function testUpdateEnsuresShadowStore(): void
829    {
830        $registry = new TableDefinitionRegistry();
831        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
832        $store = new ShadowStore();
833        $parser = new SqliteParser();
834        $guard = new SqliteQueryGuard($parser);
835        $schemaParser = new SqliteSchemaParser();
836        $selectTransformer = new SelectTransformer();
837        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
838        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
839        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
840        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
841        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
842        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
843        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
844        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
845        self::assertNotNull($plan->mutation());
846        self::assertSame('users', $plan->mutation()->tableName());
847    }
848
849    public function testDeleteFromWithoutWhereReturnsSqlWithSelectWhere0(): void
850    {
851        $registry = new TableDefinitionRegistry();
852        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
853        $store = new ShadowStore();
854        $store->ensure('users');
855        $parser = new SqliteParser();
856        $guard = new SqliteQueryGuard($parser);
857        $schemaParser = new SqliteSchemaParser();
858        $selectTransformer = new SelectTransformer();
859        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
860        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
861        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
862        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
863        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
864        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
865        $plan = $rewriter->rewrite('DELETE FROM users');
866        self::assertStringContainsString('FROM "users"', $plan->sql());
867    }
868
869    public function testDdlSimulatedReturnsSqlSelectWhere0(): void
870    {
871        $store = new ShadowStore();
872        $registry = new TableDefinitionRegistry();
873        $parser = new SqliteParser();
874        $guard = new SqliteQueryGuard($parser);
875        $schemaParser = new SqliteSchemaParser();
876        $selectTransformer = new SelectTransformer();
877        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
878        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
879        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
880        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
881        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
882        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
883        $plan = $rewriter->rewrite('CREATE TABLE t (id INTEGER PRIMARY KEY)');
884        self::assertSame('SELECT 1 WHERE 0', $plan->sql());
885        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
886    }
887
888    public function testAlterTableUsesShadowedMigrationSelect(): void
889    {
890        $registry = new TableDefinitionRegistry();
891        $registry->register('people', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
892        $store = new ShadowStore();
893        $store->set('people', [['id' => 1, 'name' => 'Alice']]);
894        $rewriterStore = $store;
895        $rewriterRegistry = $registry;
896        $rewriterParser = new SqliteParser();
897        $rewriterSchemaParser = new SqliteSchemaParser();
898        $rewriterSelectTransformer = new SelectTransformer();
899        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
900        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
901        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
902        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
903        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
904        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
905        $plan = $rewriter->rewrite('ALTER TABLE people ADD COLUMN age INTEGER DEFAULT 7');
906        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
907        self::assertInstanceOf(AlterTableMutation::class, $plan->mutation());
908        self::assertStringContainsString('WITH "people" AS', $plan->sql());
909        self::assertStringContainsString('SELECT "id", "name", 7 AS "age" FROM "people"', $plan->sql());
910    }
911
912    public function testRemovedTableIsShadowedInsteadOfFallingThrough(): void
913    {
914        $registry = new TableDefinitionRegistry();
915        $registry->register('people', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
916        $registry->markRemoved('people');
917        $rewriterStore = new ShadowStore();
918        $rewriterRegistry = $registry;
919        $rewriterParser = new SqliteParser();
920        $rewriterSchemaParser = new SqliteSchemaParser();
921        $rewriterSelectTransformer = new SelectTransformer();
922        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
923        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
924        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
925        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
926        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
927        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
928        $plan = $rewriter->rewrite('SELECT id, name FROM people');
929        self::assertStringContainsString('WITH "people" AS', $plan->sql());
930        self::assertStringContainsString('WHERE 0', $plan->sql());
931    }
932
933    public function testRewriteMultipleEmptyThrows(): void
934    {
935        $store = new ShadowStore();
936        $registry = new TableDefinitionRegistry();
937        $parser = new SqliteParser();
938        $guard = new SqliteQueryGuard($parser);
939        $schemaParser = new SqliteSchemaParser();
940        $selectTransformer = new SelectTransformer();
941        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
942        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
943        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
944        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
945        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
946        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
947        $this->expectException(UnsupportedSqlException::class);
948        $rewriter->rewriteMultiple('');
949    }
950
951    public function testSelectWithRegistryOnlyBuildTableContext(): void
952    {
953        $registry = new TableDefinitionRegistry();
954        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
955        $store = new ShadowStore();
956        $parser = new SqliteParser();
957        $guard = new SqliteQueryGuard($parser);
958        $schemaParser = new SqliteSchemaParser();
959        $selectTransformer = new SelectTransformer();
960        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
961        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
962        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
963        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
964        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
965        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
966        $plan = $rewriter->rewrite('SELECT * FROM users');
967        self::assertSame(QueryKind::READ, $plan->kind());
968        self::assertStringContainsString('WITH', $plan->sql());
969        self::assertStringContainsString('WHERE 0', $plan->sql());
970    }
971
972    public function testSelectWithShadowDataColumnsInferred(): void
973    {
974        $store = new ShadowStore();
975        $store->ensure('users');
976        $store->insert('users', [['id' => 1, 'name' => 'Alice']]);
977        $registry = new TableDefinitionRegistry();
978        $parser = new SqliteParser();
979        $guard = new SqliteQueryGuard($parser);
980        $schemaParser = new SqliteSchemaParser();
981        $selectTransformer = new SelectTransformer();
982        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
983        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
984        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
985        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
986        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
987        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
988        $plan = $rewriter->rewrite('SELECT * FROM users');
989        self::assertStringContainsString('"users"', $plan->sql());
990        self::assertStringContainsString('"id"', $plan->sql());
991        self::assertStringContainsString('"name"', $plan->sql());
992    }
993
994    public function testDeleteWithWhereProducesTransformedSql(): void
995    {
996        $registry = new TableDefinitionRegistry();
997        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
998        $store = new ShadowStore();
999        $store->ensure('users');
1000        $parser = new SqliteParser();
1001        $guard = new SqliteQueryGuard($parser);
1002        $schemaParser = new SqliteSchemaParser();
1003        $selectTransformer = new SelectTransformer();
1004        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1005        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1006        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1007        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1008        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1009        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1010        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1011        self::assertStringContainsString('SELECT', $plan->sql());
1012        self::assertStringContainsString('WHERE id = 1', $plan->sql());
1013    }
1014
1015    public function testDeleteEnsuresShadowStore(): void
1016    {
1017        $registry = new TableDefinitionRegistry();
1018        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1019        $store = new ShadowStore();
1020        $parser = new SqliteParser();
1021        $guard = new SqliteQueryGuard($parser);
1022        $schemaParser = new SqliteSchemaParser();
1023        $selectTransformer = new SelectTransformer();
1024        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1025        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1026        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1027        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1028        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1029        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1030        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1031        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1032        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
1033    }
1034
1035    public function testSelectExistingInShadowStoreIsNotUnknown(): void
1036    {
1037        $store = new ShadowStore();
1038        $store->ensure('users');
1039        $store->set('users', [['id' => 1]]);
1040        $registry = new TableDefinitionRegistry();
1041        $parser = new SqliteParser();
1042        $guard = new SqliteQueryGuard($parser);
1043        $schemaParser = new SqliteSchemaParser();
1044        $selectTransformer = new SelectTransformer();
1045        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1046        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1047        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1048        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1049        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1050        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1051        $plan = $rewriter->rewrite('SELECT * FROM users');
1052        self::assertSame(QueryKind::READ, $plan->kind());
1053    }
1054
1055    public function testInsertWithShadowDataProducesTransformedSql(): void
1056    {
1057        $registry = new TableDefinitionRegistry();
1058        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1059        $store = new ShadowStore();
1060        $store->ensure('users');
1061        $store->insert('users', [['id' => 1, 'name' => 'Alice']]);
1062        $parser = new SqliteParser();
1063        $guard = new SqliteQueryGuard($parser);
1064        $schemaParser = new SqliteSchemaParser();
1065        $selectTransformer = new SelectTransformer();
1066        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1067        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1068        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1069        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1070        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1071        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1072        $plan = $rewriter->rewrite("INSERT INTO users (id, name) VALUES (2, 'Bob')");
1073        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1074        self::assertStringContainsString('SELECT', $plan->sql());
1075        self::assertNotNull($plan->mutation());
1076    }
1077
1078    public function testBuildTableContextMergesColumnsFromMultipleRows(): void
1079    {
1080        $store = new ShadowStore();
1081        $store->ensure('users');
1082        $store->insert('users', [['id' => 1, 'name' => 'Alice']]);
1083        $store->insert('users', [['id' => 2, 'name' => 'Bob', 'email' => 'b@b.com']]);
1084        $registry = new TableDefinitionRegistry();
1085        $parser = new SqliteParser();
1086        $guard = new SqliteQueryGuard($parser);
1087        $schemaParser = new SqliteSchemaParser();
1088        $selectTransformer = new SelectTransformer();
1089        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1090        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1091        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1092        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1093        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1094        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1095        $plan = $rewriter->rewrite('SELECT * FROM users');
1096        self::assertStringContainsString('"id"', $plan->sql());
1097        self::assertStringContainsString('"name"', $plan->sql());
1098        self::assertStringContainsString('"email"', $plan->sql());
1099    }
1100
1101    public function testBuildTableContextUsesDefinitionColumns(): void
1102    {
1103        $registry = new TableDefinitionRegistry();
1104        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1105        $store = new ShadowStore();
1106        $store->ensure('users');
1107        $store->insert('users', [['id' => 1]]);
1108        $parser = new SqliteParser();
1109        $guard = new SqliteQueryGuard($parser);
1110        $schemaParser = new SqliteSchemaParser();
1111        $selectTransformer = new SelectTransformer();
1112        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1113        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1114        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1115        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1116        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1117        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1118        $plan = $rewriter->rewrite('SELECT * FROM users');
1119        self::assertStringContainsString('"name"', $plan->sql());
1120    }
1121
1122    public function testBuildTableContextRegistryOnlyTableIncluded(): void
1123    {
1124        $registry = new TableDefinitionRegistry();
1125        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1126        $registry->register('orders', new TableDefinition(['id', 'amount'], ['id' => 'INTEGER', 'amount' => 'REAL'], ['id'], [], []));
1127        $store = new ShadowStore();
1128        $parser = new SqliteParser();
1129        $guard = new SqliteQueryGuard($parser);
1130        $schemaParser = new SqliteSchemaParser();
1131        $selectTransformer = new SelectTransformer();
1132        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1133        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1134        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1135        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1136        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1137        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1138        $plan = $rewriter->rewrite('SELECT * FROM users');
1139        self::assertStringContainsString('"users"', $plan->sql());
1140    }
1141
1142    public function testSelectTableInShadowStoreNotUnknown(): void
1143    {
1144        $store = new ShadowStore();
1145        $store->ensure('users');
1146        $registry = new TableDefinitionRegistry();
1147        $registry->register('orders', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1148        $parser = new SqliteParser();
1149        $guard = new SqliteQueryGuard($parser);
1150        $schemaParser = new SqliteSchemaParser();
1151        $selectTransformer = new SelectTransformer();
1152        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1153        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1154        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1155        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1156        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1157        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1158        $this->expectException(UnknownSchemaException::class);
1159        $rewriter->rewrite('SELECT * FROM nonexistent');
1160    }
1161
1162    public function testDeleteFromQuotedTableWithoutWhereReturnsSqlWhere0(): void
1163    {
1164        $registry = new TableDefinitionRegistry();
1165        $registry->register('my_table', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1166        $store = new ShadowStore();
1167        $store->ensure('my_table');
1168        $parser = new SqliteParser();
1169        $guard = new SqliteQueryGuard($parser);
1170        $schemaParser = new SqliteSchemaParser();
1171        $selectTransformer = new SelectTransformer();
1172        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1173        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1174        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1175        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1176        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1177        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1178        $plan = $rewriter->rewrite('DELETE FROM "my_table"');
1179        self::assertStringContainsString('FROM "my_table"', $plan->sql());
1180    }
1181
1182    public function testDeleteFromWithSemicolonAndWhitespace(): void
1183    {
1184        $registry = new TableDefinitionRegistry();
1185        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1186        $store = new ShadowStore();
1187        $store->ensure('users');
1188        $parser = new SqliteParser();
1189        $guard = new SqliteQueryGuard($parser);
1190        $schemaParser = new SqliteSchemaParser();
1191        $selectTransformer = new SelectTransformer();
1192        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1193        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1194        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1195        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1196        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1197        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1198        $plan = $rewriter->rewrite('DELETE FROM users ;');
1199        self::assertStringContainsString('FROM "users"', $plan->sql());
1200    }
1201
1202    public function testUpdateEnsuresShadowStoreCalledOnTarget(): void
1203    {
1204        $registry = new TableDefinitionRegistry();
1205        $registry->register('t', new TableDefinition(['id', 'val'], ['id' => 'INTEGER', 'val' => 'TEXT'], ['id'], [], []));
1206        $store = new ShadowStore();
1207        $parser = new SqliteParser();
1208        $guard = new SqliteQueryGuard($parser);
1209        $schemaParser = new SqliteSchemaParser();
1210        $selectTransformer = new SelectTransformer();
1211        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1212        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1213        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1214        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1215        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1216        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1217        $plan = $rewriter->rewrite("UPDATE t SET val = 'x' WHERE id = 1");
1218        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1219        self::assertSame([], $store->get('t'));
1220    }
1221
1222    public function testDeleteEnsuresShadowStoreCalledOnTarget(): void
1223    {
1224        $registry = new TableDefinitionRegistry();
1225        $registry->register('t', new TableDefinition(['id', 'val'], ['id' => 'INTEGER', 'val' => 'TEXT'], ['id'], [], []));
1226        $store = new ShadowStore();
1227        $parser = new SqliteParser();
1228        $guard = new SqliteQueryGuard($parser);
1229        $schemaParser = new SqliteSchemaParser();
1230        $selectTransformer = new SelectTransformer();
1231        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1232        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1233        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1234        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1235        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1236        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1237        $plan = $rewriter->rewrite('DELETE FROM t WHERE id = 1');
1238        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1239        self::assertSame([], $store->get('t'));
1240    }
1241
1242    public function testBuildTableContextShadowStoreEmptyRowsNoDefinitionPassesThrough(): void
1243    {
1244        $store = new ShadowStore();
1245        $store->ensure('users');
1246        $registry = new TableDefinitionRegistry();
1247        $parser = new SqliteParser();
1248        $guard = new SqliteQueryGuard($parser);
1249        $schemaParser = new SqliteSchemaParser();
1250        $selectTransformer = new SelectTransformer();
1251        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1252        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1253        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1254        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1255        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1256        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1257        $plan = $rewriter->rewrite('SELECT 1');
1258        self::assertSame(QueryKind::READ, $plan->kind());
1259    }
1260
1261    public function testHasSchemaContextWithRegistryOnly(): void
1262    {
1263        $store = new ShadowStore();
1264        $registry = new TableDefinitionRegistry();
1265        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1266        $parser = new SqliteParser();
1267        $guard = new SqliteQueryGuard($parser);
1268        $schemaParser = new SqliteSchemaParser();
1269        $selectTransformer = new SelectTransformer();
1270        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1271        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1272        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1273        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1274        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1275        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1276        $this->expectException(UnknownSchemaException::class);
1277        $rewriter->rewrite('SELECT * FROM nonexistent');
1278    }
1279
1280    public function testHasSchemaContextWithShadowStoreOnly(): void
1281    {
1282        $store = new ShadowStore();
1283        $store->ensure('users');
1284        $store->insert('users', [['id' => 1]]);
1285        $registry = new TableDefinitionRegistry();
1286        $parser = new SqliteParser();
1287        $guard = new SqliteQueryGuard($parser);
1288        $schemaParser = new SqliteSchemaParser();
1289        $selectTransformer = new SelectTransformer();
1290        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1291        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1292        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1293        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1294        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1295        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1296        $this->expectException(UnknownSchemaException::class);
1297        $rewriter->rewrite('SELECT * FROM nonexistent');
1298    }
1299
1300    public function testDeleteFromBacktickQuotedTableReturnsSqlWhere0(): void
1301    {
1302        $registry = new TableDefinitionRegistry();
1303        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1304        $store = new ShadowStore();
1305        $store->ensure('t');
1306        $parser = new SqliteParser();
1307        $guard = new SqliteQueryGuard($parser);
1308        $schemaParser = new SqliteSchemaParser();
1309        $selectTransformer = new SelectTransformer();
1310        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1311        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1312        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1313        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1314        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1315        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1316        $plan = $rewriter->rewrite('DELETE FROM `t`');
1317        self::assertStringContainsString('FROM "t"', $plan->sql());
1318    }
1319
1320    public function testDeleteFromBracketQuotedTableReturnsSqlWhere0(): void
1321    {
1322        $registry = new TableDefinitionRegistry();
1323        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1324        $store = new ShadowStore();
1325        $store->ensure('t');
1326        $parser = new SqliteParser();
1327        $guard = new SqliteQueryGuard($parser);
1328        $schemaParser = new SqliteSchemaParser();
1329        $selectTransformer = new SelectTransformer();
1330        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1331        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1332        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1333        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1334        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1335        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1336        $plan = $rewriter->rewrite('DELETE FROM [t]');
1337        self::assertStringContainsString('FROM "t"', $plan->sql());
1338    }
1339
1340    public function testUpdateEnsuresShadowStoreForTargetTable(): void
1341    {
1342        $registry = new TableDefinitionRegistry();
1343        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1344        $store = new ShadowStore();
1345        $parser = new SqliteParser();
1346        $guard = new SqliteQueryGuard($parser);
1347        $schemaParser = new SqliteSchemaParser();
1348        $selectTransformer = new SelectTransformer();
1349        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1350        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1351        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1352        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1353        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1354        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1355        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
1356        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1357        self::assertNotNull($plan->mutation());
1358        self::assertSame([], $store->get('users'));
1359    }
1360
1361    public function testDeleteEnsuresShadowStoreForTargetTable(): void
1362    {
1363        $registry = new TableDefinitionRegistry();
1364        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1365        $store = new ShadowStore();
1366        $parser = new SqliteParser();
1367        $guard = new SqliteQueryGuard($parser);
1368        $schemaParser = new SqliteSchemaParser();
1369        $selectTransformer = new SelectTransformer();
1370        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1371        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1372        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1373        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1374        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1375        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1376        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1377        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1378        self::assertNotNull($plan->mutation());
1379    }
1380
1381    public function testDeleteFromLowercaseReturnsSqlWhere0(): void
1382    {
1383        $registry = new TableDefinitionRegistry();
1384        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1385        $store = new ShadowStore();
1386        $store->ensure('users');
1387        $parser = new SqliteParser();
1388        $guard = new SqliteQueryGuard($parser);
1389        $schemaParser = new SqliteSchemaParser();
1390        $selectTransformer = new SelectTransformer();
1391        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1392        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1393        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1394        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1395        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1396        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1397        $plan = $rewriter->rewrite('delete from users');
1398        self::assertStringContainsString('FROM "users"', $plan->sql());
1399    }
1400
1401    public function testBuildTableContextIncludesMultipleTablesFromRegistry(): void
1402    {
1403        $registry = new TableDefinitionRegistry();
1404        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1405        $registry->register('orders', new TableDefinition(['oid', 'uid'], ['oid' => 'INTEGER', 'uid' => 'INTEGER'], ['oid'], [], []));
1406        $store = new ShadowStore();
1407        $parser = new SqliteParser();
1408        $guard = new SqliteQueryGuard($parser);
1409        $schemaParser = new SqliteSchemaParser();
1410        $selectTransformer = new SelectTransformer();
1411        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1412        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1413        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1414        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1415        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1416        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1417        $plan = $rewriter->rewrite('SELECT * FROM users JOIN orders ON users.id = orders.uid');
1418        self::assertSame(QueryKind::READ, $plan->kind());
1419    }
1420
1421    public function testBuildTableContextWithShadowStoreColumnsInferred(): void
1422    {
1423        $registry = new TableDefinitionRegistry();
1424        $store = new ShadowStore();
1425        $store->ensure('users');
1426        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
1427        $parser = new SqliteParser();
1428        $guard = new SqliteQueryGuard($parser);
1429        $schemaParser = new SqliteSchemaParser();
1430        $selectTransformer = new SelectTransformer();
1431        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1432        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1433        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1434        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1435        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1436        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1437        $plan = $rewriter->rewrite('SELECT * FROM users');
1438        self::assertSame(QueryKind::READ, $plan->kind());
1439        self::assertStringContainsString('WITH', $plan->sql());
1440        self::assertStringContainsString('"id"', $plan->sql());
1441        self::assertStringContainsString('"name"', $plan->sql());
1442    }
1443
1444    public function testBuildTableContextSkipsAlreadyAddedFromShadowStore(): void
1445    {
1446        $registry = new TableDefinitionRegistry();
1447        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1448        $store = new ShadowStore();
1449        $store->ensure('users');
1450        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
1451        $parser = new SqliteParser();
1452        $guard = new SqliteQueryGuard($parser);
1453        $schemaParser = new SqliteSchemaParser();
1454        $selectTransformer = new SelectTransformer();
1455        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1456        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1457        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1458        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1459        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1460        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1461        $plan = $rewriter->rewrite('SELECT * FROM users');
1462        self::assertSame(QueryKind::READ, $plan->kind());
1463        self::assertStringContainsString('WITH', $plan->sql());
1464        self::assertSame(1, substr_count($plan->sql(), '"users" AS'));
1465    }
1466
1467    public function testInsertDoesNotEnsureShadowStoreForTargetTable(): void
1468    {
1469        $registry = new TableDefinitionRegistry();
1470        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1471        $store = new ShadowStore();
1472        $parser = new SqliteParser();
1473        $guard = new SqliteQueryGuard($parser);
1474        $schemaParser = new SqliteSchemaParser();
1475        $selectTransformer = new SelectTransformer();
1476        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1477        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1478        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1479        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1480        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1481        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1482        $plan = $rewriter->rewrite("INSERT INTO users (id, name) VALUES (1, 'Alice')");
1483        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1484    }
1485
1486    public function testDeleteFromWithCommentsReturnsSqlWhere0(): void
1487    {
1488        $registry = new TableDefinitionRegistry();
1489        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1490        $store = new ShadowStore();
1491        $store->ensure('users');
1492        $parser = new SqliteParser();
1493        $guard = new SqliteQueryGuard($parser);
1494        $schemaParser = new SqliteSchemaParser();
1495        $selectTransformer = new SelectTransformer();
1496        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1497        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1498        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1499        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1500        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1501        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1502        $plan = $rewriter->rewrite('/* comment */ DELETE FROM users');
1503        self::assertStringContainsString('FROM "users"', $plan->sql());
1504    }
1505
1506    public function testSelectRewritesTableAfterBlockComment(): void
1507    {
1508        $registry = new TableDefinitionRegistry();
1509        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1510        $store = new ShadowStore();
1511        $store->set('users', [['id' => 1]]);
1512        $rewriterStore = $store;
1513        $rewriterRegistry = $registry;
1514        $rewriterParser = new SqliteParser();
1515        $rewriterSchemaParser = new SqliteSchemaParser();
1516        $rewriterSelectTransformer = new SelectTransformer();
1517        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1518        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1519        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1520        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1521        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1522        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1523        $plan = $rewriter->rewrite('SELECT * FROM/* table */users');
1524        self::assertSame(QueryKind::READ, $plan->kind());
1525        self::assertStringContainsString('"users" AS', $plan->sql());
1526    }
1527
1528    public function testSelectIgnoresSqlKeywordsInsideLeadingLineComment(): void
1529    {
1530        $registry = new TableDefinitionRegistry();
1531        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1532        $store = new ShadowStore();
1533        $store->set('users', [['id' => 1]]);
1534        $rewriterStore = $store;
1535        $rewriterRegistry = $registry;
1536        $rewriterParser = new SqliteParser();
1537        $rewriterSchemaParser = new SqliteSchemaParser();
1538        $rewriterSelectTransformer = new SelectTransformer();
1539        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1540        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1541        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1542        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1543        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1544        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1545        $sql = "-- SELECT * FROM other_table WHERE DELETE UPDATE INSERT\nSELECT * FROM users";
1546        $plan = $rewriter->rewrite($sql);
1547        self::assertSame(QueryKind::READ, $plan->kind());
1548        self::assertStringContainsString('"users" AS', $plan->sql());
1549        self::assertStringNotContainsString('"other_table" AS', $plan->sql());
1550    }
1551
1552    public function testInsertResolvesTargetAfterBlockComment(): void
1553    {
1554        $registry = new TableDefinitionRegistry();
1555        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1556        $rewriterStore = new ShadowStore();
1557        $rewriterRegistry = $registry;
1558        $rewriterParser = new SqliteParser();
1559        $rewriterSchemaParser = new SqliteSchemaParser();
1560        $rewriterSelectTransformer = new SelectTransformer();
1561        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1562        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1563        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1564        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1565        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1566        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1567        $plan = $rewriter->rewrite('INSERT INTO/* table */users (id) VALUES (1)');
1568        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1569        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
1570        self::assertSame('users', $plan->mutation()->tableName());
1571    }
1572
1573    public function testUpdateResolvesTargetAfterBlockComment(): void
1574    {
1575        $registry = new TableDefinitionRegistry();
1576        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1577        $rewriterStore = new ShadowStore();
1578        $rewriterRegistry = $registry;
1579        $rewriterParser = new SqliteParser();
1580        $rewriterSchemaParser = new SqliteSchemaParser();
1581        $rewriterSelectTransformer = new SelectTransformer();
1582        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1583        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1584        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1585        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1586        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1587        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1588        $plan = $rewriter->rewrite('UPDATE/* table */users SET id = 2 WHERE id = 1');
1589        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1590        self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
1591        self::assertSame('users', $plan->mutation()->tableName());
1592    }
1593
1594    public function testDeleteResolvesTargetAfterBlockComment(): void
1595    {
1596        $registry = new TableDefinitionRegistry();
1597        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1598        $rewriterStore = new ShadowStore();
1599        $rewriterRegistry = $registry;
1600        $rewriterParser = new SqliteParser();
1601        $rewriterSchemaParser = new SqliteSchemaParser();
1602        $rewriterSelectTransformer = new SelectTransformer();
1603        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1604        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1605        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1606        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1607        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1608        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1609        $plan = $rewriter->rewrite('DELETE FROM/* table */users WHERE id = 1');
1610        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1611        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
1612        self::assertSame('users', $plan->mutation()->tableName());
1613    }
1614
1615    public function testUpdateEnsuresShadowStoreEntry(): void
1616    {
1617        $registry = new TableDefinitionRegistry();
1618        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1619        $store = new ShadowStore();
1620        $parser = new SqliteParser();
1621        $guard = new SqliteQueryGuard($parser);
1622        $schemaParser = new SqliteSchemaParser();
1623        $selectTransformer = new SelectTransformer();
1624        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1625        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1626        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1627        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1628        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1629        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1630        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
1631        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1632        self::assertArrayHasKey('users', $store->getAll());
1633    }
1634
1635    public function testDeleteEnsuresShadowStoreEntry(): void
1636    {
1637        $registry = new TableDefinitionRegistry();
1638        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1639        $store = new ShadowStore();
1640        $parser = new SqliteParser();
1641        $guard = new SqliteQueryGuard($parser);
1642        $schemaParser = new SqliteSchemaParser();
1643        $selectTransformer = new SelectTransformer();
1644        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1645        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1646        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1647        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1648        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1649        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1650        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1651        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1652        self::assertArrayHasKey('users', $store->getAll());
1653    }
1654
1655    public function testSelectWithMultipleRegisteredTablesIncludesAll(): void
1656    {
1657        $registry = new TableDefinitionRegistry();
1658        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1659        $registry->register('orders', new TableDefinition(['id', 'user_id'], ['id' => 'INTEGER', 'user_id' => 'INTEGER'], ['id'], [], []));
1660        $store = new ShadowStore();
1661        $parser = new SqliteParser();
1662        $guard = new SqliteQueryGuard($parser);
1663        $schemaParser = new SqliteSchemaParser();
1664        $selectTransformer = new SelectTransformer();
1665        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1666        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1667        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1668        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1669        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1670        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1671        $plan = $rewriter->rewrite('SELECT * FROM users JOIN orders ON users.id = orders.user_id');
1672        self::assertSame(QueryKind::READ, $plan->kind());
1673        $sql = $plan->sql();
1674        self::assertStringContainsString('"users"', $sql);
1675        self::assertStringContainsString('"orders"', $sql);
1676    }
1677
1678    public function testSelectWithShadowStoreAndRegistryTablesMerged(): void
1679    {
1680        $registry = new TableDefinitionRegistry();
1681        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1682        $store = new ShadowStore();
1683        $store->ensure('orders');
1684        $store->set('orders', [['id' => 1, 'user_id' => 1]]);
1685        $parser = new SqliteParser();
1686        $guard = new SqliteQueryGuard($parser);
1687        $schemaParser = new SqliteSchemaParser();
1688        $selectTransformer = new SelectTransformer();
1689        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1690        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1691        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1692        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1693        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1694        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1695        $plan = $rewriter->rewrite('SELECT * FROM users JOIN orders ON users.id = orders.user_id');
1696        self::assertSame(QueryKind::READ, $plan->kind());
1697        $sql = $plan->sql();
1698        self::assertStringContainsString('"users"', $sql);
1699        self::assertStringContainsString('"orders"', $sql);
1700    }
1701
1702    public function testUpdateDoesNotPromoteMaterializedUnknownTable(): void
1703    {
1704        $store = new ShadowStore();
1705        $store->insert('late_table', [['id' => 1, 'name' => 'Alice']]);
1706        $rewriterStore = $store;
1707        $rewriterRegistry = new TableDefinitionRegistry();
1708        $rewriterParser = new SqliteParser();
1709        $rewriterSchemaParser = new SqliteSchemaParser();
1710        $rewriterSelectTransformer = new SelectTransformer();
1711        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1712        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1713        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1714        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1715        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1716        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1717        try {
1718            $rewriter->rewrite("UPDATE late_table SET name = 'Bob' WHERE id = 1");
1719            self::fail('Expected an unknown schema exception.');
1720        } catch (UnknownSchemaException) {
1721            self::assertSame(ShadowTableState::Materialized, $store->state('late_table'));
1722        }
1723    }
1724
1725    public function testTransactionStatementRestoresSnapshotOnRollback(): void
1726    {
1727        $store = new ShadowStore();
1728        $store->set('users', [['id' => 1]]);
1729        $rewriterStore = $store;
1730        $rewriterRegistry = new TableDefinitionRegistry();
1731        $rewriterParser = new SqliteParser();
1732        $rewriterSchemaParser = new SqliteSchemaParser();
1733        $rewriterSelectTransformer = new SelectTransformer();
1734        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1735        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1736        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1737        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1738        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1739        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1740        $transactions = new \ZtdQuery\Shadow\ShadowTransactions($store);
1741        $begin = $rewriter->transactionStatement('BEGIN');
1742        $rollback = $rewriter->transactionStatement('ROLLBACK');
1743        self::assertNotNull($begin);
1744        self::assertNotNull($rollback);
1745        $begin->apply($transactions);
1746        $store->set('users', [['id' => 2]]);
1747        $rollback->apply($transactions);
1748        self::assertSame([['id' => 1]], $store->get('users'));
1749        self::assertNull($rewriter->transactionStatement('SELECT 1'));
1750    }
1751
1752    public function testSplitStatementsPreservesQuotedSemicolons(): void
1753    {
1754        $rewriterStore = new ShadowStore();
1755        $rewriterRegistry = new TableDefinitionRegistry();
1756        $rewriterParser = new SqliteParser();
1757        $rewriterSchemaParser = new SqliteSchemaParser();
1758        $rewriterSelectTransformer = new SelectTransformer();
1759        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1760        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1761        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1762        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1763        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1764        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1765        self::assertSame(["SELECT 'a;b'", 'SELECT 2'], $rewriter->splitStatements("SELECT 'a;b'; SELECT 2;"));
1766    }
1767
1768    public function testEmptyResultSelectProducesNoRows(): void
1769    {
1770        $rewriterStore = new ShadowStore();
1771        $rewriterRegistry = new TableDefinitionRegistry();
1772        $rewriterParser = new SqliteParser();
1773        $rewriterSchemaParser = new SqliteSchemaParser();
1774        $rewriterSelectTransformer = new SelectTransformer();
1775        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1776        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1777        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1778        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1779        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1780        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1781        $statement = (new PDO('sqlite::memory:'))->query($rewriter->emptyResultSelect());
1782        self::assertNotFalse($statement);
1783        self::assertSame([], $statement->fetchAll());
1784    }
1785
1786    public function testCommitRewriteStateKeepsGeneratedIdentitiesAcrossRewrites(): void
1787    {
1788        $registry = new TableDefinitionRegistry();
1789        $definition = (new SqliteSchemaParser())->parse('CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT)');
1790        self::assertNotNull($definition);
1791        $registry->register('users', $definition);
1792        $rewriterStore = new ShadowStore();
1793        $rewriterRegistry = $registry;
1794        $rewriterParser = new SqliteParser();
1795        $rewriterSchemaParser = new SqliteSchemaParser();
1796        $rewriterSelectTransformer = new SelectTransformer();
1797        $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1798        $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1799        $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1800        $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1801        $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1802        $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1803        $first = $rewriter->rewrite("INSERT INTO users(name) VALUES ('Alice')");
1804        $rewriter->commitRewriteState();
1805        $second = $rewriter->rewrite("INSERT INTO users(name) VALUES ('Bob')");
1806        self::assertStringContainsString('CAST(1 AS INTEGER) AS "id"', $first->sql());
1807        self::assertStringContainsString('CAST(2 AS INTEGER) AS "id"', $second->sql());
1808    }
1809}
1810